preface

When it comes to server-side rendering, we first think of JSP, PHP and other back-end template rendering.

That’s why some people scoff when they hear server-side renderings.

Why is it that the front end is going back and forth, and after a lot of SPA, you end up with the same weird tone of template rendering as the back end

Of course, those who have such problems are generally nominal-oriented programmers, trying to gain technical satisfaction from mastering the technical vocabulary.

The first thing to note is that SERVER-SIDE rendering of JSPS and modern front-end frameworks are basically two very different things.

We all know that back in the days of the actual Web, the structure of a web page was generated on the back end and returned as a document to the front end. If local changes are required, even a single character of the entire web page needs to be regenerated and a whole new document sent back to the browser. Back then the web was just a simple presentation and form and that wasn’t a problem.

With the development of modern browsers, front-end logic is getting heavier and heavier. The familiar separation occurs, where local updates are initiated by the front end itself, manipulating the DOM to modify the generated front end document itself.

To the rise of the three frameworks in modern times, the front end is basically a browser as a virtual machine to run a complete application is our SPA. But a good SPA is a bad SPA.

What we request from the server is actually an empty shell, and all the filling and drawing logic falls on the client side, so it will lead to the problem of slow loading of the front screen, which is also the source of our classic interview question: how to solve the problem of long blank time of the vue first screen.

At the same time, because the request is an empty shell at the beginning of the reason also led to SEO is not friendly. Hence our modern server rendering solutions, with Nuxt for Vue and Next for React.

It’s both called server rendering what’s the difference?

The fundamental difference is that JSPS return a document directly, whereas modern SSR takes a Web application and generates it directly on the server and sends it to the browser and then activates these static tags into a fully interactive application on the client. That is, the time-consuming steps of SPA initialization are done directly in the service, just need to mount the activation on the client side.

The purpose of this article

In the process of vUE SSR project construction learning, I found that there are too few helpful articles in this aspect, most of them are superficial.

The official website documents only give a general framework and the introduction of related concepts, and many construction details are not very clear.

The official demo is a little complicated, so it takes some time to understand.

Therefore, I set up an available SRR project based on the construction ideas of the official website, aiming to enable students who are interested in VUE SSR to get started more quickly and quickly understand the construction ideas of VUE SRR through this project.

version

This project mainly refers to the official website and the demo project vue-Hackernews-2.0 on the official website. So the version of the main framework is the same as vue2+ Webpack3

Tip: If you want to build step by step, make sure that the versions of each plug-in are consistent. Webpack project version conflicts are so lame

At the same time, please refer to the official introduction and official demo for edible effect more

ssr.vuejs.org/zh

Github.com/vuejs/vue-h…

This section describes how to set up vUE SRR

First, we need to understand the difference between normal SPA and SSR rendering.

SPA generates bundles from our packaging tools such as WebPack. We put this bundle on our Web server like Nginx. The browser makes a request to get the bundle and then parses the huffing and puffing to populate the application.

SSR, as we mentioned before, is parsed and generated on the server and then sent to the browser for activation.

So when it comes to accepting webPCAk configuration to parse and generate HTML strings that the browser needs, we have to mention this diagram:

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.

According to the introduction chart and the official website, we know very clearly.

SSR is actually equal to server-side render +SPA, or SPA rendered in advance

So we need two webPack configurations, one running on the Node server for server rendering and one running on the browser like our normal SPA configuration. The vue-server-renderer function is to mix the two bundles to generate HTML that is sent to the browser.

So our core step was to configure the webpack configuration on the server and client side, package it and pass it in to the Vue-server-renderer and send it to the browser

As shown in the example below

Const {createBundleRenderer} = require(' viee-server-renderer ') // template const template = require('fs').readFileSync('/path/to/template.html', 'utF-8 ') // client webpack configuration const serverBundle = require('/path/to/ vue-ssR-server-bundle. json') // client webpack configuration const serverBundle = require('/path/to/ vue-SSR-server-bundle. json') // client webpack configuration const serverBundle = require('/path/to/ vue-SSR-server-bundle. json' clientManifest = require('/path/to/vue-ssr-client-manifest.json') // Const renderer = createBundlerender. serverBundle, createBundlerender. serverBundle { template, clientManifest })Copy the code

Webpack configuration

Just like the ordinary SPA project, we prepare three configuration files, respectively

  1. Base Common configuration (merge with other configurations via webpack-Merge)
  2. Client Indicates the configuration of the client.
  3. Server Server configuration.

Old Webpack people look at it and think it’s not right. Where are the configurations for development and production?

We have a client side and a server side. So the configuration of the development environment and the production environment is distinguished.

Base General configuration

