Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first-class support to compute the initial props by using a Node.js controller. #13

Open
brillout opened this issue Oct 7, 2019 · 12 comments
Labels
enhancement New feature or request

Comments

@brillout
Copy link
Owner

brillout commented Oct 7, 2019

As discussed with @chriscalo in #11 we want a Goldpage user to be able to use Node.js code to add initialProps. Enabling the Goldpage user to use any SQL/ORM query in order to retrieve data that is being rendered by Vue/React.

This is already possible today but the ergonomics could be improved.

@chriscalo
Copy link

chriscalo commented Oct 10, 2019

I had some additional thoughts on this topic as I’ve been converting more of my app to use Goldpage. Hopefully this is somewhat useful as you are considering solutions.

Recap of the problem (nothing surprising or new here)

The friction originates in the .page.js files: that's where I should import .vue files, which means the .page.js files must be fed to webpack as entry points for both the server and client builds.

This has the undesirable side effect of forcing the .page.js files to be universal: no modules may be imported that can’t run in both Node.js and a browser. This feels far too restrictive.

And while a solution like Wildcard API does mitigate this somewhat, it still feels unnatural that I'm forced to move page-specific data-fetching logic like database queries outside of .page.js config files and into an API layer long before it makes sense to build an API separate from the page-rendering logic.

The .page.js files also force me to use a programming model that is wholly different from what I'm using in the rest of my app. (In my case, most of my Node.js server is written in Express, though the same argument applies for other frameworks like Hapi or Koa.)

An idealized solution

For me, the ideal interface for SSRing Vue components is just a function—let’s call it render()—that:

  • accepts a path to an unbuilt .vue source file and some additional options (such as initial props / data, a page title, or an HTML template / function),
  • returns a Promise of an HTML string that can be returned to the browser that is a complete HTML page, including any supporting resources (linked or embedded JS, CSS, etc),
  • and self-hydrates when loading in the browser.

For what it's worth, here’s the Vue SSR programming model that I would love (though I have no doubt this isn’t easy to pull off):

// In an ideal world, this file doesn't get built (it’s not a webpack entry point).
// Could be written with `require()` instead of `import` if not using `esm`.

import { render } from "@goldpage/vue";
import express from "express";
import { query } from "~/db";
import { meta, analytics } from "~/util";

const app = express();

app.get("/", async (req, res, next) => {
  
  // heavily inspired by Goldpage `.page.js` config files
  const options = {
    
    // seems reasonable that `renderToHtml = true` be the default,
    // so I shouldn't need to include it here
    renderToHtml: true,
    
    // what we really want is to pass *data* to the component, not *props*.
    // `data` is internal to the component and therefore modifiable by the component.
    // `props` are external to the component (Vue warns when modifying a prop)
    // (though I go back and forth on this. it's nice to be able to declare expected
    // `props` and have Vue do the type checking for me)
    data: {
      user: req.user,
      transactions: await query("SELECT * FROM transactions"),
    },
    
    // simple mechanism for having complete control over the final HTML output
    htmlTemplate: (result) => `
      <!DOCTYPE html>
      <html lang="en" class="loading">
        <head>
          <title>Transactions</title>
          ${ meta }
          ${ result.head }
        </head>
        <body id="transactions">
          ${ result.body }
          ${ analytics }
        </body>
      </html>
    `,

  };
  
  const html = await render("./index.vue", options);
  res.send(html);
});

app.listen(8000);

Some naïve thoughts on how this might possibly work

The code snippet I've written above somewhat forces the following assumptions:

  • I have complete control of routing via my Express configuration, and Goldpage has no idea of the mapping from routes to .vue files
  • just-in-time compilation of .vue files
  • caching of compiled .vue files so we don't pay the compilation cost for every call to render()
  • some way to precompile (and cache) all .vue files to there's no compilation in production
  • internally, the JIT compiler could receive a path to a .vue file and return a promise of a (compiled) Vue instance
  • internally, Goldpage could generate the server and client entry points for each .vue file
  • the returned Vue instance can be passed propsData (or set its data attribute, not clear which is the better pattern) and then be rendered as an HTML string (plus supporting resources)
  • this string can then be embedded in a larger HTML string that I ideally can control

I don't at all presume you should take this approach, just offering some thoughts in case it triggers some useful insights. Given Goldpage's desire to be framework agnostic, I also don't know how well these ideas translate to other frameworks like React.

