preface

SSR is certainly no stranger to everyone. Through server rendering, YOU can optimize SEO crawl and improve the loading speed of the home page. When I was learning SSR, I read a lot of articles, some of which were very inspiring to me, and some just copied the official website documents. Through a few days of learning, I have some understanding of SSR, but also from the beginning of a complete configuration of the SSR development environment, so I want to through this article, sum up some experience, and hope to learn SSR friends play a little help.

I will walk you through the SSR configuration step by step in five steps:

  1. Browser-only rendering
  2. Server render, not includedAjaxInitialize data
  3. Server render, includingAjaxInitialize data
  4. Server render, usingserverBundleandclientManifestTo optimize
  5. A complete based onVue + VueRouter + VuexThe SSR engineering

If you are not familiar with what I said above, it doesn’t matter, follow me step by step down, eventually you can also configure an INDEPENDENT SSR development project, all the source code I will put on Github, we can refer to.

The body of the

1. Browser-only rendering

This configuration is a general development configuration based on Weback + vue. I will put some key code here. The complete code can be viewed on Github.

The directory structure
- node_modules
- components  
    - Bar.vue
    - Foo.vue
- App.vue
- app.js
- index.html
- webpack.config.js
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
Copy the code
app.js
import Vue from 'vue';
import App from './App.vue';

let app = new Vue({
  el: '#app',
  render: h => h(App)
});
Copy the code
App.vue
<template>
  <div>
    <Foo></Foo>
    <Bar></Bar>
  </div>
</template>

<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';

export default {
  components: {
    Foo, Bar
  }
}
</script>
Copy the code
index.html
<! DOCTYPE html> <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> Pure browser render </title> </head> <body> <div id="app"></div>
</body>
</html>
Copy the code
components/Foo.vue
<template>
  <div class="foo">
    <h1>Foo Component</h1>
  </div>
</template>

<style>
.foo {
  background: yellowgreen;
}
</style>
Copy the code
components/Bar.vue
<template>
  <div class="bar">
    <h1>Bar Component</h1>
  </div>
</template>

<style>
.bar {
  background: bisque;
}
</style>
Copy the code
webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  mode: 'development',

  entry: './app.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader'.'css-loader'.'postcss-loader'] / / if you need to separate out the CSS file, use the following configuration / / use: ExtractTextPlugin. Extract ({/ / fallback:'vue-style-loader',
        //   use: [
        //     'css-loader', / /'postcss-loader'//})}, {test: /\.(jpg|jpeg|png|gif|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000    // 10Kb
          }
        }
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html'}), // If you need to extract the CSS file separately, use the following configuration // new ExtractTextPlugin("styles.css")]};Copy the code
postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')]};Copy the code
.babelrc
{
  "presets": [
    "@babel/preset-env"]."plugins"Const Foo = () => import()'.. /components/Foo.vue')
    "dynamic-import-webpack"]}Copy the code
package.json
{
  "name": "01"."version": "1.0.0"."main": "index.js"."license": "MIT"."scripts": {
    "start": "yarn run dev"."dev": "webpack-dev-server"."build": "webpack"
  },
  "dependencies": {
    "vue": "^ 2.5.17"
  },
  "devDependencies": {
    "@babel/core": "^ 7.1.2." "."@babel/preset-env": "^ 7.1.0"."babel-plugin-dynamic-import-webpack": "^ 1.1.0." "."autoprefixer": "^ 9.1.5." "."babel-loader": "^ 8.0.4"."css-loader": "^ 1.0.0"."extract-text-webpack-plugin": "^ 4.0.0 - beta."."file-loader": "^ 2.0.0." "."html-webpack-plugin": "^ 3.2.0"."postcss": "^ 7.0.5"."postcss-loader": "^ 3.0.0"."url-loader": "^ 1.1.1"."vue-loader": "^ 15.4.2." "."vue-style-loader": "^ 4.1.2." "."vue-template-compiler": "^ 2.5.17"."webpack": "^ 4.20.2"."webpack-cli": "^ 3.1.2." "."webpack-dev-server": "^ 3.1.9." "}}Copy the code
