Vue SSR 学习

一、什么是 SSR,优缺点?

SSRServer-Side Rendering 的缩写,意为服务端渲染,它是指前端页面是由服务端提供并渲染到浏览器;

SSR的优缺点:

  • 优点:
    • 更好的 SEO:服务端渲染会返回完整的 DOM 结构,客户端渲染只有根节点和 js 文件;
    • 首屏渲染速度快:服务端会把完整的页面返回给前端,无需等 js 下载解析;
  • 缺点:
    • 加重服务器的负担:需要服务端对页面进行处理返回,增加工作量;
    • XSS攻击的风险:服务端拼接字符串返回浏览器会直接执行,容易遭受 XSS攻击;
二、Vue SSR 的实现

Vue SSR 需要服务端的支持,我们可以用 express简单搭建一个服务器,首先通过 vue-cli 简单创建一个项目 vue-ssr,然后在项目下新建一个 server文件夹,server文件下新建一个 app.jsapp.js的内容如下:

const express = require("express")
const fs = require("fs")
const Vue = require("vue")
// 读取模板
const template = fs.readFileSync("./index.template.html", "utf-8")
// 根据传进去的模板获取 renderer
const renderer = require("vue-server-renderer").createRenderer({ template })

const server = express()
server.get("/", async (req, res) => {
    // 创建一个实例
    const app = new Vue({
        data: {
            title: "Hello SSR!"
        },
        template: "

{{ title }}

"
, methods: { changeTitle() { this.title = 'after change title' } }, }) renderer.renderToString(app) .then(html => { res.send(html) }) }) server.listen(3000, () => { console.log("server is running at 3000") })

在启动服务器之前,我们需要在终端执行命令 npm i express vue-server-render vue -S安装一下依赖,安装完成之后,在server目录下执行命令 node app.js,服务器启动之后在浏览器输入 localhost:3000就可以看到页面;

页面

index.template.html的代码如下:

DOCTYPE html>
<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>titletitle>
    head>
    <body>
        
    body>
html>

实际上,我们在代码里面设置了一个点击事件,但是在页面点击的时候,是没有触发点击事件的,因为 vue-server-renderer没有帮我们做这方面的处理,为了解决这个问题,我们需要客户端再渲染一遍,这一过程可以称为同构

三、服务端结合客户端完成 SSR 渲染

上面讲到我们需要客户端重新渲染一遍页面,因此我们需要 webpack帮我们打包两份 bundle文件,一份用于客户端,一份用于服务端,具体实现接着往下看:

1、安装依赖包

npm install vue-router vue-loader cross-env lodash.merge webpack-node-externals -S

2、在 App.vue 文件中使用路由标签






3、新建两个页面 Home.vue 和 About.vue,然后对路由的入口文件进行修改

客户端渲染的路由入口文件,是直接通过一个 new VueRouter生成路由实例,但是在服务端渲染的时候,我们需要把它包装成一个工厂函数,每个用户请求都会生成一个路由实例,因为在服务端渲染,如果没有重新生成实例,而是每个用户共享一个实例的话,会造成很严重的后果,根实例也是一样;

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const Home = () => import("../components/Home.vue")
const About = () => import("../components/About.vue")
// 每个用户请求都要创建一个路由实例和根实例
export default function createRouter() {
    return new Router({
        // 必须是 history,因为 hash 模式的"#"不会发送给服务器
        mode: 'history',
        routes: [
            {
                path: '/',
                component: Home,
                name: 'home'
            },
            {
                path: '/about',
                component: About,
                name: 'about'
            }
        ]
    })
}

4、对 main.js 进行修改

import Vue from 'vue'
import App from './App.vue'
import createRouter from './router/index'
Vue.config.productionTip = false
// 每个用户请求都要创建一个路由实例和根实例
export function createApp() {
  const router = new createRouter()
  const app = new Vue({
    router,
    render: h => h(App)
  })
  // router 后面会用到,跟 app 一起返回方便后面使用
  return { app, router }
}

5、新建 entry-client.js 和 entry-server.js

// entry-client.js 挂载激活 app
import { createApp } from './main.js'
const { app } = createApp()
app.$mount("#app")

// entry-server.js 
import {createApp} from "./main.js";
export default context => {
    return new Promise((resolve, reject) => {
        const { app, router } = createApp();
        router.push(context.url) ;
        router.onReady(() => {
            resolve(app);
        }, reject);
    })
}

6、配置 webpack 文件

// vue.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 命令时确认
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
    outputDir: `./dist/${target}`,
    configureWebpack: () => ({
        // 将 entry 指向客户端或服务端文件
        entry: `./src/entry-${target}.js`,
        devtool: "source-map",
        // 这允许 webpack 以 Node 使用方式处理动态导入
        // 在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码
        target: TARGET_NODE ? "node" : "web",
        node: TARGET_NODE ? undefined : false,
        output: {
            // 配置服务器端使用 node 的风格构建
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        optimization: { 
            splitChunks: TARGET_NODE ? false : undefined
        }, 
        // 根据环境变量打包相应的文件
        plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
    }),
    chainWebpack: config => {
        config.module.rule("vue")
                    .use("vue-loader")
                    .tap(options => { 
                        merge(options, { optimizeSSR: false }); 
                    });
    }
}

7、修改 package.json

{
    "scripts": {
        "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",
        "serve": "cd server && nodemon app.js"
    }
}

8、对 server / app.js 进行修改

const express = require("express")
const fs = require('fs')
const server = express()

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("./index.template.html", "utf-8");
const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
});

// index: false 是为了忽略 dist/client/index.html
server.use(express.static("../dist/client", { index: false }))

server.get("*", async (req, res) => {
    try {
        // 这里的 context 就是之前 createApp 传入的 context
        const context = {
            url: req.url,
            title: 'vue ssr'
        }
        // 这里不用 renderToString 是因为防止文件大的情况下出现卡顿现象
        const stream = renderer.renderToStream(context)
        let buffer = []
        stream.on("data", chunk => {
            buffer.push(chunk)
        })
        stream.on("end", () => {
            res.end(Buffer.concat(buffer))
        })
    } catch (err) {
        res.status(500).send("服务器内部错误")
    }
})


server.listen(3000, () => {
    console.log("server is running at 3000")
})

9、终端执行命令

npm run build进行打包,生成客户端和服务端相应的 json 文件;

npm run serve启动服务器;

注意:vue-server-renderer 的版本需要跟 vue 的版本一致,否则可能会报错,还有 vue-router 对应版本也不能太高

"vue": "^2.6.14",
"vue-router": "^3.5.3",
"vue-server-renderer": "^2.6.14"

你可能感兴趣的:(vue.js,前端)