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
andCSS
- 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
exposescreateApp()
method toServer entry
andClient entry
respectively, so that each request will generate a newapp
instance- And
Server entry
andClient entry
will be packaged bywebpack
intovue-ssr-server-bundle.json
andvue-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 thevue
project (that is, mentioned above) - Server-side rendering of
react
project usingnode
+React renderToStaticMarkup/renderToString
- Use a template engine to implement
ssr
(such asejs
,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 buildCSS
- 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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。