brillout / goldpage

Page Builder.
Creative Commons Zero v1.0 Universal
57 stars 3 forks source link

End-to-end example for Goldpage + Express + Vue? #2

Closed chriscalo closed 4 years ago

chriscalo commented 4 years ago

This is a super-exciting repo. I tried my best to follow the docs, but I wasn't able to get things running. Is there a complete, end-to-end, working example somewhere that we can checkout and tinker with?

brillout commented 4 years ago

Is there [...} working example somewhere

I just created a Vue + Express starter: https://github.com/brillout/goldpage-vue-express-starter.

If you want more: https://github.com/reframejs/goldpage/tree/master/examples.

I tried my best to follow the docs, but I wasn't able to get things running

The docs are fresh — I'm currently working on them and they will improve a lot in the next coming days/weeks.

super-exciting

Thanks & I'm curious; what is it about Goldpage that excites you?

chriscalo commented 4 years ago

Huge thank you for putting that starter repo together ๐Ÿ™ just gave it a shot, and it seems to be working great.

Thanks & I'm curious; what is it about Goldpage that excites you?

Glad you asked.

SSR + client-side hydration is the only path to a great user experience for fast, interactive web apps. However, I don't want to author components in two different ways, once on the server and once on the client. And the solutions for SSR + client-side hydration in the Vue ecosystem I've tried way overcomplicate things.

I never could quite get Vue SSR working right, and they seem to want to force you to use their Router and Vuex, even though I'd prefer to use express's much simpler routing system. I'd rather treat Vue like any other server-side templating language: fetch data, pass that data to a render function, and return the string I get a string back to the browser. And I don't want an explicit build step or anything beyond very minimal configuration.

Nuxt.js is a huge improvement, especially because of how easily it integrates with express, but they kinda force you to pretend that fetching data from the server and the client are the same by asking you to call API endpoints from the server in their asyncData() method, which fails for me on the client when there are no Express req or res objects. It just feels simpler to fetch data server-side and worry about fetching data from an API on the client when the need arises.

Anyway, Goldpage is the first solution I've seen that hits all of these points, and it even does so without being tightly coupled to any particular ecosystem (React, Vue, express, hapi, etc). I love that.


There are a few things that I would change about Goldpage to make it better match what I'm trying to do. (Do you agree with these? Should I open issues for these or is it too early for that?)

  1. I really love using ES modules via esm in my server-side code and config files, but that doesn't seem to be possible with goldpage. Might it be possible to include a -r esm flag to require a module like node and nodemon have? Example package.json file:
{
  "scripts": {
    "start": "goldpage dev -r esm index.js"
  },
  "dependencies": {
    "esm": "^3.2.25",
    "goldpage": "^0.5.1"
  },
 }
  1. Better yet, I'd prefer to have full control of the server and run it in Node rather than via CLI. Nuxt.js supports this pretty well. I love how easy it is to integrate Nuxt with express without using the nuxt CLI:
import { Nuxt, Builder } from "nuxt";
import express from "express";
import config from "~/nuxt.config.js";

export const server = express();
export default server;

const nuxt = new Nuxt(config);

// render Nuxt routes
server.use(nuxt.render);

