Basic concepts of server-side rendering

Client-side rendering

The most common SPA page that front-end developers do is typical client rendering. When sending a request, we get a simple HTML structure and then load static resources, resulting in a blank screen on the front page. When the resource load is executed, the page elements created by the client are displayed, which is called client rendering

Server side rendering

Server-side rendering has been around for a long time, such as in the days of typical PHP output pages, where the entire HTML content of the page was sent to the client at once on request, with good rendering speed and SEO friendly support. But for the current front-end framework server rendering, we are not only output HTML, but to play the framework to provide a variety of functions, that is, today to talk about VUE SSR

Problems that need to be solved

In order to implement a straight HTML structure while preserving the framework’s ability, we need to solve the following problems:

1. Problems with converting vUE instances to HTML strings

The output to the browser should preserve the vUE capability, that is, interaction

3. Engineering ability

The directory structure

├ ─ ─ index. The HTML ├ ─ ─ package. The json ├ ─ ─ the readme. Md ├ ─ ─ for server js # server runs the script ├ ─ ─ the SRC | ├ ─ ─ components | ├ ─ ─ the router | | └ ─ ─ Index. Js | └ ─ ─ views | └ ─ ─ Home. Vue | ├ ─ ─ App. Vue | ├ ─ ─ App. Js | ├ ─ ─ client_entry. Js | ├ ─ ─ server_entry. JsCopy the code

Start coding

The basic case

Install the base dependencies first

yarn add express vue vue-router vue-server-renderer -S
yarn add nodemon -D
Copy the code

The vue-server-renderer dependency is officially provided to help us handle server-side rendering. Express is the back-end framework for this experiment

Server codeserver.js

const express = require('express')
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = express()

app.get(The '*'.async (req, res) => {
    const vm = new Vue({
        template: `<button @click="count++">{{count}}</button>`.data: {
            count: 1
        },
    })
    renderer.renderToStream(vm).pipe(res)
})

app.listen(3000)
Copy the code

Add a script to package.json to run the server-side code

 "start": "nodemon server.js"
Copy the code

A blank button appears on the page, but clicking it has no effect. By looking at the source code found is very pure source code, even the standard HTML has no.

Write a template to enrich the page

CreateRenderer can pass some configuration parameters, you can refer to the documentation or click in to see how to configure

server.js

const express = require('express')
const Vue = require('vue')
const template = require('fs').readFileSync('./index.html'.'utf-8')
const renderer = require('vue-server-renderer').createRenderer({
    template
})
const app = express()

app.get(The '*'.async (req, res) => {
    const vm = new Vue({
        template: `<button @click="count++">{{count}}</button>`.data: {
            count: 1
        },
    })
    renderer.renderToStream(vm, {
        title: 'hello vue ssr'
    }).pipe(res)
})

app.listen(3000)
Copy the code

index.html

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>{{title}}</title>
</head>

<body>
    <! --vue-ssr-outlet-->
</body>

</html>
Copy the code

If you are careful, you will notice that I passed the second parameter to renderToStream and rendered it in HTML. So far, I have experienced SSR rendering, but it is far from my usual vUE – CLI project development. Can’t play with our single file components and routing and so on, let’s start to change

Engineering solution

To do it as a normal development project, there will be no dependency on webpack packaging, etc. Install the dependency before you start.

yarn add webpack webpack-cli vue-loader vue-template-compiler vue-style-loader css-loader babel-loader @babel/core @babel/preset-env webpack-merge -D
Copy the code

After installing the dependencies, we first created a build folder in the root directory to hold our WebPack build configuration files and created three files:

  • build/base.config.js
  • build/client.config.js
  • build/server.config.js

The previous basic part obviously only solves part of the problem on the server side, how to ensure the back-end output and front-end interaction and code reuse and engineering construction have not been solved. Before we write the configuration of the packaged build, let’s write the entry file for the server and client respectively. Let’s look at the following figure:

Because the server process is resident in memory, we need to avoid singletons to avoid cross-request state contamination, so the general code app.js is as follows:

const Vue = require('vue')

module.exports = function () {
    const app = new Vue({
        data: {
            count: 1
        },
        template: `
      
`
,})return app } Copy the code

We haven’t used webpack yet, so now we’re writing the code directly in CommonJS, and the corresponding server.js will look like this:

const express = require('express')
const Vue = require('vue')
const template = require('fs').readFileSync('./index.html'.'utf-8')
const renderer = require('vue-server-renderer').createRenderer({
    template
})
const app = express()

app.get(The '*'.async (req, res) => {
    const createApp = require('./src/app.js')
    renderer.renderToStream(createApp(), {
        title: 'hello vue ssr'
    }).pipe(res)
})

app.listen(3000)
Copy the code

