Vue project transformation SSR (server rendering)

1. What is SSR (Server Side Rendering)?

Traditional vue project browser rendering mode

browser rendering mode

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

ssr server rendering mode

SSR

Advantages:
1. Better SEO, because the search engine crawler crawler can directly view the fully rendered page
2. The rendering speed of the first screen is fast

In simple terms, SSR is to display the page directly on the client after the server-side rendering is completed.

2. Principle of SSR

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>访问的 URL 是: {
   
   { 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

The vue project is mounted to the html through the virtual DOM, so for the spa project, the crawler will only see the initial structure. The virtual DOM needs to be transformed into a real DOM through a certain method. Virtual DOM is also a JS object, and the entire server-side rendering process 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 to render it again, referred to as isomorphism. So Vue server-side rendering is actually rendered twice. An official picture is given below:SSR construction diagram

Two bundle files need to be generated through Webpack packaging:
Client Bundle, for browsers. Similar to
Server Bundle with pure Vue front-end project Bundle, it is used for server-side SSR, a json file

No matter what the project looked like before, whether it was generated using vue-cli or not. There will be this build transformation process. The vue-server-renderer library will be used in the construction and transformation. It should be noted here that the vue-server-renderer version should be the same as the Vue version.

Directory structure after packaging
SSR packaging directory

3. Vue project transforms SSR (vue-cli4/vue2 is an example)

1. Install dependent packages

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

2. Modify the router

// 原来的写法
// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })
//
// export default router

// 修改后的写法
export default function createRouter() {
  return new VueRouter({
    mode: "history",  // 一定要history 
    base: process.env.BASE_URL,
    routes,
  });
}

3. Modify main.ts

// 原来的写法
// new Vue({
//   router,
//   store,
//   render: h => h(App)
// }).$mount('#app')

// 修改后的写法
export function createApp() {
  // 创建 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实际上就是server/index.js里面传参,后面会说到server/index.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(()=>{
            // 是否匹配到我们要用的组件
            const matchs = router.getMatchedComponents();
            if(!matchs) {
                return reject({code: 404})
            }
            resolve(app);
        }, reject);
    })
}

6. Modify the webpack package file

view.config.js

// 服务器渲染插件
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); // 生成服务端包
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); // 生成客户端包
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");

// 环境变量:决定入口是客户端还是服务端,WEBPACK_TARGET在启动项中设置的,见package.json文件
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    css: { extract: false
    },
    outputDir: "./dist/" + target,
    configureWebpack: () => ({
        // 将 entry 指向应用程序的 server / client 文件
        entry: `./src/entry-${target}.js`,
        // 对 bundle renderer 提供 source map 支持
        devtool: "source-map",
        // 这允许 webpack 以 Node 适用方式处理动态导入(dynamic import), // 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
        target: TARGET_NODE ? "node" : "web",
        node: TARGET_NODE ? undefined : false,
        output: {
            // 此处配置服务器端使用node的风格构建
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        // 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的 bundle 文件。
        externals: TARGET_NODE ? nodeExternals({
            // 不要外置化 webpack 需要处理的依赖模块。(以前whitelist,改成allowlist)
            allowlist: [/\.css$/]
        }) : undefined,
        optimization: { splitChunks: TARGET_NODE ? false : undefined}, // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 // 服务端默认文件名为 `vue-ssr-server-bundle.json` // 客户端默认文件名为 `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}}测试ssr</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

8. nodejs server


// nodejs服务器
const express = require("express");
const fs = require("fs");
// 创建express实例和vue实例
const app = express();

// 创建渲染器 获得一个createBundleRenderer
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模板文件
const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
});

// 中间件处理静态文件请求
app.use(express.static("../dist/client", { index: false })); // 为false是不让它渲染成dist/client/index.html
// app.use(express.static('../dist/client'))  //如果换成这行代码,那么需要把dist/client/index.html 删除 ,不然会优先渲染该目录下的index.html文件

// 前端请求返回数据
app.get("*", async (req, res) => {
    try {
        const context = { url: req.url, title: "ssr",};
    // nodejs流数据,文件太大,用renderToString会卡
        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("服务器内部错误");
    }
});

/*服务启动*/
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 the service

Packaged as client and server

npm run build

Start the node service

npm run service

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

Guess you like

Origin blog.csdn.net/wang15180138572/article/details/118549088