The Base configuration is not much different from the base configuration of a normal project. The main configuration content is

  • Inward and outward
  • Special File Resolution (Rules)
  • Some general optimizations for production environments
  • Special handling of Node, etc
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

// Whether it is a build environment
const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  devtool: isProd
    ? false
    : '#cheap-module-source-map'.output: {
    path: path.resolve(__dirname, '.. /dist'),
    publicPath: '/dist/'.filename: '[name].[chunkhash].js'},...module: {
    noParse: /es6-promise\.js$/.// avoid webpack shimming process
    rules: [
      {
        test: /\.vue$/. },... ] },// node processing is used to load node modules
  node: {
    fs: 'empty',},plugins: [
  new VueLoaderPlugin()
  ]
}

Copy the code

Client Indicates the configuration of the client

Client entry file

We know from the previous section that two WebPack configurations are required to run on the Node side and the browser side, so the same entry file also requires two different files

Since the client entry file is running in the browser, we simply create the application and mount it into the DOM


import { createApp } from './app'

// Client-specific boot logic......

const { app } = createApp()

// This assumes that the root element in the app. vue template has' id=" App "'
app.$mount('#app')

Copy the code

Client WebPack configuration

The main functions of client configuration are as follows

  • Declare client entry
  • Configure the browser hot replacement plug-in
  • Configure the vue-server-renderer client plug-in to build buildsvue-ssr-client-manifest.json
const webpack = require("webpack");
const merge = require("webpack-merge");
const baseConfig = require("./webpack.base.config.js");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");

module.exports = merge(baseConfig, {
  / / thermal load
  entry: ["./src/entry-client.js"."webpack-hot-middleware/client"].output: {
    filename: "[name][hash].js",},plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // Important information: This separates the WebPack runtime into a boot chunk,
    // So that asynchronous chunks can be properly injected later.
    // This also provides better caching for your application /vendor code.
    new webpack.optimize.CommonsChunkPlugin({
      name: "manifest".minChunks: Infinity,}).// This plug-in is in the output directory
    // Generate 'vue-ssr-client-manifest.json'.
    new VueSSRClientPlugin(),
  ],
});

Copy the code

Server Server configuration

Server entry file

Server Entry uses the default Export function to export and calls this function repeatedly on each render


import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}
Copy the code

Server WebPack configuration

The main configuration purposes are as follows:

  • Declare the Node environment
  • Declare the VueSSRServerPlugin plug-in to generate the server configuration

Other configurations are already configured in Base

const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(baseConfig, {
  // Point entry to the application's Server Entry file
  entry: './src/entry-server.js'.// 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'.// Provide source map support for the bundle renderer
  devtool: 'source-map'.// Server bundle is told to use Node-style exports.
  output: {
    filename: 'server-bundle.js'.libraryTarget: 'commonjs2'
  },

  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // Externalize application dependency modules. Can make server builds faster,
  // Generate smaller bundles.
  externals: nodeExternals({
    // Do not externalize dependent modules that WebPack needs to handle.
    // You can add more file types here. For example, the *.vue raw file is not processed,
    // You should also whitelist dependent modules that modify 'global' (e.g. polyfill)
    whitelist: /\.css$/
  }),

  // This is the entire output of the server
  // Build a plug-in as a single JSON file.
  // The default file name is' vue-ssR-server-bundle. json '
  plugins: [
    new VueSSRServerPlugin()
  ]
})
Copy the code

Express Server Configuration

After configuring webPack, build json files according to the official website, and then pass in VueSSRServerPlugin.

It’s okay to do that, but in real development, if every change is packaged to generate a configuration file to disk, regenerated into HTML and sent to the browser to see what it looks like. What a slow sha BI process that must be.

Old Webpack people know that in the development environment webpack generated package files are stored in memory, so as to meet our hot update hot load fast response needs.

So we need to read the packaging in memory separately in the development environment

The development environment reads the memory file

What we need to do with this configuration is:

  1. Read the Client and server WebPack configurations separately
  2. The client configuration is mounted on the Express server via webpack-dev-Middleware generation middleware, which is used to listen for file changes and is automatically packaged in memory
  3. Mount the client configuration on the Express server through webpack-hot-middleware generation middleware, which is used to refresh the browser through Websorcke after file changes to achieve the purpose of hot loading
  4. Listen for client and server packaging completion events, read packaging artifacts in memory, and perform callbacks
const path = require("path");
const fs = require('fs')
const MFS = require("memory-fs");
const webpack = require("webpack");
// Read the client configuration
const clientConfig = require("./webpack.client.config");
// Read the server configuration
const serverConfig = require("./webpack.server.config");
// Hot update plugin
const webpackHotMiddleware = require("webpack-hot-middleware");

