Recently, both in the company and my own research projects, have been engaged in the exploration of H5 page server rendering, so this article will discuss the necessity of server rendering and the principle behind it.

Let’s start with a couple of questions

Why is H5 of To C suitable for SSR

To C’s marketing H5 page typically features:

  • Large flow
  • The interaction is relatively simple (especially the activity page built by the build platform)
  • The first screen of a page generally has high requirements

So why isn’t traditional CSR rendering appropriate at this point?

Read the next section and you may have the answer

Why is server rendering faster than client rendering?

Let’s compare the DOM rendering process for both.

The Benefits of Server Side Rendering Over Client Side Rendering

Client-side rendering

Server side rendering

For client rendering, you need to get an empty HTML page (at this point the page has gone white) and then you need to go through:

  • Request and parseJavaScriptandCSS
  • Request the back-end server to get the data
  • Render the page based on the data

It takes several steps to see the final page.

Especially in complex applications, JavaScript scripts need to be loaded. The more complex the application is, the more and larger the JavaScript scripts need to be loaded. As a result, the loading time of the first screen of the application is very long, which affects the user experience.

Compared with the client side rendering, after the user sends a page URL request, the HTML string returned by the application server is fully calculated and can be directly rendered by the browser, so that DOM rendering is no longer limited by static resources and Ajax.

What are the server-side rendering restrictions?

But is server-side rendering really that good?

Well, no.

In order to achieve server-side rendering, the application code needs to be compatible with both server-side and client-side running conditions, which requires high requirements on third-party libraries. If you want to call third-party libraries directly during Node rendering, the library must support server-side rendering. The corresponding code complexity has increased considerably.

As the server has increased the demand for rendering HTML, nodeJS service, which only needs to output static resource files, has increased IO for data acquisition and CPU for rendering HTML. If the traffic increases sharply, the server may break down. Therefore, appropriate cache policies and server loads need to be prepared.

The previous SPA application can be directly deployed on the static file server, while the server rendering application needs to be in the Node.js server running environment.

Vue SSR principle

In case you are not quite clear about the principles of server-side rendering, I will use the followingVueServer rendering is an example of how this works:

This is from the Vue SSR guide

How to build a highly available server rendering project

Source is our Source code area, the project code.

The Universal Appliation Code is exactly the same as our usual client-side rendering Code organization. Because the rendering process is on the Node side, there are no DOM and BOM objects. So don’t do DOM and BOM operations in the beforeCreate and Created lifecycle hooks.

The main functions of app.js, Server Entry and Client entry are as follows:

  • app.jsRespectively toServer entryClient entryexposedcreateApp()Method so that a new one is generated for each requestappThe instance
  • whileServer entryandClient entryWill be respectivelywebpackPackaged invue-ssr-server-bundle.jsonandvue-ssr-client-manifest.json

The Node side generates the renderer instance by calling createBundleRenderer based on the vue-ssR-server-bundle. json package. Renderer. RenderToString is then called to generate the complete HTML string.

The Node side returns the render HTML string to Browser, and the JS generated by the Node side based on vue-SSR -client-manifest.json and the HTML string hydrate completes the client-side ACTIVATION of HTML and makes the page interactive.

Write a demo to implement SSR

We know that there are several ways to implement server-side rendering in the market:

  • usenext.js/nuxt.jsServer side rendering scheme
  • usenode+vue-server-rendererimplementationvueServer-side rendering of the project (as mentioned above)
  • usenode+React renderToStaticMarkup/renderToStringimplementationreactServer-side rendering of the project
  • Use a template engine to do thisssr(e.g.,ejs.jade.pugEtc.)

The latest project to be modified happened to be developed by Vue and is currently being considered for server-side rendering based on vue-server-renderer. Based on the above analysis principle, I built a minimum VUE-SSR step by step from zero, we can directly take the need to use ~

Here are a few things to note:

useSSRThere is no singleton pattern

We know that the Node.js server is a long-running process. When our code enters the process, it takes a value and keeps it in memory. This means that if you create a singleton object, it will be shared between each incoming request. So a new Vue instance is created for each user request, again to avoid cross-request state contamination.

Therefore, instead of creating an application instance directly, we should expose a factory function that can be executed repeatedly to create new application instances for each request:

// main.js
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
import createStore from "./store";

export default() = > {const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: (h) = > h(App),
  });
  return { app, router, store };
};
Copy the code

Server-side code builds

The difference between server-side code and client-side code builds is:

  • You don’t need to compileCSS, the server side rendering will automaticallyCSSbuilt-in
  • The construction objective isnodejsThe environment
  • You don’t have to cut code,nodejsIt’s more efficient to load all the code into memory at once
// vue.config.js
// Two plug-ins are responsible for packaging the client and server, respectively
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");
// Determine the entry file and corresponding configuration items based on the incoming environment variables
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
  css: {
    extract: false,},outputDir: "./dist/" + target,
  configureWebpack: () = > ({
    // Point entry to the application's server/client file
    entry: `./src/${target}-entry.js`.// Provide source map support for the bundle renderer
    devtool: "source-map".// Target is set to Node so that webPack handles dynamic imports in the same way node does,
    // It also tells' vue-loader 'to output server-oriented code when compiling Vue components.
    target: TARGET_NODE ? "node" : "web".// Whether to emulate node global variables
    node: TARGET_NODE ? undefined : false.output: {
      // Use Node style to export modules here
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined,},externals: TARGET_NODE
      ? nodeExternals({
          allowlist: [/\.css$/]}) :undefined.optimization: {
      splitChunks: undefined,},// This is a plug-in that builds the entire output of the server into a single JSON file.
    // The default file name on the server is' vue-ssr-server-bundle.json '
    // The default client file name is' vue-ssr-client-manifest.json '.
    plugins: [
      TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
    ],
  }),
  chainWebpack: (config) = > {
    // Cli4 project added
    if (TARGET_NODE) {
      config.optimization.delete("splitChunks");
    }

    config.module
      .rule("vue")
      .use("vue-loader")
      .tap((options) = > {
        merge(options, {
          optimizeSSR: false}); }); }};Copy the code