@brillout
Copy link
Owner Author

I hugely like the simplicity of having such render function.

It comes with couple of problems but I'll try to find a way.

This is super interesting, thanks for the inspiration.

I keep you updated.

@brillout
Copy link
Owner Author

Btw about data vs props: I find props and the following to be more idiomatic.

Vue.component('transactionsView', {
    props: ['transactions'],
    data: function () {
        return {
            mutableTransactions: this.transactions.slice(),
        }
    }
});

props are external to the component

The props do come from outside the component since they are provided by Goldpage.

@chriscalo
Copy link

chriscalo commented Oct 10, 2019

This is super interesting, thanks for the inspiration.

So great to hear 🙏 Really excited to hear if there's a way to make that work.

If something like this turns out to be doable, it could be especially exciting for Express users if Goldpage could offer an Express template engine for Vue components.

import express from "express";
import { query } from "~/db";

const app = express();

app.set("view engine", "vue");

app.get("/", function (req, res) {
  // not sure where to set other options (page title, attributes on `<html>` or
  // `<body>` elements, custom `<meta>` tags, analytics `<script>`s, etc)
  const props = {
    user: req.user,
    transactions: await query("SELECT * FROM transactions"),
  };
  res.render("index", props);
});

(Though, that said, I tend to use template libs directly and not as Express template engines because that gives me more control when something doesn't work how I want it to.)

Btw about data vs props: I find props and the following to be more idiomatic.

Thanks for sharing the code snippet. Can definitely follow that pattern in the pages I write. 👍

@brillout
Copy link
Owner Author

👍

Let's see about templates if/once we get this render design implemented.

In case you're curious; I'm currently looking into how to implement SSR with Parcel.

@chriscalo
Copy link

That's exciting. I do love the no-config spirit of Parcel.

@chriscalo
Copy link

chriscalo commented Oct 18, 2019

Just found Servue, which seems to do something very similar:
https://futureaus.github.io/servue/

The promise is great. Haven't tried it yet, so I just don't know how well it delivers on that promise.

In particular, this seems really promising:

Supports server-side Rendering
Supports head management
No build or compile files, all in-memory
Supports custom [HTML] templating

@brillout
Copy link
Owner Author

Oh cool - I'll have a look at it. Thanks!

How did you find this library? And how did you find Goldpage btw.?

@chriscalo
Copy link

I think I found both Goldpage and Servue searching for possible options for server-rendering Vue components in Node.js.

I think I learned about Goldpage after arriving at https://github.com/brillout/awesome-universal-rendering.

I may have learned about Servue by way of an email newsletter than mentioned Ream and then finding https://vue-community.org/guide/ecosystem/server-side-rendering.html#available-options-for-vue.

@brillout
Copy link
Owner Author

Thanks!

@AlbertMarashi
Copy link

AlbertMarashi commented Nov 16, 2019

I would like to raise a point.

Vue code should be universal, and you shouldn't ever need server-side libraries in a vue app, I would consider that an anti-pattern since vue is for the client side

In my experience, i've never had any issues building universal code. If you need to access window and document, you can do so in mounted(), otherwise you can rely on the render functions and templates to do what you need to do

What you need is basically done by servue, which does it's job perfectly fine. However, there are some issues that might arise in big apps.

When a page is rendered, a bundle is created for every page, which means every dependency included in that bundle, which is stored in memory - this means high memory usage for larger apps

Compared to what I am currently building, my webpack bundler creates a full app with vue-router for client-side routing, with lazy-loading supported, however all files created are stored in a dist folder instead of in-memory, and accessible to the client via a static dist folder (webpack magic).

It's also better because code-splitting works and different pages can refer to the same dist files, and it supports hot-reloading

It's much faster, but it means that build files are generated in a folder. Although, it is possible to still store this type of app in a memory file system, less than that of servue (since pages can share code-split dependencies and lazy-loaded modules)

image
Paired with http2, i've reached some pretty good lighthouse stats (node, koa, koa-router, vue, vue-router, webpack)

@brillout
Copy link
Owner Author

Vue code should be universal, and you shouldn't ever need server-side libraries in a vue app, I would consider that an anti-pattern since vue is for the client side

That's the currently the case with Goldpage. Vue and Goldpage are independent of each other.

@brillout brillout added the enhancement New feature or request label Oct 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants