3
头图

Recently, both in the company and in my own research projects, I have been exploring the H5 page server-side rendering, so this article discusses the necessity of server-side rendering and the principles behind it.

Let's look at a few questions

Why is H5 of To C suitable for SSR?

Typical features of the To C marketing H5 page of are:

  • high flow
  • The interaction is relatively simple (especially the activity page built by the building platform)
  • There are generally relatively high requirements for the first screen of the page

So why is it inappropriate to use the traditional CSR rendering method at this time?

After reading the following section, maybe you have the answer

Why is server-side rendering faster than client-side rendering?

Let's compare the DOM rendering process of the two.

Image credit The Benefits of Server Side Rendering Over Client Side Rendering

client-side rendering

server-side rendering

For client-side rendering, you need to first get an empty HTML page (the page has entered a white screen at this time), and then you need to go through:

  • Request and resolve JavaScript and CSS
  • Request backend server to get data
  • Render pages based on data

It takes a few processes to see the final page.

Especially in complex applications, since the JavaScript script needs to be loaded, the more complex the application, the more and larger the JavaScript script needs to be loaded, which will cause the first screen of the application to take a long time to load, which will affect the user experience. .

Compared with client-side rendering, server-side rendering after the user sends a page url request, the html string returned by the application server is fully calculated and can be handed over to the browser for direct rendering, so that the rendering of DOM is no longer affected by static resources and ajax limit.

What are the server-side rendering limitations?

But is server-side rendering really that good?

Actually, neither.

In order to achieve server-side rendering, the application code needs to be compatible with both server-side and client-side operating conditions, and the requirements for third-party libraries are relatively high. If you want to call third-party libraries directly in the Node rendering process, the library must support the server-side render. The corresponding code complexity is greatly improved.

Since the server has increased the demand for rendering HTML , the nodejs service that originally only needs to output static resource files has added CPU for data acquisition and IO for rendering HTML . Corresponding caching strategy and preparation for corresponding server load.

There are also higher requirements for construction and deployment. 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 operating environment.

Vue SSR principle

After talking so much, you may not be very clear about the principle of server-side rendering. Let me briefly describe the principle by taking Vue server-side rendering as an example:

This image is from Vue SSR guide

Principle analysis reference How to build a highly available server-side rendering project

Source is our source code area, that is, the project code.

Universal Appliation Code fully consistent with our usual client rendering code form of organization, because the rendering process in Node end, so there is no DOM and BOM objects, so do not beforeCreate and created do relate to the life cycle of a hook in DOM and BOM operations.

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

  • app.js exposes createApp() method to Server entry and Client entry respectively, so that each request will generate a new app instance
  • And Server entry and Client entry will be packaged by webpack into vue-ssr-server-bundle.json and vue-ssr-client-manifest.json

Node terminal will generate renderer instance by calling webpack according to the packaged createBundleRenderer vue-ssr-server-bundle.json , and then generate the complete html string by calling renderer.renderToString .

Node end of the render good html string is returned to the Browser , while Node terminal according vue-ssr-client-manifest.json generated js will then html string hydrate , complete client to activate html , so that the interactive page.

Write a demo to implement SSR

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

  • Server-side rendering scheme using next.js / nuxt.js
  • Use node + vue-server-renderer to achieve server-side rendering of the vue project (that is, mentioned above)
  • Server-side rendering of react project using node + React renderToStaticMarkup/renderToString
  • Use a template engine to implement ssr (such as ejs , jade , pug , etc.)

The project to be transformed recently happened to be developed by Vue , and it is also considered to be transformed into server-side rendering based on vue-server-renderer . Based on the principle of the above analysis, I built a minimal vue-ssr step by step from scratch. You can use it directly if you need it~

Here are a few things to note:

Singleton pattern does not exist using SSR

We know that 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 every incoming request. Therefore, a new instance of Vue will be created every time a user requests, which is also to avoid the occurrence of cross-request status pollution.

Therefore, instead of creating an application instance directly, we should expose a factory function that can be executed repeatedly, creating a new application instance 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 };
};

Server-side code construction

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

  • No need to compile CSS , server-side rendering will automatically build CSS
  • Build target is nodejs environment
  • No need for code cutting, nodejs Loading all code into memory at one time is more conducive to running efficiency
