1. The concept

1. Traditional ways:

The client requests once, the server returns once, including the interface data query, the client does not need to process.Copy the code
  • Pros: Quick response, GOOD SEO
  • Disadvantages: waste of network resources, reload resources and parse each time

2. Traditional SPA methods:

The server returns all the content to the client, and the client renders itCopy the code
  • Advantages: convenient development, easy maintenance, rendering calculation in the client, save traffic
  • Disadvantages: Not conducive to SEO search, slow first screen loading, client performance requirements

SSR server side render

For the first request of the client, the first screen rendering is performed on the server side, and the subsequent operations realize single-page application through the activation of the client, integrating the advantages of SPA and traditional methodsCopy the code
  • Advantages: Quick first screen response, SEO is very good
  • Disadvantages: difficult to maintain, difficult to develop, difficult to understand, and inconvenient to transplant

2. Code implementation

2.1 Express Simple SSR implementation

vue-server-renderer + express

const express = require("express");
const app = express();
// Import the Vue constructor
const Vue = require("vue"); Web Full stack Architect// createRenderer is used to get the renderer
const { createRenderer } = require("vue-server-renderer");
// Get the renderer
const renderer = createRenderer();

app.get("/".async (req, res) => {
    // Create a vue instance to render
    const vm = new Vue({
        data: { name: "xxxxx" },
        template: `
<div >
<h1>{{name}}</h1>
</div>
`
    });
    try {
        // renderToString renders the vue instance as an HTML string that returns a Promise
        const html = await renderer.renderToString(vm);
        // Return HTML to the client
        res.send(html);
    } catch (error) {
        Render error returns 500 errors
        res.status(500).send("Internal Server Error"); }}); app.listen(3000);
Copy the code

2.2 Route Implementation

