Introduction to the

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.

The Vue SSR guide is introduced as follows.

In layman’s terms, you render the HTML template on the server and send it back to the browser.

Different end render difference

The difference between Client-side rendering Server side rendering
SEO Search engine optimization Is not conducive to Good for
Spiders crawling It is difficult to can
First screen load time slow fast
CPU and memory resources normal More and more
The browser API normal Some cannot be used normally
Framework life cycle normal Some cannot be used normally

Handwritten SSR

Core principles

Package the two entry files with Webpack to generate their respective JS and HTML. Use createRenderer to convert the vue instance returned by server.bundle into a string, insert it into the index.ssr. HTML file, and import client.

Let’s go deeper into the realization of SSR through handwriting.

SSR directory

├ ─ ─ the config │ ├ ─ ─ webpack. Base. Js │ ├ ─ ─ webpack. Client. Js │ └ ─ ─ webpack. Server js ├ ─ ─ dist │ ├ ─ ─ the client. The bundle, js │ ├ ─ ─ Index. SSR. HTML │ └ ─ ─ for server bundle. Js ├ ─ ─ package. The json ├ ─ ─ public │ ├ ─ ─ index. The HTML │ └ ─ ─ index. The SSR. HTML ├ ─ ─ for server js ├ ─ ─ the SRC │ ├ ─ ─ App. Vue │ ├ ─ ─ components │ │ ├ ─ ─ Bar. The vue │ │ └ ─ ─ Foo vue │ ├ ─ ─ entry - client. Js │ ├ ─ ─ entry - server. Js │ ├ ─ ─ ├─ ├─ ├.js app.js │ ├─ ├.js │ ├─ webpack.config.jsCopy the code

Entrance to the file

app.js

To ensure the uniqueness of the instance, we export a function that creates the instance.

import Vue from "vue";
import App from "./App.vue";
export default() = > {const app = new Vue({
    render: h= > h(App)
  });
  return { app };
};
Copy the code

client-entry.js

The client render is manually mounted to the DOM element

import createApp from './app.js';
let {app} = createApp();
app.$mount('#app');

Copy the code

server-entry.js

Server-side rendering simply exports the rendered instance

import createApp from "./app";
export default() = > {const { app } = createApp();
  return app;
};

Copy the code

Configure client packaging and server packaging

webpack.base.js

Basic packaging configuration

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    mode: 'development'.output: {
        filename: '[name].bundle.js' ,
        path:path.resolve(__dirname,'.. /dist')},module: {
        rules: [{
            test: /\.vue$/,
            use: 'vue-loader'
        }, {
            test: /\.js$/,
            use: {
                loader: 'babel-loader'.options: {
                    presets: ['@babel/preset-env'],}},exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ['vue-style-loader', {
                loader: 'css-loader'.options: {
                    esModule: false,}}]}]},plugins: [
        new VueLoaderPlugin()
    ]
}
Copy the code

webpack.client.js

Client package configuration

const {merge} = require('webpack-merge');
const base =require('./webpack.base');
const path = require('path');
module.exports = merge(base,{
    entry: {
        client:path.resolve(__dirname, '.. /src/client-entry.js')}})Copy the code

webpack.server.js

Server package configuration

const base =require('./webpack.base');
const {merge} = require('webpack-merge');
const  HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = merge(base,{
    target:'node'.entry: {
        server:path.resolve(__dirname, '.. /src/server-entry.js')},output: {libraryTarget:"commonjs2" / / module exports export
    },
    plugins: [new HtmlWebpackPlugin({
            template: path.resolve(__dirname, '.. /public/index.ssr.html'),
            filename:'server.html'.excludeChunks: ['server']./ / ignore server. Js
            minify:false./ / no compression
            client:'/client.bundle.js'})]})Copy the code

