Why use SSR?

In the traditional vue single-page application, the rendering of the page is done by JS. As shown in the figure below, in the HTML file returned by the server, there is only a div tag and a script tag in the body, and the rest of the DOM structure of the page is generated by bundle.js.

This makes it impossible for search engine crawlers to crawl the content of the page, and if SEO is important to your site, you may need server-side rendering (SSR) to solve this problem.

In addition to SEO, the use of SSR also speeds up the first screen rendering because the server returns the HTML of the rendered page directly and can see the fully rendered page without js. This part of the code is small compared to the usually large JS file in a single-page application, so the first screen arrival time is faster and the white screen time is shorter.

Of course, there are some limitations to the use of SSR. First of all, development conditions are limited. In server-side rendering, life cycle hooks other than created and beforeCreate are not available. Second, more server side load, rendering a complete application on the server side, obviously consumes more CPU resources than a server that merely serves static files. In addition, SSR has more requirements in terms of deployment. Unlike a completely static single-page application (SPA), which can be deployed on any static file server, a server rendering application needs to be in the node.js runtime environment. Therefore, when it comes to the selection of SSR technology, we should consider its advantages and disadvantages comprehensively to see whether it is necessary to use.

Ii. Realization of basic functions

The essence of SSR is that the server returns a rendered HTML document. We start a server in the project root directory and return an HTML document. Here we use KOA as the server side framework.

//server.js const Koa = require('koa') const router = require('koa-router')() const koa = new Koa() koa.use(router.routes()) router.get('/',(ctx)=>{ ctx.body = `<! DOCTYPE HTML >< HTML lang="en"> <head><title>Vue SSR</title></head> <body> <div>This is a server render page</div> </body> </html>` }) koa.listen(9000, () => { console.log('server is listening in 9000'); })Copy the code

Start the server from the command line: node server.js, and then go to http://localhost:9000/ in the browser. The server returns the following, and the browser will render the page according to the HTML.

vue-server-renderer

Of course, the HTML string to be returned can be generated by the Vue template. This requires the Vue-server-renderer, which generates the HTML string based on the Vue instance. It is the core of the VUE SSR. Make a few changes in server.js above:

const Koa = require('koa') const router = require('koa-router')() const koa = new Koa() koa.use(router.routes()) const Vue = require('Vue'); Const renderer = require('vue-server-renderer').createrenderer () // Create a renderer instance const app = new Vue({ / / create instances template: Vue ` < div > {{MSG}} < / div > `, data () {return {MSG: 'This is renderred by Vue-server-renderer '}}}) router-. get('/',(CTX)=>{// Call the renderToString method on the renderer instance, Render the Vue instance as a string // This method takes two arguments, the first is the Vue instance and the second is a callback function, Renderer.rendertostring (app, (err, HTML) => {// Pass the rendered string as the second argument of the callback function to ctx.body = '<! DOCTYPE HTML >< HTML lang="en"> <head><title>Vue SSR</title></head> <body> ${HTML  }) }) koa.listen(9000, () => { console.log('server is listening in 9000'); })Copy the code

Restart the server and then access:

In this way, we have completed an extremely basic Vue SSR. But not very practical, we in the actual project development, it is impossible to write this, we will modular build the project, and then through the packaging tool packaged into one or more JS files.

Use it more formally

Build a modular VUE project

We built a simple VUE project modularized, using vue-Router to manage routing.

SRC /main.js import Vue from 'Vue' import App from './ app.vue 'import router from './router' Vue.config.productionTip = false new Vue({ el: '#app', router, render: h => h(App) })Copy the code
//  src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
<style lang="less">
#app{
  margin: 0 auto;
  width: 700px;
  #nav{
    margin-bottom: 20px;
    text-align: center;
  }
}
</style>
Copy the code
// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import Home from '.. /views/Home.vue' import About from '.. /views/About.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About } ] export default new VueRouter({ mode: 'history', routes })Copy the code
// src/views/Home.vue
<template>
  <div class="home">
    <h1>This is home page</h1>
  </div>
</template>
Copy the code
// src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>
Copy the code

With SRC /main.js as the entry file, package it as a single page on the client side, and open it in the browser. The render result is as follows:

Convert the project to server-side rendering