Vue / / in the router
const Router = require('vue-router')
Vue.use(Router)
// Change path to wildcard
app.get(The '*'.async function (req, res) {
    // Create one routing instance at a time
    const router = new Router({
        routes: [{path: "/".component: { template: '<div>index page</div>'}}, {path: "/detail".component: { template: '<div>detail page</div>'}}});const vm = new Vue({
        data: { msg: 'xxx' },
        // Add the router-view to display the contents
        template: `
<div>
<router-link to="/">index</router-link>
<router-link to="/detail">detail</router-link>
<router-view></router-view>
</div>`,
        router, / / a mount
    })

    try {
        // Jump to the corresponding route
        router.push(req.url);
        const html = await renderer.renderToString(vm)
        res.send(html)
    } catch (error) {
        res.status(500).send('Render error')}})Copy the code

3. Engineering construction

3.1 Implementation Principle

Generated through webpack packaging

  1. The server configures the JSON file
  2. Active JS information of the client.

Request process

  • At the first request, the server returns the necessary content on the first screen
  • It also returns the JS file required by spa
  • Subsequent operations follow the SPA logic

3.2 Code logic implementation

  • All instances need to be returned via factory methods such as App, Router,store

  • When different users request the server, each time the server accepts a request, it instantiates an instance for processing. (Occupies server resources)

  • Main.js provides the createApp method, which outputs factory app, Router, and Store

  • Entry-server. js is created using the createApp method

Since asynchrony is possible on the server, use promise to handle asynchrony.
export default context => {
  // Return a Promise that there may be asynchronous operations in the route
  return new Promise((resolve, reject) = > {
    const { app, router, store } = createApp() 
        resolve(app)
    })
}
Copy the code
  • The entry-client.js client is relatively simple and can be mounted directly using createApp
import {createApp} from './main'
// Create vue and Router instances
const {app, router, store}=createApp()
router.onReady(() = > {
  / / a mount
  app.$mount('#app')})Copy the code
  • Set vue.config.js to define the code for different entry output client and server deployment

3.3 Implementation of routing code

//server/04-ssr.js
// The final server rendering script
/ / the node code
// express server
const express = require('express')
const app = express()
const {createBundleRenderer} = require('vue-server-renderer')

// Get the absolute path of the specified file
const resolve = dir= > require('path').resolve(__dirname, dir)

Dist /client: open dist/client directory and turn off the option to download the index page by default
app.use(express.static(resolve('.. /dist/client'), {index: false}))


// Get the renderer
// Step 3: server package file address
const bundle = resolve(".. /dist/server/vue-ssr-server-bundle.json");

const renderer = createBundleRenderer(bundle, {
  runInNewContext: false.// https://ssr.vuejs.org/zh/api/#runinnewcontext
  template: require('fs').readFileSync(resolve(".. /public/index.html"), "utf-8"), // Host file
  clientManifest: require(resolve(".. /dist/client/vue-ssr-client-manifest.json")) // Client list
});

app.get(The '*'.async (req, res) => {

  // Construct the renderer context
  const context = {
    title: 'ssr test'.url: req.url // First screen address requested by the user
  }
  
  try {
    // renderToString converts Vue instances to HTML strings
    const html = await renderer.renderToString(context)
    res.send(html)
  } catch (error) {
    res.status(500).send('Server rendering error')
  }
  
})

app.listen(3000)


//src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/index.vue'
import Detail from '@/views/detail.vue'

Vue.use(Router)
// Route configuration
const routes =
  [
    // There is no compiler on the client side
    { path: "/".component: Index },
    { path: "/detail".component: Detail }
  ]
// The difference is that this is the factory function that creates the router instance
export function createRouter() {
  return new Router({
    mode: 'history',
    routes
  })
}

//main.js This is a factory
import Vue from "vue";
import App from "./App.vue"; 
// Each request must be a new vUE instance
// Future callers of this method will be renderer
// Context is the argument passed to us by the renderer
export function createApp(context) {
  // Create a router instance
  const router = createRouter()

  const app = new Vue({
    router,
    context,
    render: h= > h(App)
  })

  return { app, router }
}

//src/entry-client.js
// Client entry for client activation
// The following code is executed in the browser
import {createApp} from './main'

// Create vue and Router instances
const {app, router}=createApp()

router.onReady(() = > {
  / / a mount
  app.$mount('#app')})//src/entry-server.js

// Mainly used for first screen rendering
import { createApp } from './main'

// Renderer is passed a URL, the first screen address
export default context => {
  // Return a Promise that there may be asynchronous operations in the route
  return new Promise((resolve, reject) = > {
    const { app, router } = createApp()
    // Get the first screen address and jump to it
    router.push(context.url) 
    router.onReady(() = > {
        resolve(app) 
    }, reject)
  })
}


//vue.config.js
// Two plug-ins are responsible for packaging the client and server, respectively
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");

const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");

// Determine the entry file and corresponding configuration items based on the incoming environment variables
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
  css: {
    extract: false
  },
  outputDir: './dist/'+target,
  configureWebpack: () = > ({
    // Point entry to the application's server/client file
    entry: `./src/entry-${target}.js`.// Provide source map support for the bundle renderer
    devtool: 'source-map'.// Target is set to Node so that webPack handles dynamic imports in the same way node does,
    // It also tells' vue-loader 'to output server-oriented code when compiling Vue components.
    target: TARGET_NODE ? "node" : "web".// Whether to emulate node global variables
    node: TARGET_NODE ? undefined : false.output: {
      // Use Node style to export modules here
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // Externalize application dependency modules. You can make server builds faster and generate smaller package files.
    externals: TARGET_NODE
      ? nodeExternals({
          // Do not externalize dependent modules that WebPack needs to handle.
          // More file types can be added here. For example, the *.vue raw file is not processed,
          // Dependencies that modify 'global' (for example, polyfill) should also be whitelisted
          whitelist: [/\.css$/]}) :undefined.optimization: {
      splitChunks: undefined
    },
    // This is a plug-in that builds the entire output of the server into a single JSON file.
    // The default file name on the server is' vue-ssr-server-bundle.json '
    // The default client file name is' vue-ssr-client-manifest.json '.
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config= > {
    // Cli4 project added
    if (TARGET_NODE) {
        config.optimization.delete('splitChunks')
    }
      
    config.module
      .rule("vue")
      .use("vue-loader")
      .tap(options= > {
        merge(options, {
          optimizeSSR: false}); }); }};//package.json
  "scripts": {
    "build:client": "vue-cli-service build"."build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"."build": "npm run build:server && npm run build:client"
  },
  
  
// public/index.html 
 / / <! -- vue-ssR-outlet --> Insert the active SPA code to the client<! DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta Name ="viewport" content="width=device-width,initial-scale=1.0"> <title>Document</title> </head> <body>! --vue-ssr-outlet--> </body> </html>Copy the code

3.4 Storage and asynchronous code implementation

Server-side rendering is a “snapshot” of the application, and if the application relies on asynchronous data, this data needs to be prefetched and parsed before rendering can begin.

  • In the corresponding Compoent page, the methods starting with asyncData are all asynchronous methods that return a promise through Vuex’s Dispatch,
  • Server: When rendering traverse the router. GetMatchedComponents asynchronous () all of the components, through the Promise. All execute completely resolve after (app), INITIAL_STATE = ‘XXXXX’ and save the result to window.INITIAL_STATE = ‘XXXXX’
  • Client: Pass when renderingstore.replaceState(window.__INITIAL_STATE__)Replace what the server has parsed

3.4.1 Complete code

//src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// The difference is to create a factory function
export function createStore () {
  return new Vuex.Store({
    state: {
        count:108
    },
    mutations: {
      add(state){
        state.count += 1;
      },
      init(state, count){ state.count = count; }},actions: {
      // add an asynchronous request count action
      getCount({ commit }) {
        console.log('action:getCount');
        return new Promise(resolve= > {
          setTimeout(() = > {
            commit("init".Math.random() * 100);
            resolve();
          }, 1000); }); }},})}//src/main.js
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from './router/index';
import { createStore } from './store/index';

Vue.config.productionTip = false;

Vue.mixin({
  beforeMount() {
    const { asyncData } = this.$options;
    if (asyncData) {
      // Assign the fetch data operation to the Promise
      // So that in the component, we can do this after the data is ready
      // By running 'this.dataPromise.then(...) 'to perform other tasks
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$route, }); }}});// Each request must be a new vUE instance
// Future callers of this method will be renderer
// Context is the argument passed to us by the renderer
export function createApp(context) {
  // Create a router instance
  const router = createRouter()
  const store = createStore()

  const app = new Vue({
    router,
    store,
    context,
    render: h= > h(App)
  })

  return { app, router, store }
}

//src/entry-client.js
// Client entry for client activation
// The following code is executed in the browser
import {createApp} from './main'

// Create vue and Router instances
const {app, router, store}=createApp()

// Restore the store to its initial state
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() = > {
  / / a mount
  app.$mount('#app')})//src/entry-server.js
