background

Spa single page SEO is not friendly, because vUE has only one HTML page, the page switch is realized by listening to the router for routing distribution, combined with Ajax loading data for rendering, but the search engine crawler can not recognize JS, so there will be no good ranking. But usually the mobile terminal does not need to do SEO optimization, party A so requirements, said that now there is also a realization on the market, so also by the way.

What is SSR (Server Side Rendering)?

The simple idea is to pass a component or page through the server to generate AN HTML string, send it to the browser, and finally “blend” the static markup into a fully interactive application on the client side

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, SSR can also render the same component as HTML strings on the server side, send them directly to the browser, and finally “activate” those 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. Isomorphism, in layman’s terms, is a set of VUE code that runs once on the server and again on the browser client. The VUE component renders to HTML strings (i.e. page structures) on the server side and manipulates the DOM on the client side.

When we open the browser’s Network, we see the initialized rendered HTML, and the structure we want to initialize, completely independent of the client-side JS file. If you look more closely, there’s an initialized DOM structure, there’s CSS, and there’s a Script tag. The script tag mounts the window to the data we received at the server entry. Originally just a pure static HTML page ah, there is no interaction logic, so ah, now know why the server run a vUE client run a VUE, the server vUE is just mixed with a data render a static page, the client VUE is to achieve interaction!

SSR pros and cons

advantages

  • Better SEO

Because the content of the SPA page is obtained through Ajax, and the search engine crawl tool does not wait for the Ajax asynchronous completion of the page content to be captured, so the content of the page obtained through Ajax cannot be captured in SPA. SSR is directly returned by the server to the rendered page (data has been included in the page), so the search engine crawler can grab the rendered page;

  • Better for first screen rendering

The rendering of the first screen is an HTML string sent from Node, not a JS file, which makes it easier for the user to see the content of the page. Especially for large single-page applications, the file volume is relatively large after packaging, and it takes a long time for ordinary clients to render and load all required files. The home page will have a long white screen waiting time.

Disadvantages and constraints

  • Since there is no dynamic update, of all the lifecycle hook functions, only beforeCreate and Created are called during server-side rendering (SSR). This means that any code in other lifecycle hook functions (such as beforeMount or Mounted) will only be executed on the client side.
  • If you use third-party apis in the beforeCreat and Created hooks, it is normal and reasonable to make sure that the API runs on the Node side without errors, such as initializing a data request in the Created hook. However, using XHR alone will cause problems in node rendering, so use axios, a third-party library supported by both the browser and server.
  • When writing client-only code, we are used to evaluating the code in a new context every time. However, the Node.js server is a long-running process. When our code enters the process, it takes a value and keeps it in memory. This means that if you create a singleton object, it will be shared between each incoming request. So for server-side rendering, we also want each request to be a new, separate application instance so that there is no cross-request state pollution. Therefore, factory functions should be used to ensure independence between each request

Principle of SSR

  • On the left is the source code. App.js is shared by the server/client and is not executed in production.
  • On the right is the compiled file, two packaged files.
  • One copy runs on the server side, where a packaged rendering function renders the HTML page required by the output browser
  • One runs on the browser side and interacts with the HTML page to determine whether the data returned by the current server is the data corresponding to the current URL page. If not, it asks the server to render again; if so, it takes over the page in normal VUE architecture

According to the trigger time of the application, we can divide into the following steps to explain how SSR works in detail:

Compilation phase

Vue-ssr provides the means to configure two entry files (entry-client.js, entry-server.js) and compile your code into two bundles via Webpack.

  • Server Bundle forvue-ssr-server-bundle.json:
  • The Client Bundle forvue-ssr-client-manifest.json

Initialization (vue-ssR-server-bundle. json) :

  • The renderer singleton object is created by the createBundleRenderer function of the Ue -server- Renderer library. The function takes two arguments, ServerBundle content and options configuration
  • Get the serverBundle entry file code and parse it into entry functions, instantiating the Vue object each time
  • Instantiate render and templateRenderer objects, responsible for rendering vue components and assembling HTML

