I personally encountered a lot of problems when I first read the official VUE SSR documentation, it was built on the basis that you have a runnable build environment, so it directly tells the implementation of the code, but there is no runnable environment for new developers, so all the code fragments can not run. Why doesn’t the author talk about the build first and then the implementation? I think it’s probably because building and running relies heavily on specific code implementation, and building first doesn’t help to understand the overall process, so it’s not a good balance.

In this demo, we’re going to start with the build process, some of which we may need to come back to later, but try to make the whole process clear. At the same time, each step in the article will be represented in the DEMO. You can quickly locate the different stages of the DEMO with different commit ids as follows:

* e06aee792a59ffd9018aea1e3601e220c37fedbd (HEAD - > master, origin/master) optimization: Add cache * c65f08beaff1dea1eaf05d02fb30a7e8776ce289 application development: preliminary complete demo * 2 fb0d28ee6d84d2b1bdbbe419c744efdad3227de application development: Complete store definition, API to write and program synchronous * 9604 aec0de526726f4fe435385f7c2fa4009fa63 development: The first version can be run independently, without store * 7 d567e254fc9dc5a1655d2f0abbb4b8d53bccfce build configuration: Webpack configuration, server js back-end entry documentation * 969248 b64af82edd07214a621dfd19cf357d6c53 build configuration: Babel configuration * a5453fdeb20769e8c9e9ee339b624732ad14658a initialization program, completed the first run the demoCopy the code

Git reset — Hard Commitid can be used to switch between different phases to see the implementation.

What is Server-side rendering (SSR)?

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.

Why server side Rendering (SSR)?

The main advantages of server-side rendering (SSR) over traditional SPA (single-page Application) are:

  • Better SEO, thanks to search engine crawler crawler tools can view fully rendered pages directly.
  • Faster time-to-content, especially for slow network conditions or slow-running devices.

Basic usage

Templates to install

npm install vue vue-server-renderer express –save

Create /server.js and/SRC /index.template.html

const server = require('express') ()const Vue = require('vue')
const fs = require('fs')

const Renderer = require('vue-server-renderer').createRenderer({
  template:fs.readFileSync('./src/index.template.html'.'utf-8')
})

server.get(The '*'.(req, res) = > {

  const app = new Vue({
    data: {
      name: 'vue app~'.url: req.url
    },
    template:'<div>hello from {{name}}, and url is: {{url}}</div>'
  })
  const context = {
    title: 'SSR test#'
  }
  Renderer.renderToString(app, context, (err, html) = > {
    if(err) {
      console.log(err)
      res.status(500).end('server error')
    }
    res.end(html)
  })
})

server.listen(4001)
console.log('running at: http://localhost:4001');
Copy the code

From the above program, you can see that the Vue instance is compiled via vue-server-renderer and finally exported to the browser via Express.

However, it can also be seen that the output is a static pure HTML page. Since there are no javascript files loaded, there is no front-end user interaction, so the demo above is just a minimalist example. In order to implement a complete VUE SSR program, VueSSRClientPlugin(vuE-server-renderer /client-plugin) is also needed to compile the file into vuE-SSR-client-manifest. json file, JS, CSS and other files that can be run by the front-end browser. VueSSRServerPlugin(vue-server-renderer/server-plugin) compiles the file to vue-ssR-server-bundle. json that node can call

Before we can really get started, we need to understand a few concepts

Writing generic code

Constraints on “generic” code – that is, code running on the server and client will not be exactly the same when running in different environments due to differences in use cases and platform apis.

Data response on the server

Each request should be a new, separate application instance so that there is no cross-request state pollution.

Component lifecycle hook functions

Since there is no dynamic update, of all the lifecycle hook functions, only beforeCreate and Created are called during server-side rendering (SSR)

Access platform-specific apis

Generic code does not accept platform-specific apis, so if your code directly uses browser-only global variables like Window or Document, it will throw an error when executed in Node.js, and vice versa.

Build configuration

How to provide the same Vue application to both the server and client. To do this, we need to use WebPack to package the Vue application.

  • 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).

  • While the latest versions of Node.js fully support ES2015 features, we still need to translate the client code to accommodate older browsers. This also involves a build step.

