I personally encountered a lot of problems when I first read the official VUE SSR documentation, it was built on the basis that you have a runnable build environment, so it directly tells the implementation of the code, but there is no runnable environment for new developers, so all the code fragments can not run. Why doesn’t the author talk about the build first and then the implementation? I think it’s probably because building and running relies heavily on specific code implementation, and building first doesn’t help to understand the overall process, so it’s not a good balance.
In this demo, we’re going to start with the build process, some of which we may need to come back to later, but try to make the whole process clear. At the same time, each step in the article will be represented in the DEMO. You can quickly locate the different stages of the DEMO with different commit ids as follows:
* e06aee792a59ffd9018aea1e3601e220c37fedbd (HEAD - > master, origin/master) optimization: Add cache * c65f08beaff1dea1eaf05d02fb30a7e8776ce289 application development: preliminary complete demo * 2 fb0d28ee6d84d2b1bdbbe419c744efdad3227de application development: Complete store definition, API to write and program synchronous * 9604 aec0de526726f4fe435385f7c2fa4009fa63 development: The first version can be run independently, without store * 7 d567e254fc9dc5a1655d2f0abbb4b8d53bccfce build configuration: Webpack configuration, server js back-end entry documentation * 969248 b64af82edd07214a621dfd19cf357d6c53 build configuration: Babel configuration * a5453fdeb20769e8c9e9ee339b624732ad14658a initialization program, completed the first run the demoCopy the code
Git reset — Hard Commitid can be used to switch between different phases to see the implementation.
What is Server-side rendering (SSR)?
Vue.js is a framework for building client applications. By default, the Vue component can be exported to the browser for DOM generation and DOM manipulation. However, it is also possible to render the same component as HTML strings on the server side, send them directly to the browser, and finally “activate” these static tags into a fully interactive application on the client side.
Server-rendered vue.js applications can also be considered “isomorphic” or “generic” because most of the application code can be run on both the server and the client.
Why server side Rendering (SSR)?
The main advantages of server-side rendering (SSR) over traditional SPA (single-page Application) are:
- Better SEO, thanks to search engine crawler crawler tools can view fully rendered pages directly.
- Faster time-to-content, especially for slow network conditions or slow-running devices.
Basic usage
Templates to install
npm install vue vue-server-renderer express –save
Create /server.js and/SRC /index.template.html
const server = require('express') ()const Vue = require('vue')
const fs = require('fs')
const Renderer = require('vue-server-renderer').createRenderer({
template:fs.readFileSync('./src/index.template.html'.'utf-8')
})
server.get(The '*'.(req, res) = > {
const app = new Vue({
data: {
name: 'vue app~'.url: req.url
},
template:'<div>hello from {{name}}, and url is: {{url}}</div>'
})
const context = {
title: 'SSR test#'
}
Renderer.renderToString(app, context, (err, html) = > {
if(err) {
console.log(err)
res.status(500).end('server error')
}
res.end(html)
})
})
server.listen(4001)
console.log('running at: http://localhost:4001');
Copy the code
From the above program, you can see that the Vue instance is compiled via vue-server-renderer and finally exported to the browser via Express.
However, it can also be seen that the output is a static pure HTML page. Since there are no javascript files loaded, there is no front-end user interaction, so the demo above is just a minimalist example. In order to implement a complete VUE SSR program, VueSSRClientPlugin(vuE-server-renderer /client-plugin) is also needed to compile the file into vuE-SSR-client-manifest. json file, JS, CSS and other files that can be run by the front-end browser. VueSSRServerPlugin(vue-server-renderer/server-plugin) compiles the file to vue-ssR-server-bundle. json that node can call
Before we can really get started, we need to understand a few concepts
Writing generic code
Constraints on “generic” code – that is, code running on the server and client will not be exactly the same when running in different environments due to differences in use cases and platform apis.
Data response on the server
Each request should be a new, separate application instance so that there is no cross-request state pollution.
Component lifecycle hook functions
Since there is no dynamic update, of all the lifecycle hook functions, only beforeCreate and Created are called during server-side rendering (SSR)
Access platform-specific apis
Generic code does not accept platform-specific apis, so if your code directly uses browser-only global variables like Window or Document, it will throw an error when executed in Node.js, and vice versa.
Build configuration
How to provide the same Vue application to both the server and client. To do this, we need to use WebPack to package the Vue application.
-
Typically Vue applications are built from Webpack and vue-loader, and many Webpack-specific functions cannot run directly in Node.js (e.g. importing files through file-loader, importing CSS through CSS-loader).
-
While the latest versions of Node.js fully support ES2015 features, we still need to translate the client code to accommodate older browsers. This also involves a build step.
So the basic idea is that we use Webpack packaging for both client and server applications – the server needs a “server bundle” for server-side rendering (SSR), and the “client bundle” is sent to the browser for mixing static markup.
Let’s look at the implementation process
Babel configuration
New /.babelrc configuration
// es6 compile to ES5 configuration
{
"presets": [["env",
{
"modules": false}]],"plugins": ["syntax-dynamic-import"]
}
npm i -D babel-loader@7 babel-core babel-plugin-syntax-dynamic-import babel-preset-env
Copy the code
Webpack configuration
Create a new build folder for webPack-related configuration files
/ ├─ build │ ├─ setup-dev-server.jsSet up the Webpack-dev-Middleware development environment│ ├ ─ ─ webpack. Base. Config. JsBase common configuration│ ├ ─ ─ webpack. Client. Config. Js# Compile vue-ssr-client-manifest.json file and JS, CSS and other files for the browser to call│ └ ─ ─ webpack. Server config. JsVue - SSR -server-bundle.json for nodeJS call
Copy the code
Install the relevant packages first
Install webPack-related packages
npm i -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware webpack-merge webpack-node-externals
Install build dependent packages
npm i -D chokidar cross-env friendly-errors-webpack-plugin memory-fs rimraf vue-loader
Let’s look at the details of each file:
webpack.base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
context: path.resolve(__dirname, '.. / '),
devtool: isProd ? 'source-map' : '#cheap-module-source-map'.output: {
path: path.resolve(__dirname, '.. /dist'),
publicPath: '/dist/'.filename: '[name].[chunkhash].js'
},
resolve: {
// ...
},
module: {
rules: [{test: /\.vue$/,
loader: 'vue-loader'.options: {
compilerOptions: {
preserveWhitespace: false}}}// ...]},plugins: [new VueLoaderPlugin()]
}
Copy the code
Webpack.base.config.js This is the general configuration, which is basically the same as our previous SPA development configuration.
webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const config = merge(base, {
mode: 'development'.entry: {
app: './src/entry-client.js'
},
resolve: {},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(
process.env.NODE_ENV || 'development'
),
'process.env.VUE_ENV': '"client"'
}),
new VueSSRClientPlugin()
]
})
module.exports = config
Copy the code
Webpack.client.config.js does two main things
- Defining entry files
entry-client.js
- Through plug-ins
VueSSRClientPlugin
generatevue-ssr-client-manifest.json
This manifest.json file is referenced by server.js
const { createBundleRenderer } = require('vue-server-renderer')
const template = require('fs').readFileSync('/path/to/template.html'.'utf-8')
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
const renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
})
Copy the code
With the above Settings, all HTML code rendered by the server after being built using the code splitting feature is automatically injected.
webpack.server.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(base, {
mode: 'production'.target: 'node'.devtool: '#source-map'.entry: './src/entry-server.js'.output: {
filename: 'server-bundle.js'.libraryTarget: 'commonjs2'
},
resolve: {},
externals: nodeExternals({
whitelist: /\.css$/ // Prevent packages from being packaged into the bundle, instead fetching these extension dependencies externally at runtime
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
Copy the code
What webpack.server.config.js does is:
- through
target: 'node'
The directory code that tells Webpack to compile is the Node application - through
VueSSRServerPlugin
Plug-in that compiles the code tovue-ssr-server-bundle.json
After generating vue-ssR-server-bundle. json, you just need to pass the file path to createBundleRenderer.
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
/ /... Other options for renderer
})
Copy the code
At this point, the build is almost complete
Complete the first runnable instance
Install VUE dependencies
npm i axios vue-template-compiler vue-router vuex vuex-router-sync
Add and improve the following files:
/ ├ ─ ─ server. Js# Implement long-running Node applications├─ SRC │ ├─ app.js# new│ ├ ─ ─ the router. Js# Add route definition│ ├ ─ ─ App. Vue# new│ ├ ─ ─ entry - client. Js# browserside entry│ ├ ─ ─ entry - server. Js# node application side entry└ ─ ─ views └ ─ ─ Home. Vue# page
Copy the code
Let’s look at each file one by one:
server.js
const fs = require('fs');
const path = require('path');
const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');
const devServer = require('./build/setup-dev-server')
const resolve = file= > path.resolve(__dirname, file);
const isProd = process.env.NODE_ENV === 'production';
const app = express();
const serve = (path, cache) = >
express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
});
app.use('/dist', serve('./dist'.true));
function createRenderer(bundle, options) {
return createBundleRenderer( bundle, Object.assign(options, {
basedir: resolve('./dist'),
runInNewContext: false})); }function render(req, res) {
const startTime = Date.now();
res.setHeader('Content-Type'.'text/html');
const context = {
title: 'SSR test'.// default title
url: req.url
};
renderer.renderToString(context, (err, html) = > {
res.send(html);
});
}
let renderer;
let readyPromise;
const templatePath = resolve('./src/index.template.html');
if (isProd) {
const template = fs.readFileSync(templatePath, 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json') // Inject the js file into the page
renderer = createRenderer(bundle, {
template,
clientManifest
});
} else {
readyPromise = devServer( app, templatePath, (bundle, options) = >{ renderer = createRenderer(bundle, options); }); } app.get(The '*',isProd? render : (req, res) = > {
readyPromise.then(() = >render(req, res)); });const port = process.env.PORT || 8088;
app.listen(port, () = > {
console.log(`server started at localhost:${port}`);
});
Copy the code
Server.js does the following
- When you perform
npm run dev
Is called/build/setup-dev-server.js
Start ‘webpack-dev-middleware’ to develop middleware - through
vue-server-renderer
Generated before the call is compiledvue-ssr-server-bundle.json
Starting the Node Service - will
vue-ssr-client-manifest.json
Injected into thecreateRenderer
T automatic injection of front-end resources - through
express
To deal withhttp
request
Server.js is the entry program for the whole site, through which the compiled files are called, and the final output to the page, is a key part of the whole project
app.js
import Vue from 'vue'
import App from './App.vue';
import { createRouter } from './router';
export function createApp(context) {
const router = createRouter();
const app = new Vue({
router,
render: h= > h(App)
});
return { app, router };
};
Copy the code
App.js exposes a factory function that can be executed repeatedly, creating new application instances for each request, submitted to ‘entry-client.js’ and entry-server.js calls
entry-client.js
import { createApp } from './app';
const { app, router } = createApp();
router.onReady(() = > {
app.$mount('#app');
});
Copy the code
Entry-client.js routinely instantiates the Vue object and mounts it to the page
entry-server.js
import { createApp } from './app';
export default context => {
// Since it might be an asynchronous routing hook function or component, we'll return a Promise,
// So that the server can wait for all the content before rendering,
// We are ready.
return new Promise((resolve, reject) = > {
const { app, router } = createApp(context);
// Set the router location on the server
router.push(context.url);
// Wait until the router has resolved possible asynchronous components and hook functions
router.onReady(() = > {
const matchedComponents = router.getMatchedComponents();
Reject if the route cannot be matched, reject, and return 404
if(! matchedComponents.length) {return reject({ code: 404 });
}
resolve(app);
});
});
};
Copy the code
As the server entry, entry-server.js is finally compiled into vue-ssr-server-bundle.json for vue-server-renderer to call through VueSSRServerPlugin
Router. js and home. vue are regular vue programs that are not expanded here.
At this point, we have completed the first vUE SSR instance that can be fully compiled and run
Data prefetch and status management
The programs that have been done before just render the variables that you want to define as HTML and return them to the client, but if you want to implement a truly usable Web program, you need to have dynamic data support. Now let’s look at how to get data remotely and render it as HTML and export it to the client.
During server-side rendering (SSR), we are essentially rendering a “snapshot” of our application, so if the application relies on some asynchronous data, it needs to be prefetched and parsed before starting the rendering process.
Prefetch Storage container (Data Store)
Start by defining an api.js that gets the data, using axios:
import axios from 'axios';
export function fetchItem(id) {
return axios.get('https://api.mimei.net.cn/api/v1/article/' + id);
}
export function fetchList() {
return axios.get('https://api.mimei.net.cn/api/v1/article/');
}
Copy the code
We will use the official state management library Vuex. We will create a store.js file that will get a list of files and the content of the article by id:
import Vue from 'vue';
import Vuex from 'vuex';
import { fetchItem, fetchList } from './api.js'
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
items: {},
list: []},actions: {
fetchItem({commit}, id) {
return fetchItem(id).then(res= > {
commit('setItem', {id, item: res.data})
})
},
fetchList({commit}){
return fetchList().then(res= > {
commit('setList', res.data.list)
})
}
},
mutations: {
setItem(state, {id, item}) {
Vue.set(state.items, id, item)
},
setList(state, list) {
state.list = list
}
}
});
}
Copy the code
Then modify app.js:
import Vue from 'vue'
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
export function createApp(context) {
const router = createRouter();
const store = createStore();
sync(store, router)
const app = new Vue({
router,
store,
render: h= > h(App)
});
return { app, router, store };
};
Copy the code
Components with logical configuration
Now that the Store action is defined, let’s look at how to trigger the request. The official recommendation is to put it in the routing component, and then look at home.vue:
<template>
<div>
<h3>The article lists</h3>
<div class="list" v-for="i in list">
<router-link :to="{path:'/item/'+i.id}">{{i.title}}</router-link>
</div>
</div>
</template>
<script>
export default {
asyncData ({store, route}){
return store.dispatch('fetchList')},computed: {
list () {
return this.$store.state.list
}
},
data(){
return {
name:'wfz'}}}</script>
Copy the code
Data prefetch on the server
At entry – server. Js, we can through the routing and the router. GetMatchedComponents () that match the component, if the component exposed asyncData, we call this method. Then we need to append the parsing state to the render context.
// entry-server.js
import { createApp } from './app';
export default context => {
// Since it might be an asynchronous routing hook function or component, we'll return a Promise,
// So that the server can wait for all the content before rendering,
// We are ready.
return new Promise((resolve, reject) = > {
const { app, router, store } = createApp(context);
// Set the router location on the server
router.push(context.url);
// Wait until the router has resolved possible asynchronous components and hook functions
router.onReady(() = > {
const matchedComponents = router.getMatchedComponents();
Reject if the route cannot be matched, reject, and return 404
if(! matchedComponents.length) {return reject({ code: 404 });
}
Promise.all(
matchedComponents.map(component= > {
if (component.asyncData) {
return component.asyncData({
store,
route: router.currentRoute
});
}
})
).then(() = > {
context.state = store.state
// Promise should resolve the application instance so that it can be rendered
resolve(app);
});
});
});
};
Copy the code
When template is used, context.state is automatically embedded in the final HTML as the window.__initial_state__ state. On the client side, store should get the state before it is mounted to the application:
// entry-client.js
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
Copy the code
Client data prefetch
On the client side, data prefetch can be handled in two different ways: parsing the data before routing, matching the view to be rendered, and retrieving the data. In our demo, we used the first scheme:
// entry-client.js
import { createApp } from './app';
const { app, router, store } = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() = > {
router.beforeResolve((to, from, next) = > {
const matched = router.getMatchedComponents(to);
const prevMatched = router.getMatchedComponents(from);
let diffed = false;
const activated = matched.filter((c, i) = > {
returndiffed || (diffed = prevMatched[i] ! == c); });if(! activated.length) {return next();
}
Promise.all(
activated.map(component= > {
if (component.asyncData) {
component.asyncData({
store,
route: to
});
}
})
)
.then(() = > {
next();
})
.catch(next);
});
app.$mount('#app');
});
Copy the code
Retrieve interface data by checking for matching components and executing asyncData in the global routing hook function.
Since the demo is two pages, you also need to add a routing information to router.js and a routing component, item.vue. Now you have a basic Vue SSR instance.
Cache optimization
Because server-side rendering is computationally intensive, performance issues are likely if concurrency is large. Appropriate use of caching strategies can dramatically improve response times.
const microCache = LRU({
max: 100.maxAge: 1000 // Important: Entries expire after 1 second.
})
const isCacheable = req= > {
// The implementation logic is to check whether the request is user-specific.
// Only non-user-specific pages are cached
}
server.get(The '*'.(req, res) = > {
const cacheable = isCacheable(req)
if (cacheable) {
const hit = microCache.get(req.url)
if (hit) {
return res.end(hit)
}
}
renderer.renderToString((err, html) = > {
res.end(html)
if (cacheable) {
microCache.set(req.url, html)
}
})
})
Copy the code
Basically, performance bottlenecks can be largely solved with Nginx and caching.