Render phase :(execute vue-ssr-server-bundle.json)

  • When a user requests to the node, called bundleRenderer. RenderToString function is introduced into user context and context, the context object can contain some information from the server, such as: url, ua, etc., can also contain user information. Instantiate the VUE object by executing the application entry function derived from serverBundle.
  • The Renderer object is responsible for recursively converting vUE objects into VNodes, and the vNodes call different rendering functions based on different node types to assemble HTML.
  • When template is used, context.state is used aswindow.__INITIAL_STATE__The state is automatically embedded in the final HTML and returned to the client. On the client side, store should get the state before it is mounted to the application:

Content output stage:

  • In the last stage, we have got the rendering result of vUE component, which is an HTML string. We also need the import label of CSS, JS and other dependent resources and render data on the server side to display the page in the browser. These are finally assembled into a complete HTML message and output to the browser.

Client phase (get execute client code vue-ssR-client-manifest.json) :

  • When the client initiates the request and the server returns the rendering result and the CSS is loaded, the user can already see the page rendering result without waiting for the JS to load and execute. There are two kinds of data output by the server side, one is the result of the page rendered by the server side, and another is the state of the data output by the server side to the browserwindow.__INITIAL_STATE__.
  • The data must be synchronized to the browser; otherwise, the status of the components on both ends will be inconsistent. We usually use vuex to store these data states. Previously, the state of vuex was copied to the context.state of the user context after rendering on the server.context.state = store.state
  • When the client starts executing JS, we can read the data state from the window global variable and replace it with our own data statestore.replaceState(window.__INITIAL_STATE__);Implement both the server and the clientstoreData synchronization
  • After that, before we call $mount to mount the Vue object, the client compares the DOM generated by the server to the one it is about to generate (vuex Store data synchronization is necessary to keep it consistent).
  • Call if they are the sameapp.$mount('#app')By mounting the client vUE instance to this DOM, the server rendered HTML is “activated” and becomes a DOM dynamically managed by the VUE in response to subsequent changes in data, meaning that all interactions and jumps between vue-Router pages are then run on the browser side.
  • If the virtual DOM tree vDOM constructed by the client is inconsistent with the HTML structure returned by the server rendering, the client will request the server to render the entire application again, which makes the SSR ineffective and fails to achieve the purpose of server rendering.

V1

SSR has two import files, namely the import file of the client and the import file of the server. Webpack is packaged into the Server bundle and the client bundle for the client through the two import files. When the server receives the request from the client, it creates a renderer called bundleRenderer. The bundleRenderer reads the generated Server bundle file, executes its code, and sends the generated HTML to the browser. After loading the Client bundle, the client learns to encounter the DOM generated by the server (if the DOM is the same as the one he is about to generate, mount the client’s vue instance to the DOM)

V2

Our Server code and Client code are respectively packaged by Webpack to generate Server Bundle and Client Bundle. The former will run on the Server and generate pre-rendered HTML strings through Node, which will be sent to our Client for initial rendering. The client bundle is free, and the initial rendering is completely independent of it. After receiving the HTML string returned by the server, the client “activates” the static HTML into a DOM dynamically managed by the Vue in response to subsequent changes in data.

V3

  • First, our entry-server will get the component matched by the current router, call the asyncData method on the component, store the data in the server vuEX, and then pass the data in the server VUEX to our context.
  • The Node.js server sends the HTML string needed for the first screen rendering to our client via renderToString, which is mixed in with window.INITIAL_STATE to store our server vuex data.
  • After entry-client, the data obtained by the server during rendering is written to the VUEX of the client.
  • Finally, the client and server components do the diff, updating the status update components.

V4

The client requests the server, and the server gets the matching component based on the requested address. When it calls the matching component, it returns a Promise (officially asyncData method) to get the data it needs. Finally, window.__initial_state=data is used to write it to the web page, and finally the rendered web page of the server is returned. The client then replaces the original store state with the new store state to ensure data synchronization between the client and server. When a component is not rendered by the server, send an asynchronous request for data

Rendering as HTML strings on the server is possible thanks to the vUE component structure being based on VNode. A VNode is an abstract representation of the DOM. It is not a real DOM, but a tree of JS objects, each node representing a DOM. Because of VNode, vUE can parse JS objects into HTML strings on the server side

Also, on the client side, vnode is stored in memory, so memory operation is always much faster than DOM operation. When the DOM needs to be updated every time data changes, the old and new VNode trees go through the DIff algorithm to calculate the minimum change set, which greatly improves the performance.

Create vue instance (main.js/app.js)