// vue.config.js
// 两个插件分别负责打包客户端和服务端
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");
// 根据传⼊环境变量决定⼊⼝⽂件和相应配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
  css: {
    extract: false,
  },
  outputDir: "./dist/" + target,
  configureWebpack: () => ({
    // 将 entry 指向应⽤程序的 server / client ⽂件
    entry: `./src/${target}-entry.js`,
    // 对 bundle renderer 提供 source map ⽀持
    devtool: "source-map",
    // target设置为node使webpack以Node适⽤的⽅式处理动态导⼊,
    // 并且还会在编译Vue组件时告知`vue-loader`输出⾯向服务器代码。
    target: TARGET_NODE ? "node" : "web",
    // 是否模拟node全局变量
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此处使⽤Node⻛格导出模块
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined,
    },
    externals: TARGET_NODE
      ? nodeExternals({
          allowlist: [/\.css$/],
        })
      : undefined,
    optimization: {
      splitChunks: undefined,
    },
    // 这是将服务器的整个输出构建为单个 JSON ⽂件的插件。
    // 服务端默认⽂件名为 `vue-ssr-server-bundle.json`
    // 客户端默认⽂件名为 `vue-ssr-client-manifest.json`。
    plugins: [
      TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
    ],
  }),
  chainWebpack: (config) => {
    // cli4项⽬添加
    if (TARGET_NODE) {
      config.optimization.delete("splitChunks");
    }

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

Handling CSS

For normal server-side routing, we might write:

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

But after packaging like this, start server you will find that the style does not take effect. We need to solve this problem by 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";
  }
});

handle events

The reason why the event did not take effect is because we did not activate the client, that is, mount the clientBundle.js packaged by the client to HTML .

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

<template>
  <!-- 客户端激活 -->
  <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>

Then, server-plugin and vue-ssr-client-manifest.json files are generated respectively through vue-server-renderer and client-plugin in vue-ssr-server-bundle.json , that is, server mapping and client mapping.

Finally, make the following association in 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, // 推荐
  template,
  clientManifest,
});

This completes the client activation operation and supports css and events.

Data Model Sharing and State Synchronization

Before server-side rendering generates html , we need to obtain and parse the dependent data in advance. At the same time, before the client is mounted (mounted), it is necessary to obtain the data that is completely consistent with the server, otherwise the client will fail to mix in due to inconsistent data.

To solve this problem, prefetched data is stored in a state manager (store) to ensure data consistency.

The first is to create an instance of store for use by both the client and the server:

// 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;
};

Add createStore to createApp , and inject store into the vue instance, so that all Vue components can get the store instance:

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 };
};

Use store in the page:

// src/components/Foo.vue
<template>
  <div>
    Foo
    <button @click="clickMe">点击</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("测试点击");
    },
  },
};
</script>

If you have used nuxt , you must know that there is a hook called asyncData in nuxt . We can initiate some requests in this hook, and these requests are sent from the server.

Then let's see how to implement asyncData . In server-entry.js , we get all the components that match the current route through const matchs = router.getMatchedComponents() , that is, we can get the asyncData method of all components:

// src/server-entry.js
// 服务端渲染只需将渲染的实例导出即可
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中方法会改变store中的state
          // 把vuex的状态挂载到上下文中
          context.state = store.state;
          resolve(app);
        })
        .catch(reject);
    }, reject);
  });
};

Through Promise.all we can execute asyncData in all matched components, and then modify store on the server. And also synchronize the latest store of the server to store of the client.

Client activation status data

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

It can be seen that the state has been serialized into window.__INITIAL_STATE__ , all we need to do is to synchronize this window.__INITIAL_STATE__ to the client's store before rendering on the client side, and modify client-entry.js below:

// 客户端渲染手动挂载到 dom 元素上
import createApp from "./main";
const { app, router, store } = createApp();

// 浏览器执行时需要将服务端的最新store状态替换掉客户端的store
if (window.__INITIAL_STATE__) {
  // 激活状态数据
  store.replaceState(window.__INITIAL_STATE__);
}

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

By using the store function of replaceState , window.__INITIAL_STATE__ is synchronized to the inside of store to complete the state synchronization of the data model.


前端森林
2.4k 声望13.2k 粉丝