What is server-side rendering (SSR)?
Vue.js is the framework for building client applications. However, you can render components as HTML strings on the server side, send them directly to the browser, and finally "activate" these static tags as fully interactive applications on the client side.
Why use server-side rendering (SSR)?
- Better SEO, because the search engine crawler can directly view the fully rendered page.
- Faster response time without waiting for JavaScript to load
balance?
- Limited by development conditions
- More requirements for build setup and deployment
- More server-side load
Basic use
install
npm install vue vue-server-renderer --S
Render
const Vue = require('vue') const { createRenderer } = require('vue-server-renderer') const app = new Vue({ template: `<div>Hello World</div>` }) createRenderer().renderToString(app, (err, html) => { console.log(html); })
Integration server
install Express
npm install express --save
const Vue = require('vue') const { createRenderer } = require('vue-server-renderer') const express = require('express') const server = express() const app = new Vue({ template: `<div>Hello Xiao Wang</div>` }) server.get('/', (req, res) => { createRenderer().renderToString(app, (err, html) => { res.end(html) }) }) server.listen('3000')
Using page templates
In the above case, we will find that the Chinese display is garbled because the compiled HTML file lacks coding and other instructions
In this example, we must wrap the generated HTML tags with an additional HTML page wrapping container. To simplify this, you can directly provide a page template when creating the renderer. We will put the page template in a unique file, such as index.template.html
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta charset="utf-8" /> <!-- Use double curly braces(double-mustache)conduct HTML Escape interpolation(HTML-escaped interpolation) --> <title>{{ title }}</title> <!-- Use three curly braces(triple-mustache)conduct HTML Non escape interpolation(non-HTML-escaped interpolation) --> </head> <body> <!--vue-ssr-outlet--> </body> </html>
Attention <-- Vue SSR outlet -- > comment, which will be where the application HTML tag is injected.
Template interpolation: we can provide interpolation data by passing in a "rendering context object" as the second parameter of the renderToString function
const Vue = require('vue') const fs = require('fs') const { createRenderer } = require('vue-server-renderer') const express = require('express') const server = express() const app = new Vue({ template: `<div>Hello Xiao Wang</div>` }) const context = { title: 'hello' } const renderer = createRenderer({ template: fs.readFileSync('./index.template.html', 'utf-8') }) server.get('/', (req, res) => { renderer.renderToString(app, context ,(err, html) => { res.end(html) }) }) server.listen('3000')
global template
Source code structure using webpack
We need to use webpack to package our Vue application because
- Usually, Vue applications are built by webpack and Vue loader, and many webpack specific functions cannot be run directly in Node.js
- Although the latest version of Node.js can fully support the ES2015 feature, we still need to translate the client code to adapt to the old browser.
Therefore, for both client applications and server applications, we need to use webpack packaging. The server needs a "server bundle" for server-side rendering (SSR), and the "client bundle" will be sent to the browser for mixing static tags.
A basic project
build # webpack build file ├── setup-dev-server.js # render file in develop ment mode ├── webpack.base.config.js # General packaging profile ├── webpack.client.config.js # Client packaging profile └── webpack.server.config.js # Server packaging configuration file src ├── pages │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── App.vue ├── app.js # Universal entry ├── entry-client.js # Run on browser only └── entry-server.js # Run on server only └── server.js # Startup file
Entry file
- app.js: it is the "general entry" of our application. In a pure client application, we will create a root Vue instance in this file and mount it directly to the DOM. However, for server-side rendering (SSR), responsibility shifts to the pure client-side entry file. app.js simply uses export to export a createApp function:
import Vue from 'vue' import App from './App.vue' // Export a factory function to create a new // Application, router, and store instances export function createApp () { const app = new Vue({ // The root instance is a simple rendering application component. render: h => h(App) }) return { app } }
- entry-client.js: the client entry simply creates the application and mounts it in the DOM
import { createApp } from './app' // Client specific boot logic const { app } = createApp() // It is assumed that the root element in the App.vue template has ` id="app"` app.$mount('#app')
- entry-server.js: the server uses the default export export function and calls this function repeatedly in each rendering. At this point, it won't do much except create and return application instances. However, we will perform server-side route matching and data prefetching logic here later.
import { createApp } from './app' export default context => { const { app } = createApp() return app }
webpack build configuration
- webpack.base.config.js: the general webpack packaging configuration file defines the packaging mode and export file path. The fake pig loader packages various files and uses vueLoaderPlugin
const path = require('path') const vueLoaderPlugin = require('vue-loader/lib/plugin') const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') const resolve = file => path.resolve(__dirname, file) const isProd = process.env.NODE_ENV === 'production' module.exports = { mode: isProd ? 'production' : 'development', output: { path: resolve('../dist'), publicPath: '/dist', filename: '[name].[chunkhash].js' }, resolve: { alias: { '@': resolve('../src') }, extensions: ['.js','.vue','.json'] }, devtool: 'source-map', module: { rules: [ { test: /\.(png|jpg|gif)$/i, use: [ { loader: 'url-loader', options: { limit: 8192, }, } ] }, // Processing font resources { test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader', ], }, // Processing. vue resources { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, ] }, plugins: [ new vueLoaderPlugin(), new FriendlyErrorsWebpackPlugin() ] }
- webpack.client.config.js: client webpack packaging configuration file, defining client packaging entry, syntax conversion of ES6, using VueSSRClientPlugin
/** * Client packaging configuration */ const { merge } = require('webpack-merge'); const baseConfig = require('./webpack.base.config.js'); const VueSSRClientPlugin = require('vue-server-renderer/client-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = merge(baseConfig, { entry: { app: './src/entry-client.js', }, module: { rules: [ // ES6 to ES5 { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], cacheDirectory: true, plugins: ['@babel/plugin-transform-runtime'], }, }, }, ], }, // Important: this separates the webpack runtime into a boot chunk, // So that asynchronous chunk s can be injected correctly later. optimization: { splitChunks: { name: 'manifest', minChunks: Infinity, }, }, plugins: [ new CleanWebpackPlugin(), // This plug-in generates' Vue SSR client manifest. JSON 'in the output directory. new VueSSRClientPlugin(), ], });
- webpack.server.config.js: the server-side webpack packaging configuration file defines the server-side packaging entry, tells Vue loader to transport server-oriented code, and uses VueSSRClientPlugin
/** * Server packaging configuration */ const { merge } = require('webpack-merge'); const nodeExternals = require('webpack-node-externals'); const baseConfig = require('./webpack.base.config.js'); const VueSSRServerPlugin = require('vue-server-renderer/server-plugin'); module.exports = merge(baseConfig, { // Point the entry to the server entry file of the application entry: './src/entry-server.js', // This allows the webpack to handle module loading in a Node appropriate manner // And when compiling Vue components, // Tell 'Vue loader' to transport server oriented code. target: 'node', output: { filename: 'server-bundle.js', // Here, tell the server bundle to use Node style exports libraryTarget: 'commonjs2', }, // Do not package node_modules is a third-party package, but it is loaded directly in the require mode externals: [ nodeExternals({ // The resources in the white list are still packaged normally allowlist: [/\.css$/], }), ], plugins: [ // This is a plug-in that builds the entire output of the server into a single JSON file. // The default file name is ` vue-ssr-server-bundle.json` new VueSSRServerPlugin(), ], });
Start application
Installation development dependency
package | explain |
---|---|
webpack webpack-cli | webpack core package |
webpack-merge | webpack configuration information merging tool |
webpack-node-externals | Exclude Node modules in webpack |
friendly-errors-webpack-plugin | Friendly webpack error tips |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | Babel related tools |
vue-loader vue-template-compiler | Processing. vue resources |
file-loader css-loader url-loader | Processing resource files |
cross-env | Setting cross platform environment variables through npm scripts |
Modify the startup file server.js
const express = require('express') const { createBundleRenderer } = require('vue-server-renderer') const server = express() server.use('/dist', express.static('./dist')) const template = require('fs').readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createBundleRenderer(serverBundle, { template, clientManifest }) const render = async (req, res) => { try { const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, url: req.url, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) } catch (err) { res.status(500).end('Internal Server Error.') } } server.get('*', render) server.listen(8080)
Execute charge packaging command
npm run build:client npm run build:server
After the packaging is successful, I can see the packaged results under the dist file, and run the server.js file to view the page
Separation of production from the develop ment environment
Although we can run the above successfully, there are still some problems
- Every time you finish writing code, you have to repackage and build it
- Restart the Web service
Therefore, let's realize the construction of development mode in the project, that is, we hope to realize:
- Write the code and build it automatically
- Automatic restart of Web Services
- Automatically refresh page content
thinking
Cross env is used in the node command to carry NODE_ENV variable to distinguish the execution environment
Production mode
- npm run build
- node server.js starts
Development mode:
- Monitor code changes, hot updates
Add the command script to the package.json file
"scripts": { "build": "npm run build:client && npm run build:server", "start": "cross-env NODE_ENV=production node server.js", "dev": "node server.js" },
Install expansion pack
package | explain |
---|---|
chokidar | Listen for changes to local files |
webpack-dev-middlewar | middleware |
webpack-hot-middleware | Hot renewal |
Modify the startup script, directly use the packaged files in the production environment, and wait for the generation of the renderer function in the development environment
const express = require('express') const setupDevServer = require('./build/setup-dev-server') const { createBundleRenderer } = require('vue-server-renderer') const server = express() server.use('/dist', express.static('./dist')) const isProd = process.env.NODE_ENV === 'production' let onReady, renderer if (isProd) { // Production mode, directly create a renderer based on the constructed package const template = require('fs').readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createBundleRenderer(serverBundle, { template, clientManifest }) } else { // Development mode package build (client + server) - > create renderer onReady = setupDevServer(server, (serverBundle, template, clientManifest) => { renderer = createBundleRenderer(serverBundle, { template, clientManifest }) }) } const render = async (req, res) => { try { if (!isProd) { await onReady } /** * There is no need to pass in an application here, because it has been created automatically when the bundle is executed. * bundle renderer When renderToString is called, it will automatically execute the function exported by the "application instance created by bundle" (pass in the context as a parameter), and then render it. */ const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) } catch (err) { res.status(500).end('Internal Server Error.') } } server.get('*', render) server.listen(8080)
The renderer function is generated by setup-dev-server.js in the development environment
const fs = require('fs') const path = require('path') const chokidar = require('chokidar') const webpack = require('webpack') const devMiddleware = require('webpack-dev-middleware') const hotMiddleware = require('webpack-hot-middleware') const serverConfig = require('./webpack.server.config') const clientConfig = require('./webpack.client.config') const resolve = file => path.resolve(__dirname, file) const templatePath = path.resolve(__dirname, '../index.template.html') module.exports = (server, callback) => { let ready, template, serverBundle, clientManifest const onReady = new Promise(r => ready = r) const update = () => { if (template && serverBundle && clientManifest) { ready() callback(serverBundle, template, clientManifest) } } // Monitor build template template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utf-8') update() }) // Monitor build serverBundle const serverCompiler = webpack(serverConfig) const serverDevMiddleware = devMiddleware(serverCompiler) serverCompiler.hooks.done.tap('server', () => { serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')) update() }) // Monitor build clientManifest clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) clientConfig.entry.app = [ 'webpack-hot-middleware/client?quiet=true&reload=true', // Interact with the server to process a client script for hot update clientConfig.entry.app ] const clientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler, { publicPath: clientConfig.output.publicPath, }) clientCompiler.hooks.done.tap('client', () => { clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')) update() }) // clientDevMiddleware is mounted in the Express service to provide access to data in its internal memory server.use(clientDevMiddleware) server.use(hotMiddleware(clientCompiler, { log: false // Turn off its own log output })) return onReady }
Routing management
Install Vue router and create router.js file
import Vue from 'vue' import VueRouter from 'vue-router' import Home from '@/pages/Home' Vue.use(VueRouter) export const createRouter = () => { const router = new VueRouter({ mode: 'history', // Compatible with front and rear ends routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: () => import('@/pages/About') }, { path: '/posts', name: 'post-list', component: () => import('@/pages/Posts') }, { path: '*', name: 'error', component: () => import('@/pages/404') } ] }) return router }
Update app.js
/** * General entrance */ import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' Vue.use(VueMeta) // Export a factory function to create a new // Application, router, and store instances export function createApp () { const router = createRouter() const app = new Vue({ router, render: h => h(App) }) return { app, router } }
Implement server-side routing logic in entry-server.js
/** * Server */ import { createApp } from './app' export default async context => { const { app, router } = createApp() router.push(context.url) await new Promise(router.onReady.bind(router)) return app }
Implement client-side routing logic in entry-client.js
/** * client */ import { createApp } from './app' // Client specific boot logic const { app, router } = createApp() // It is assumed that the root element in the App.vue template has ` id="app"` router.onReady(() => { app.$mount('#app') })
Modify App.vue file
<template> <div id="app"> <h1>{{ message }}</h1> <div><input type="" v-model="message"></div> <button @click="onClick">onClick</button> <ul> <li> <router-link to="/">Home</router-link> </li> <li> <router-link to="/about">About</router-link> </li> <li> <router-link to="/posts">Posts</router-link> </li> </ul> <!-- Route exit --> <router-view/> </div> </template> <script> export default { name: 'App', data () { return { message: 'vue-ssr' } }, methods: { onClick () { console.log('Hello World!') } } } </script>
After successful startup, when visiting the page, we will find that in addition to the main resources of the app, other resources have been downloaded, but our routing configuration is introduced dynamically, that is, it should be loaded only when we visit, but it is loaded immediately here.
The reason is the link tag with preload and prefetch in the header of the page.
We expect the client JavaScript script to load as soon as possible and take over the content rendered by the server as soon as possible, so that it has dynamic interaction ability, but
If you put the script tag here, the browser will download it and execute the code inside. This process will block the page
Rendering.
So the real script tag is at the bottom of the page. This just tells the browser to preload this resource. But no
To execute the code inside, do not affect the normal rendering of the web page. It won't go until the real script tag loads the resource
Execute the code inside. At this time, it may have been preloaded and can be used directly. If it is not loaded, it will not cause duplication
Reload, so don't worry about this problem.
The prefetch resource is the resource that may be used to load the next page. The browser will load it when it is free, so it does not
Resources are not necessarily loaded, but preload must be preloaded. So you can see that when we visit the about page
Wait, its resources are prefetched through prefetch, which improves the response speed of client page navigation.
Manage page Head
The body in the page is rendered dynamically, but the head of the page is written dead. Use vue-meta
Vue Meta is a third-party Vue.js plug-in supporting SSR, which allows you to easily manage the head content of different pages.
<template> ... </template> <script> export default { metaInfo: { title: 'My Example App', titleTemplate: '%s - Yay!', htmlAttrs: { lang: 'en', amp: true } } } </script>
Install NPM I Vue meta - S
Register Vue meta in Vue through plug-in in the general portal app.js.
import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: '%s - vue-ssr' } })
Then adapt Vue meta in the server render entry-server.js file:
/** * Server */ import { createApp } from './app' export default async context => { const { app, router } = createApp() const meta = app.$meta() router.push(context.url) context.meta = meta await new Promise(router.onReady.bind(router)) return app }
Finally, meta information is injected into the template page index.template.html
<head> {{{ meta.inject().meta.text() }}} {{{ meta.inject().title.text() }}} </head>
Data prefetching
Assume that the requirement is to render the article list
- Server side rendering: in the case of the server side, this requirement is very simple. It is nothing more than sending a request to get data rendering
Client side rendering: there will be the following problems on the client side
- Only beforeCreate and created lifecycles are supported
- Do not wait for asynchronous operations in the beforeCreate and created lifecycles
- Responsive data is not supported, that is, the data can not be dynamically rendered to the page
The core idea of the solution given in the official document is to store the data obtained during server rendering in the Vuex container,
Then synchronize the data in the container to the client, so as to keep the data state of front and rear rendering synchronized and avoid re rendering by the client
Problems.
Installing Vuex: npm i vuex
Create Vuex container store/index.js
import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) export const createStore = () => { return new Vuex.Store({ state: () => ({ posts: [] }), mutations: { setPosts (state, data) { state.posts = data } }, actions: { // During server-side rendering, make sure that the action returns a Promise async getPosts ({ commit }) { // return new Promise() const { data } = await axios.get('https://cnodejs.org/api/v1/topics') commit('setPosts', data.data) } } }) }
Mount the Vuex container to the Vue root instance in the general application portal
/** * General entrance */ import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' import { createStore } from './store' import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: '%s - vue-ssr' } }) // Export a factory function to create a new // Application, router, and store instances export function createApp () { const router = createRouter() const store = createStore() const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store } }
Serialize the container state into the page in the server-side rendering application portal, so as to avoid
The inconsistent state of the two ends leads to the problem of client re rendering.
- Convert the state in the container to a JSON format string
- Generation code: window__ INITIAL__ The state = store statement is inserted into the template page
- Client through window__ INITIAL__ State gets the data
entry-server.js
context.rendered = () => { // The Renderer will inline the context.state data object into the page template // The final page sent to the client will contain a script: window__ INITIAL_ STATE__ = context.state // The client needs to put the window in the page__ INITIAL_ STATE__ Take it out and fill it in the client store container context.state = store.state }
entry-client.js fills the status data passed from the server into the client Vuex container in the client rendering entry
if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) }
Page cache
Although Vue's server-side rendering (SSR) is quite fast, due to the overhead of creating component instances and virtual DOM nodes, it cannot match the performance of the template based on pure string splicing. When SSR performance is critical, using caching policies wisely can greatly improve response time and reduce server load.
Page level cache
A caching strategy called micro caching can be used to greatly improve the ability of applications to handle high traffic. However, not all pages are suitable for micro caching. We can divide resources into three categories:
- Static resources: such as js, css, images, etc
- User specific dynamic resources: different users accessing the same resources will get different content.
- User independent dynamic resource: any user accessing the resource will get the same content, but the content may change at any time
Personalization, such as blog posts
Installation dependency
npm i lru-cache -S
server.js
const express = require('express') const setupDevServer = require('./build/setup-dev-server') const { createBundleRenderer } = require('vue-server-renderer') const LRU = require('lru-cache') const server = express() server.use('/dist', express.static('./dist')) const cache = new LRU({ max: 100, maxAge: 10000 // Important: entries expires after 1 second. }) const isCacheable = req => { console.log(req.url) if (req.url === '/posts') { return true } } const isProd = process.env.NODE_ENV === 'production' let onReady, renderer if (isProd) { // Production mode, directly create a renderer based on the constructed package const template = require('fs').readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createBundleRenderer(serverBundle, { template, clientManifest }) } else { // Development mode package build (client + server) - > create renderer onReady = setupDevServer(server, (serverBundle, template, clientManifest) => { renderer = createBundleRenderer(serverBundle, { template, clientManifest }) }) } const render = async (req, res) => { try { const cacheable = isCacheable(req) if (cacheable) { const html = cache.get(req.url) if (html) { return res.end(html) } } if (!isProd) { await onReady } /** * There is no need to pass in an application here, because it has been created automatically when the bundle is executed. * bundle renderer When renderToString is called, it will automatically execute the function exported by the "application instance created by bundle" (pass in the context as a parameter), and then render it. */ const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, url: req.url, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) if (cacheable) { cache.set(req.url, html) } } catch (err) { res.status(500).end('Internal Server Error.') } } server.get('*', render) server.listen(8080)
Component level cache
Vue server renderer supports component level caching built in. To enable component level caching, you need to provide a specific cache implementation when creating the renderer.
const LRU = require('lru-cache') const renderer = createRenderer({ cache: LRU({ max: 10000, maxAge: ... }) })
Then, you can cache components by implementing the serverCacheKey function.
export default { name: 'item', // Required options props: ['item'], serverCacheKey: props => props.item.id, render (h) { return h('div', this.item.id) } }