So the basic idea is that we use Webpack packaging for both client and server applications – the server needs a “server bundle” for server-side rendering (SSR), and the “client bundle” is sent to the browser for mixing static markup.

Let’s look at the implementation process

Babel configuration

New /.babelrc configuration

// es6 compile to ES5 configuration
{
  "presets": [["env",
      {
        "modules": false}]],"plugins": ["syntax-dynamic-import"]
}

npm i -D babel-loader@7 babel-core babel-plugin-syntax-dynamic-import babel-preset-env
Copy the code

Webpack configuration

Create a new build folder for webPack-related configuration files

/ ├─ build │ ├─ setup-dev-server.jsSet up the Webpack-dev-Middleware development environment│ ├ ─ ─ webpack. Base. Config. JsBase common configuration│ ├ ─ ─ webpack. Client. Config. Js# Compile vue-ssr-client-manifest.json file and JS, CSS and other files for the browser to call│ └ ─ ─ webpack. Server config. JsVue - SSR -server-bundle.json for nodeJS call
Copy the code

Install the relevant packages first

Install webPack-related packages

npm i -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware webpack-merge webpack-node-externals

Install build dependent packages

npm i -D chokidar cross-env friendly-errors-webpack-plugin memory-fs rimraf vue-loader

Let’s look at the details of each file:

webpack.base.config.js

const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

const isProd = process.env.NODE_ENV === 'production'
module.exports = {
  context: path.resolve(__dirname, '.. / '),
  devtool: isProd ? 'source-map' : '#cheap-module-source-map'.output: {
    path: path.resolve(__dirname, '.. /dist'),
    publicPath: '/dist/'.filename: '[name].[chunkhash].js'
  },
  resolve: {
    // ...
  },
  module: {
    rules: [{test: /\.vue$/,
        loader: 'vue-loader'.options: {
          compilerOptions: {
            preserveWhitespace: false}}}// ...]},plugins: [new VueLoaderPlugin()]
}
Copy the code

Webpack.base.config.js This is the general configuration, which is basically the same as our previous SPA development configuration.

webpack.client.config.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
  mode: 'development'.entry: {
    app: './src/entry-client.js'
  },
  resolve: {},
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development'
      ),
      'process.env.VUE_ENV': '"client"'
    }),
    new VueSSRClientPlugin()
  ]
})
module.exports = config
Copy the code

Webpack.client.config.js does two main things

  • Defining entry filesentry-client.js
  • Through plug-insVueSSRClientPlugingeneratevue-ssr-client-manifest.json

This manifest.json file is referenced by server.js

const { createBundleRenderer } = require('vue-server-renderer')

const template = require('fs').readFileSync('/path/to/template.html'.'utf-8')
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')

const renderer = createBundleRenderer(serverBundle, {
  template,
  clientManifest
})

Copy the code

With the above Settings, all HTML code rendered by the server after being built using the code splitting feature is automatically injected.

webpack.server.config.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
  mode: 'production'.target: 'node'.devtool: '#source-map'.entry: './src/entry-server.js'.output: {
    filename: 'server-bundle.js'.libraryTarget: 'commonjs2'
  },
  resolve: {},
  externals: nodeExternals({
    whitelist: /\.css$/ // Prevent packages from being packaged into the bundle, instead fetching these extension dependencies externally at runtime
  }),
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
  ]
})

Copy the code

What webpack.server.config.js does is:

  • throughtarget: 'node'The directory code that tells Webpack to compile is the Node application
  • throughVueSSRServerPluginPlug-in that compiles the code tovue-ssr-server-bundle.json

After generating vue-ssR-server-bundle. json, you just need to pass the file path to createBundleRenderer.

const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
  / /... Other options for renderer
})
Copy the code

At this point, the build is almost complete

Complete the first runnable instance

Install VUE dependencies

npm i axios vue-template-compiler vue-router vuex vuex-router-sync

Add and improve the following files:

/ ├ ─ ─ server. Js# Implement long-running Node applications├─ SRC │ ├─ app.js# new│ ├ ─ ─ the router. Js# Add route definition│ ├ ─ ─ App. Vue# new│ ├ ─ ─ entry - client. Js# browserside entry│ ├ ─ ─ entry - server. Js# node application side entry└ ─ ─ views └ ─ ─ Home. Vue# page
Copy the code

