SSR (Server Side Render)

The server renders the Vue components as HTML strings, sends the HTML strings directly to the browser, and finally “activates” these static tags into fully interactive applications on the client side.

advantages

  • Better SEO, thanks to search engine crawler crawler tools can view fully rendered pages directly
  • Faster content arrival time

disadvantages

  • Development conditions are limited. (The server only executes beforeCreated and Created lifecycle functions and has no Window, DOM, BOM, etc.).
  • More requirements related to build setup and deployment need to be in the Node Server running environment
  • More server side load

SSR essence

  • The server renders the Vue component as an HTML string and sends the HTML string directly to the browser
  • Separate application instances so that there is no state contamination from cross requests

Demo can be performed in the following situations

  • Render the Vue component directly as an HTML string and return it to the browser
  • Introduces server rendering of the Router
  • Server-side rendering that needs to initialize data (a full Vue SSR)

Start by creating a simple VUE projectCode Address 01

| - components / / subcomponents | | - Foo vue | | - Bar. The vue | | - App. Vue / / root component | -- index. Js / / entry file | -- webpack. Config. JsCopy the code

The code is very simple and is a very ordinary VUE project (including some click events, data binding), typical client rendering.

Render the Vue component directly as an HTML string and return itCode address 02/demos

When I first started working on Web development, I used HTML pages as templates, stuffing back-end data into templates, such as.php and.jsp files. ArtTemplate, EJS and so on are used in combination with Node.

Vue server rendering is also divided into two steps:

  1. Parse Vue files (template files) into HTML, CSS, JS static files
  2. Return the static file to the client

Vue server-renderer can render vue instances directly into Dom tags

demo1

   const Vue = require('vue')
   const app = new Vue({
     template: `<div>Hello World</div>`
   })
   
   // Step 2: Create a renderer
   const renderer = require('vue-server-renderer').createRenderer()
   
   // Step 3: Render the Vue instance as HTML
   renderer.renderToString(app, (err, html) => {
     if (err) throw err
     console.log(html)
     // => <div data-server-rendered="true">Hello World</div>
   })
   
   // In 2.5.0+, if no callback function is passed in, a Promise is returned:
   renderer.renderToString(app).then(html= > {
     console.log(html)
   }).catch(err= > {
     console.error(err)
   })
Copy the code

demo2

In combination with the server, the HTML page is returned on request

   const Vue = require('vue')
   const Koa = require('koa');
   const Router = require('koa-router');
   const renderer = require('vue-server-renderer').createRenderer()
   
   const app = new Koa();
   const router = new Router();
   
   router.get(The '*'.async (ctx, next) => {
     const app = new Vue({
       data: {
         url: ctx.request.url
       },
       template: '
      
accesses the URL: {{URL}}
'
}) renderer.renderToString(app, (err, html) => { if (err) { ctx.status(500).end('Internal Server Error') return } ctx.body = ` <! DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> ` }) }) app .use(router.routes()) .use(router.allowedMethods()); app.listen(8080, () = > {console.log('listen 8080')})Copy the code

As you can see from Demo1, vue-server-renderer returns an HTML fragment called markup, not a complete HTML page. We have to wrap the generated HTML tags around the container with an additional HTML page as we did in Demo2.

We can provide a template page. For example,


      
<html lang="en">
 <head><title>Hello</title></head>
 <body>
   <! --vue-ssr-outlet-->  
 </body>
</html>
Copy the code

Note that
comments This is where the APPLICATION’s HTML tags will be injected. This is provided by the plug-in, and if you don’t use
Can also be, that is to go to a simple processing of their own. Such as demo3

demo3

 
      
   <html lang="en">
     <head><title>Hello</title></head>
     <body>
       {injectHere}
     </body>
   </html>
Copy the code
demo3.js
const template = require('fs').readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8')
ctx.body = template.replace('{injectHere}', html)
Copy the code

A few points to note:

  • 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.
  • In a client-only app, each user uses the new application instance in their respective browser. For server-side rendering, we expect the same: each request should be a new, separate application instance so that there is no cross-request state pollution.
On the first point:
  • Since it works on both the client and server, there should be two entry files. Some Dom and Bom operations are definitely not available on the server.

  • Typically Vue applications are built from Webpack and vue-loader, and many Webpack-specific functions cannot run directly in Node.js (e.g. importing files through file-loader, importing CSS through CSS-loader)

On the second point:
  • It needs to be wrapped as a factory function that generates a brand new root component each time it is called

app.js

    import Vue from 'vue'
    import App from './App.vue'
    
    export function createApp() {
        const app = new Vue({
            render: h= > h(App)
        })
        return { app }
    }