App.js is our general entry, and its purpose is to build an instance of Vue to be used by both the server and the Client. Note that in the pure Client application our app.js will mount the instance into the DOM, while in SSR this part of the function is done in the Client Entry.

When writing client-only code, we are used to evaluating the code in a new context every time. However, the Node.js server is a long-running process. When our code enters the process, it takes a value and keeps it in memory. This means that if you create a singleton object, it will be shared between each incoming request.

As shown in the basic example, we create a new root Vue instance for each request. This is similar to each user using a new instance of the application in their own browser. If we use a shared instance between multiple requests, we can easily lead to cross-request state pollution.

Therefore, instead of creating an application instance directly, we should expose a factory function that can be executed repeatedly, creating new application instances for each request. App.js simply uses export to export a createApp function:

// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

export function createAppConst router = createRouter() const store = createStore() // Synchronize route state to store Const app = new Vue({router, store, render: H => h(App)}) // Expose App, Router, and Store.return { app, router, store }
}
Copy the code

Server entry file (entry-server.js)

During server-side rendering (SSR), we are essentially rendering a “snapshot” of our application, so if the application relies on some asynchronous data, it needs to be prefetched and parsed before starting the rendering process.

Server Entry is a function that is exported using export. It is mainly responsible for calling the data acquisition method defined in the component, obtaining the data required for SSR rendering, and storing it in the context. This function is called repeatedly in every server render.

All you need to do here is get the component that matches the current route and call a method defined in the component (asyncData) to get the initial rendered data. This method is also very simple to call our vuex Store method to get the data asynchronously.

The main job of this file is to take the context parameter passed from the server (server.js). The context contains the url of the current page, use getMatchedComponents to get the components of the current URL, return an array of components, and walk through the array. If the component has an asyncData hook function, it passes store for data and returns a Promise object. Context. state = store.state is used to mount the server data to the context object, and the server’s pre-rendered data will be stored in window.INITIAL_STATE. Store. ReplaceState (window.__initial_state__) is then called in client-entry.js to ensure that the client and server have the same state.

At entry – server. Js, we can through the routing and the router. GetMatchedComponents () that match the component, if the component exposed asyncData (define specifically request data using in the component), we call this method. Then we need to append the parsing state to the render context