// Mainly used for first screen rendering
import { createApp } from './main'

// Renderer is passed a URL, the first screen address
export default context => {
  // Return a Promise that there may be asynchronous operations in the route
  return new Promise((resolve, reject) = > {
    const { app, router, store } = createApp()

    // Get the first screen address and jump to it
    router.push(context.url)

    router.onReady(() = > {
      // Check whether the currently matched component needs to request asynchronous data
      const matchedComponents = router.getMatchedComponents();
        
      // If there is no match, an exception is thrown
      if(! matchedComponents.length) {return reject({ code: 404 });
      }

      console.log(matchedComponents);

      Promise.all(
        matchedComponents.map(Component= > {
          if (Component.asyncData) {
            return Component.asyncData({
              store,
              route: router.currentRoute,
            });
          }
        }),
      ).then(() = > {
        // All asynchronous requests end and these states need to be synchronized to the front end
        // The state will be automatically serialized to window.__initial_state__ = 'XXXXX'
        // The job is to have renderer
        context.state = store.state
        resolve(app)
      }).catch(reject)
     
    }, reject)
  })

}

//App.vue
<template>
  <div id="app">
    <nav>
      <router-link to="/">index</router-link>
      <router-link to="/detail">detail</router-link>
    </nav>
    <h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
    <HelloWorld msg="vue ssr"></HelloWorld>
    <router-view></router-view>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  components: {
    HelloWorld
  }
}
</script>
 
 
 //src/views/index.vue
 <template>
  <div>index page</div>
</template>

<script>
export default {
  asyncData({ store }) {
    // The convention prefetch logic is written in the prefetch hook asyncData
    // After the action is triggered, a Promise is returned to determine the result of the request
    return store.dispatch("getCount"); }};</script>