The command
Start the development environment
yarn start
Copy the code
Build the production environment
yarn run build
Copy the code

Final effect screenshot:

See Github for the full code

2. Server rendering, not includedAjaxInitialize data

Server rendering SSR, similar to isomorphism, is ultimately about making a piece of code that can run on both the server and the client. If something goes wrong with SSR, you can roll back to browser-only rendering to make sure the user sees the page normally.

There must be two webpack entry files, one for rendering weboack.client.config.js on the browser side and one for rendering webpack.server.config.js on the server side. Extract their public parts as webpack.base.cofig.js and then merge them via webpack-merge. You also need a server to provide HTTP services, and I’m using KOA here.

Let’s take a look at the new directory structure:

-node_modules-config // Added -webpack.base.config. js -webpack.client.config. js -webpack.server.config. js-src - Components - bar.vue-foo.vue-app.vue-app.js - entry-client.js // added - entry-server.js // added - index.html - Index.ssr. HTML // Added -package. json -yarn.lock-postcss.config. js -.babelrc -.gitignoreCopy the code

In a client-only app, each user uses the new application instance in their respective browser. This is also expected for server-side rendering: each request should be a new, separate application instance so that there is no cross-request state pollution.

So, we’ll change app.js to wrap it as a factory function that generates a new root component every time it’s 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

On the browser side, we simply create a new root component and mount it.

entry-client.js

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

const { app } = createApp();

app.$mount('#app');
Copy the code

On the server side, we return a function that takes a context argument and returns a new root component each time. This context is not going to be used here, it’s going to be used in the next steps.

entry-server.js

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

export default context => {
  const { app } = createApp();

  return app;
}
Copy the code

Then take a look at index.ssr. HTML

index.ssr.html

<! DOCTYPE html> <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> </head> <body> <! --vue-ssr-outlet--> <scripttype="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>
Copy the code

serves as a placeholder for the subsequent insertion of the component’S HTML string parsed by the server via vue-server-renderer.

is to put the file that webpack packaged with webpack.client.config.js here (this is for a simple demonstration, there will be other ways to do this later).

Since all the server spit out is an HTML string, the subsequent Vue responses, event responses, etc., need to be taken over by the browser, so the browser-side rendering package needs to be introduced here.

Officially, it is called client-side hydration.

Client-side activation refers to the process by which the Vue takes over static HTML sent by the server at the browser side and turns it into a dynamic DOM managed by the Vue.

In entry-client.js, we mount the application with the following line:

// This assumes the app. vue template root element's 'id="app"`
app.$mount('#app')
Copy the code

Since the server has rendered the HTML, we obviously don’t have to throw it away and recreate all the DOM elements. Instead, we need to “activate” the static HTML and then make it dynamic (able to respond to subsequent data changes).

If you examine the server rendered output, you’ll notice that a special attribute has been added to the root element of the application:

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

Vue relies on this attribute to activate HTML from the server on the browser side, as we’ll see in a moment.

Let’s take a look at the webPack configuration:

webpack.base.config.js

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  mode: 'development',

  resolve: {
    extensions: ['.js'.'.vue']
  },

  output: {
    path: path.resolve(__dirname, '.. /dist'),
    filename: '[name].bundle.js'
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader'.'css-loader'.'postcss-loader'] {},test: /\.(jpg|jpeg|png|gif|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000    // 10Kb
          }
        }
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin()
  ]
};
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, '.. /src/entry-client.js')
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '.. /src/index.html'),
      filename: 'index.html']}}));Copy the code

Notice that the entry file becomes entry-client.js, and insert its packaged client.bundle.js into index.html.

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, {
  target: 'node',
  entry: {
    server: path.resolve(__dirname, '.. /src/entry-server.js')
  },
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '.. /src/index.ssr.html'),
      filename: 'index.ssr.html',
      files: {
        js: 'client.bundle.js'
      },
      excludeChunks: ['server']]}}));Copy the code

There are a few points to note here:

  1. The entry file isentry-server.js
  2. Because it is packaged server-side dependent code, sotargetTo set anodeAnd at the same time,outputthelibraryTargetTo set acommonjs2