But ah but, now there is no use of Webpack, so we can not play well ah, certainly not in line with the style of front-end son, right? Let’s write the client side and the server side entry file separately, incidentally also transformed into esModule app.vue

<template> <div id="app"> <h1>this is app vue</h1> <button @click="count++">{{ count }}</button> </div> </template> <script> export default { data() { return { count: 1, }; }}; </script> <style> </style>Copy the code

app.js

import Vue from 'vue'
import App from './App.vue'

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

client_entry.js

import createApp from './app.js'
createApp().$mount('#app')
Copy the code

server_entry.js

import createApp from './app.js'

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

Js can not be used directly in the source file, so we need to write a package configuration to package the ability to enrich the project, we only need to package the resources needed by the ue-server-renderer, so that our server program can complete SSR and client isomers:

build/base.config.js

const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
    mode: 'development'.output: {
        filename: '[name].bundle.js'
    },
    module: {
        rules: [{test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'.options: {
                        presets: ['@babel/preset-env'],}},}, {test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    {
                        loader: 'css-loader',}]}]},plugins: [
        new VueLoaderPlugin()
    ]
}
Copy the code

build/client.config.js

const path = require('path')
const { merge } = require('webpack-merge')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const base = require('./base.config.js')
module.exports = merge(base, {
    entry: {
        client: path.resolve(__dirname, '.. /src/client_entry.js')},plugins: [
        new VueSSRClientPlugin()
    ]
})
Copy the code

build/server.config.js

const path = require('path')
const { merge } = require('webpack-merge')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const base = require('./base.config.js')
module.exports = merge(base, {
    entry: {
        client: path.resolve(__dirname, '.. /src/server_entry.js')},target: 'node'.output: {
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new VueSSRServerPlugin()
    ]
})
Copy the code

Next we add a few commands to package.json:

"scripts": {
    "start": "nodemon server.js"."build": "npm run build:client && npm run build:server"."build:client": "webpack --config build/client.config.js"."build:server": "webpack --config build/server.config.js"
}
Copy the code

After the build is executed and the packaged file is generated, we will modify our server-side code server.js

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

const app = express()
app.use(express.static('dist')) //
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
})




app.get(The '*'.async (req, res) => {
    // const createApp = require('./src/app.js')
    // renderer.renderToStream(createApp(), {
    // title: 'hello vue ssr'
    // }).pipe(res)
    renderer.renderToString({
        title:'hello vue ssr'
    }, (err, html) = > {
        // Handle exceptions......
        res.end(html)
    })
})

app.listen(3000)
Copy the code

Run yarn Build and YARN start again. After running yarn build and YARN start again, the console displays an error.

1. Specify publicPath in the basic configuration of WebPack

2, app. Use (express. Static (‘ dist ‘))

At this point, a basic server rendering is complete, the page is straight OUT of HTML, and the buttons are clickable!

Resolve routing problems

So far I have not used routing, first think about it, the service is from the background, and then route JS code here certainly do not know, certainly need to tell the server to pass to our application code! Okay, so let’s write some basic routing code, and then we’ll solve the routing problem

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import About from '.. /views/About.vue'
import Home from '.. /views/Home.vue'

function createRouter() {
    return new VueRouter({
        mode: 'history'.routes: [{path: '/about'.component: About
            },
            {
                path: '/'.component: Home
            }
        ]
    })
}


export default createRouter
Copy the code

Change the corresponding app.js to:

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

The server and client entry files also need to be modified to deconstruct the app and route, especially the server entry needs to be modified to a promise form because there may be asynchronous components

server_entry.js

import createApp from './app.js'

export default (ctx) => {
    return new Promise((resolve, reject) = > {
        const { app, router } = createApp()
        router.push(ctx.url)
        router.onReady(function () {
            resolve(app)
        })
    })
}
Copy the code

Finally, we modify server.js to pass the requested URL to our application code

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

const app = express()
app.use(express.static('dist'))
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
})




app.get(The '*'.async (req, res) => {
    
    renderer.renderToString({
        title:'hello vue ssr'.url:req.url
    }, (err, html) = > {
        // Handle exceptions......
        console.log(err);
        res.end(html)
    })
})

app.listen(3000)
Copy the code

At this point, we can run the build and start commands again and enjoy the routing perfectly.

The last

Finally, I would like to say that routing can be handled dynamically by referring to nuxt.js. Of course, you can also inject some server-side data fetching functions and some 404 scenarios. There are still unfinished parts of the project: code splitting, VUex, etc! Hopefully this article has given you a good understanding of how to render on the server side. Especially that picture must be carefully and repeatedly look at oh, the last project code repository is: SSr_demo, if there is any problem or error, please reply in the comment area ~ ~