Let’s look at each file one by one:

server.js

const fs = require('fs');
const path = require('path');
const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');
const devServer = require('./build/setup-dev-server')
const resolve = file= > path.resolve(__dirname, file);

const isProd = process.env.NODE_ENV === 'production';
const app = express();

const serve = (path, cache) = >
  express.static(resolve(path), {
    maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
  });
app.use('/dist', serve('./dist'.true));

function createRenderer(bundle, options) {
  return createBundleRenderer( bundle, Object.assign(options, {
      basedir: resolve('./dist'),
      runInNewContext: false})); }function render(req, res) {
  const startTime = Date.now();
  res.setHeader('Content-Type'.'text/html');

  const context = {
    title: 'SSR test'.// default title
    url: req.url
  };
  renderer.renderToString(context, (err, html) = > {
    res.send(html);
  });
}

let renderer;
let readyPromise;
const templatePath = resolve('./src/index.template.html');

if (isProd) {
  const template = fs.readFileSync(templatePath, 'utf-8');
  const bundle = require('./dist/vue-ssr-server-bundle.json');
  const clientManifest = require('./dist/vue-ssr-client-manifest.json') // Inject the js file into the page
  renderer = createRenderer(bundle, {
    template,
    clientManifest
  });
} else {
  readyPromise = devServer( app, templatePath, (bundle, options) = >{ renderer = createRenderer(bundle, options); }); } app.get(The '*',isProd? render : (req, res) = > {
        readyPromise.then(() = >render(req, res)); });const port = process.env.PORT || 8088;
app.listen(port, () = > {
  console.log(`server started at localhost:${port}`);
});

Copy the code

Server.js does the following

  • When you performnpm run devIs called/build/setup-dev-server.jsStart ‘webpack-dev-middleware’ to develop middleware
  • throughvue-server-rendererGenerated before the call is compiledvue-ssr-server-bundle.jsonStarting the Node Service
  • willvue-ssr-client-manifest.jsonInjected into thecreateRendererT automatic injection of front-end resources
  • throughexpressTo deal withhttprequest

Server.js is the entry program for the whole site, through which the compiled files are called, and the final output to the page, is a key part of the whole project

app.js

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

export function createApp(context) {
  const router = createRouter();

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

App.js exposes a factory function that can be executed repeatedly, creating new application instances for each request, submitted to ‘entry-client.js’ and entry-server.js calls

entry-client.js

import { createApp } from './app';
const { app, router } = createApp();
router.onReady(() = > {
  app.$mount('#app');
});
Copy the code

Entry-client.js routinely instantiates the Vue object and mounts it to the page

entry-server.js

import { createApp } from './app';

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(context);

    // Set the router location on the server
    router.push(context.url);

    // Wait until the router has resolved possible asynchronous components and hook functions
    router.onReady(() = > {
      const matchedComponents = router.getMatchedComponents();

      Reject if the route cannot be matched, reject, and return 404
      if(! matchedComponents.length) {return reject({ code: 404 });
      }

      resolve(app);
    });
  });
};
Copy the code

As the server entry, entry-server.js is finally compiled into vue-ssr-server-bundle.json for vue-server-renderer to call through VueSSRServerPlugin

Router. js and home. vue are regular vue programs that are not expanded here.

At this point, we have completed the first vUE SSR instance that can be fully compiled and run

Data prefetch and status management

The programs that have been done before just render the variables that you want to define as HTML and return them to the client, but if you want to implement a truly usable Web program, you need to have dynamic data support. Now let’s look at how to get data remotely and render it as HTML and export it to the client.

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.

Prefetch Storage container (Data Store)

Start by defining an api.js that gets the data, using axios:

import axios from 'axios';

export function fetchItem(id) {
  return axios.get('https://api.mimei.net.cn/api/v1/article/' + id);
}
export function fetchList() {
  return axios.get('https://api.mimei.net.cn/api/v1/article/');
}
Copy the code

We will use the official state management library Vuex. We will create a store.js file that will get a list of files and the content of the article by id:

import Vue from 'vue';
import Vuex from 'vuex';
import { fetchItem, fetchList } from './api.js'

Vue.use(Vuex);


