Vue2.6 Vue cli4.X 从零实现SSR服务端渲染 第二篇 vue项目集成SSR

179 阅读4分钟

SSR的原理

通过如下的原理图我们可以分析出SSR的基本原理:

左侧Source部分就是我们编写的源代码,所有代码有一个公共入口,即app.js,与app.js右侧相邻的就是服务端的入口
(entry-server.js)和客户端的入口(entry-client.js)。当完成所有源代码的编写之后,我们通过webpack打包出两个bundle,分别是server bundle和client bundle,当用户进行页面访问的时候,先是经过服务端的入口,将vue组件组装为html字符串,并混入客户端所访问的html模板中,最终就完成了整个SSR渲染的过程。

改造vue cli项目支持SSR

使用Vue Cli 创建项目

// 创建项目
vue create ssr-example
// 进入项目目录
cd ssr-example
// 安装依赖
npm install express -d // 创建node服务
npm install cross-env -s
npm install webpack-node-externals -s
npm install lodash.merge -s
npm install vue-router -s // 如果创建项目时没带需要安装下路由
// 用于将 Vue 实例渲染为 HTML
npm install vue-server-renderer -s

1. 添加 vue.config.js

// 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')
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',
    target: TARGET_NODE ? 'node' : 'web',
    node: TARGET_NODE ? undefined : false,

    output: {
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: TARGET_NODE
      ? nodeExternals({
        // 不要外置化 webpack 需要处理的依赖模块。
        // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
        allowlist: [/\.css$/]
      })
      : 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 => {
        return merge(options, {
          optimizeSSR: false
        })
      })
  }
}

2. 在项目根目录(ssr-example)创建一个serve.js, 使用express 创建一个node服务

  // serve.js 内容
    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("./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 = 5001;
    app.listen(port, () => {
        console.log(`server started at localhost:${port}`);
    });

**3. 项目根目录下添加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>Vue-SSR-demo</title>
</head>
<body>
  <!--vue-ssr-outlet-->
</body>
</html>

3. package.json 内修改webpack配置 添加s-serve 命令 用于启动node服务 package.json 完整内容 如下

{
  "name": "ssr-example",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "s-serve": "node serve",
    "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"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "cross-env": "^7.0.3",
    "lodash.merge": "^4.6.2",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0",
    "vue-server-renderer": "^2.7.14",
    "webpack-node-externals": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.12",
    "@vue/cli-plugin-eslint": "~4.5.12",
    "@vue/cli-plugin-router": "^4.5.19",
    "@vue/cli-service": "~4.5.12",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

  1. SRC目录下新建entry-client.js 和 entry-server.js
// entry-client.js
import { createApp } from './app'
const { app } = createApp()//这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中

// entry-server.js
import {createApp}  from "./app"

export default context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()

    // 设置服务器端 router 的位置
    router.push(context.url)

    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
}

服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。

  1. 修改router

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

// 采用了vue-router的Vue的SSR渲染,
// 必须使用history作为路由模式,因为hash模式的路由提交不到服务器上,
// 如果之前使用的是hash模式,需要进行修改:
export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      { path: '/', component: () => import('../views/Home.vue') },
      { path: '/about', component: () => import('../views/About.vue') }
    ]
  })
}

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'


export function createApp() {
  const router = createRouter();
  const app = new Vue({
    router,//根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app, router }
}
  1. SRC目录下新建app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'


export function createApp() {
  const router = createRouter();
  const app = new Vue({
    router,//根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app, router }
}

main.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数:

app.vue 内容

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

  1. 执行npm run build

Screen Shot 2023-05-08 at 17.31.11.png

可以看到生成的dist包

  1. 执行npm run s-serve 启动node 服务

Screen Shot 2023-05-08 at 17.33.06.png

至此 Vue 2.X 集成SSR 基础部分就完成啦, 源码链接 此外还有如何实现开发模式下热更新的内容,具体可以查看这篇文章