What is Server-side rendering (SSR)?

Before the emergence of SPA(Single Page Application), web pages were rendered on the Server Side. After receiving the client request, the server splices the data and template into a complete page response to the client, and the client renders the response result. If the user needs to view a new page, this process needs to be repeated. With the rise of Angular, React, and Vue, spAs have become popular. Single-page applications can interact with the server via Ajax to update parts of the page efficiently without reloading the entire page, which makes for a great user experience. However, SPA is bad for pages that need SEO and are looking for first-screen speed. What if we want to use Vue and need to consider SEO, first screen rendering speed? Fortunately, Vue supports server-side rendering, so we’ll focus on Vue server-side rendering.

Vue SSR application scenarios and solved problems

We mainly use Vue in the management background system and the embedded H5 e-commerce page. For the management background system, there is no need to consider SEO and the first screen rendering time, so it is not a problem whether to use SPA. For e-commerce pages, SEO is not required, but the first screen rendering becomes very important. When a SPA page is opened, the general structure of HTML is as follows:


      
<html lang="zh-CN">

<head>
  <meta charset="utf-8">
  <title></title>
</head>

<body>
  <div id="app"></div>
  <script type="text/javascript" src="/app.js"></script>
</body>
</html>
Copy the code

In this case, after the HTML and JS are successfully loaded, the request is made through JS, and the content of the response is filled into the div container, which causes the problem of blank screen at the beginning of the page. Server-side rendering puts this process on the server side, where the server fills in the HTML and sends it back to the browser, which renders the entire HTML. Obviously, server-side rendering eliminates the browser loading process, solves the problem of a blank screen at the beginning of the page, and significantly improves the rendering speed of the first screen.

At present, we mainly use Vue SSR in e-commerce shopping guide page and digging customer sharing page. Next, we mainly talk about the realization of SSR.

Realize the principle of

The implementation process

As shown in the figure above, there are two entry files, Server Entry and Client Entry, which are respectively packaged by Webpack into Server Bundle for the Server and Client Bundle for the Client.

Server: When Node Server receives a request from the client, the BundleRenderer reads the Server Bundle and executes it. The Server Bundle implements the data prefetch and mounts the Vue instance that fills the data on the HTML template. BundleRenderer then renders the HTML as a string and finally returns the complete HTML to the client.

Client: After the browser receives the HTML, the Client Bundle loads and mounts the Vue instance to the static HTML returned by the server via app.$mount(‘#app’). Such as:

<div id="app" data-server-rendered="true">
Copy the code

Data-server-rendered special attributes let the client Vue know that the HTML portion was rendered by Vue on the server and that it should be mounted in activation mode.

The directory structure

.├ ─ build │ ├─ Setup-dev-server.js# Dev server-side setup adds middleware support│ ├ ─ ─ webpack. Base. Config. Js# Basic configuration│ ├ ─ ─ webpack. Client. Config. Js# Client configuration│ └ ─ ─ webpack. Server config. JsServer configuration├ ─ ─ cache_key. JsCheck whether it is fetched from the cache based on the parameter├ ─ ─ package. Json# Project dependencies├ ─ ─ process. The debug. Json# debug PM2 configuration file in the environment├ ─ ─ process. JsonPm2 configuration file in production environment├ ─ ─ server. JsExpress server entry file├─ SRC │ ├─ API │ ├─ create-api-clientThe client requests the configuration│ │ ├ ─ ─ the create - API - server. JsThe server requests the configuration│ │ └ ─ ─ index. Js# API request│ ├ ─ ─ app. JsMain entry file│ ├ ─ ─ the config# configuration│ ├ ─ ─ entry - client. JsClient entry file│ ├ ─ ─ entry - server. JsServer entry file│ ├ ─ ─ the router# routing│ ├ ─ ─ store# store│ ├ ─ ─ templates# template│ └ ─ ─ viewsCopy the code

The relevant documents

server.js

// Create an Express application
const app = express()
// Read the template file
const template = fs.readFileSync(resolve('./src/templates/index.template.html'), 'utf-8')
// Call the createBundleRenderer method of viee-server-renderer to create the renderer and set up the HTML template, which is then filled with data prefetched by the server
function createRenderer (bundle, options) {
  return createBundleRenderer(bundle, Object.assign(options, {
    template,
	 basedir: resolve('./dist'),
    runInNewContext: false}}))let renderer
let readyPromise
if(! isDev) {// In production environment, introduce server bundle generated by webpack vuE-SSR -webpack-plugin
  const bundle = require('./dist/vue-ssr-server-bundle.json')
  // Import the client build manifest object generated by vue-server-renderer/client-plugin. This object contains information about the entire WebPack build process, allowing the Bundle renderer to automatically derive what needs to be injected into the HTML template.
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  // Vue-server-renderer creates the bundle renderer and binds the server bundle
  renderer = createRenderer(bundle, {
    clientManifest
  })
} else {
  // In the development environment, dev-server is used to retrieve the bundle files from memory via a callback
  // Use webpack-dev-Middleware and webpack-hot-middleware for hot updates of client code
  readyPromise = require('./build/setup-dev-server')(app, (bundle, options) => {
    renderer = createRenderer(bundle, options)
  })
}
// Set static resource access
const serve = (path, cache) = > express.static(resolve(path), {
  maxAge: cache && isDev ? 0 : 1000 * 60 * 60 * 24 * 30
})

// Related middleware compresses response files to handle static resources, etc
app.use(...)

// Set the cache time
const microCache = LRU({
  maxAge: 1000 * 60 * 1
})

const isCacheable = req= > useMicroCache

function render (req, res) {
  const s = Date.now()

  res.setHeader('Content-Type'.'text/html')

  // Error handling
  const handleError = err= > {}
  // Get the cacheKey based on path and Query
  let cacheKey = getCacheKey(req.path, req.query)
  // Caching is enabled by default in the production environment
  const cacheable = isCacheable(req)
  if (cacheable) {
    const hit = microCache.get(cacheKey)
    if (hit) {
    // Fetch from the cache
      console.log(`cache hit! key: ${cacheKey} query: The ${JSON.stringify(req.query)}`)
      return res.end(hit)
    }
  }
  // Set the requested URL
  const context = {
    title: ' '.url: req.url,
  }
  // Render the Vue instance as a string, passing in the context object.
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return handleError(err)
    }
    res.end(html)
    // Set the cache
    if (cacheable) {
      if(! isProd) {console.log(`set cache, key: ${cacheKey}`)
      }
      microCache.set(cacheKey, html)
    }
    if(! isProd) {console.log(`whole request: The ${Date.now() - s}ms`)}}}// Start a service and listen on port 8080
