Skip to content

Latest commit

 

History

History
242 lines (189 loc) · 8.29 KB

ch14.md

File metadata and controls

242 lines (189 loc) · 8.29 KB

Chapter 14: Server-side rendering

This chapter needs upgrade after breaking changes in the VDOM structure

Separating client dependencies from server dependencies

In the next section, you'll be adding server-side code living in the same codebase as Hyperapp client-side code.

You've been using Snowpack to inspect all production code for imports and translating appropriate node_modules to web_modules. It leads to 2 problems:

  • you install the same dependency twice: in the node_modules and the web_modules
  • you have to exclude server code from the translation

To address those problems, you'll a Snowpack feature called webDependencies.

Change your package.json to:

{
  "type": "module",
  "scripts": {
    "snowpack": "snowpack install --exclude '**/*' --dest=src/web_modules",
    "postinstall": "npm run snowpack",
    "format": "prettier --write '**/!(web_modules)/*.js'",
    "test": "mocha test/*.test.js"
  },
  "dependencies": {
    "hyperapp-render": "3.2.0"
  },
  "webDependencies": {
    "@hyperapp/events": "0.0.4",
    "htm": "3.0.4",
    "hyperapp": "2.0.4",
    "hyperapp-fx": "2.0.0-beta.1",
    "page": "1.11.6"
  },
  "devDependencies": {
    "mocha": "7.1.2",
    "prettier": "2.0.5",
    "snowpack": "2.0.0-beta.20"
  }
}

After this change:

  • server-side dependencies come from dependencies. I already added a hyperapp-render dependency you'll be using in the next section.
  • client-side dependencies come from webDependencies. I moved all previous dependencies here.

Also, exclude all code from Snowpack import analysis (--exclude '**/*'). In our new workflow, all client-side dependencies have to be listed explicitly in webDependencies.

Test the install: npm i

Figure: Snowpack web_modules installed from webDependencies

With your new workflow, Snowpack uses its CDN to download web_modules.

Rendering view and state into a string

So far, you've been using Hyperapp to serialize Virtual DOM nodes into real DOM nodes.

However, you can also translate Hyperapp Virtual DOM to HTML string with a library called hyperapp-render.

Create Server.js in a root directory of your project (one level above src):

import render from "hyperapp-render";
import { state, view } from "./src/Posts.js";

const html = render.renderToString(view(state));

console.log(html);

Make sure your src/Posts.js exports the main view function and the initial state. hyperapp-render should serialize your view and state into the HTML string.

Run it in Node.js: node Server.js

Rendering view and state from an HTTP server

You've just seen how to turn your Hyperapp views and state into HTML. You can serve this HTML from any HTTP server.

Install a popular and minimal Node.js Web application server framework express:

  "dependencies": {
    "hyperapp-render": "3.2.0",
    "express": "4.17.1"
  },

npm i

Expose your Hyperapp posts as a resource with HTML representation. Update Server.js:

import render from "hyperapp-render";
import express from "express";
import { state, view } from "./src/Posts.js";

const app = express();

app.get("/", (req, res) => {
  const html = render.renderToString(view(state));
  res.send(html);
});
app.listen(3000, () => {
  console.log("Listening on 3000");
});

Run it: node Server.js

Open http://localhost:3000

It should return unstyled HTML with a form and empty list of posts.

Figure: Serving server-rendered Hyperapp

Fetching data on the server

In this section, you'll add a list of posts to your HTML representation.

Server and client code have different data fetching patterns. Client-side code usually fetches data in the background. For example, LoadLatestPosts effect starts when you open a browser, and fetches data in the background. When the data arrives, Hyperapp re-renders the view with newly fetched response data.

The server-side code has to wait for the data first and only then render the response. So this "wait before render" behavior is a major difference.

In this book, you won't be trying to create universal data fetching effects shared between a client and a server. In a real app, you will probably load your data directly from a database. We'll use axios to show you how to do it without complicating the setup.

  "dependencies": {
    "axios": "0.19.2",
    "express": "4.17.1",
    "hyperapp-render": "3.2.0"
  },

npm i

import render from "hyperapp-render";
import express from "express";
import {state, view, SetPosts} from "./src/Posts.js";
import axios from "axios";

const app = express();

app.get("/", async (req, res) => {
    const response = await axios.get("https://hyperapp-api.herokuapp.com/api/post");
    const posts = response.data;
    const stateWithPosts = SetPosts(state, posts);
    const html = render.renderToString(view(stateWithPosts));
    res.send(html);
});
app.listen(3000, () => {
    console.log("Listening on 3000");
});

Make sure to export the SetPosts action. Notice that you're using it the same way in your server-side code.

Open your app in a browser. You should see the rendered list of posts.

Figure: Serving server-rendered Hyperapp with data

To reiterate what you've just learned: Hyperapp can be used as a server-side template engine with hyperapp-render.

Hydrating server-side code on the client-side

Hydration is a fancy name for taking control over server-side rendered content on the client-side. For large applications, server-side rendering improves the time to visible content. However, hydration always performs worse than server-side rendering alone or client side-rendering alone when you care about time to interactivity. With hydration, you pay the rendering tax twice (HTML rendering and JS rendering/hydrating). You often have to make a tradeoff of getting visible content or interactivity faster. But, since Hyperapp is really small, you may not even notice the hydration penalty.

Move src/index.html content into Server.js and put it into a template string:

const htmlTemplate = (content) => /* HTML */ `
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>HyperPosts</title>
      <link rel="stylesheet" href="https://andybrewer.github.io/mvp/mvp.css" />
      <script type="module" src="Start.js"></script>
    </head>
    <body>
      <main>
        <div id="app">${content}</div>
      </main>
    </body>
  </html>
`;

You'll be replacing content placeholder with dynamically rendered content. Note: /* HTML */ comment is for prettier to auto-format the string as HTML.

Update Server.js:

import {layout} from "./src/Layout.js";

app.get("/", async (req, res) => {
  const response = await axios.get(
    "https://hyperapp-api.herokuapp.com/api/post"
  );
  const posts = response.data;
  const stateWithPosts = SetPosts(state, posts);
  const content = render.renderToString(layout(view)(stateWithPosts));
  res.send(htmlTemplate(content));
});
app.use(express.static("src"));

What it does: Wraps your view in a layout to get the full page. Passes a server generated content into your HTML template function. Serves static files from the src directory. Puts the static handler after GET "/" handler, so src/index.html has lower precedence than our root resource.

Run it: node Server.js

Test your posts page at http://localhost:3000 with JS enabled and disabled. (You can do that in your browser Dev Tools)

Exercise: server side-rendering and hydration

Implement server-side rendering and hydration for the login page. Test your app with JS enabled and disabled.

Solution
import { state as loginState, view as loginView } from "./src/Login.js";

app.get("/login", async (req, res) => {
  const content = render.renderToString(layout(loginView)(loginState));

  res.send(htmlTemplate(content));
});

You can expand on this exercise. Some ideas for further experiments:

  • serialize state and pass it from server to client to avoid double fetching the initial list of posts
  • build shared routing abstraction for the client and the server