// hot-reloading in dev
if (config.dev) {
  new Builder(nuxt).build();
}
  1. I like structuring files in web apps in a way that matches URL paths. However, goldpage yells at me when more than one page config file has the same name. Here's how I prefer to structure things, but this doesn't work with goldpage because there are two index.page.js files (even though they're in different folders):
/ => pages/index.page.js
/foo => pages/foo/index.page.js

Thanks again for putting effort into this long-standing problem! ๐Ÿ™

brillout commented 4 years ago

Huge thank you for putting that starter repo together

Let me know how the starter works out for you.

they seem to want to force you to use their Router and Vuex

Thanks to Goldpage's domRender and htmlRender you have full control over how your pages are rendered. This means that you can easily integrate Goldpage with any Vue library you want.

even though I'd rather to use express's much simpler routing system. I'd rather treat Vue like any other server-side templating language: fetch data, pass that data to a render function, and return the string I get a string back to the browser.

The thing is that Goldpage (or any other tool that does SSR such as Nuxt.js) need to know the routes of your pages. For example, if a page has the route /hello/:name then Goldpage needs to provide the :name parameter to your page root Vue component.

And I don't want an explicit build step or anything beyond very minimal configuration.

SSR is tightly coupled with building. A specificity of SSR is that your Vue components run in the browser as well as in your Node.js server. Adding SSR to an app requires changes in the Webpack build config. At that point Goldpage might as well take care of all the build.

Nuxt.js is a huge improvement, especially because of how easily it integrates with express, but they kinda force you to pretend that fetching data from the server and the client are the same by asking you to call API endpoints from the server in their asyncData() method

Something like asyncData (Goldpage's pendant is addInitialProps) is required. That's because Nuxt.js (or Goldpage) needs to fetch all the data of your page before rendering it to HTML. Otherwise your data couldn't be rendered to HTML. (Vue renders the initial state of your Vue components to HTML. There is no such thing as stateful components when doing SSR.)

which fails for me on the client when there are no Express req or res objects.

With Goldpage you can render a page only to HTML. Your page is then not loaded nor rendered in the browser. And addInitialProps is not loaded nor called in the browser.

It just feels simpler to fetch data server-side and worry about fetching data from an API on the client when the need arises.

As far as I see, you are totally free to fetch data as you wish with Goldpage.

Anyway, Goldpage is the first solution I've seen that hits all of these points, and it even does so without being tightly coupled to any particular ecosystem (React, Vue, express, hapi, etc). I love that.

Yea, domRender & htmlRender is one of my favorite Goldpage feature. It makes so many things much simpler and much easier.

(Do you agree with these? Should I open issues for these or is it too early for that?)

Yes please let me know what you need. And yes, you can write all of it in this ticket.

I really love using ES modules via esm in my server-side code

Goldpage builds your server-side code thus you can use ES modules for your server-side code.

For example: /examples/es-modules/server.js.

Better yet, I'd prefer to have full control of the server and run it in Node rather than via CLI. Nuxt.js supports this pretty well. I love how easy it is to integrate Nuxt with express without using the nuxt CLI:

You can do that with Goldpage.

Instead of:

$ goldpage dev path/to/server.js

Omit the server path and do:

$ goldpage dev

Same for goldpage build.

Goldpage then doesn't build your server code. But note that you loose server auto-reload and you'll have to take care of auto-reloading your server yourself.

I'd recommend against going down that path; what's your issue with letting Goldpage build all your code?

Goldpage is based on Webpack but we will use Parcel once Parcel v2 is out. This will mean that you will never ever have to deal with build again; things will just work, that's the nice thing about Parcel.

I like structuring files in web apps in a way that matches URL paths. However, goldpage yells at me when more than one page config file has the same name. Here's how I prefer to structure things, but this doesn't work with goldpage because there are two index.page.js files (even though they're in different folders):

I thought about this as well but didn't implement it yet. I opened a ticket for it: https://github.com/reframejs/goldpage/issues/3.

Thanks again for putting effort into this long-standing problem!

It's fun to build Goldpage :). Let me know if you have any further questions. Happy to talk to you.

I've been very busy the last couple of days — I'll reply more promptly in the future.

chriscalo commented 4 years ago

No worries about the response delay. I wrote quite a lot there ๐Ÿ˜. Take all of this feedback only as far as it's useful for you. And to be clear, everything in the first half of my last response was trying to explain why Goldpage feels like a much better approach than other SSR + client-side hydration approaches I've tried.

Goldpage builds your server-side code thus you can use ES modules for your server-side code.

That is fantastic ๐Ÿ™ I will try this out.

Omit the server path and do:

$ goldpage dev

What does this do? I found examples/static-website/package.json, but it's not clear to me what goldpage dev does.

what's your issue with letting Goldpage build all your code?

Trust, I guess. If I can be confident that it's flexible enough to meet my future needs and is extendable when I need to do something Goldpage didn't foresee, it could work. But I'm skeptical of this because I haven't seen it work many times in the past; sooner or later, I run into some situation where I need to modify the behavior of a library to do what I need, and if I can't, I remove that library and look for replacements that are more flexible.

I guess for me it really comes down to the Do One Thing Well principle:

Here's how I'd really love to use Goldpage, inspired by Nuxt: (are there issues with this approach?)

// server.js, an express server that I completely control
import { ssr, dev } from "goldpage";
import express from "express";
import { start } from "express-start";
import config from "./goldpage.config.js";

export const server = express();
export default server;

server.use(ssr.express);

// dev build with hot-reloading
if (process.env.NODE_ENV !== "production") {
  dev(config);
}

if (process.mainModule === module) {
  start(server);
}
chriscalo commented 4 years ago

I just tried to recreate your example and was able to get it working to my liking. I see now how I'm still in full control of the server even when running goldpage dev ./server.js.

I think the only thing keeping me from really giving Goldpage a try and fully integrating it into my app is #3. I don't need to do that anytime soon, so I'll just wait to hear any updates on that issue.

Thanks again

brillout commented 4 years ago

What does this do?

goldpage dev ./path/to/your/server.js starts your server with auto-rebuild, browser auto-reload, and server auto-reload. It's meant to be used while developing.

If I can be confident that it's flexible enough to meet my future needs and is extendable when I need to do something Goldpage didn't foresee

With Parcel v2 everything will just work and you won't have to worry about build anymore. That's why I want to migrate from Webpack to Parcel. Note that Parcel will be zero-config the vast majority of cases but it will still be configurable and extendable. So your back will be covered.

if it uses webpack and it's easy to modify the config

Yes, Goldpage uses webpack and you can easily modify the webpack config. That said, once Goldpage uses Parcel v2, the vast majority of your build problems will disappear.

I see now how I'm still in full control of the server

Yes exactly, from the perspective of the server, Goldpage is just a middleware.

I think the only thing keeping me from really giving Goldpage a try and fully integrating it into my app is #3.

Done & published as 0.5.2.


Did I answer all your questions?

Please let me know if you have other questions or concerns :)

chriscalo commented 4 years ago

Done & published as 0.5.2.

Thanks so much for that update. ๐Ÿ™ It seems to be working great.

goldpage dev ./path/to/your/server.js starts your server with auto-rebuild, browser auto-reload, and server auto-reload. It's meant to be used while developing.

What I meant was: What does goldpage dev do when a server path is not included?

Yes exactly, from the perspective of the server, Goldpage is just a middleware.

It's actually the server, too, not just middleware. When I point Goldpage to my Express app, it:

  1. builds client and server bundles for my Express app and Vue pages,
  2. injects its own middleware into my Express app,
  3. starts a web server

Taking care of the build for me (1.) is clearly valuable. But, for what it's worth, I would really prefer to have more control over both how Goldpage integrates with my Express server (2.) and starting the web server (3.).

The ideal workflow (for me) would be:

  1. there's a Goldpage build() function that I call myself
  2. I can get a reference to a Goldpage Express middleware function that I integrate it into my Express app exactly where I want it
  3. I start the server myself

Because what happens if I need to pass parameters to the node process? I'm not calling node myself, so I'm not able to do that. Or what if one day I need to move from Goldpage to another solution? It's not as simple as replacing one Express middleware function with another one, and I have to start the server myself.

I'll open a couple issues with suggestions, but obviously close them if you disagree. ๐Ÿ‘

brillout commented 4 years ago

What does goldpage dev do when a server path is not included?

Goldpage [...] 3. starts a web server

Note that in production you start the server yourself, see the usual Goldpage scripts:

{
  "scripts": {
    "dev": "goldpage dev ./path/to/your/server.js",
    "prod": "npm run build && npm run start",
    "build": "goldpage build ./path/to/your/server.js",
    "start": "export NODE_ENV='production' && node ./.build/nodejs/server"
  }
}

For production you:

  1. Build your code: goldpage build ./path/to/your/server.js
  2. Then start the server yourself: export NODE_ENV='production' && node ./.build/nodejs/server

In dev, Goldpage starts the server on your behalf in order to auto-restart the sever whenever you change your server code.

I actually dislike that part myself; I'd prefer a Goldpage user to have full control over the server, even in dev. I will think about a solution.

there's a Goldpage build() function that I call myself

You can actually do that today but it's purposely not documented. I'd rather have Goldpage take care of all the build. If you a have a specific use case that require you to have more control over the build, then let me know and I will reconsider.

I would really prefer to have more control over both how Goldpage integrates with my Express server

What additional control would you want?

I'll open a couple issues with suggestions

:+1:

brillout commented 4 years ago

I found a solution which would allow you to do something like this:

{
 "scripts": {
   "dev": "npm run build-dev && npm run start-dev",
   "build-dev": "goldpage build --watch ./path/to/your/server.js",
   "start-dev": "nodemon ./.build/nodejs/server"
   "prod": "npm run build && npm run start",
   "build": "goldpage build ./path/to/your/server.js",
   "start": "export NODE_ENV='production' && node ./.build/nodejs/server"
 }
}

with https://github.com/remy/nodemon.

@chriscalo Would that be something that would interest you?

chriscalo commented 4 years ago

Thanks for pushing on this some more.

Small point: in the dev script I think you'd need to run build-dev and start-dev in parallel because build-dev will never exit when in watch mode (right?):

{
  "scripts": {
    "dev": "run-p build-dev start-dev",
    "build-dev": "goldpage build --watch ./path/to/your/server.js",
    "start-dev": "nodemon ./.build/nodejs/server",
    "prod": "npm run build && npm run start",
    "build": "goldpage build ./path/to/your/server.js",
    "start": "export NODE_ENV='production' && node ./.build/nodejs/server"
  },
  "devDependencies": {
    "npm-run-all": "^4.1.5"
  }
}

This approach still doesn't let me run my own server file and forces me to be aware of the implementation details of the build process (that built server is located at ./.build/nodejs/server).

Would it not be possible for the ssr.express handler to know to look for build artifacts inside the .build directory (so I don't have to know that)?

With one additional changeโ€”exposing a dev() function that runs the goldpage builder in watch modeโ€”that would enable the following approach (same as from an earlier comment):

import { ssr, dev } from "goldpage";
import express from "express";
import { start } from "express-start";
import config from "./goldpage.config.js";

export const server = express();
export default server;

server.use(ssr.express);

// dev build with hot-reloading
if (process.env.NODE_ENV !== "production") {
  dev(config);
}

if (process.mainModule === module) {
  start(server);
}

Here's an example of doing something similar in Nuxt:

import { Nuxt, Builder } from "nuxt";
import express from "express";
import config from "~/nuxt.config.js";

export const server = express();
export default server;

const nuxt = new Nuxt(config);

// render Nuxt routes
server.use(nuxt.render);

// hot-reloading in dev
if (config.dev) {
  new Builder(nuxt).build();
}

Thoughts on that approach?

I tried to describe this approach in #5 and #6. Hope that's helpful and not just creating more noise.

chriscalo commented 4 years ago

Goldpage now seems like a pretty good match for my app. Iโ€™ll try to convert it and let you know if I run into any issues. Thanks again for indulging all of my questions.