// entry-server.js
import { createApp } from './app'// Context is the user context objectexportDefault context => {// Since it might be an asynchronous routing hook function or component, we will return a Promise so that the server can wait for everything to be ready before rendering.returnnew Promise((resolve, reject) => { const { app, Router.push (context.url) // Wait until the router has finished parsing possible asynchronous components and hook functions router.onReady(() => {/ / we can be gained through the routing and the router. GetMatchedComponents () the component matching the const matchedComponents. = the router getMatchedComponents () / / Reject the route that cannot be matched, and return 404if(! matchedComponents.length) {returnreject({ code: 404})} // Call 'asyncData()' on all matching routing components to request backend data // All requests in parallel execute promise.all (matchedComponents. Map (Component => {if (Component.asyncData) {
          returnComponent.asyncData({ store, route: Router.currentroute})}})).then(() => {// After all preFetch hooks resolve, // our store is now populated with the state needed to render the application. // When we attach the state to the context, // and the 'template' option is used for renderer, // the state is automatically serialized to 'window.__INITIAL_STATE__' and injected into HTML. __INITIAL_STATE__ is what window.__initial_state__ is. // State = store.state // Promise should resolve application instance, So that it can render resolve(app)}). Catch (reject)}, reject)})}Copy the code

Client entry file (entry-client.js)

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

// entry-client.js

import { createApp } from './app'
import Vue from 'vue'

Vue.mixin({
  beforeRouteUpdate(to, from, next) {
    const {
      asyncData
    } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else{ next() } } }) const { app, router, Store} = createApp() // Write the server render state to vuex // replace the entire state with window.__initial_state__ data, so that when the server finishes rendering, The client is also free to manipulate data in state. // If the server vuEX data changes, the client data will be replaced to ensure data synchronization between the client and the serverif(window.__initial_state__) {store.replacestate (window.__initial_state__)} // Client Data Fetching (Client Data Fetching) // Router.onready (() => {// Add a route hook function for asyncData. // Execute after the initial resolve route so that we don't double-fetch the existing data. // Use 'router.beforeresolve ()' to ensure that all asynchronous components are resolved. router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // We only care about non-prerendered components // so we compare their DOM structures to see if they are consistentlet diffed = false
    const activated = matched.filter((c, i) => {
      returndiffed || (diffed = (prevMatched[i] ! == c)) })if(! activated.length) {returnNext ()} // If there is a loading indicator, the server will send an asynchronous request for data when it sees a component that is not rendered by the server. The client will request the server to render the entire application promise.all (activated. Map (c => {if (c.asyncData) {
        returnc.asyncData({ store, route: To})}}).then(() => {// Stop loading indicator next()}).catch(next)}) Client-side activation refers to the process in which the Vue takes over static HTML sent by the server at the browser side and turns it into a dynamic DOM managed by the Vue. app.$mount('#app')})Copy the code

Questions about store.replacestate (window.__ INITIAL_STATE__)

Why should I replace states? I don’t think the state will change after the server renders.

As mentioned earlier, because app.js, vue components and so on are executed once on the server side, they are executed again on the client side. Therefore, when the server gets the store and gets the data, the JS code of the client executes again and creates an empty store during the execution of the client code. The data of the two stores cannot be synchronized.

If the server and the client’s data store are not synchronized, the client builds the virtualDOM tree virtualDOM and the HTML structure returned by the server rendering is inconsistent, at this time, the client will request the server to render the entire application again, which makes the SSR ineffective and can not reach the purpose of the server rendering

Water injection and dehydration of data

How do you synchronize the data between the two stores?

  • After fetching data at the server, the server’s Store data is injected into the Window global environmentcontext.state = store.stateThat is, context.state will be the window.INITIAL_STATEThe state is automatically embedded in the final HTML and returned to the client
  • Then there is the dehydration treatment: on the client side, throughstore.replaceState(window.__INITIAL_STATE__)Replace the vuex Store data sent from the server with the current vuex Store data of the client to achieve data synchronization between the client and the server.

Create a server renderer (server.js)

// server.js

const Vue = require('vue')
const express = require('express')
const path = require('path')
const LRU = require('lru-cache')
const resolve = file => path.resolve(__dirname, file)
const { createBundleRenderer } = require('vue-server-renderer')
const fs = require('fs')
const net = require('net')
const http = require('http');
const compression = require('compression');


const template = fs.readFileSync('./src/index.template.html'.'utf-8')
const isProd = process.env.NODE_ENV === 'production'Const server = express() // Returns the server rendering functionfunction createRenderer (bundle, options) {
  returnCreateBundleRenderer (bundle, object. assign(options, {// template HTML file is index.template. HTML template: template, cache: LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), basedir: resolve('./dist'),
    runInNewContext: false}}))let renderer;

let readyPromise
if (isProd) {
  const bundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  renderer = createRenderer(bundle, {
    clientManifest
  })
} else {
  readyPromise = require('./build/setup-dev-server')(server, (bundle, options) => {
    renderer = createRenderer(bundle, options)
  })
}


const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})
server.use(compression())
server.use('/dist', serve('./dist'.true))
server.use('/static', serve('./src/static'.true))
server.use('/service-worker.js', serve('./dist/service-worker.js'))


server.get(The '*', (req, res) => {
  const context = {
    title: 'Netease Yan Xuan',
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return} // Return server-side rendered HTML to client res.end(HTML)})})function probe(port, callback) {
    let servers = net.createServer().listen(port)
    let calledOnce = false
    let timeoutRef = setTimeout(function() {
        calledOnce = true
        callback(false, port)
    }, 2000)
    timeoutRef.unref()
    let connected = false

    servers.on('listening'.function() {
        clearTimeout(timeoutRef)
        if (servers)
            servers.close()
        if(! calledOnce) { calledOnce =true
            callback(true, port)
        }
    })

    servers.on('error'.function(err) {
        clearTimeout(timeoutRef)
        let result = true
        if (err.code === 'EADDRINUSE')
            result = false
        if(! calledOnce) { calledOnce =true
            callback(result, port)
        }
    })
}
const checkPortPromise = new Promise((resolve) => {
    (function serverport(_port = 6180) {
        // let pt = _port || 8080;
        let pt = _port;
        probe(pt, function(bl, _pt) {// The port is occupiedfalse// _pt: incoming port numberif (bl === true) {
                // console.log("\n Static file server running at" + "\n\n=> http://localhost:" + _pt + '\n');
                resolve(_pt);
            } else {
                serverport(_pt + 1)
            }
        })
    })()

})
checkPortPromise.then(data => {
    uri = 'http://localhost:' + data;
    console.log('Start service path'+uri)
    server.listen(data);
});

