History of front-end development

Before we officially start to contact SSR, let’s first understand the development history of the front end. In the development of the front end, there are several stages:

Traditional server render SSR -> single page application SPA -> server render SSR

For the first two, let’s make a brief introduction.

Traditional server rendering SSR

  • Representative framework: ASP.net, JSP
  • Features: The request back end returns an HTML page

Single page application SPA

Single page application is the most widely used development method at present. The page content is rendered by JS, which is called client side rendering.

  • Represents frameworks: vue and React
  • Features: Page content rendered by JS

What is Vue SSR

SSR (Server Side Render) is the Server Side rendering, the official explanation is as follows:

Vue.js is a framework for building client applications. By default, the Vue component can be exported to the browser for DOM generation and DOM manipulation. However, it is also possible to render the same component as HTML strings on the server side, send them directly to the browser, and finally “activate” these static tags into a fully interactive application on the client side.

Server-rendered vue.js applications can also be considered “isomorphic” or “generic” because most of the application code can be run on both the server and the client.

Reading this, we can draw a conclusion:

  1. Vue SSR is an improved server-side rendering on SPA
  2. Pages rendered by Vue SSR need to be activated on the client for interaction
  3. The Vue SSR will consist of two parts: the first screen for server rendering and the SPA containing the interaction

Vue SSR modified

When we use SSR server rendering, there will be multiple clients requesting the first screen from the server in the future. In order to keep the data and routing of each user independent, we need to transform the original project.

Host HTML template

<! DOCTYPEhtml>
<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

Transform the vue router

The original route needs to be transformed into a factory function.

// router/index.js
import Vue from "vue";
import Router from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";

Vue.use(Router);
// Export the factory function
export function createRouter() {
  return new Router({
    routes: [{path: "/".component: Home },
      { path: "/about".component: About }
    ]
  })
}
Copy the code

Vue instance creation

The creation of the VUE instance is also modified to a factory function

// main.js
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from "./router";
// Export the Vue instance factory function to create a separate instance for each request
// Context is used to pass parameters to vue instances
export function createApp(context) {
  const router = createRouter();
  const app = new Vue({
    router,
    context,
    render: h= > h(App)
  })
  return { app, router }
}
Copy the code

Server entry

// entry-server.js
import { createApp } from "./main"

// Returns a function that receives the request context and returns the created vue instance
export default context => {
  // Return a Promise to make sure the route or component is ready
  return new Promise((resolve, reject) = > {
    const { app, router } = createApp(context)
    // Jump to the address on the first screen
    router.push(context.url)
    // The route is ready and the result is returned
    router.onReady(() = > {
      resolve(app)
    }, reject)
  })
}
Copy the code

Client entry

// entry-client.js
// The client also needs to create vue instances
import { createApp } from './main';

const { app, router } = createApp()

router.onReady(() = > {
  // Mount active
  app.$mount('#app')})Copy the code

Webpack configuration

An environment variable is used to dynamically determine whether to run from a server or a client packaged.

// 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}}})})Copy the code

The script to modify

When we run an SSR project, we actually need colleagues to start the client and server.

// 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"
}
Copy the code

Server startup file

/ / nodejs code
// Express is our web server
const express = require('express')
const path = require('path')
const fs = require('fs')

// Get the Express instance
const server = express()

// Get the absolute routing function
function resolve(dir) {
  // Concatenate the absolute address of the currently executing js file with the passed dir
  return path.resolve(__dirname, dir)
}


/ / handle the favicon
const favicon = require('serve-favicon')
server.use(favicon(path.join(__dirname, '.. /public'.'favicon.ico')))

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

// Get a createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");

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

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


// Write a route to handle different URL requests
server.get(The '*'.async (req, res) => {
  // Construct the context
  const context = {
    title: 'ssr test'.url: req.url // First screen address
  }
  // Render output
  try {
    const html = await renderer.renderToString(context)
    // Respond to the front end
    res.send(html)
  } catch (error) {
    res.status(500).send('Server rendering error')}})// Listen on the port
server.listen(3000.() = > {
  console.log('server running! ');

})
Copy the code

Transform vuex

Like the Vue Router, the Vuex is transformed into a factory function.

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export function createStore () {
  return new Vuex.Store({
    state: {
      count:108
    },
    mutations: {
      init (state, count) {
        state.count = count;
      },
      add (state) {
        state.count += 1; }},actions: {
      // add an asynchronous request count action
      getCount({ commit }) {
        return new Promise(resolve= > {
        setTimeout(() = > {
          commit("init".Math.random() * 100);
          resolve()
        }, 1000)})}}})}Copy the code

For vuEX data acquired asynchronously, we need to do some additional processing:

Add to the component that needs to fetch asynchronous data:

export default {
  asyncData({ store, route }) { // 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"); }}Copy the code

Server data prefetch:

// entry-server.js
import { createApp } from "./app";

export default context => {
  return new Promise ((resolve, reject) = > {
    // Take out the store and router instances
    const { app, router, store } = createApp(context);
    router.push(context.url);
    router.onReady(() = > {
      // Get the array of matching routing components
      const matchedComponents = router.getMatchedComponents();
      // If there is no match, an exception is thrown
      if(! matchedComponents.length) {return reject({ code: 404})}// Call possible 'asyncData()' on all matching routing components
      Promise.all(
        matchedComponents.map(Component= > {
          if (Component.asyncData) {
            return Component.asyncData({
              store,
              route: router.currentRoute,
            })
          }
        })
      )
      .then(() = > {
        // All prefetch hooks resolve,
        // Store has been populated with the required state of the render application
        // When state is attached to the context and the 'template' option is used for renderer,
        // The state will be automatically serialized to 'window.__initial_state__' and injected with HTML.
        context.state = store.state;
        resolve(app)
      })
      .catch(reject)
    }, reject)
  })
}
Copy the code

Mount data on the client:

// entry-client.js
/ / export store
const { app, router, store } = createApp()

// When template is used, context.state is automatically embedded in the final HTML as the window.__initial_state__ state
// Store should get the state before the client is mounted to the application:
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}
Copy the code

After rendering the first screen, if the first screen does not contain the asynchronously requested VUex data, the vuex will not be available for subsequent jumps to other pages, so the client should also obtain the state before mounting the store to the application:

// 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
      })
    }
  }
})
Copy the code

Why use SSR

The reason for introducing SSR is that we found that SPA has these disadvantages:

  • Poor SEO: A page has only one host HTML template
  • The response time on the front screen is long

In contrast, the advantages of SSR are obvious:

  • Better SEO: Fully rendered pages can be viewed directly thanks to search engine crawler scraping tools.
  • Faster time-to-content: Especially for slow network conditions or slow-running devices. There is no need to wait for all JavaScript to finish downloading and executing before the server renders the markup.

But SSR also has problems:

  • Development constraints: Browser-specific code can only be used in lifecycle hooks because there is no mount operation on the server, so the mount and subsequent lifecycle will not be used
  • There are more requirements involved in build setup and deployment: Unlike a fully static single-page application (SPA) that can be deployed on any static file server, a server rendering application needs to be in a Node.js Server runtime environment.
  • More server-side load. Rendering a complete application in Node.js is obviously more CPU-intensive (CPU-intensive-CPU intensive) than a server that merely serves static files.

Therefore, before we choose whether to use SSR, we need to carefully ask ourselves these questions:

  1. Are there only a few pages that need SEO, and can these be done using the Prerender SPA Plugin
  2. Whether the request response logic of the first screen is complex, and whether the data return is large and slow