app.get(The '*', !isDev ? render : (req, res) = > {
  readyPromise.then((a)= > render(req, res))
})

const port = process.env.PORT || 8080
const server = http.createServer(app)
server.listen(port, () => {
  console.log(`server started at localhost:${port}`)})Copy the code

The whole process is roughly as follows:

  1. Create the renderer, set the render template, and bind the Server Bundle
  2. A series of Express middleware is loaded in turn to compress responses, process static resources, and so on
  3. The renderer renders an instance of the loaded Vue as a string, responds to the client, and sets the cache (identified by the cacheKey)
  4. The next access is marked by the cacheKey to determine whether it is fetched from the cache

entry.server.js

import { createApp } from './app'

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

    const { url, req } = context
    const fullPath = router.resolve(url).route.fullPath

    if(fullPath ! == url) {return reject({ url: fullPath })
    }
	// Switch the route to the requested URL
    router.push(url)

	 // When the route completes the initial navigation, it can resolve all asynchronous incoming hooks and route initialization associated asynchronous components, effectively ensuring that the server and client output is consistent when the server renders.
    router.onReady((a)= > {
    // Get the matching Vue components of the route
      const matchedComponents = router.getMatchedComponents()
      if(! matchedComponents.length) { reject({code: 404})}// Execute asyncData in the matching component
      Promise.all(matchedComponents.map(({ asyncData }) = > asyncData && asyncData({
        store,
        route: router.currentRoute,
        req
      }))).then((a)= > {
        // After all preFetch hooks resolve,
        // Our store is now populated with the state needed to render the application.
        // When we append state to context,
        // When the 'template' option is used with renderer,
        // The state will be automatically serialized to 'window.__initial_state__' and injected with HTML.
		  context.state = store.state
        if (router.currentRoute.meta) {
          context.title = router.currentRoute.meta.title
        }
        // Return a fully initialized Vue instance
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

Copy the code

entry-client.js


import 'es6-promise/auto'
import { createApp } from './app'

const { app, router, store } = createApp()

Context. state is automatically embedded in the final HTML as the window.__initial_state__ state when rendered by the server. On the client side, state is window.__initial_state__ before mounting to the application.
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady((a)= > {
	// Add a routing hook function to handle asyncData.
	// execute after initial route resolve,
	// So that we do not 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 previously unrendered components
      	 // So we compare them to find the different components of the two matching lists
    let diffed = false
    const activated = matched.filter((c, i) = > {
      returndiffed || (diffed = prevMatched[i] ! == c) })const asyncDataHooks = activated.map(c= > c.asyncData).filter(_= > _)
    if(! asyncDataHooks.length) {return next()
    }

    Promise.all(asyncDataHooks.map(hook= > hook({ store, route: to })))
      .then((a)= > {
        next()
      })
      .catch(next)
  })

	// Mount to DOM
  app.$mount('#app')})Copy the code

Problems encountered

1. Local storage

In the past, we used to use localStorage and sessionStorage to store some information locally, and sometimes we need to bring these information when we initiate a request. However, when using SSR, we send a request to get data in the asyncData hook, but cannot get the localStorage object under the window object. We store the information in a cookie, which is retrieved via req.headers when asyncData retrieves the data.

2. Avoid differences between the server and browser

This problem is similar to the first one. The biggest difference between a server and a browser is whether there are window objects. We can use judgment to avoid:

// Solve the 300ms delay problem on the mobile terminal
if (typeof window! = ="undefined") {
  const Fastclick = require('fastclick')
  Fastclick.attach(document.body)
}
Copy the code

A better solution would be in entry-client.js:

import FastClick from 'fastclick'

FastClick.attach(document.body)
Copy the code

3. not matching

[vue warn]The client-side rendered virtual DOM tree is not matching server-rendered content
Copy the code

The problem is caused by inconsistent HTML rendered by the server and the client. This is most likely due to extra Spaces in {{MSG}}, which we’ll try to avoid using in template.