Copy the code

router.js

Like createApp, we also need to give each request a new Router instance, so the file exports a createRouter function

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter () {
  return new Router({
    mode: 'history'/ / insteadhistoryMode routes: [{path:'/', component: () => import('./components/Home.vue') },
      { path: '/item/:id', component: () => import('./components/Item.vue')}]})}Copy the code

store.js

During server-side rendering (SSR), we are essentially rendering a “snapshot” of our application, so if the application relies on some asynchronous data, it needs to be prefetched and parsed before starting the rendering process.

Another concern is that on the client side, you need to get exactly the same data as the server-side application before mounting it to the client-side application – otherwise, the client application will use a different state than the server-side application and then cause the mixing to fail.

To solve this problem, the retrieved data needs to be located outside the view component, that is, in a dedicated 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.

To do this, we will use the official state management library Vuex. We’ll create a store.js file that will simulate some logic to get an item by ID:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex) import {fetchItem} from, assuming we have a generic // API that returns promises'./api'

export function createStore () {
  returnnew Vuex.Store({ state: { items: {} }, actions: {fetchItem ({commit}, id) {// 'store.dispatch()' returns a Promise, // so we know when the data will be updatedreturn fetchItem(id).then(item => {
          commit('setItem', { id, item })
        })
      }
    },
    mutations: {
      setItem (state, { id, item }) {
        Vue.set(state.items, id, item)
      }
    }
  })
}
Copy the code

Component item.vue with logical configuration

So where do we put the code for the “Dispatch data prefetch Action”?

We need to access the route to determine which parts of the data to fetch – this also determines which components to render. In fact, the data required for a given route is also the data required to render components on that route. So it is natural to place data prefetch logic in the routing component.

We will expose a custom static function asyncData on the routing component. Note that since this function is called before the component is instantiated, it does not have access to this. We need to pass store and route information as parameters:

What is syncData for? This function is used to create data for new applications. If you create a new function for new applications, you need to create a new function for new applications. Although beforeCreate and Created are also executed on the server (other periodic functions are only executed on the client), we all know that the request is asynchronous, which means that after the request is sent, the rendering will end before the data is returned, so we can’t render the data returned by Ajax. So you need to find a way to wait until all the data is returned before rendering the component

<! -- Item.vue -->
<template>
  <div>{{ item.title }}</div>
</template>

<script>
export default {
  asyncData ({ store, route }) {
    // When the action is triggered, a Promise is returned
    // fetchItem is an API for getting background data
    return store.dispatch('fetchItem', route.params.id)
  },
  computed: {
    // Get the item from the store state object.
    item () {
      return this.$store.state.items[this.$route.params.id]
    }
  }
}
</script>
Copy the code

conclusion

Vuex role

Vuex is not necessary. Vuex is the key to state sharing between our client and server. We don’t need to use Vuex, but we need to implement a set of data prefetch logic.

  • It’s a headache not to use Vuex, but it gives me some inspiration. How do we deal with communication between components when we develop projects? One is Vuex and the other is EventBus, and EventBus is an instance of Vue.
  • The author’s idea here is to create an instance of Vue as a warehouse, so that we can use the data of this instance to store our prefetch data, and use the methods in Methods to do asynchronous data acquisition, so that we only need to call this method in the components that need prefetch data
  • There is another idea that I learned from other people’s blogs when I was studying: vuex store and some API supporting server rendering were only used, instead of action and mutation, data was manually written into the state

Refer to the article

  • Examples of SSR – VUE in Jianshu Douban
  • Let vue-CLI initialization project integration support SSR, two solutions
  • “Front-end about SEO personal understanding and transformation optimization combat (based on VUE-SSR)”
  • Decrypting Vue SSR
  • Vue-ssr Official Demo
  • Zhihu In-depth Analysis of the IMPLEMENTATION process of SSR Principle
  • From the Ground up: Understanding Server-side Rendering from the ground up