// Read the file
const readFile = (fs, file) = > {
  try {
    return fs.readFileSync(path.join(clientConfig.output.path, file), "utf-8");
  } catch (e) {}
};
/* App server templatePath: template cb: callback method */
module.exports = function setupDevServer(app,templatePath,callback) {
  / / webpack package
  let bundle;
  // Client list
  let clientManifest;
  / / template
  let template;
  return new Promise((resolve) = > {
    const r = () = >{
        if (bundle && clientManifest) {
            resolve()
            callback(bundle,{
                clientManifest,
                template
            })
        }
    }
    template = fs.readFileSync(templatePath, 'utf-8')
    // Generate a middleware bound to WebPack's Compiler and invoke this middleware in the Service app started by Express
    // Monitor the changed files and pack them automatically in memory
    const clientCompiler = webpack(clientConfig);
    const devMiddleware = require("webpack-dev-middleware")(clientCompiler, {});
    app.use(devMiddleware);
    // hot middleware
    app.use(webpackHotMiddleware(clientCompiler));
    clientCompiler.plugin("done".(stats) = >{... clientManifest =JSON.parse(
        readFile(devMiddleware.fileSystem, "vue-ssr-client-manifest.json")); r(); });const serverCompiler = webpack(serverConfig);
    // Get files in memory
    const mfs = new MFS();
    serverCompiler.outputFileSystem = mfs;
    serverCompiler.watch({}, (err, stats) = >{... bundle =JSON.parse(readFile(mfs, "vue-ssr-server-bundle.json"));
      r();
    });
  });
};

Copy the code

Express Server Configuration

After VueSSRServerPlugin generates HTML we need to build an Express service to listen for browser requests and decide whether to get webPCAk compilation files in memory or on disk depending on whether it is a development environment

const path = require("path");
const express = require("express");
const { createBundleRenderer } = require("vue-server-renderer");

// Read the absolute path
const resolve = (file) = > path.resolve(__dirname, file);
// Whether it is a production environment
const isProd = process.env.NODE_ENV === "production";
// Template path
const templatePath = resolve("./index.template.html");

let renderer, setupPromise, bundle, clientManifest;

const app = express();

const createRenderer = (bundle, options) = > {
  return createBundleRenderer(
    bundle,
    Object.assign(options, {
      basedir: resolve("./dist"),
      // recommended for performance
      runInNewContext: false,})); };// If it is a production environment, fetch the packaged files directly
if (isProd) {
  bundle = require(".. /dist/vue-ssr-server-bundle.json");
  clientManifest = require(".. /dist/vue-ssr-client-manifest.json");
} else {
  // Note: The promise internal logic needs to be executed only once
  setupPromise = require(".. /build/setup-dev-server")(
    app,
    templatePath,
    // Pass in the callback and re-render the page after the webPcak, done callback is complete
    (bundle, options) = >{ renderer = createRenderer(bundle, options); }); }// Processing error
const handleError = (err, res) = >{... };const render = async (req, res) => {
  res.setHeader("Content-Type"."text/html");
  const context = {
  ...
  };
  renderer.renderToString(context, (err, html) = > {
    if (err) {
      return handleError(err, res);
    }
    res.send(html);
  });
};

const appRender = (req, res) = > {
  // If it is a production environment, generate the rendering directly from the file
  if (isProd) {
    render(req, res);
    return;
  }
  // In a development environment, you need to read the package from memory before rendering
  setupPromise.then(render(req, res));
};

app.get("*", appRender);

const port = process.env.PORT || 8888;
app.listen(port, () = > {
  console.log(`server started at localhost:${port}`);
});

Copy the code

Special note: there is a very easy pit in this, according to the website requires both server and client packaging. It would be natural to use the promise control on each request to read the file in memory and resolve after both wrap completion callback events are triggered.

This error occurs if you do so

Error: [VueLoaderPlugin Error] No matching use for vue-loader is found.
Make sure the rule matching .vue files include vue-loader in its use.
Copy the code

That’s because the same configuration is packaged multiple times, as Utah explains:

That is to say, the logic in our promise only needs to be executed once to ensure that both configuration files have been packaged, instead of executing the packaging on each request and generating multiple instances of an error.

So we define the promise of the previous step in the external setupPromise, after the successful execution of each request only need to execute its callback, at the same time when the file send changes trigger the Webpack callback event we also listen to his and callback to reproduce the render page

Vue project configuration

In fact, here is basically a VUE server rendering process is complete. However, since it is a VUE project, how can there be less vUE ecology: VUE-Router, Vuex, Axios

There is only one principle you need to understand for these configurations

The server configuration is used for in-server pre-rendering and the client configuration is used for browser-side rendering.