Copy the code

3.5 Data Prefetch on the Client

After the client is activated, if other pages also want to get data, you need to set the mixin to add common methods to each page

//main.js
Vue.mixin({
    beforeMount() {
        const { asyncData } = this.$options;
        if (asyncData) {
            // Assign the fetch data operation to the Promise
            // So that in the component, we can do this after the data is ready
            // By running 'this.dataPromise.then(...) 'to perform other tasks
            this.dataPromise = asyncData({
                store: this.$store,
                route: this.$route, }); }}});// The corresponding vue page is detail.vue
<template>
  <div>
    detail 
  </div>
</template>

<script>
  export default { 
  asyncData({ store }) {
    // The convention prefetch logic is written in the prefetch hook asyncData
    // After the action is triggered, a Promise is returned to determine the result of the request
    return store.dispatch("getCount");
  },
  mounted(){
    The dataPromise called here is the Pormise object returned by the self-defined asyncData method above
    this.dataPromise.then( (res) = > {
      // The corresponding service is processed here}})},</script> 
Copy the code

4. Hot update development

Implementation logic: Determine whether the dev environment is present, and each request returns the latest rendering code via the factory method

Install dependencies (also installed because NPM is executed in code)

npm i chokidar npm browser-sync -D
Copy the code

Code transformation


// Get the renderer
// Step 3: server package file address
const bundle = resolve(".. /dist/server/vue-ssr-server-bundle.json");
// Return the latest render each time
function createRenderer() { 
  const renderer = createBundleRenderer(bundle, {
  runInNewContext: false.// https://ssr.vuejs.org/zh/api/#runinnewcontext
  template: require('fs').readFileSync(resolve(".. /public/index.html"), "utf-8"), // Host file
  clientManifest: require(resolve(".. /dist/client/vue-ssr-client-manifest.json")) // Client list
});
return renderer
}
const isDev = process.env.NODE_ENV === 'development'
if (isDev) {
  const cp = require('child_process')
  const bs = require('browser-sync').create()
  const chokidar = require('chokidar')
  const watch = chokidar.watch("src/**/*.*")// Scan all folders and files
  watch.on('change'.(path) = > {
    console.log("Current data is changing..... Recompiling...")
    cp.exec("npm run build".function (error,stdout) {
      if(error) {
        console.log("Failed to compile",error.stack)
        return
      }
      console.log(stdout)//
      console.log("Compile complete")//
      // Refresh the browser
      bs.reload()
    })// Execute the build again
  })
  bs.init({proxy:"http://localhost:3000"}) // Bind the current BS to the same address as the debugged Node service, then the corresponding page of the browser can be reloaded
}

letRenderer defines the global variable app.get(The '*'.async (req, res) => {
  try { 
    // Make sure the content is up to date each time
    if(isDev || ! renderer) {// The first time the renderer might be empty
      renderer = createRenderer() 
    } 
    console.log("renderer",renderer)
    // renderToString converts Vue instances to HTML strings
    const html = await renderer.renderToString(context)
    res.send(html)
  } catch (error) {
    res.status(500).send('Server rendering error')}})//package.json New startup environment variable development uses Nodemon to monitor file changes in real time

  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon ./server/04-ssr.js --watch server"
  },

Copy the code

5. To summarize

The server side renders some views

  • Advantages:
  1. Output the actual HTML content, easy to search engine crawler collection.
  2. Fast response to initial loading
  3. All the operations are performed on the server, so the client directly parses faster
  4. The code is more secure and the logic is executed on the server side
  • Disadvantages:
  1. Return each time on the server, increase the number of server requests, server performance requirements
  2. Increased the front-end development of talent skills comprehensive requirements of the difficulty, not easy to maintain later
  3. Development and debugging is different from the traditional SPA development mode, and the cost of debugging and error checking is higher.
  4. Poor performance, need to do cache optimization later
  • Scenarios used
  1. For the external system that needs to be promoted and the system that needs SEO optimization, the return response of different logic can be realized by distinguishing between crawler and ordinary user
  2. Suitable for large, multi-concurrent applications where the same result generates the same STATIC HTML content.
  3. Suitable for applications with high user experience requirements, use caching without requesting the server if the last and current results are unchanged.