Let’s transform the above demo into a server rendering.

Main modification point: Server rendering requires a Vue instance. Every time a client requests a page, server rendering uses a new Vue instance. Different users cannot access the same Vue instance. So the server needs a factory function that generates an instance of Vue for each rendering.

Create a new entry file entry.server.js dedicated to server-side rendering:

Import {createApp} from './main' export default context => {// generate Vue instance factory function,  return new Promise((resolve, Reject) => {const app = createApp() const router = app.$router const {url} = context // Context contains some data that the server needs to pass to the Vue instance. Const {fullPath} = router.resolve(url).route if(fullPath! == url){// Determine whether the current route exists in the Vue instance. Return reject({url: FullPath})} router.push(URL) // Set the current route of the Vue instance router.onReady(() => {const matchedComponents = The router. GetMatchedComponents () / / is corresponding to determine whether the current routing component if (! Matchedcomponent.length){return reject({code: 404})} resolve(app) // Return Vue instance}, reject)})}Copy the code

SRC /main.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'

Vue.config.productionTip = false

export function createApp(){
  const router = createRouter()
  const app = new Vue({
    router,
    render: h => h(App)
  })
  return app
}
Copy the code

The WebPack configuration, based on the entry.server.js package, also needs to be modified:

target: 'node', entry: './src/entry.server.js', output: { path: path.join(__dirname, '.. /dist'), filename: 'bundle.server.js', libraryTarget: 'commonjs2' },Copy the code

Then, on the server side, we can render the server side using the bundled bundle.server.js.

Const renderer = require('vue-server-renderer').createrenderer ({// create a renderer instance template based on the template: require('fs').readFileSync('./index.template.html', }) const app = require('./dist/bundle.server.js').default // import Vue instance factory function router.get('/(.*)', async (CTX, Next) => {const context = {// get the route to pass to Vue instance URL: } let htmlStr await app(context).then(res => {// generate Vue instance, Renderer. RenderToString (res, context, (err, HTML)=>{if(! err){ htmlStr = html } }) }) ctx.body = htmlStr });Copy the code

As you can see, here we have finished rendering on the server side, and the page DOM structure appears in the HTML document returned by the server.

Client Activation

We’ve already seen glimpses of using SSR in real projects, but this is only the first step. Now that every click on Home/About requests HTML resources from the server, the single-page front-end routing advantage is not taken advantage of. Next we’ll add a client activation step to give web applications the benefit of a single page. This is also the official Vue SSR process.

Client activation refers to the process in which Vue takes over the static HTML sent by the server at the browser side and turns it into a dynamic DOM managed by Vue. For details about activation principles, see the official website.

It is simple to add the client bundle to the returned HTML page, which is used to manage the current HTML on the client side. Let’s package it and build it.

Create a new entry.client.js:

import { createApp } from './main' const app = createApp() const router = app.$router router.onReady(() => { App. $mount('#app') // The server render will generate a div with the id of app by default})Copy the code

Packaged WebPack configuration:

entry: './src/entry.client.js', output: { path: path.join(__dirname, '.. /dist'), filename: 'bundle.client.js' },Copy the code

Once packaged, we add bundle.client.js to the HTML, which we previously rendered based on the template:

Const renderer = require('vue-server-renderer').createrenderer ({// create a renderer instance based on the template: require('fs').readFileSync('./index.template.html', 'utf-8') })Copy the code

So just add bundle.client.js to index.template-html.

//index.template.html <! DOCTYPE html> <html lang="en"> <head><title>Vue SSR</title></head> <body> <! --vue-ssr-outlet--> </body> <script src="bundle.client.js"></script> </html>Copy the code

Restart the service, access it again, and you can see that when you click Home/About to switch routes, the HTML document is no longer requested from the server.

Request data

In a real project, the page is often rendered with data requested from the interface, and we will render the page with the requested data. For the sake of convenience (save trouble), instead of writing another data interface, we went to request the top 20 movie data of Douban.

The specific idea is that if a component needs to request data, when it is rendered on the server side, we can request data on the server side, when the client uses the component in the WAY of SPA routing switch, we can also send ajax request data on the client side.

We’re going to do that with vuex. Because it mounts the data on the Vue instance, passing access data is really convenient.

Request data on the server side

Recall our scenario where the client renders regular request data, sending the Ajax request in the Created or Mounted hook function and writing the returned data to the instance’s data when the request succeeds. This can’t be done with SSR request data, because Ajax requests are asynchronous requests. After the request is sent, the back-end will be rendered before the data is returned, and the ajax request data cannot be filled into the page.

So we get the data directly from the server by sending a request, that is, one server sending an HTTP request to another server, as opposed to the client sending a request to the server. In this case, we use Axios, which supports both.

For each component that needs to request data, we will expose a custom static method asyncData on the component. Since this function is called before the component is instantiated, it cannot access this. You need to pass the store and routing information as parameters.

//Home.vue <template> <div class="movie-list"> <div v-for="(item, index) in list" class="movie"> <img class="cover" :src="item.cover"> <p> <span class="title">{{item.title}}</span> <span  class="rate">{{item.rate}}</span> </p> </div> </div> </template> <script> export default { name: 'MovieList', asyncData ({store,route}) {// custom static method asyncData return store.dispatch('getTopList')}, /***** here, Executing asyncData calls getTopList to request data and update it to the $store.state of the vue instance. Actions: { getTopList (store) { return top20().then((res) => { store.commit('setTopList', res.data.subjects) }) } } *****/ computed: { list () { return this.$store.state.topList } }, created () { if(! this.$store.state.topList){ this.$store.dispatch('getTopList') } } } </script>Copy the code

In entry. Server. Js, we obtained with the router. Through routing getMatchedComponents () that match the component, if the component exposed asyncData, this method is invoked. Then we need to append the parsed state to the Render context.

import { createApp } from './main' export default context => { return new Promise((resolve, reject) => { const app = createApp() const router = app.$router const store = app.$store ... router.onReady(() => { const matchedComponents = router.getMatchedComponents() if(! matchedComponents.length){ return reject({ code: 404})} promise.all (matchedComponent.map (Component => {if (component.asyncData) {// call this method if the Component exposes asyncData // In this case, $store.state return Component.asyncData({store, route: Router.currentroute})}}).then(() => {context.state = store.state // assign app.$store.state to the context context.state, This will be used later when synchronizing data to the client. resolve(app) }).catch(reject) }, reject) }) }) }Copy the code

When the data is updated to app.$store.state, the server renders the data in the HTML. But the page is blank and the Ajax request is sent. The reason is that when the client is activated it is actually rendered a second time. That is, after bundle.client.js is loaded and executed, the page is rendered again by bundle.client.js. Usually, the rendering result is the same as before, so it is not noticed.

Avoid repeated requests from clients for data

This is cross-domain, so the Ajax request did not succeed. If the page does not cross domains, the content is rendered from the data that the client sends to ajax. However, the data that has been requested on the server should be avoided on the client. How to synchronize the data to the client?

When using a template, context.state is used aswindow.__INITIAL_STATE__State, automatically embedded in the final HTML. When the client is activated, the vm.$store on the client should be fetched before it is mounted to the applicationwindow.__INITIAL_STATE__State.

In server.js, add a second argument, context, to the renderer. RenderToString method. Context. state will be automatically embedded in the final HTML as window.__initial_state__ state.

router.get('/(.*)', async (ctx, next) => { const context = { url: ctx.url } let htmlStr await app(context).then( res => { renderer.renderToString(res, context, (err, HTML)=>{// Add the second argument context if(! err){ htmlStr = html } }) }) ctx.body = htmlStr });Copy the code

  1. Modify theentry.client.js:
import { createApp } from './main' const app = createApp() const router = app.$router const store = app.$store if (window.__initial_state__) {// If window.__initial_state__ has content, $store store.replacestate (window.__initial_state__)} router-.onready (() => {app.$mount('#app')})Copy the code

This prevents the client from requesting the data repeatedly, and the end result is as follows: You can see that the client is not sending the Ajax request.

This project is very simple to set up. The main task is to sort out the principle and process of Vue SSR. Nuxt.js, a relatively mature framework, can be selected for actual development.

Project address: github.com/alasolala/v…

The resources

Decryption Vue SSR

Vue/VUE – CLI + Express

Vue SSR Guide