The HtmlWebpackPlugin plugin is not a package of server.bundle.js in index.ssr. HTML, but a package of client. This is so that Vue can activate the HTML spit out by the server and take over the subsequent response.

So where does the packaged server.bundle.js work? Read on to find out

package.json

{
  "name": "01"."version": "1.0.0"."main": "index.js"."license": "MIT"."scripts": {
    "start": "yarn run dev"."dev": "webpack-dev-server"."build:client": "webpack --config config/webpack.client.config.js"."build:server": "webpack --config config/webpack.server.config.js"
  },
  "dependencies": {
    "koa": "^ 2.5.3." "."koa-router": "^ 7.4.0"."koa-static": "^ 5.0.0"."vue": "^ 2.5.17"."vue-server-renderer": "^ 2.5.17"
  },
  "devDependencies": {
    "@babel/core": "^ 7.1.2." "."@babel/preset-env": "^ 7.1.0"."autoprefixer": "^ 9.1.5." "."babel-loader": "^ 8.0.4"."css-loader": "^ 1.0.0"."extract-text-webpack-plugin": "^ 4.0.0 - beta."."file-loader": "^ 2.0.0." "."html-webpack-plugin": "^ 3.2.0"."postcss": "^ 7.0.5"."postcss-loader": "^ 3.0.0"."style-loader": "^ 0.23.0"."url-loader": "^ 1.1.1"."vue-loader": "^ 15.4.2." "."vue-style-loader": "^ 4.1.2." "."vue-template-compiler": "^ 2.5.17"."webpack": "^ 4.20.2"."webpack-cli": "^ 3.1.2." "."webpack-dev-server": "^ 3.1.9." "."webpack-merge": "^ 4.1.4." "}}Copy the code

Let’s look at the HTTP server code:

server/server.js

const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs');
const backendApp = new Koa();
const frontendApp = new Koa();
const backendRouter = new Router();
const frontendRouter = new Router();

const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')}); // Backendrouter.get (backendRouter)'/index'Renderer. renderToString((err, HTML) => {// There is a problem with the HTML returned with renderToString promise.if (err) {
      console.error(err);
      ctx.status = 500;
      ctx.body = 'Server internal error';
    } else{ console.log(html); ctx.status = 200; ctx.body = html; }}); }); backendApp.use(serve(path.resolve(__dirname,'.. /dist')));

backendApp
  .use(backendRouter.routes())
  .use(backendRouter.allowedMethods());

backendApp.listen(3000, () => {
  console.log('Server side render address: http://localhost:3000'); }); // Frontendrouter.get ('/index', (ctx, next) => {
  let html = fs.readFileSync(path.resolve(__dirname, '.. /dist/index.html'), 'utf-8');
  ctx.type = 'html';
  ctx.status = 200;
  ctx.body = html;
});

frontendApp.use(serve(path.resolve(__dirname, '.. /dist')));

frontendApp
  .use(frontendRouter.routes())
  .use(frontendRouter.allowedMethods());

frontendApp.listen(3001, () => {
  console.log('Browser rendering address: http://localhost:3001');
});
Copy the code

Port 3000 is used for server rendering and port 3001 is used for direct output of index.html. Then Vue will be used in the browser, mainly for comparison with server rendering.

The key code here is how to output the HTML string on the server side.

const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.bundle.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')});Copy the code

As you can see, server.bundle.js is used here because its entry is a function that takes context as an argument (not mandatory) and outputs a root component app.

Here we use the jue-server-renderer plugin, which has two methods for rendering: createRenderer and createBundleRenderer.

const { createRenderer } = require('vue-server-renderer') const renderer = createRenderer({/* options */})Copy the code
const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer(serverBundle, {/* options */})Copy the code

CreateRenderer cannot receive the server.bundle.js file packaged for the server, so createBundleRenderer is used instead.

The serverBundle parameter can be one of the following:

  • Absolute path, pointing to an already built onebundleFile (.js.json). Have to be/The beginning is recognized as the file path.
  • bywebpack + vue-server-renderer/server-pluginThe generatedbundleObject.
  • JavaScriptCode strings (not recommended).

