With the birth and evolution of major front-end frameworks,SPA
At first it became popular. The advantage of single-page application is that it can be used without reloading the whole pageajax
And the whole system is realizedWeb
The application refused to update, bringing the ultimate user experience. However, for needsSEO
, the pursuit of extreme first-screen performance applications, front-end renderingSPA
It’s bad. luckilyVue 2.0
After that, the server-side rendering was supported. It took two or three weeks for the incident to be scattered and scattered. Through the renovation of the existing project, the practice in the existing project was basically completed.Vue
Server rendering.
AboutVue server renderingThe principle, construction and official documents have been described in more detail. Therefore, this article is not a copy of the document, but a supplement to the document. In particular, it still takes a lot of effort to combine well with the existing projects. This paper mainly focuses on my projectVue
The transformation process of server-side rendering is described, and some personal understanding is added as sharing and learning.
summarize
This article is mainly divided into the following aspects:
- What is server-side rendering? What is the principle of server-side rendering?
-
How is it based on
Koa
TheWeb Server Frame
On the configuration server rendering? -
- Basic usage
-
Webpack
Configuration
-
Construction of development environment
- Rendering middleware configuration
-
How to reform the existing project?
- Reform of the basic catalogue;
-
For service
vue-router
Split code;- Pre-pulling data at the server;
- The client hosts the global state;
- Solutions to common problems;
What is server-side rendering? What is the principle of server-side rendering?
Vue.js
Is the framework for building client applications. By default, it can be output in the browser.Vue
Component to generateDOM
And operationDOM
. However, the same component can also be rendered as server-sideHTML
String, send them directly to the browser, and finally “activate” these static tags as fully interactive applications on the client.
The above passage comes fromVue server rendering documentThe explanation, in popular terms, can probably be understood as follows:
- The purpose of server-side rendering is: performance advantage. Generating a corresponding
HTML
String, the client receives the correspondingHTML
String that can be rendered immediatelyDOM
The most efficient first screen takes time. In addition, because the server directly generates the correspondingHTML
String, rightSEO
Also very friendly; - The essence of server-side rendering is to generate “snapshots” of applications. will
Vue
And that correspond library runs on the serv, at this time,Web Server Frame
In fact, it acts as a proxy server to access the interface server to pre-pull the data, thus taking the pulled data asVue
The initial state of the component. - The principle of server-side rendering is: virtual
DOM
. InWeb Server Frame
After accessing the interface server as a proxy server to pre-pull the data, this is the data that the server needs to initialize the component. After that, the component’sbeforeCreate
Andcreated
The life cycle will be called by the server and the corresponding components will be initialized.Vue
Enable virtualizationDOM
Forming an initializedHTML
String. After that, it will be hosted by the client. And realize that isomorphic application of the front end and the rear end.
How is it based onKoa
TheWeb Server Frame
On the configuration server rendering?
Basic usage
Need to useVue
Server Rendering Corresponding Libraryvue-server-renderer
Throughnpm
Installation:
npm install vue vue-server-renderer --save
The simplest is to render one firstVue
Examples:
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue');
const app = new Vue({
template: `<div>Hello World</div>`
});
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer();
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) {
throw err;
}
console.log(html);
// => <div data-server-rendered="true">Hello World</div>
});
Integration with server:
module.exports = async function(ctx) {
ctx.status = 200;
let html = '';
try {
// ...
html = await renderer.renderToString(app, ctx);
} catch (err) {
ctx.logger('Vue SSR Render error', JSON.stringify(err));
html = await ctx.getErrorPage(err); // 渲染出错的页面
}
ctx.body = html;
}
Use page templates:
When you are renderingVue
When applying the program,renderer
Generated only from applicationsHTML
Mark. In this example, we must use an extraHTML
The page wraps the container to wrap the generatedHTML
Mark.
To simplify this, you can createrenderer
Provides a page template when. Most of the time, we will put the page template in a unique file:
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
Then, we can read and transfer files toVue renderer
Medium:
const tpl = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');
const renderer = vssr.createRenderer({
template: tpl,
});
Webpack configuration
However, in actual projects, it is not as simple as the above examples, and many aspects need to be considered: routing, data prefetching, componentization, global state, etc. Therefore, server-side rendering is not only a simple template, and then added to use.vue-server-renderer
Completed, as shown in the following schematic diagram:
As shown in the schematic diagram, generallyVue
The server-side rendering project has two project entry files, namelyentry-client.js
Andentry-server.js
, one only runs on the client, one only runs on the server, throughWebpack
After packaging, two will be generatedBundle
, server-sideBundle
It will be used to use virtualization on the server side.DOM
Generate a “snapshot” of the application, the client’sBundle
It will run in the browser.
Therefore, we need twoWebpack
Configuration, named respectivelywebpack.client.config.js
Andwebpack.server.config.js
Which are used to generate clients respectivelyBundle
With the serverBundle
, named respectivelyvue-ssr-client-manifest.json
Andvue-ssr-server-bundle.json
About how to configure,Vue
Officials have relevant examplesvue-hackernews-2.0
Construction of development environment
My project usesKoa
As aWeb Server Frame
, project usekoa-webpackBuild a development environment. If it is in the product environment, it will generatevue-ssr-client-manifest.json
Andvue-ssr-server-bundle.json
A that contains the correspondingBundle
To provide client-side and server-side references, while in a development environment, it is usually stored in memory. Usememory-fs
The module reads.
const fs = require('fs')
const path = require( 'path' );
const webpack = require( 'webpack' );
const koaWpDevMiddleware = require( 'koa-webpack' );
const MFS = require('memory-fs');
const appSSR = require('./../../app.ssr.js');
let wpConfig;
let clientConfig, serverConfig;
let wpCompiler;
let clientCompiler, serverCompiler;
let clientManifest;
let bundle;
// 生成服务端bundle的webpack配置
if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) {
serverConfig = require(path.resolve(cwd, 'webpack.server.config.js'));
serverCompiler = webpack( serverConfig );
}
// 生成客户端clientManifest的webpack配置
if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) {
clientConfig = require(path.resolve(cwd, 'webpack.client.config.js'));
clientCompiler = webpack(clientConfig);
}
if (serverCompiler && clientCompiler) {
let publicPath = clientCompiler.output && clientCompiler.output.publicPath;
const koaDevMiddleware = await koaWpDevMiddleware({
compiler: clientCompiler,
devMiddleware: {
publicPath,
serverSideRender: true
},
});
app.use(koaDevMiddleware);
// 服务端渲染生成clientManifest
app.use(async (ctx, next) => {
const stats = ctx.state.webpackStats.toJson();
const assetsByChunkName = stats.assetsByChunkName;
stats.errors.forEach(err => console.error(err));
stats.warnings.forEach(err => console.warn(err));
if (stats.errors.length) {
console.error(stats.errors);
return;
}
// 生成的clientManifest放到appSSR模块,应用程序可以直接读取
let fileSystem = koaDevMiddleware.devMiddleware.fileSystem;
clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8'));
appSSR.clientManifest = clientManifest;
await next();
});
// 服务端渲染的server bundle 存储到内存里
const mfs = new MFS();
serverCompiler.outputFileSystem = mfs;
serverCompiler.watch({}, (err, stats) => {
if (err) {
throw err;
}
stats = stats.toJson();
if (stats.errors.length) {
console.error(stats.errors);
return;
}
// 生成的bundle放到appSSR模块,应用程序可以直接读取
bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8'));
appSSR.bundle = bundle;
});
}
Rendering middleware configuration
Under the product environment, after packaging the client and serverBundle
Stored asvue-ssr-client-manifest.json
Andvue-ssr-server-bundle.json
Through the file stream modulefs
Read it, but in the development environment, I created oneappSSR
Module that fires when a code change occursWebpack
Hot update,appSSR
Correspondingbundle
It will also be updated.appSSR
The module code is as follows:
let clientManifest;
let bundle;
const appSSR = {
get bundle() {
return bundle;
},
set bundle(val) {
bundle = val;
},
get clientManifest() {
return clientManifest;
},
set clientManifest(val) {
clientManifest = val;
}
};
module.exports = appSSR;
By introductionappSSR
Modules are available in the development environment.clientManifest
AndssrBundle
, the project’s rendering middleware is as follows:
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const vue = require('vue');
const vssr = require('vue-server-renderer');
const createBundleRenderer = vssr.createBundleRenderer;
const dirname = process.cwd();
const env = process.env.RUN_ENVIRONMENT;
let bundle;
let clientManifest;
if (env === 'development') {
// 开发环境下,通过appSSR模块,拿到clientManifest和ssrBundle
let appSSR = require('./../../core/app.ssr.js');
bundle = appSSR.bundle;
clientManifest = appSSR.clientManifest;
} else {
bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8'));
clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8'));
}
module.exports = async function(ctx) {
ctx.status = 200;
let html;
let context = await ctx.getTplContext();
ctx.logger('进入SSR,context为: ', JSON.stringify(context));
const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8');
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: tpl, // (可选)页面模板
clientManifest: clientManifest // (可选)客户端构建 manifest
});
ctx.logger('createBundleRenderer renderer:', JSON.stringify(renderer));
try {
html = await renderer.renderToString({
...context,
url: context.CTX.url,
});
} catch(err) {
ctx.logger('SSR renderToString 失败: ', JSON.stringify(err));
console.error(err);
}
ctx.body = html;
};
How to reform the existing project?
Basic catalog renovation
UseWebpack
To deal with server and client applications, most of the source code can be written in a common way, can useWebpack
All supported functions.
A basic project might look like this:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── frame
│ ├── app.js # 通用 entry(universal entry)
│ ├── entry-client.js # 仅运行于浏览器
│ ├── entry-server.js # 仅运行于服务器
│ └── index.vue # 项目入口组件
├── pages
├── routers
└── store
app.js
Is the “universal” of our applicationentry
」。 In a pure client application, we will create the root in this fileVue
Instance and mount it directly to theDOM
. However, for server-side rendering (SSR
), the responsibility is transferred to the pure cliententry
Documents.app.js
Simply useexport
Export onecreateApp
Functions:
import Router from '~ut/router';
import { sync } from 'vuex-router-sync';
import Vue from 'vue';
import { createStore } from './../store';
import Frame from './index.vue';
import myRouter from './../routers/myRouter';
function createVueInstance(routes, ctx) {
const router = Router({
base: '/base',
mode: 'history',
routes: [routes],
});
const store = createStore({ ctx });
// 把路由注入到vuex中
sync(store, router);
const app = new Vue({
router,
render: function(h) {
return h(Frame);
},
store,
});
return { app, router, store };
}
module.exports = function createApp(ctx) {
return createVueInstance(myRouter, ctx);
}
Note: In my project, it is necessary to dynamically judge whether registration is required.
DicomView
, initialized only at the clientDicomView
Due toNode.js
No environmentwindow
Object, for the judgment of the code running environment, can passtypeof window === 'undefined'
To judge.
Avoid creating a single instance
Such asVue SSR
The document states:
When writing client-only code, we are used to taking values in the new context each time. However, the Node.js server is a long-running process. When our code enters the process, it will take a value and keep it in memory. This means that if a singleton object is created, it will be shared between each incoming request. As shown in the basic example, we create a new root Vue instance for each request. This is similar to the instance where each user uses a new application in their browser. If we use a shared instance among multiple requests, it is easy to cause cross-request state pollution. Therefore, instead of directly creating an application instance, we should expose a factory function that can be repeatedly executed to create a new application instance for each request. The same rules apply to router, store, and event bus instances. Instead of exporting directly from the module and importing it into the application, you need to create a new instance in createApp and inject it from the root Vue instance.
As described in the above code,createApp
The method is created by returning a return valueVue
Instance of the object’s function call, in the functioncreateVueInstance
, created for each requestVue
,Vue Router
,Vuex
Examples. And exposed toentry-client
Andentry-server
Modules.
On the client sideentry-client.js
Just create the application and mount it to theDOM
Medium:
import { createApp } from './app';
// 客户端特定引导逻辑……
const { app } = createApp();
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app');
Server sideentry-server.js
Usedefault export
Export the function and call it repeatedly in each rendering. At this point, it does not do much other than create and return application instances-but later we will perform server-side route matching and data prefetching logic here:
import { createApp } from './app';
export default context => {
const { app } = createApp();
return app;
}
For servicevue-router
Split code
AndVue
Instance, also need to create a single instance of thevueRouter
Object. For each request, a new one needs to be createdvueRouter
Examples:
function createVueInstance(routes, ctx) {
const router = Router({
base: '/base',
mode: 'history',
routes: [routes],
});
const store = createStore({ ctx });
// 把路由注入到vuex中
sync(store, router);
const app = new Vue({
router,
render: function(h) {
return h(Frame);
},
store,
});
return { app, router, store };
}
At the same time, there is a need toentry-server.js
To implement server-side routing logic in, use therouter.getMatchedComponents
The method gets the component that the current route matches, and if the current route does not match the corresponding component, thenreject
to404
Page, otherwiseresolve
The wholeapp
ForVue
Rendering virtualDOM
And use that corresponding template to generate the correspondHTML
String.
const createApp = require('./app');
module.exports = context => {
return new Promise((resolve, reject) => {
// ...
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject('匹配不到的路由,执行 reject 函数,并返回 404');
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app);
}, reject);
});
}
Pre-pulling data at the server
InVue
Server-side rendering is essentially rendering a “snapshot” of our application, so if the application depends on some asynchronous data, it needs to prefetch and parse the data before starting the rendering process. Server sideWeb Server Frame
As a proxy server, the server initiates a request to the interface service and assembles the data to the globalVuex
In status.
Another issue that needs to be paid attention to is that the client needs to obtain exactly the same data as the server-side application before mounting the client-side application-otherwise, the client-side application will use a different state from the server-side application, and then the mixing will fail.
At present, the better solution is to give one level sub-component for route matching.asyncData
InasyncData
In the method,dispatch
Correspondingaction
.asyncData
Is our agreed function name, which means that the rendering component needs to execute it in advance to obtain initial data, and it returns a.Promise
So that we can know when the operation is completed when we render at the back end. Note that this function cannot be accessed because it is called before the component is instantiatedthis
. Need tostore
And routing information are passed in as parameters:
For example:
<!-- Lung.vue -->
<template>
<div></div>
</template>
<script>
export default {
// ...
async asyncData({ store, route }) {
return Promise.all([
store.dispatch('getA'),
store.dispatch('myModule/getB', { root:true }),
store.dispatch('myModule/getC', { root:true }),
store.dispatch('myModule/getD', { root:true }),
]);
},
// ...
}
</script>
Inentry-server.js
In, we can obtain and through routingrouter.getMatchedComponents()
Matching components, if the components are exposedasyncData
, we call this method. Then we need to attach the state of parsing to the rendering context.
const createApp = require('./app');
module.exports = context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp(context);
// 针对没有Vue router 的Vue实例,在项目中为列表页,直接resolve app
if (!router) {
resolve(app);
}
// 设置服务器端 router 的位置
router.push(context.url.replace('/base', ''));
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject('匹配不到的路由,执行 reject 函数,并返回 404');
}
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
})).then(() => {
// 在所有预取钩子(preFetch hook) resolve 后,
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state;
resolve(app);
}).catch(reject);
}, reject);
});
}
Client Managed Global State
When the server uses templates for rendering,context.state
Will act aswindow.__INITIAL_STATE__
Status, automatically embedded into finalHTML
China. On the client side, before the application is mounted,store
We should get the status, and eventually ourentry-client.js
It has been modified as follows:
import createApp from './app';
const { app, router, store } = createApp();
// 客户端把初始化的store替换为window.__INITIAL_STATE__
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
if (router) {
router.onReady(() => {
app.$mount('#app')
});
} else {
app.$mount('#app');
}
Solutions to Common Problems
At this point, the basic code transformation has been completed. The following are solutions to some common problems:
- Not on the server side
window
、location
Object:
For old projects migrated toSSR
The problems you will definitely experience are usually at the entrance of the project orcreated
、beforeCreate
Life Cycle UsedDOM
Operation, or obtainedlocation
Object, the common solution is generally to judge the execution environment, throughtypeof window
Is it'undefined'
, if encounter must uselocation
Object is used to get theurl
The relevant parameters in, inctx
The corresponding parameters can also be found in the object.
-
vue-router
Report an errorUncaught TypeError: _Vue.extend is not _Vue function
, not found_Vue
Problems with instances:
By viewingVue-router
Source code found no manual callVue.use(Vue-Router);
. No callsVue.use(Vue-Router);
There are no problems on the browser side, but there will be problems on the server side. CorrespondingVue-router
Source code shows:
VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;
process.env.NODE_ENV !== 'production' && assert(
install.installed,
"not installed. Make sure to call `Vue.use(VueRouter)` " +
"before creating root instance."
);
// ...
}
- Server cannot obtain
hash
Parameters of route
Due tohash
The parameters of the route result invue-router
It doesn’t work, but it is used forvue-router
The front and rear end isomorphic application of must be replaced byhistory
Routing.
- Could not get at interface
cookie
Question of:
Because each request of the client will correspondingly sendcookie
To the interface side, while the server sideWeb Server Frame
As a proxy server, it will not be maintained every time.cookie
, so we need to manuallycookie
Through to the interface side, the common solution is toctx
Mounted in global state, manually loaded when asynchronous request is initiated.cookie
, as shown in the following code:
// createStore.js
// 在创建全局状态的函数`createStore`时,将`ctx`挂载到全局状态
export function createStore({ ctx }) {
return new Vuex.Store({
state: {
...state,
ctx,
},
getters,
actions,
mutations,
modules: {
// ...
},
plugins: debug ? [createLogger()] : [],
});
}
When an asynchronous request is initiated, manually take thecookie
Which is used in the projectAxios
:
// actions.js
// ...
const actions = {
async getUserInfo({ commit, state }) {
let requestParams = {
params: {
random: tool.createRandomString(8, true),
},
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
};
// 手动带上cookie
if (state.ctx.request.headers.cookie) {
requestParams.headers.Cookie = state.ctx.request.headers.cookie;
}
// ...
let res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);
commit(globalTypes.SET_A, {
res: res.data,
});
}
};
// ...
- Interface request times
connect ECONNREFUSED 127.0.0.1:80
The problem of
The reason is that before the transformation, when using client-side rendering, usingdevServer.proxy
The proxy configuration is used to solve the cross-domain problem, while the server, as a proxy server, will not read the corresponding asynchronous request to the interfacewebpack
Configuration, for the server will request the corresponding under the current domainpath
The interface under the.
The solution is to removewebpack
ThedevServer.proxy
Configuration, for the corresponding on the interface request tapeorigin
Just:
const requestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin;
const res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);
- For
vue-router
Configuration items arebase
Parameter, the corresponding route cannot be matched during initialization
In the official exampleentry-server.js
:
// entry-server.js
import { createApp } from './app';
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 设置服务器端 router 的位置
router.push(context.url);
// ...
});
}
The reason is to set up the server side.router
At the same time,context.url
For accessing the pageurl
And brought itbase
Inrouter.push
When should be removedbase
, as follows:
router.push(context.url.replace('/base', ''));
Summary
This article is for the author to addVue
Summary of the practical process of server-side rendering.
First of all, what is elaboratedVue
Server rendering, its purpose, essence and principle, through the use of the serverVue
Virtual ofDOM
A that forms an initializedHTML
String, which is the “snapshot” of the application. Bring great performance benefits, includingSEO
Advantages and fast experience of first screen rendering. It was then explainedVue
The basic usage of server-side rendering, i.e. two entrances, twowebpack
Configuration, acting on client and server respectively, generatesvue-ssr-client-manifest.json
Andvue-ssr-server-bundle.json
As a result of packaging. Finally, through the transformation process of the existing project, including the route transformation, data pre-acquisition and state initialization, this paper explains the current situation of the project in theVue
Common problems in server-side rendering project transformation process help us to carry out existing projectsVue
Migration of server rendering.
At the end of the article, there is an advertisement: Tencent medical department has recruited front-end engineers, there are unlimited HC, and both social and school recruit can be pushed in. If you have a small partner who wants to come to Tencent, you can add my WeChat: xingbofeng001. If you have a small partner who wants to make friends and exchange technology, you are also welcome to add my WeChat ~