With CSS

For a normal server route we might write:

router.get("/".async (ctx) => {
  ctx.body = await render.renderToString();
});
Copy the code

But once you’ve done this, start the server and you’ll find that the style doesn’t work. We need to solve this problem in the form of promise:

pp.use(async (ctx) => {
  try {
    ctx.body = await new Promise((resolve, reject) = > {
      render.renderToString({ url: ctx.url }, (err, data) = > {
        console.log("data", data);
        if (err) reject(err);
        resolve(data);
      });
    });
  } catch (error) {
    ctx.body = "404"; }});Copy the code

Handle events

The event did not take effect because we did not perform the client activation operation, which is to mount the client bundled clientbundle.js to the HTML.

First we need to add the App id to the root of app. vue:

<template> <! -- Client activation --><div id="app">
    <router-link to="/">foo</router-link>
    <router-link to="/bar">bar</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
import Bar from "./components/Bar.vue";
import Foo from "./components/Foo.vue";
export default {
  components: {
    Bar,
    Foo,
  },
};
</script>

Copy the code

Then, vue-ssr-server-bundle.json and vue-SSR -client-manifest.json files are generated by server-plugin and client-plugin in vue-server-renderer respectively. That is, server-side mapping and client-side mapping.

Finally, do the following association with the Node service:

const ServerBundle = require("./dist/server/vue-ssr-server-bundle.json");

const template = fs.readFileSync("./public/index.html"."utf8");
const clientManifest = require("./dist/client/vue-ssr-client-manifest.json");
const render = VueServerRender.createBundleRenderer(ServerBundle, {
  runInNewContext: false./ / recommend
  template,
  clientManifest,
});
Copy the code

This completes the client activation, which supports CSS and events.

Data model sharing and state synchronization

Before the server renders the HTML, we need to pre-fetch and parse the dependent data. Before mounting a client to a Mounted server, ensure that data on the client is the same as that on the server. Otherwise, the client may fail to be mounted due to data inconsistency.

To solve this problem, pre-acquired data is stored in a state manager (Store) to ensure data consistency.

The first step is to create a store instance that can be used by both clients and servers:

// src/store.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default() = > {const store = new Vuex.Store({
    state: {
      name: "",},mutations: {
      changeName(state) {
        state.name = "cosen"; }},actions: {
      changeName({ commit }) {
        return new Promise((resolve, reject) = > {
          setTimeout(() = > {
            commit("changeName");
            resolve();
          }, 1000); }); ,}}});return store;
};
Copy the code

Add createStore to createApp and inject store into vue instance to make store instance available to all VUE components:

import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
+ import createStore from "./store";

export default() = > {const router = createRouter();
+  const store = createStore();
  const app = new Vue({
    router,
+    store,
    render: (h) = > h(App),
  });
+  return { app, router, store };
};

Copy the code

Using store in a page:

// src/components/Foo.vue
<template>
  <div>
    Foo
    <button @click="clickMe">Click on the</button>
    {{ this.$store.state.name }}
  </div>
</template>
<script>
export default {
  mounted() {
    this.$store.dispatch("changeName");
  },
  asyncData({ store, route }) {
    return store.dispatch("changeName");
  },
  methods: {
    clickMe() {
      alert("Test click"); ,}}};</script>

Copy the code

For those of you who have used NUxt, there is a hook in Nuxt called asyncData where you can make requests that are made on the server side.

That we’ll look at how to implement asyncData, on server – entry. In js, we through the const matchs = router. GetMatchedComponents () to obtain all the components to match the current routing, This is the asyncData method that we get for all components:

// src/server-entry.js
// Server rendering only needs to export the rendered instance
import createApp from "./main";
export default (context) => {
  const { url } = context;
  return new Promise((resolve, reject) = > {
    console.log("url", url);
    // if (url.endsWith(".js")) {
    // resolve(app);
    // return;
    // }
    const { app, router, store } = createApp();
    router.push(url);
    router.onReady(() = > {
      const matchComponents = router.getMatchedComponents();
      console.log("matchComponents", matchComponents);
      if(! matchComponents.length) { reject({code: 404 });
      }
      // resolve(app);

      Promise.all(
        matchComponents.map((component) = > {
          if (component.asyncData) {
            return component.asyncData({
              store,
              route: router.currentRoute,
            });
          }
        })
      )
        .then(() = > {
          // promise. all Will change the state in store
          // Mount the vuex state into the context
          context.state = store.state;
          resolve(app);
        })
        .catch(reject);
    }, reject);
  });
};
Copy the code

With promise.all we can make asyncData execute in all matching components and then modify the store on the server side. It also synchronizes the latest store on the server to the store on the client.

Client activation status data

After storing state into context in the previous step, context.state will be serialized to window.__initial_state__ when the server renders the HTML, that is, when the template is rendered:

As you can see, the state has been serialized to window.__initial_state__. All we need to do is to synchronize this window.__initial_state__ to the client store before rendering it to the client. Client-entry.js:

// The client render is manually mounted to the DOM element
import createApp from "./main";
const { app, router, store } = createApp();

// The browser needs to replace the latest store state on the server side with the store on the client side
if (window.__INITIAL_STATE__) {
  // Activate status data
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() = > {
  app.$mount("#app".true);
});
Copy the code

The state synchronization of the data model is accomplished by synchronizing window.__initial_state__ inside the store using the store’s replaceState function.