The index. The SSR. HTML template

  1. placeholder

  2. The resulting string is then automatically inserted into the placeholder in index.ssr. HTML

  3. Import the client.bundle.js file packaged by the client through the variable client defined on htmlWebpackPlugin

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, > <title>Document</title> </head> <body> <! --vue-ssr-outlet--> <script src="<%=htmlWebpackPlugin.options.client%>"></script> </body> </html>Copy the code

Configure to run the script package.json

Install the CONCURRENTLY plug-in to run multiple commands simultaneously

"scripts": {
    "client:dev": "webpack-dev-server --config ./build/webpack.client.js".// Client development environment
    "client:build": "webpack --config ./build/webpack.client.js".// The client package environment
    "server:build": "webpack --config ./build/webpack.server.js" // Server packaging environment
    "run:all": "concurrently \"npm run client:build\" \"npm run server:build\""
 },
Copy the code

Server.js configures the server

  1. Enable the service through server.js

  2. Create a packaged renderer vueServerrender. CreateBundleRenderer

  3. CreateBundleRenderer finds the webPack packed function (server.bundle.js), which is called internally to retrieve the vue instance

  4. Render. RenderToString () generates a string

const Koa = require("koa");
const app = new Koa();
const Router = require("koa-router");
const router = new Router();
const VueServerRenderer = require("vue-server-renderer");
const static = require("koa-static");
const fs = require("fs");
const path = require("path");

const serverBundle = fs.readFileSync(
  path.resolve(__dirname, "dist/server.bundle.js"),
  "utf8"
);
const template = fs.readFileSync(
  path.resolve(__dirname, "dist/server.html"),
  "utf8"
);
// Generate a template string from the instance and insert it into server.html
const render = VueServerRenderer.createBundleRenderer(serverBundle, {
  template,
});
router.get("/".async ctx => {
  ctx.body = await new Promise((resolve, reject) = > {
    render.renderToString((err, html) = > {
      // Must be written as a callback function or the style will not take effect
      resolve(html);
    });
  });
});

// When a client sends a request, it first looks in the dist directory
app.use(static(path.resolve(__dirname, "dist")));
app.use(router.routes());
app.listen(3000);
Copy the code

Integrate the VueRouter configuration

router.js

Exporting Route Configuration

import Vue from "vue";
import VueRouter from "vue-router";
import Foo from "./components/Foo.vue";
Vue.use(VueRouter);

export default() = > {let router = new VueRouter({
        mode:'history'.routes:[
            {path:'/'.component:Foo},
            {path:'/bar'.component:Bar},
        ]
    });
    return router;
}
Copy the code

Modify import file

Each person accessing the server needs to generate a routing system

import Vue from "vue";
import App from "./App.vue";
+ import createRouter from "./router";
export default () => {
+ const router = createRouter();
  const app = new Vue({
+ router,
    render: h => h(App)
  });
+ return { app, router };
};
Copy the code

App.vue

<template>
  <div id="app">
    <router-link to="/">foo</router-link>
    <router-link to="/bar">bar</router-link>
    <router-view></router-view>
  </div>
</template>
Copy the code

Three little questions

1. The route history mode does not exist

One problem with routing history mode is that it will refresh 404. That is, when a user accesses a page (path) that does not exist on the server, how does the server match the front-end route?

Stupid method: load the front page of whatever server you visit, and re-render the component according to the path when the front-end JS renders. Of course, we wouldn’t take such a stupid approach. Rendertostring ({incoming path}), routes to the incoming path, matches the component, renders the string corresponding to the URL, and returns the template HTML to the browser.

For example 🌰 if the user renders a bar, we pass /bar to the renderString. Render. RenderToString generates a string corresponding to the bar component, which is inserted into index.ssr. HTML and returned to the browser. The browser loads the js script according to the path and finds that the path is also /bar, so it only renders once.

2. Non-page-level components, page 404

There is also the problem with user access to non-page-level components, so we need to return page 404. We check matchComponents. Length == 0 in the entry file, if yes, return 404. Server.js returns not found according to code==404

3. How to ensure that the server renders the asynchronous route after loading it

