vue project transformation SSR (server rendering)

Posted by kye on Thu, 20 Jan 2022 12:29:11 +0100

1. What is SSR (server-side rendering)?

Traditional vue project browser rendering mode

Disadvantages: 1. SEO problems
2. First screen speed problem
3. Performance issues

ssr server rendering mode

advantage:
1. Better SEO, because the search engine crawler can directly view the fully rendered page
2. Fast first screen rendering

SSR is simply to display the page directly on the client after rendering on the server.

2. SSR principle

Simple example

index.template.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
    {{{ metas }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

server.js

// eslint-disable-next-line @typescript-eslint/no-var-requires
const Vue = require('vue');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const server = require('express')();

// eslint-disable-next-line @typescript-eslint/no-var-requires
const template = require('fs').readFileSync('./index.template.html', 'utf-8');

// eslint-disable-next-line @typescript-eslint/no-var-requires
const renderer = require('vue-server-renderer').createRenderer({
    template,
});
const context = {
    title: 'vue ssr',
    metas: `
        <meta name="keyword" content="vue,ssr">
        <meta name="description" content="vue srr demo">
    `,
};

server.get('*', (req, res) => {
    const app = new Vue({
        data: {
            url: req.url
        },
        template: `<div>Visited URL Yes: {{ url }}</div>`,
    });

    renderer
        .renderToString(app, context, (err, html) => {
            console.log(html);
            console.log(err)
            if (err) {
                res.status(500).end('Internal Server Error')
                return;
            }
            res.end(html);
        });
})
server.listen(8081);

Build steps

vue projects are mounted to html through virtual DOM, so for spa projects, crawlers only see the initial structure. Virtual DOM should be finally transformed into real DOM through certain methods. Virtual DOM is JS object. The rendering process of the whole server is completed by compiling virtual DOM into complete html.

After we parse the virtual DOM into html through server-side rendering, you will find that the events on the page cannot be triggered. That's because the server-side rendering Vue server renderer plug-in does not do this, so we need the client-side rendering again, referred to as isomorphism. Therefore, Vue server rendering is actually rendered twice. An official figure is given below:

Two bundle files need to be generated through Webpack packaging:
Client Bundle for browser. It is similar to the pure Vue front-end project Bundle
Server Bundle, used by server-side SSR, is a json file

Whether or not the project was built using Vue cli, regardless of what it was like before. There will be this construction and transformation process. The Vue server renderer library will be used in the construction and transformation. Note that the Vue server renderer version should be the same as the Vue version.

Directory structure after packaging

3. Reconstruction SSR of Vue project (vue-cli4 / vue2 as an example)

1. Install dependent packages

npm install vue-server-renderer lodash.merge  webpack-node-externals cross-env

2. Modify router

// Original writing
// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })
//
// export default router

// Revised writing
export default function createRouter() {
  return new VueRouter({
    mode: "history",  // Be sure to history 
    base: process.env.BASE_URL,
    routes,
  });
}

3. Modify main ts

// Original writing
// new Vue({
//   router,
//   store,
//   render: h => h(App)
// }).$mount('#app')

// Revised writing
export function createApp() {
  // Create router 
  const router = createRouter();
  const app = new Vue({
    router,
    render: (h) => h(App),
  });
  return { app, router };
}

4. Create entry client js

import { createApp }  from './main'

const { app } = createApp()

app.$mount('#app')

5. Create entry server js

import {createApp} from "./main.ts";
// context is actually server / index JS, and server / index will be mentioned later js
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default context => {
    return new Promise((resolve, reject) => {
        const {app, router} = createApp();
        router.push(context.url) ;
        router.onReady(()=>{
            // Does it match the components we want to use
            const matchs = router.getMatchedComponents();
            if(!matchs) {
                return reject({code: 404})
            }
            resolve(app);
        }, reject);
    })
}

6. Modify the webpack package file

vue.config.js

// Server rendering plug-in
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); // Generate service end package
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); // Generate client package
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");

// Environment variable: determines whether the portal is a client or a server. WEBPACK_TARGET is set in the startup item. See package JSON file
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    css: { extract: false
    },
    outputDir: "./dist/" + target,
    configureWebpack: () => ({
        // Point the entry to the server / client file of the application
        entry: `./src/entry-${target}.js`,
        // Provide source map support for bundle renderer
        devtool: "source-map",
        // This allows webpack to handle dynamic import in a Node appropriate manner / / and also tells' Vue loader 'to transport server oriented code when compiling Vue components.
        target: TARGET_NODE ? "node" : "web",
        node: TARGET_NODE ? undefined : false,
        output: {
            // Here, configure the server side to build in the style of node
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        // External application dependent modules. You can make the server build faster and generate smaller bundle files.
        externals: TARGET_NODE ? nodeExternals({
            // Do not externalize the dependent modules that webpack needs to handle. (formerly whitelist, changed to allowlist)
            allowlist: [/\.css$/]
        }) : undefined,
        optimization: { splitChunks: TARGET_NODE ? false : undefined}, // This is a plug-in that builds the entire output of the server into a single JSON file// The default server file name is ` Vue SSR server bundle JSON ` / / the default file name of the client is ` Vue SSR client manifest json`
         plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
    }),
    chainWebpack: config => {
        config.module .rule("vue") .use("vue-loader") .tap(options => { merge(options, { optimizeSSR: false }); });
    }
};

7. Create SSR html template

index.template.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{title}}test ssr</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

8. nodejs server

// nodejs server
const express = require("express");
const fs = require("fs");
// Create express instance and vue instance
const app = express();

// Create a renderer to get a createbundlerender
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require("../dist/server/vue-ssr-server-bundle.json");
const clientManifest = require("../dist/client/vue-ssr-client-manifest.json");
const template = fs.readFileSync("../src/index.template.html", "utf-8"); // ssr template file
const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
});

// The middleware handles static file requests
app.use(express.static("../dist/client", { index: false })); // false prevents it from rendering as dist / client / index html
// app.use(express.static('../dist/client') / / if you change to this line of code, you need to put dist / client / index Html is deleted, otherwise the index.html in this directory will be rendered first HTML file

// Front end request return data
app.get("*", async (req, res) => {
    try {
        const context = { url: req.url, title: "ssr",};
    // nodejs stream data. The file is too large. Using renderToString will cause a card
        const stream = renderer.renderToStream(context);
        let buffer = [];
        stream.on("data", (chunk) => {
            buffer.push(chunk);
        });
        stream.on("end", () => {
            res.end(Buffer.concat(buffer));
        });
    } catch (error) {
        console.log(error);
        res.status(500).send("Server internal error");
    }
});

/*Service startup*/
const port = 8091;
app.listen(port, () => {
    console.log(`server started at localhost:${port}`);
});

9. Modify package json

"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
 "build": "npm run build:server && npm run build:client",
 "service": "cd server && node index.js"

10. Start service

Packaged as client and server

npm run build

Start node service

npm run service

GitHub address: https://github.com/wang12321/SSR

Topics: Vue