Here we’re going to introduce.js files, and we’ll show you how to use.json files and what the benefits are.

renderer.renderToString((err, html) => {
    if (err) {
      console.error(err);
      ctx.status = 500;
      ctx.body = 'Server internal error';
    } else{ console.log(html); ctx.status = 200; ctx.body = html; }});Copy the code

The renderer function returned by createRenderer and createBundleRenderer contains two methods, renderToString and renderToStream, RenderToString returns a complete string on success, and renderToStream returns a Node stream.

RenderToString supports promises, but when I use the Prmoise form, the style will not render. I don’t know why yet, but please leave a comment if you know.

The configuration is almost complete. Let’s see how it works.

Yarn run build:client yarn run build:client yarn run build:server Providing HTTP servicesCopy the code

Final effect display:

Go to http://localhost:3000/index

We see the data-server-Rendered =”true” attribute mentioned earlier, and the client-bundle. js file will be loaded so that Vue can take over on the browser side.

http://localhost:3001/index is the first step and the effect of the implementation, pure browser rendering, here don’t put the screenshots.

See Github for the full code

3. Server-side rendering, including Ajax initialization data

If SSR needs to initialize some asynchronous data, the process becomes a little more complicated.

Let’s start with a few questions:

  1. Where does the server take asynchronous data?
  2. How do I determine which components need to fetch asynchronous data?
  3. Do I get asynchronous data back into the component like jose?

Let’s move on with the questions, and hopefully by the end of this article you’ll have answers to all of them.

Server-side rendering and browser-side rendering components go through different life cycles. On the server side, only beforeCreate and CREATED life cycles are used. There is no beforeMount and Mounted, there is no updating, there is no beforeUpdate and updated, because the SSR server simply spits out HTML strings and does not render DOM structures.

Let’s start by thinking about how, in a browser-only rendered Vue project, we get asynchronous data and render it into a component. A created or Mounted life cycle initiates an asynchronous request, and then executes this.data = XXX in the successful callback. Vue executes a Dom Diff to update the created data.

Can we do the same for server rendering? The answer is no.

  1. inmountedI’m not gonna be able to becauseSSRAll have nomountedLife cycle, so definitely not here.
  2. inbeforeCreateIs it ok to make an asynchronous request? Because the request is asynchronous, the server may send the request before the interface returnshtmlThe string is concatenated.

So, take a look at the official documentation to get the following ideas:

  1. Before rendering, all asynchronous data is pre-fetched and stored inVuexthestoreIn the.
  2. Back end when rendering throughVuexThe obtained data is injected into the corresponding component.
  3. thestoreIs set towindow.__INITIAL_STATE__Attribute.
  4. In the browser environment, pass theVuexwillwindow.__INITIAL_STATE__The data inside is injected into the corresponding component.

Normally, through these steps, the server spit out the HTML string corresponding to the component data is up to date, so step 4 does not cause DOM updates. However, if something goes wrong and the spit out HTML string does not have corresponding data, Vue can also inject data in the browser via ““Vuex. Update the DOM.

Updated directory structure:

- node_modules - config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js - src - Component-bar.vue-foo.vue-store // Add store.js-app.vue-app.js-entry-client.js-entry-server.js - index.html - index.ssr.html - package.json - yarn.lock - postcss.config.js - .babelrc - .gitignoreCopy the code

Take a look at store.js:

store/store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const fetchBar = function() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Bar component returns Ajax data');
    }, 1000);
  });
};

function createStore() {
  const store = new Vuex.Store({
    state: {
      bar: ' '
    },

    mutations: {
      'SET_BAR'(state, data) {
        state.bar = data;
      }
    },

    actions: {
      fetchBar({ commit }) {
        return fetchBar().then((data) => {
          commit('SET_BAR', data); }).catch((err) => { console.error(err); }}}}));if(typeof window ! = ='undefined' && window.__INITIAL_STATE__) {
    console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
    store.replaceState(window.__INITIAL_STATE__);
  }
  
  return store;
}