Copy the code

enter-client.js

    import { createApp } from './app.js'
    
    const { app } = createApp()
    
    // app. vue template root element has' id=" App "'
    app.$mount('#app')
Copy the code

enter-server.js

    import { createApp } from './app.js';
    
    export default context => { / / the context of koa
        const { app } = createApp()
        return app
    }
Copy the code
    
      
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Server side rendering</title>
    </head>
    <body>
      <! --vue-ssr-outlet-->
      <! -- Import the client packaged JS file (client.bundle.js) -->
      <script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
    </body>
    </html>
Copy the code

webpack.server.config.js

    const path = require('path');
    const merge = require('webpack-merge');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const base = require('./webpack.base.config');
    
    module.exports = merge(base, {
        // This allows Webpack to handle dynamic imports in a Node-appropriate fashion,
        // Also when compiling Vue components,
        // Tell the VUe-loader to transport server-oriented code.
      target: 'node'.entry: {
        server: path.resolve(__dirname, '.. /entry-server.js')},output: {
          // Server bundle is told to use Node-style exports.
        libraryTarget: 'commonjs2'
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, '.. /.. /index.ssr.html'),
          filename: 'index.ssr.html'.files: {
            js: 'client.bundle.js' // The js file introduced in index.ssr. HTML is the client.bundle.js package. This is because the Vue needs to take over the static HTML sent by the server on the browser side and turn it into a dynamic DOM managed by the Vue. This process is officially called client activation
          },
          excludeChunks: ['server']]}}));Copy the code

webpack.client.config.js

    const path = require('path')
    const merge = require('webpack-merge')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const base = require('./webpack.base.config')
    
    module.exports = merge(base, {
        entry: {
            client: path.resolve(__dirname, '.. /entry-client.js')},plugins: [
            new HtmlWebpackPlugin({
                template: path.resolve(__dirname, '.. /.. /index.html'),
                filename: 'index.html'})]})Copy the code

This is a more complete example of a client taking over static HTML sent by a server rendering Vue instance and a dynamic Dom managed by the Vue. Full Code 03

Importing server rendering of the router

Vue-router is responsible for routing management for the Vue project. As with the 02 project, the server returns the rendered HTML and Vue takes care of the rest.

router.js

    import Vue from 'vue'
    import Router from 'vue-router'
    import Bar from "./components/Bar.vue";
    import Foo from "./components/Foo.vue";
    const routes = [
      { path: '/foo'.component: Foo },
      { path: '/bar'.component: Bar }
    ]
    
    Vue.use(Router)
    
    export function createRouter() {
      // Create a router instance and pass the 'routes' configuration
      You can also pass other configuration parameters, but keep it simple for now.
      return new Router({
        mode: 'history',
        routes
      })
    }
Copy the code

App. Js into the router

    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    
    // Export a factory function to create a new one
    // Application, Router, and Store instances
    export function createApp() {
        // Create a Router instance
        const router = createRouter()
        const app = new Vue({
            // Inject router into the root Vue instance
            router,
            // Root instance of a simple render application component.
            render: h= > h(App)
        })
        return { app, router }
    }
Copy the code

For Vue optimizations, we tend to lazy-load components rather than load them all at once. Then we need to make simple changes to the entry-server.js and router.js files.

router.js

    import Vue from 'vue'
    import Router from 'vue-router'
    
    const routes = [
     // webpack.base.config.js requires @babel/plugin-syntax-dynamic-import
      { path: '/foo'.component: (a)= > import('./components/Foo.vue')}, {path: '/bar'.component: (a)= > import('./components/Bar.vue') }
    ]
    
    Vue.use(Router)
    
    export function createRouter() {
      // Create a router instance and pass the 'routes' configuration
      You can also pass other configuration parameters, but keep it simple for now.
      return new Router({
        mode: 'history',
        routes
      })
    }
Copy the code

With the addition of 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. Our current entry-server.js is updated to look like this

entry-server.js

    import { createApp } from './app.js';
    
    export default context => {
        // Since it might be an asynchronous routing hook function or component, we'll return a Promise,
        // So that the server can wait for all the content before rendering,
        // We are ready.
        return new Promise((resolve, reject) = > {
            const { app, router } = createApp()
            if (context.url.indexOf('. ') = = =- 1) { // Prevent matching the favicon.ico *.js file
                router.push(context.url)
            }
            // Set the router location on the server
    
            console.log(context.url, '* * * * * *')
            // Wait until the router has resolved possible asynchronous components and hook functions
            router.onReady((a)= > {
                const matchedComponents = router.getMatchedComponents()
                Reject if the route cannot be matched, reject, and return 404
                if(! matchedComponents.length) {return reject({ code: 404})}// Promise should resolve the application instance so that it can be rendered
                resolve(app)
            }, reject)
        })
    }
