Build your own Vue SSR

Render a Vue instance

Start by creating a directory and initializing the package.json file using NPM

npm init -y
Copy the code

Install vue and vue-server-renderer:

Pay attention to the version, it is not certain that different configurations will cause unexpected errors

NPM [email protected] I [email protected]Copy the code

Create a server.js in the root directory where the vue instance will be created:

Const Vue = require(' Vue ') const app = new Vue({// template: ` <div id="app"> <h1>{{ message }}</h1> </div> `, data: { message: 'hello world' } })Copy the code

The goal now is to replace the message in data with the difference expression in template, and then render the template.

The vue-server-renderer package can be used to render instances:

Const Vue = require(' Vue ') // The createRenderer method can be loaded and executed to get a renderer, Const renderer = require(' jue-server-renderer '). CreateRenderer () const app = new vue ({template: ` <div id="app"> <h1>{{ message }}</h1> </div> `, data: { message: 'Hello world'}}) // The first argument to renderToString is the vue instance, the second is the callback function, Renderer. RenderToString (app, (err, HTML) => {if (err) throw err console.log(HTML)})Copy the code

Use Node to execute this code:

node server.js
Copy the code

You can see that the contents of the template have been rendered and the contents of the internal difference expression have been replaced with real data.

In addition, a data-server-renderer property was added to div#app, which acts as an entry point for subsequent client rendering activations.

Combine to a Web server

Install Express as a local server:

NPM I [email protected]Copy the code
const Vue = require('vue') const renderer = require('vue-server-renderer').createRenderer() const express = Get ('/', (req, res) => {const app = new Vue({template:) require('express') const server = express() ` <div id="app"> <h1>{{ message }}</h1> </div> `, data: { message: 'hello world' } }) renderer.renderToString(app, (err, HTML) => {if (err) {// Return 500 status code on Error return res.status(500).end('Internal Server Error.')} // Prevent Chinese garbled characters res.setHeader('Content-Type', 'text/html; Charset =utf8') // Return HTML content res.end(HTML)})}) // Listen to port 3000 server.listen(3000, () => { console.log('server is running at port 3000') })Copy the code

Use Nodemon to start the service:

nodemon server.js
Copy the code

Open localhost:3000 in your browser and you can see that the page also displays normally

Using HTML Templates

Create an HTML template in the root directory called index.template.html:

<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, Initial-scale =1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>Document</title> </head> <body> --vue-ssr-outlet--> </body> </html>Copy the code

The comment in the body is a special tag that will be rendered in the future when the content needs to be rendered

Also, when creating the renderer, configure the template options:

Const fs = require('fs') const renderer = Require (' viet-server-renderer '). CreateRenderer ({// use fs to read template file template: fs.readFileSync('./index.template.html', 'utf-8') }) const express = require('express') const server = express() server.get('/', (req, res) => { const app = new Vue({ template: ` <div id="app"> <h1>{{ message }}</h1> </div> `, data: { message: 'hello world' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) }) server.listen(3000, () => { console.log('server is running at port 3000') })Copy the code

Start the service and open the browser. You can see the complete page on the server:

Use external data in templates

The renderToString method passes a second argument that can be rendered in the template:

Renderer. RenderToString (app, {title: 'VueSSR' },(err, HTML) => {if (err) {return res.status(500).end('Internal Server Error.')} res.setheader (' content-type ', 'text/html; charset=utf8') res.end(html) })Copy the code

We can also bind the incoming data with interpolation expressions in the title tag of index.template. HTML:

<title>{{ title }}</title>
Copy the code

Restart the service, refresh the page, you can see that the browser TAB has been changed to the configured title:

You can also configure meta tags in this way, but inside the template you need to use {{{}}} to output the content as it is:

renderer.renderToString(app, { title: 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> ` },(err, HTML) => {if (err) {return res.status(500).end('Internal Server Error.')} res.setheader (' content-type ', 'text/html; charset=utf8') res.end(html) })Copy the code
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, Minimum - scale = 1.0 "> {{{meta}}} < title > {{title}} < / title >Copy the code

Restart the service and you can see that the manually configured meta has been added:

Build configuration

The above server-side rendering simply processes the Vue instance into a purely static HTML string and sends it to the client, with additional processing required when the content requires dynamic interaction. Let’s simulate dynamic interaction:

Add additional functionality when creating vUE instances:

const app = new Vue({ template: ` < div id = "app" > < h1 > {{message}} < / h1 > < h2 > dynamic interaction < / h2 > < input v - model = "message" type = "text" > "button @click="onClick"> </button> </div>, data: {message: 'hello world'}, methods: { onClick() { alert(this.message) } } })Copy the code

It is now expected that when the input field changes, the h1 tag will change as well. Clicking the button will also pop up the contents of the input box.

But in the browser, there’s only a view of these things, and no functionality:

If you open the web log, you can see that there is no required JS file in the page, only the content of the page:

So there is additional processing required to implement the client-side interaction.

Open vUE official website, you can find a picture of SSR source code structure:

On the left is the application source (Source), in the middle is the Webpack, and on the right is the Node Server.

For all the work done above, there are only server entries, which are used to handle server-side rendering. For server rendered content to have the ability to interact dynamically with the client, there also needs to be a client entry to handle the client interaction.

For Server Entry, it is packaged into a server bundle through Webpack, which is mainly used for server-side rendering. For Client Entry, it is eventually packaged as a Client bundle, which takes over the generated pages rendered by the server and activates them into dynamic client pages.

So the next step is to deal with the source code structure, the approximate structure of the source code should be like this:

Create a SRC directory under the root directory and create an app. vue file inside it. The contents of the file can be written as option from the vue instance created earlier:

<template> <div id="app"> < H1 >{{message}}</h1> < H2 > <input V-model ="message" type="text"> <button @click="onClick"> </button> </div> </template> <script> export default {name: "App", data() {return {message: 'hello world' } }, methods: { onClick() { alert(this.message) } } } </script> <style scoped> </style>Copy the code

Create app.js under SRC, which is the generic startup entry for the homogeneous application

Import Vue from 'Vue' import App from './ app. Vue '// export a factory function Export function createApp () {const app = new Vue({// Root instance simple render application component for creating new // application, Router and Store instances. render: h => h(App) }) return { app } }Copy the code

App.js is the “universal entry” for our app. In a pure client application, we would create the root Vue instance in this file and mount it directly into the DOM. However, for server-side rendering (SSR), the responsibility shifts to pure client-side Entry files.

– vue SSR guide

Create an entry-client.js file in SRC

Import {createApp} from './app' // client-specific boot logic...... Const {app} = createApp() const {app} = createApp() const {app} = createApp() const {app} = createApp()Copy the code

Client Entry simply creates the application and mounts it into the DOM

Create an entry-server.js file under SRC:

import { createApp } from './app'
​
export default context => {
  const { app } = createApp()
  return app
}
​
Copy the code

Server Entry uses the default Export function to export and calls this function repeatedly on each render. At this point, it does not do much except to create and return an application instance – but we will perform server-side route matching and data pre-fetching logic here later.

The code is not ready to run at this point

Install dependencies

  1. Installation production dependency

In addition to the vUE, VUe-server-renderer, and Express already installed, we need cross-env (which sets up cross-platform environment variables via NPM scripts) :

NPM I [email protected]Copy the code
  1. Installation development dependency
NPM I -D [email protected] [email protected] [email protected] [email protected] @babel/ [email protected] @babel/[email protected] @babel/[email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]Copy the code

The Webpack version should be in 4.x, otherwise unexpected errors may occur

Webpack configuration

Create a build folder in the root directory to store the package configuration files:

  1. webpack.base.config.js :
/** * const VueLoaderPlugin = require('vue-loader/lib/plugin') const path = require('path') const VueLoaderPlugin = require('path') 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: {// path alias, @ to SRC '@': resolve('.. / SRC /')}, // The extensions can be omitted from front to back: ['.js', '.vue', '.json']}, devtool: isProd? 'the source - the map' : 'being - the module - the eval - source - the map', the module: {{rules: [/ / processing image resources test: /. (PNG | JPG | GIF) $/ I, use: [{loader: 'url - loader, the options: {limit: 8192,},},] and {}, / / handle font resources test: /. (woff | woff2 | eot | the vera.ttf | otf) $/, use: [' file - loader '],}, {/ / processing. Vue resources test: /. Vue $/, loader: 'vue-loader'}, // handle CSS resources // It will be applied to ordinary '. CSS 'files // and'. Vue 'files in' '<style>' block {test: /. CSS $/, use: ['vue-style-loader', 'css-loader']}, // CSS preprocessor, see: https://vue-loader.vuejs.org/zh/guide/pre, processors, HTML / / / / {such as dealing with Less resources / / test: /. Less $/, / / use: [ // 'vue-style-loader', // 'css-loader', // 'less-loader' // ] // }, ] }, plugins: [ new VueLoaderPlugin(), new FriendlyErrorsWebpackPlugin() ] }Copy the code
  1. webpack.client.config.js:
/ const {merge} = require('webpack-merge') const baseConfig = require('./ webpack.bas.config.js ') const VueSSRClientPlugin = require('vue-server-renderer/client-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 chunks can be injected properly later. optimization: { splitChunks: { name: "manifest", minChunks: Infinity } }, plugins: [// This plugin generates' vue-ssr-client-manifest.json 'in the output directory. New VueSSRClientPlugin()]})Copy the code
  1. webpack.server.config.js:
Const {merge} = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const {merge} = require('webpack-node-externals') const baseConfig = require('./webpack.base.config.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') Module.exports = merge(baseConfig, {// Set entry to the server entry file of the application: './ SRC /entry-server.js', // this allows Webpack to handle module loading in a manner suitable for Node // and also when compiling Vue components, // Tell the VUe-loader to transport server-oriented code. target: 'node', output: { filename: 'server-bundle.js', // this tells server bundle to use Node-style exports: 'commonjs2'}, // opt for externals: [nodeExternals({// allowList: [/.css$/] })], plugins: [// This is a plug-in that builds the entire output of the server into a single JSON file.Copy the code

Configuring build Commands

Add the scripts property to the package.json file and configure the following command:

{
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
    "build": "rimraf dist && npm run build:client && npm run build:server"
}
Copy the code

Build :client package script;

Build :server :server packaging script

Build: Package the client and server together;

Try it out:

All is well ~

Start the application

The above package generates the following files:

For server – bundle, the website also has a detailed method of use: ssr.vuejs.org/zh/guide/bu…

Use it here:

In server.js, replace the createRenderer method with the createBundleRenderer method:

Const fs = require('fs') const serverBundle = require('./dist/ vue-ssR-server-bundle. json') // Const clientManifest = require('./dist/vue- SSR -client-manifest.json') const template = Fs.readfilesync ('./index.template.html', 'utF-8 ') // createBundleRenderer; the first argument is serverBundle; Renderer = require(' viet-server-renderer '). CreateBundleRenderer (serverBundle, {template, clientManifest }) // ... Other codeCopy the code

Delete the instance created in the routing configuration:

server.get('/', (req, res) => { renderer.renderToString({ title: 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> ` },(err, HTML) => {if (err) {return res.status(500).end('Internal Server Error.')} res.setheader (' content-type ', 'text/html; charset=utf8') res.end(html) }) })Copy the code

Here is the diff before and after deletion:

The instance of vue is removed here, and it finds entry-Server, calls the methods inside to get the instance and then renders.

Type in the command line to start the service and open the browser to see that the page is still displayed as before, but the dynamic interaction does not work properly yet:

You can see that there is an app.js request 404 below, but there is the file in the dist folder. This is because the current service does not expose the resources in this folder, so we need to deal with these files:

Server.use ('/dist', express.static('./dist'))Copy the code

Refresh the browser to see that app.js has been loaded and the client dynamic interaction is working properly:

Building development patterns

During development, the above process alone requires repackaging the Web service through the Server after each change in the source code. So implement a development mode where you write code and automatically build and restart the Web service, then refresh the browser page, etc.

Configure the scripts command in package.json first:

{
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
    "build": "rimraf dist && npm run build:client && npm run build:server",
    "start": "cross-env NODE_ENV=production node server.js",
    "dev": "node server.js"
}
Copy the code

Start: starts services in the production environment

Dev: Starts the service in the development environment

In server.js, to handle development mode and production mode respectively:

Const isProd = process.env.node_env === 'production' Let renderer if (isProd) {const serverBundle = require('./dist/ vue-ssR-server-bundle. json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') const template = fs.readFileSync('./index.template.html', 'utf-8') renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { template, ClientManifest})} else {// development mode}Copy the code

The production environment just needs to operate as before, but the development environment also needs to monitor the files to automatically package the build and reproduce the Renderer.

At the same time, in the route handler function, it is also necessary to handle different environments:

Const render = (req, res) => {renderer. RenderToString ({title: 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> ` },(err, HTML) => {if (err) {return res.status(500).end('Internal Server Error.')} res.setheader (' content-type ', 'text/html; Charset =utf8') res.end(HTML)})} // Set route server.get('/', isProd? render : (res, req) => { // ... // Wait for render()})Copy the code

setupDevServer

To generate the renderer for your development environment, you also need a setupDevServer function that takes two arguments, the first being the server instance (to mount some middleware, etc.) and the second being a callback function (to regenerate the Renderer). The serverBundle, template, and clientManifest can be received in the callback function.

Clean up the code:

Const Vue = require(' Vue ') const fs = require('fs') const {createBundleRenderer} = // require(' vie-server-renderer ') const express = require('express') const server = express() const isProd = Process.env.node_env === 'production' let renderer let onReady if (isProd) {// Production mode const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') const  template = fs.readFileSync('./index.template.html', 'utf-8') renderer = createBundleRenderer(serverBundle, { template, // setupDevServer returns a promise, OnReady = setupDevServer(server, (serverBundle, template, clientManifest) => { renderer = createBundleRenderer(serverBundle, { template, clientManifest }) }) } server.use('/dist', express.static('./dist')) const render = (req, res) => { renderer.renderToString({ title: 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> ` },(err, html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) } server.get('/', isProd ? render : async (res, req) => { // ... }) server.listen(3000, () => { console.log('server is running at port 3000') })Copy the code

This is the expected process before we start implementing setupDevServer:

Create a new setup-dev-server.js file in the build directory and export a function that returns a promise:

Module.exports = (server, callback) => {const onReady = new Promise()}Copy the code

Import this function in server.js as well

const setupDevServer = require('./build/setup-dev-server')
Copy the code

The next step is to refine the setupDevServer function.

In this function, template, serverBundle, and clientManifest are built to be used by the callback, and an update function is required to determine if all three exist. If all three exist, the callback will be called and pass them in:

Module.exports = (server, callback) => {const onReady = new Promise() serverBundle, clientManifest const update = () => { if (template && serverBundle && clientManifest) { callback(serverBundle, template, clientManifest) } } return onReady }Copy the code

After calling the callback, we need to change the state of the promise to fullfilled:

module.exports = (server, Let ready const onReady = new Promise(r => Ready = r) // Monitor build -> update renderer let template, serverBundle, clientManifest const update = () => { if (template && serverBundle && clientManifest) { // Call the resolve method to change the promise state to fullfilled. Ready () callback(serverBundle, template, clientManifest)}} return onReady}Copy the code

At the same time, the execution of the update function also need to be done in building the template/serverBundle clientManifest, then update the renderer renderer. Then you need to implement each of these build processes one by one.

Build the template

To build the template, you need to read the index.template. HTML file, so you also need to introduce the fs and PATH modules. After reading the file, save it to template and call the update function:

const path = require('path') const fs = require('fs') module.exports = (server, Callback) => {let ready const onReady = new Promise(r => Ready = r) clientManifest const update = () => { if (template && serverBundle && clientManifest) { ready() callback(serverBundle, Const templatePath = path.resolve(__dirname,)}} // Build template -> call update -> update renderer const templatePath = path.resolve(__dirname,) '.. /index.template.html') template = fs.readFileSync(templatePath, 'utF-8 ') update() console.log(template) // Build serverBundle -> call renderer -> update renderer // build clientManifest -> call update -> Update renderer return onReady}Copy the code

Run the NPM run dev command to see the print:

You can see that the template file has been successfully read.

If this is not enough, you need to do this again when the file changes, so you need the Chokidar module to help you monitor the file:

NPM I [email protected]Copy the code

Introduce Chokidar after installation:

const path = require('path') const fs = require('fs') const chokidar = require('chokidar') module.exports = (server, Callback) => {let ready const onReady = new Promise(r => Ready = r) clientManifest const update = () => { if (template && serverBundle && clientManifest) { ready() callback(serverBundle, Const templatePath = path.resolve(__dirname,)}} // Build template -> call update -> update renderer const templatePath = path.resolve(__dirname,) '.. /index.template.html') template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utF-8 ') update()}) // Build serverBundle -> call update -> update renderer // build clientManifest -> call update -> update renderer return onReady }Copy the code

Build serverBundle

Building the serverBundle requires a build package of the project source code, so you need to import webPack and its configuration files:

const path = require('path') const fs = require('fs') const chokidar = require('chokidar') const webpack = require('webpack') module.exports = (server, callback) => { // ... Const templatePath = path.resolve(__dirname, '.. /index.template.html') template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utF-8 ') update()}) const serverConfig = // Build serverBundle -> call update -> update renderer Require ('./webpack.server.config') // pass the configuration file to webpack and compile it, Const serverCompiler = Webpack (serverConfig) // The compiler has a watch method that monitors file changes and automatically repackages them when they are sent. // The watch method takes the configuration object as its first argument, The second argument is the callback function, and the first argument to the function is an error that occurred to WebPack itself when the build was packaged, Servercompiler. watch({}, (err, stats) => {if (err) throw err // stats has an hasErrors method, If (stats.haserrors ()) return // Read the packaged file with readfile. ServerBundle = json.parse (fs.readfilesync (path.resolve(__dirname, '.. /dist/vue-ssr-server-bundle.json'), 'utF-8 ')) // Call update method update()}) // Build clientManifest -> call update -> update renderer return onReady}Copy the code

Writes the packing results to memory

The Webpack package generates the results to the specified folder, but the files are frequently modified during development, and the files are time-consuming to read and write from disk, so it needs to be optimized to write the packed results to memory.

Webpack-dev-middleware can be used to read and write results to memory:

NPM I [email protected]Copy the code
const path = require('path') const fs = require('fs') const chokidar = require('chokidar') const webapck = // Require ('webpack') // introduce const devMiddleware = require('webpack-dev-middleware') module.exports = (server, callback) => { // ... Const templatePath = path.resolve(__dirname, '.. /index.template.html') template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utF-8 ') update()}) // Build serverBundle -> call update -> update renderer const serverConfig = Require ('./webpack.server.config') const serverCompiler = webpack(serverConfig) The second parameter is the configuration option const serverDevMiddleware = devMiddleware(serverCompiler, {logLevel: 'silent' / / close log}) / / to the compiler's done register an event on a hook, done a hook will call at the end of each compile serverCompiler. Hooks. Done. Tap (' server '() = > {/ /, unlike the fs, ServerBundle = json.parse () serverDevMiddleware.fileSystem.readFileSync(path.resolve(__dirname, '.. /dist/ vue-ssR-server-bundle. json'), 'utF-8 ')) update()}) stats) => { // if (err) throw err // if (stats.hasErrors()) return // serverBundle = JSON.parse( // fs.readFileSync(path.resolve(__dirname, '.. /dist/vue-ssr-server-bundle.json'), 'utF-8 ') //) // update() //}) // Build clientManifest -> call update -> update renderer return onReady}Copy the code

You can print serverBundle before the update function is called:

Successful printing means it’s not a problem.

Build clientManifest

ClilentManifest is built in a similar way to serverBundle, with only the naming part modified:

Const clientConfig = require('./webpack.client.config') const ClientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler, { Best path from the configuration file for publicPath: clientConfig. Output. PublicPath, logLevel: 'silent' / / close log}) clientCompiler. Hooks. Done. Tap (' client ', () => { clientManifest = JSON.parse( clientDevMiddleware.fileSystem.readFileSync(path.resolve(__dirname, '.. /dist/vue-ssr-client-manifest.json'), 'utf-8') ) update() })Copy the code

To tidy up setupDevServer:

const path = require('path') const fs = require('fs') const chokidar = require('chokidar') const webpack = Require ('webpack') const devMiddleware = require('webpack-dev-middleware') const resolve = file => path.resolve(__dirname, file) module.exports = (server, Callback) => {let ready const onReady = new Promise(r => Ready = r) clientManifest const update = () => { if (template && serverBundle && clientManifest) { ready() callback(serverBundle, }} // create template -> update -> update renderer const templatePath = resolve('.. /index.template.html') template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utF-8 ') update()}) // Build serverBundle -> call update -> update renderer const serverConfig = require('./webpack.server.config') const serverCompiler = webpack(serverConfig) const serverDevMiddleware = devMiddleware(serverCompiler, { logLevel: 'silent' / / close log}) serverCompiler. Hooks. Done. Tap (' server ', () => { serverBundle = JSON.parse( serverDevMiddleware.fileSystem.readFileSync(resolve('.. /dist/vue-ssr-server-bundle.json'), 'utF-8 ')) update()}) // Build clientManifest -> call update -> update renderer const clientConfig = require('./webpack.client.config') const clientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler, { publicPath: clientConfig.output.publicPath, logLevel: 'silent' / / close log}) clientCompiler. Hooks. Done. Tap (' client ', () => { clientManifest = JSON.parse( clientDevMiddleware.fileSystem.readFileSync(resolve('.. /dist/vue-ssr-client-manifest.json'), 'utf-8') ) update() }) return onReady }Copy the code

At this time, I found that the page can not be opened, the console also has an error:

Error: server. Js error: res’ ‘req “was not passed when render function was executed in route setting.

// Set route server.get('/', isProd? render : async (res, req) => { // ... // Wait for renderer to render(res, req)})Copy the code

Restart dev command, open browser and find that the page has been rendered normally, but the page interaction does not work properly. In the network log, you can also see that app.js reported 404:

In development mode, the data is stored in memory, so the setupDevServer function is used to handle static files. Mount devMiddleware to the incoming Express instance (Server) :

Module.exports = (server, callback) => {//... ServerBundle -> update renderer -> update renderer -> update renderer -> Call update -> update renderer // mount clientDevMiddleware to express service, Provides access to data in its internal memory. Server. use(clientDevMiddleware) return onReady}Copy the code

So when you access dist, you try to access it from memory.

Restart the service, you can see that the client interaction is normal again:

Hot update

Currently, you need to manually refresh the browser when the project changes, so you need to configure the hotfollow module again:

NPM [email protected] I - DCopy the code

After importing the WebPack configuration file in setupDevServer, push the plugins option directly into the hot update module. Change the entry option in the configuration file to an array with an additional script. Don’t use hash file names for output either; Finally, mount the hot update module to the service:

Const hotMiddleware = require('webpack-hot-middleware') // Build clientManifest -> call update -> update renderer Const clientConfig = the require (". / webpack. Client. The config ') / / direct push clientConfig. Plugins. Push (new webpack.HotModuleReplacementPlugin()) clientConfig.entry.app = [ 'webpack-hot-middleware/client?quiet=true&reload=true', / / interact with the server processing hot update a client-side script clientConfig. Entry. The app] clientConfig. The output. The filename = '[name]. Js' / / hot update mode to ensure a consistent hash const clientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler, { publicPath: clientConfig.output.publicPath, logLevel: 'silent' / / close log}) clientCompiler. Hooks. Done. Tap (' client ', () => { clientManifest = JSON.parse( clientDevMiddleware.fileSystem.readFileSync(resolve('.. Json '), 'utF-8 ')) update()}) // Mount server.use(hotMiddleware(clientCompiler, {log: false }))Copy the code

Restart the service and try to modify the static content under the component.

The routing process

Install vue router first:

NPM I [email protected]Copy the code

Create pages subdirectory under SRC to store page components. Create several sample pages in the pages directory: home, About, and 404:

Create a router subdirectory under SRC and create an index.js directory for routing configuration:

// src/router/index.js 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: Routes: [{path: '/', name: 'home', Component: home}, {path: '/about', name: 'about', component: () => import('@/pages/about') }, { path: '*', name: 'error', component: () => import('@/pages/404') } ] }) return router }Copy the code

Add the createRouter function to app.js and execute to mount the returned router instance to the vue root instance:

import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' export function createApp () { Const router = createRouter() const app = new Vue({router, // render: H => h(App)})Copy the code

Adapter server

In entry-server.js, adapt routes to server rendering (official website example) :

// entry-server.js import {createApp} from './app' export default context => { So we'll return a Promise, // so that the server can wait for everything to be ready before rendering. return new Promise((resolve, reject) => { const { app, Router.push (context.url) // Wait until the router has finished parsing possible asynchronous components and hook functions router.onReady(() => {/ / the code here is dealing with illegal routing, because in the routing rules have configured the 404 components, so the following code can be deleted const matchedComponents. = the router getMatchedComponents () / / can't match the routing, Reject, return 404 if (! matchedComponents.length) { return reject({ code: 404})} // The Promise should resolve the application instance so that it can render resolve(app)}, reject)})}Copy the code

Modify it with async and await to make it more intuitive:

// entry-server.js import {createApp} from './app' export default async Context => {// Because there may be asynchronous route hook functions or components, So we'll return a Promise, // so that the server can wait for everything to be ready before rendering. const { app, Router} = createApp() router.push(context.url) router.push(context.url) router.push(context.url) router.push(context.url) router.push(context.url) Router.push (context.url await new Promise(router.onReady.bind(router)) return app }Copy the code

RenderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString) {renderToString (renderToString);

// server.js const render = (req, res) => { renderer.renderToString({ title: 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> `, url: req.url },(err, HTML) => {if (err) {return res.status(500).end('Internal Server Error.')} res.setheader (' content-type ', 'text/html; Charset =utf8') res.end(HTML)})} server.get('*', isProd? render : async (req, res) => { // ... // Wait for renderer to render(req, res)})Copy the code

Since the renderToString method supports Promises, the render function can also be modified with async and await:

Const render = async (req, res) => {const HTML = await renderer. RenderToString ({title: const HTML = await renderer. 'VueSSR', meta: ` <meta name="description" content="VueSSR"/> `, url: Req.url}) // Prevent Chinese garble res.setheader (' content-type ', 'text/ HTML; charset=utf8') res.end(html) } catch (e) { res.status(500).end('Internal Server Error.') } }Copy the code

Adaptation client

In entry-client.js, you need to mount the router after router.onReady:

// entry-client.js import {createApp} from './app' // client-specific boot logic...... Const {app, router} = createApp() const {app, router} = createApp() const {app, router} = createApp()Copy the code

Process pages on the road by exit

In the app. Vue template, set router-link and router-view:

<template> <div id="app"> < H1 >{{message}}</h1> < H2 > <input V-model ="message" type="text"> <button </button> <ul> <li><router-link to="/">Home</router-link></li> <li><router-link to="/about">about</router-link></li> </ul> <router-view/> </div> </template>Copy the code

The route configuration is almost complete. Use NPM run dev to start the project again and see what happens.

Oh can

Management page head

Nuxt’s vue-meta can be used to manage page heads:

NPM I [email protected]Copy the code

Importing and registering plug-ins in app.js:

// app.js import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' import VueMeta from Vue. Use (VueMeta) // Mixin vue. Mixin ({metaInfo: {titleTemplate: '% s-vuessr '// %s represents the original title, which will now be displayed on the tag along with the contents of the template}}) Export function createApp () {// createRouter = createRouter() const app = new Vue({router, // mount route to Vue root instance // Root instance simple render application component. render: h => h(App) }) return { app, router } }Copy the code

In the entry – server. In js:

// entry-server.js import { createApp } from './app' export default async context => { const { app, Router} = createApp() // Get meta const meta = app.$meta() router.push(context.url) // replace meta context.meta = meta await new Promise(router.onReady.bind(router)) return app }Copy the code

Also reserve space in the template file:

<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, Initial - scale = 1.0, the maximum - scale = 1.0, Minimum - scale = 1.0 "> {{{meta. Inject (). The title. The text ()}}} {{{meta. Inject (). The meta. The text ()}}} < / head > < body > <! --vue-ssr-outlet--> </body> </html>Copy the code

If you restart the service, you can see that the page title has changed to the desired one:

Data prefetch and status management

During server-side rendering, the component’s beforeCreate and Created execute but do not wait for the asynchronous operation, so when the server-side rendering project continues to fetch data using asynchronous operations during the Created cycle, it does not achieve the desired effect.

And before mounting to the client application, you need to get exactly the same data as the server application – otherwise, the client application will use a different state than the server application and then cause the mixing to fail.

To solve this problem, the retrieved data needs to be located outside of the view component, in a specialized data store or state container. First, on the server side, we can prefetch data and populate the store before rendering. In addition, we will serialize and inline the state in HTML. This allows you to get the inline state directly from store before mounting the client application.

Install vuex and AXIOS first:

NPM I [email protected] [email protected]Copy the code

Create a store subdirectory under SRC and create index.js for store:

import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) export const createStore = () => { Return vuex.store ({return vuex.store ({return vuex.store () => ({posts: []}), mutations: { setPosts(state, payload) { state.posts = payload } }, actions: {// Make sure the action returns a promise async getPosts({commit}) {const {data} = await axios.get('https://cnodejs.org/api/v1/topics') commit('setPosts', data.data) } } }) }Copy the code

Call action within the About component:

<template> <div> <h1>about</h1> <ol> <li v-for="(post, index) in posts" :key="index">{{ post.title }}</li> </ol> </div> </template> <script> import { mapState, mapActions } from 'vuex' export default { name: "about", metaInfo: { title: 'about' }, computed: { ... MapState (['posts'])}, // vue SSR provides a lifecycle hook for server-side rendering, ServerPrefetch () {// Initiate action return this.getposts ()}, methods: {... mapActions(['getPosts']) } } </script> <style scoped> </style>Copy the code

Restart the project and enter the About page. There is no list displayed in the page. Open the network log to check the requests for the current page:

You can see that the page is loaded with content, but because the data is still on the server, the client does not have the data when the client takes over the page, causing the content to be lost.

In entry-server.js, we need to get the data state and assign it to the context after the server has rendered:

// entry-server.js import { createApp } from './app' export default async context => { const { app, router, Store} = createApp() const meta = app.$meta() router.push(context.url) context.meta = meta The router will await new Promise(router.onready.bind (router)) with possible asynchronous components and hook functions after parsing await new Promise(router.onready.bind (router)). Structure.rendered = () => {// The Renderer will inline the context.state data object into the page template // The page will be sent to the client with a script: __INITIAL_STATE__ = context.state // The client takes the window.__INITIAL_STATE__ out of the page and fills it with context.state = in the client store container store.state } return app }Copy the code

In entry-cilent.js, you need to check whether the __INITIAL_STATE__ attribute exists in the current window object. If so, replace it with:

import { createApp } from './app' const { app, router, Store} = createApp() // Replace if (window.__initial_state__) {store.replacEstate (window.__initial_state__)} router.onReady(() => { app.$mount('#app') })Copy the code

You can also see this script in the element review:

At the end

Use NPM run build command to package and then use NPM run start to start production mode. When opening port 3000, you can see that the page has been started normally, but there is still a bug, that is, when entering the home page for the first time, it jumps to the About page from this page. The data cannot be loaded properly.