export default createStore;

typeof window
Copy the code

If you don’t know much about Vuex, go to the Vuex website to see some basic concepts.

Here the fetchBar can be thought of as an asynchronous request, which is simulated with setTimeout. Commit corresponding mutation changes the state in a successful callback.

Here’s a key piece of code:

if(typeof window ! = ='undefined' && window.__INITIAL_STATE__) {
    console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
    store.replaceState(window.__INITIAL_STATE__);
}
Copy the code

Since store.js is also packaged into server.bundle.js, the runtime environment does not have to be a browser. Note The server has obtained all the asynchronous data required for initialization. You need to replace the status in the Store to ensure consistency.

components/Bar.vue

<template>
  <div class="bar">
    <h1 @click="onHandleClick">Bar Component</h1> <h2> </h2> <span>{{ msg }}</span> </div> </template> <script> const fetchInitialData = ({ store }) => { store.dispatch('fetchBar');
  };

  export default {
    asyncData: fetchInitialData,

    methods: {
      onHandleClick() {
        alert('bar'); }},mounted() {// Since the server only has a beforeCreate and created life cycle, it will not go here. // So the Ajax initialization data is also written herelet store = this.$store;
      fetchInitialData({ store });
    },

    computed: {
      msg() {
        return this.$store.state.bar;
      }
    }
  }
</script>

<style>
.bar {
  background: bisque;
}
</style>

Copy the code

Here we add a method asyncData to the default export object of the Bar component, which dispatches the corresponding action for asynchronous data retrieval.

In Mounted, I also write code to retrieve data. Why? Since the server does not have mounted life cycle, so I write here can solve the problem of using in the browser environment can also initiate the same asynchronous request to initialize data.

components/Foo.vue

<template>
  <div class="foo">
    <h1 @click="onHandleClick">Foo Component</h1>
  </div>
</template>

<script>
export default {
  methods: {
    onHandleClick() {
      alert('foo');
    }
  },
}
</script>

<style>
.foo {
  background: yellowgreen;
}
</style>

Copy the code

Here I add a click event to both components to prove that after the server spits out the home page HTML, the subsequent steps are taken over by the browser-side Vue and can do the rest.

app.js

import Vue from 'vue';
import createStore from './store/store.js';
import App from './App.vue';

export function createApp() {
  const store = createStore();

  const app = new Vue({
    store,
    render: h => h(App)
  });

  return { app, store, App };
}
Copy the code

When creating the root component, pass in the Vuex store and return it, which will be used later.

Finally, take a look at entry-server.js. Here are the key steps:

entry-server.js

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

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

    let components = App.components;
    let asyncDataPromiseFns = [];
  
    Object.values(components).forEach(component => {
      if(component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); }}); Promise.all(asyncDataPromiseFns).then((result) => {// When template is used, context.state will be the window.__initial_state__ state, Context. state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); }); }Copy the code

We use the exported App to retrieve all the components below it, and then iterate to find out which components have asyncData methods. If so, we call them and pass them to store. This method returns a Promise. To resolve (app).

Context. state = store.state when createBundleRenderer is used, if template is set, The value of context.state is automatically inserted into the template HTML as window.__initial_state__.

This is where you need to think a little bit more and figure out the whole server-side rendering logic.

How it works:

yarn run build:client
yarn run build:server

yarn start
Copy the code

Final effect screenshot:

Server render: Openhttp://localhost:3000/index

You can see that window.__initial_state__ is automatically inserted.

Let’s compare the effect of SSR on loading performance.

Server rendering performance screenshot:

Pure browser rendering performance screenshot:

Also in fast 3G network mode, pure browser rendering first screen loading takes 2.9s, because client.js loading takes 2.27s, because without client.js there is no Vue, there is no behind things.

The server side takes 0.8s to render the first screen. Although the client.js loading and throwing cost 2.27s, it is no longer needed for the first screen. It is in order for Vue to take over from the browser side.

From this we can really see that server-side rendering is very useful for improving the response speed of the first screen.

Of course, some students may ask, we also delayed the server rendering to get the initial Ajax data for 1s, during which time the user can not see the page. Yes, the interface time we can not avoid, even pure browser rendering, the home page of the interface still have to adjust, if the interface is slow, then pure browser rendering to see the full page will be slower.

See Github for the full code

4. Optimize with serverBundle and clientManifest

We created the server-side renderer as follows:

const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')});Copy the code

ServerBundle We use a packaged server.bundle.js file. To do so, the service must be stopped and restarted each time the application source code is edited. This can affect development efficiency during development. In addition, Node.js itself does not support source maps.

Vue-server-renderer provides an API called createBundleRenderer to handle this problem, using a custom plugin for WebPack, The Server bundle will generate special JSON files that can be passed to the Bundle Renderer. The created Bundle renderer is used in the same way as the normal renderer, but the bundle renderer provides the following advantages:

  • The built-insource mapSupport (inwebpackUse in configurationdevtool: 'source-map')
  • Hot reloading in the development environment or even during deployment (by reading the updatedbundleAnd then recreate itrendererInstance)
  • The keyCSS(critical CSS)Injection (in use*.vueFile time) : automatically inlines the components needed during renderingCSS. Check out more detailsCSSChapter.
  • useclientManifestPerform resource injection: automatically infer the best preload (preload) and prefetch (prefetch) instructions, and the code segmentation required for initial renderingchunk.

Preload and prefetch are useful if you don’t know about them.

So let’s modify the WebPack configuration:

webpack.client.config.js

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

module.exports = merge(base, {
  entry: {
    client: path.resolve(__dirname, '.. /src/entry-client.js'}, plugins: [new VueSSRClientPlugin(), // add new HtmlWebpackPlugin({template: path.resolve(__dirname,'.. /src/index.html'),
      filename: 'index.html']}}));Copy the code

webpack.server.config.js

const path = require('path');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const base = require('./webpack.base.config');

module.exports = merge(base, {
  target: 'node'// The bundle renderer is providedsourceThe map support devtool:'#source-map',
  entry: {
    server: path.resolve(__dirname, '.. /src/entry-server.js'}, externals: [nodeExternals()], // Add output: {libraryTarget:'commonjs2'}, plugins: [new VueSSRServerPlugin(), // This should be the first one to write, or else CopyWebpackPlugin will not work. New HtmlWebpackPlugin({template: path.resolve(__dirname,'.. /src/index.ssr.html'),
      filename: 'index.ssr.html',
      files: {
        js: 'client.bundle.js'
      },
      excludeChunks: ['server']]}}));Copy the code

Externals: [nodeExternals()] externals: [nodeExternals()]

The two configuration files generate vue-ssR-client-manifest. json and vue-SSR-server-bundle. json, respectively. As a parameter to createBundleRenderer.

Look at the server. Js

server.js

const serverBundle = require(path.resolve(__dirname, '.. /dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '.. /dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8');

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: template,
  clientManifest: clientManifest
});
Copy the code

The effect and the third step is the same, not screenshots, the complete code to see github.

5. Configure a complete SSR based on Vue + VueRouter + Vuex

Unlike step 4, vue-Router is introduced, which is closer to the actual development project.

Add the router directory under SRC.

router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import Bar from '.. /components/Bar.vue';

Vue.use(Router);

function createRouter() {
  const routes = [
    {
      path: '/bar',
      component: Bar
    },
    {
      path: '/foo',
      component: () => import('.. /components/Foo.vue'}]; const router = new Router({ mode:'history',
    routes
  });

  return router;
}

export default createRouter;
Copy the code

Here we introduce the Foo component as an asynchronous component and make it load on demand.

Add router to app.js and export:

app.js

import Vue from 'vue';
import createStore from './store/store.js';
import createRouter from './router';
import App from './App.vue';

export function createApp() {
  const store = createStore();
  const router = createRouter();

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

  return { app, store, router, App };
}
Copy the code

Modify app.vue import route component:

App.vue

<template>
  <div id="app">
    <router-link to="/bar">Goto Bar</router-link> 
    <router-link to="/foo">Goto Foo</router-link> 
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('App.vue beforeCreate');
  },

  created() {
    console.log('App.vue created');
  },

  beforeMount() {
    console.log('App.vue beforeMount');
  },

  mounted() {
    console.log('App.vue mounted');
  }
}
</script>
Copy the code