Solution:

+ router.onReady(() => {})
Copy the code

The specific implementation

Modify server. Js
// Requests are sent to the server whenever the user refreshes
router.get('/' (. *).async (ctx)=>{
    ctx.body = await new Promise((resolve, reject) = > {
        render.renderToString({url:ctx.url},(err, html) = > { // Render through the server and return
           if (err && err.code == 404) resolve(`not found`);
            resolve(html)
        })
    })
})
Copy the code
Modify the server.entry.js entry
import createApp from "./index.js";

export default ({ url }) => {
  return new Promise((resolve, reject) => {
+ let { app, router } = createApp();
+ router.push(url);

+ router.onReady(() => {
+ const matchComponents = router.getMatchedComponents();
+ if (matchComponents.length == 0) {
+ return reject({ code: 404 });
+ } else {
+ resolve(app);}}); }); };Copy the code

Integrate vuEX configuration

Increase store. Js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);

export default() = > {let store = new Vuex.Store({
        state: {username:'Eat baozi in the morning'
        },
        mutations: {changeName(state){
                state.username = 'hello'; }},actions: {changeName({commit}){
                return new Promise((resolve,reject) = >{
                    setTimeout(() = > {
                        commit('changeName');
                        resolve();
                    }, 1000); }}}}));return store
}
Copy the code

Modify the app. Js

The introduction of vuex

import Vue from 'vue';
import App from './App.vue'
import createRouter from './router.js'
+import createStore from './store.js'
export default () => {
    const router = createRouter();
+ const store = createStore()
    const app = new Vue({
+ router,
        store,
        render: h => h(App)
    });
+ return { app, router,store }
}
Copy the code

Vuex is registered on both the server vUE instance and the client VUE instance. Note: VuEX server fetching data can only be used in page-level components (routed) then the problem arises

Two questions

1. How does the server ensure that the vuEX data returned is up to date?

Write an asyncData method to the component for the server to call. When we send a request to the server to jump to the page-level component, the server will get the component matched by the current route during rendering, call the asyncData method in the component, and pass the store of the server into asyncData. The string of updated vuex data is then returned to the browser.

2. How to transmit data from the server’s VUex to the client’s Vuex?

This is done using the window global variable. Context. state=store.state The server uses vuex to save data to the global variable Window, and the browser replaces store.state with data rendered by the server.

The specific implementation

Update vuex on the server

Modify server. Entry. Js

import createApp from "./index.js";

export default (context) => {
  const { url } = context;
  return new Promise((resolve, reject) => {
    let { app, router, store } = createApp();

    router.push(url);

    router.onReady(() => {
      const matchComponents = router.getMatchedComponents();
      if (matchComponents.length == 0) {
        return reject({ code: 404 });
      } else {
+ Promise.all(
+ matchComponents.map((component) => {
+ if (component.asyncData) {
+ return component.asyncData(store);
+}
+})
+ ).then(() => {
+ context.state = store.state;
+ resolve(app);}); }}); }); };Copy the code

As shown in the figure abovecontext.state = store.state;Will be automatically inserted in the returned HTML<script>window.__INITIAL_STATE__={"name":"baozi"}</script>

Replace store while browser is running

If window.__initial_state__ exists, replace state in the current client VUex with data on window.__initial_state__.

store.js

if(typeof window! ='undefined'&&window.__INITIAL_STATE__){
    store.replaceState(window.__INITIAL_STATE__)
}
Copy the code

summary

The core of server rendering is parsing an instance of the VUE, generating a string that is returned to the browser.

createRender.renderToString(vm)
let vm=new Vue({
  template:`<div>hello world</div>`
})
Copy the code

CreateBundleRenderer returns a promise->vue instance createBundleRenderer returns a client entry. RenderToString (VM)=> generates a string and returns it to the browser. The vueRouter and vuex configurations are integrated, and the page refresh 404 and vuex data transfer are resolved.

Wuhu finally finished ha ha ~