That is, the browser configuration is useless until you send it to the browser. Once the project runs in the browser, everything is left to the client to configure, and there is nothing left for the server to configure. But at the same time we must ensure that the server and client configurations produce the same artifacts.

App. Js root instance

Old VUE developers know that a VUE project must have a root instance as an entry point. However, in server-side rendering to avoid multiple requests sharing the same instance causes state contamination

We need to generate a new root instance for every new request, that is, every refresh, so we directly create a factory method that is specifically used to generate vUE instances.


import Vue from "vue";
import App from "./App.vue";
import { createRouter } from ".. /src/config/router.js";
import { createStore } from ".. /src/config/store.js";
import { sync } from "vuex-router-sync";

// 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 store = createStore();
  // Synchronize route state to store
  sync(store, router);
  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

The routing configuration

First we build a vue-Router routing file again familiar old recipe, familiar Webpack code segmentation, familiar history

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

Vue.use(Router)

export function createRouter () {
  return new Router({
    mode: 'history'.routes: [{path: '/'.name:'home'.component: () = > import('.. /views/Home/index.vue')},... ] })}Copy the code

Then introduce into the previous step our vue root instance, up to this step is the same as the old SPA

const router = createRouter();

  const app = new Vue({
    router,
    // Root instance of a simple render application component.
    render: (h) = > h(App),
  });
Copy the code

Finally, to synchronize the server with the client, we need to manually update the Router object in the server instance

The url is the server request field of request, which is exactly equal to the routing field in SPA

   // Set the router location on the server
    router.push({ path: 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}); }}Copy the code

The effect

Data prefetch and status

Finally, the last and most important step of our SSR,

First, the official joke

However, for me, the first screen and SEO optimization are beautiful, but the data prefetch is the most exciting part of SSR project.

A created/Mounted hook in vue requests a number of back-end interfaces for each business page to retrieve the original state data.

Wouldn’t it be nice if this step was done on the server side? And SSR does just that.

The retrieved data needs to be located outside of 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

According to the official explanation, we need to insert a store on the client side and a store on the server side. On the server side, we can access the Node environment. At this time, we can do all the operations on the server side, including adding, deleting, modifying, searching database, and file operation. This is when we populate the server’s store with the data obtained by the server.


// 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.

Copy the code

The server store is then automatically serialized to window.INITIAL_STATE during browser rendering

The real place to use this data is on the browser client, so this step is the key to connecting the server to the client by serializing the server’s data and assigning it to the Window’s __INITIAL_STATE__ property.

The last thing we need to do is populate window.INITIAL_STATE into our client’s store.

entry-client.js
if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
  }
Copy the code

In this way, the data obtained by the server is actually injected into our VUE component

General process:

Graph TD server store --> window.__initial_state__ --> Client store --> Component state

Specific operation

Scenario: Obtain the names of all files in a folder without invoking the interface

  1. The server defines the method to get the file name
const fs = require('fs')
const path = require('path')
export const readFileName = (folderPath) = >{
 return   fs.readdirSync(folderPath).map(fileName= > {
        return path.join(folderPath, fileName)
      })
}
Copy the code
  1. Register the method in store
import { readFileName } from '.. /api/file'
export function createStore () {
  return new Vuex.Store({
    state: {
      fileNames: ' '
    },
    actions: {
        readFileName ({ commit }) {
        return commit('setFileNames', readFileName("D://file"}})),mutations: {
      setFileNames (state, v) {
        state.fileNames = v
      }
    }
  })
}
Copy the code

3. In the component asyncData hook (which is called on the server), send the action in the server store to the server to fetch data and inject it into the state.

Note that the server store has data, but the client store is still empty. Only when the browser starts rendering will the server store be injected into window.INITIAL_STATE, and then we will be done injecting it into the browser store.

Finally, we inject the corresponding state of the client store into the component and render it to the page

<template>
    <div class>
        home
        <p v-for="it in item">{{it}}</p>
    </div>
</template>

<script>
export default {
    asyncData({ store, route }) {
        return store.dispatch('readFileName'.'D://file')},computed: {
        item() {
            return this.$store.state.fileNames
        }
    }
}
</script>
 
Copy the code

The effect

conclusion

At this point, our Vue server rendering project is basically built. Of course, all of these feature points are just a basic implementation. There is a lot of room for optimization, just for learning to understand the Vue SSR rendering process.

Although it might seem like we were doing a lot more by configuring the VUE SSR project directly than by using the NuXT off-the-shelf framework, we actually called the VUe-server-Renderer plug-in to achieve our purpose.

This article is only a record and summary of my personal learning, I hope to help students who are interested in VUE SSR, if there are mistakes, please kindly advise.

Address of this project: github.com/ou-jin/vue-…