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 parse
JavaScript
andCSS
- 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 followingVue
Server 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.js
Respectively toServer entry
、Client entry
exposedcreateApp()
Method so that a new one is generated for each requestapp
The instance- while
Server entry
andClient entry
Will be respectivelywebpack
Packaged invue-ssr-server-bundle.json
andvue-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:
- use
next.js
/nuxt.js
Server side rendering scheme - use
node
+vue-server-renderer
implementationvue
Server-side rendering of the project (as mentioned above) - use
node
+React renderToStaticMarkup/renderToString
implementationreact
Server-side rendering of the project - Use a template engine to do this
ssr
(e.g.,ejs
.jade
.pug
Etc.)
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:
useSSR
There 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 compile
CSS
, the server side rendering will automaticallyCSS
built-in - The construction objective is
nodejs
The environment - You don’t have to cut code,
nodejs
It’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.