The most important changes are in entry-server.js,

entry-server.js

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

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

    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();

      console.log(context.url)
      console.log(matchedComponents)

      if(! matchedComponents.length) {return reject({ code: 404 });
      }

      Promise.all(matchedComponents.map(component => {
        if (component.asyncData) {
          returncomponent.asyncData({ store }); }})).then(() => {// When template is used, context.state will be the window.__initial_state__ state, Context. state = store.state; // Return root resolve(app); }); }, reject); }); }Copy the code

The context mentioned above is useful here, passing in the URL that the user accesses for use by vue-Router. Since there are asynchronous components, the router.onReady success callback looks for the component that the URL route matches and gets asynchronous data is the same as before.

So, we have a basically complete configuration based on Vue + VueRouter + VuexSSR, complete the code and look at Github.

Final effect demonstration:

Go to http://localhost:3000/bar:

See Github for the full code

subsequent

Above, we completed the isomorphism from pure browser rendering to full server rendering in five steps. The code can be run on both the browser side and the server side. So, let’s go back and see if there’s room for improvement, or expansion thinking.

Optimization:
  1. We are currently usingrenderToStringMethod, full generationhtmlAfter that, it will be returned to the client if usedrenderToStreamApplication,bigpipeTechnology can continually return a stream to the browser so that the file loading browser can display something as early as possible.
const stream = renderer.renderToStream(context)
Copy the code

The value returned is Node.js stream:

let html = ' '

stream.on('data', data => {
  html += data.toString()
})

stream.on('end', () => {console.log(HTML) // Render complete}) stream.on()'error', err => {
  // handle error...
})
Copy the code

In streaming rendering mode, the renderer sends data as quickly as possible as it traverses the virtual DOM tree. This means we can get the “first chunk” as quickly as possible and start sending it to clients faster.

However, when the first chunk of data is issued, the child components may not even be instantiated and their lifecycle hooks may not be called. This means that if a child component needs to append data to the render context in its lifecycle hook function, that data will not be available when the stream starts. This is because a lot of context information (such as head information or inline critical CSS) needs to appear before application markup, We basically have to wait for the stream to complete before we can start using the context data.

Therefore, if you rely on context data populated by component lifecycle hook functions, streaming mode is not recommended.

  1. webpackTo optimize the

Webpack optimization is a big topic, so I don’t want to discuss it here. If you are interested, you can find some information by yourself. I may write an article on Webpack optimization later.

thinking
  1. Is it mandatory to usevuex?

The answer is no. Vuex just to help you to achieve a set of data storage, updating, access mechanism, if you don’t have to Vuex, then you have to think a plan can to save asynchronous access to data, and put it into components at the right time, there are some article put forward some solutions, I will be on the reference articles, everyone can read.

  1. Whether to useSSRMust be good?

This is also not certain, any technology has usage scenarios. SSR can help you improve the home page loading speed and optimize search engine SEO, but at the same time, because it needs to render the entire Vue template in node, it will take up server load, and only execute beforeCreate and created two life cycles. Some external extension libraries need to do some processing before they can run in SSR and so on.

conclusion

This article through five steps, from the pure browser side rendering, to configure a complete SSR environment based on Vue + VUE-Router + Vuex, introduced a lot of new concepts, perhaps you read a time do not understand, then combined with the source code, to their own hand several times, and then look at the article several times, I believe you can master SSR.

Finally, all the source code of this article is on my Github. If it helps you, please give it a like

Welcome to follow my public number

Refer to the link

  • ssr.vuejs.org/zh/
  • zhuanlan.zhihu.com/p/35871344
  • www.cnblogs.com/qingmingsan…
  • Juejin. Cn/post / 684490…
  • Github.com/youngwind/b…

My blog is synchronized to tencent cloud + community, invite everyone to come together: cloud.tencent.com/developer/s…