export function createStore() {
  return new Vuex.Store({
    state: {
      items: {},
      list: []},actions: {
      fetchItem({commit}, id) {
        return fetchItem(id).then(res= > {
          commit('setItem', {id, item: res.data})
        })
      },
      fetchList({commit}){
        return fetchList().then(res= > {
          commit('setList', res.data.list)
        })
      }
    },
    mutations: {
      setItem(state, {id, item}) {
        Vue.set(state.items, id, item)
      },
      setList(state, list) {
        state.list = list
      }
    }
  });
}
Copy the code

Then modify 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 createApp(context) {
  const router = createRouter();
  const store = createStore();

  sync(store, router)

  const app = new Vue({
    router,
    store,
    render: h= > h(App)
  });
  return { app, router, store };
};
Copy the code

Components with logical configuration

Now that the Store action is defined, let’s look at how to trigger the request. The official recommendation is to put it in the routing component, and then look at home.vue:

<template>
  <div>
    <h3>The article lists</h3>
    <div class="list" v-for="i in list">
      <router-link :to="{path:'/item/'+i.id}">{{i.title}}</router-link>
      </div>
  </div>
</template>
<script>
export default {
  asyncData ({store, route}){
    return store.dispatch('fetchList')},computed: {
    list () {
      return this.$store.state.list
    }
  },
  data(){
    return {
      name:'wfz'}}}</script>
Copy the code

Data prefetch on the server

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

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

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(context);

    // Set the router location on the server
    router.push(context.url);

    // Wait until the router has resolved possible asynchronous components and hook functions
    router.onReady(() = > {
      const matchedComponents = router.getMatchedComponents();

      Reject if the route cannot be matched, reject, and return 404
      if(! matchedComponents.length) {return reject({ code: 404 });
      }

      Promise.all(
        matchedComponents.map(component= > {
          if (component.asyncData) {
            return component.asyncData({
              store,
              route: router.currentRoute
            });
          }
        })
      ).then(() = > {
        context.state = store.state
        // Promise should resolve the application instance so that it can be rendered
        resolve(app);
      });
    });
  });
};

Copy the code

When template is used, context.state is automatically embedded in the final HTML as the window.__initial_state__ state. On the client side, store should get the state before it is mounted to the application:

// entry-client.js

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

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

Copy the code

Client data prefetch

On the client side, data prefetch can be handled in two different ways: parsing the data before routing, matching the view to be rendered, and retrieving the data. In our demo, we used the first scheme:

// entry-client.js
import { createApp } from './app';
const { app, router, store } = createApp();

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() = > {
  router.beforeResolve((to, from, next) = > {
    const matched = router.getMatchedComponents(to);
    const prevMatched = router.getMatchedComponents(from);

    let diffed = false;
    const activated = matched.filter((c, i) = > {
      returndiffed || (diffed = prevMatched[i] ! == c); });if(! activated.length) {return next();
    }

    Promise.all(
      activated.map(component= > {
        if (component.asyncData) {
          component.asyncData({
            store,
            route: to
          });
        }
      })
    )
      .then(() = > {
        next();
      })
      .catch(next);
  });
  app.$mount('#app');
});
Copy the code

Retrieve interface data by checking for matching components and executing asyncData in the global routing hook function.

Since the demo is two pages, you also need to add a routing information to router.js and a routing component, item.vue. Now you have a basic Vue SSR instance.

Cache optimization

Because server-side rendering is computationally intensive, performance issues are likely if concurrency is large. Appropriate use of caching strategies can dramatically improve response times.

const microCache = LRU({
  max: 100.maxAge: 1000 // Important: Entries expire after 1 second.
})

const isCacheable = req= > {
  // The implementation logic is to check whether the request is user-specific.
  // Only non-user-specific pages are cached
}

server.get(The '*'.(req, res) = > {
  const cacheable = isCacheable(req)
  if (cacheable) {
    const hit = microCache.get(req.url)
    if (hit) {
      return res.end(hit)
    }
  }

  renderer.renderToString((err, html) = > {
    res.end(html)
    if (cacheable) {
      microCache.set(req.url, html)
    }
  })
})
Copy the code

Basically, performance bottlenecks can be largely solved with Nginx and caching.