Copy the code

entry.client.js

    import { createApp } from './app.js'
    
    const { app, router } = createApp()
    
    router.onReady((a)= > {
      // This assumes that the root element in the app. vue template has' id=" App "'
      app.$mount('#app')})Copy the code

Due to asynchronous routing, the bundle.js package does not include the asynchronous component’s JS file. Error: server.bundle.js is not available for the asynchronous component.

So here we use vue-server-renderer/server-plugin to package server.entry.js file into a JSON file. Json files will map all asynchronous components and associated JS.

Server rendering that needs to initialize data

A few examples can be seen from above, on the server side rendering (SSR), we are essentially in rendering a static file, follow-up interactions or to the client’s vue, so if your application relies on some need to initialize the asynchronous data, so before starting the rendering process, need to prefetch and parse the data well.

Another problem is that before mounting the client application, you need to get exactly the same data as the server application – otherwise, the client application will use a different state than the server 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.

That is, after all preFetch hooks resolve, our store has filled in the state needed to render the application. When we attach the state to the context and the Template option is used for the renderer, the state is automatically serialized to window.__INITIAL_STATE__ and injected into HTML. On the client side we can retrieve data from the global variable window.__initial_state__.

We use VueX from the official state management library.

store.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    // an API that returns promises
    import { fetchItem } from './api'
    
    export function createStore () {
      return new Vuex.Store({
        state: {
          items: {}},actions: {
          fetchItem ({ commit }, id) {
            // 'store.dispatch()' returns Promise,
            // So that we know when the data is updated
            return fetchItem(id).then(item= > {
              commit('setItem', { id, item })
            })
          }
        },
        mutations: {
          setItem (state, { id, item }) {
            Vue.set(state.items, id, item)
          }
        }
      })
    }
Copy the code

app.js

    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    import { createStore } from './store'
    
    // Export a factory function to create a new one
    // Application, Router, and Store instances
    export function createApp() {
        const router = createRouter()
        const store = createStore()
        const app = new Vue({
            router,
            store,
            // Root instance of a simple render application component.
            render: h= > h(App)
        })
        return { app, router, store }
    }
Copy the code

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 in store and route information as parameters, so our entry-server.js now looks like this

entry-server.js

    import { createApp } from './app.js';

    export default context => {
      // Since it might be an asynchronous routing hook function or component, we'll return a Promise,
      // So that the server can wait for all the content before rendering,
      // We are ready.
      return new Promise((resolve, reject) = > {
        const { app, router, store } = createApp()
        if (context.url.indexOf('. ') = = =- 1) {
          // Set the router location on the server
          router.push(context.url)
        }
        
        console.log(context.url, '* * * * * *')
        // Wait until the router has resolved possible asynchronous components and hook functions
        router.onReady((a)= > {
          const matchedComponents = router.getMatchedComponents()
          Reject if the route cannot be matched, reject, and return 404
          if(! matchedComponents.length) { router.push('/foo')   // You can add a default page, or 404 page
            // return reject({ code: 404 })
          }
    
          Promise.all(matchedComponents.map(component= > {
            if (component.asyncData) {
              return component.asyncData(
                {
                  store,
                  route: router.currentRoute
                })
            }
          })).then((a)= > {
            // When template is used, context.state will be the window.__initial_state__ state,
            // Automatically embedded in the final HTML. On the client side, store should get the state before it is mounted to the application
            context.state = store.state
            // Promise should resolve the application instance so that it can be rendered
            resolve(app)
          }).catch(reject)
        }, reject)
      })
    }
Copy the code

The server stores the data in context.state, and the client gets the data in window.__initial_state__

entry.client.js

    import { createApp } from './app.js'
    
    const { app, router, store } = createApp()
    // Restore the state
    if(window.__INITAL__STATE__) {
        store.replaceState(window.__INITAL__STATE)
    }
    router.onReady((a)= > {
      // This assumes that the root element in the app. vue template has' id=" App "'
      app.$mount('#app')})Copy the code

How do asyncData functions load on non-first screen pages

When asyncData is loaded on multiple pages, how can asyncData be executed on non-first screen loaded page? After loading the first screen, jump to another page, the execution of asyncData on other pages is executed in beforeMount hook function, mixed with mixins.

Vue.mixin({
    beforeMount() {
        const { asyncData } = this.$options
        if(asyncData) {
            this.dataPromise = asyncData({
                store: this.$store,
                route: this.$route}}}})Copy the code

subsequent

You can’t refresh in real time right now, you need to optimize it further