# vue-cli怎么改造成ssr

安装插件

npm install vue-server-renderer webpack-node-externals lodash.merge express --save-dev

# 基本配置

router/index.js

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

Vue.use(VueRouter)
// 需要改造变为工厂函数每次返回全新router实例
export function createRouter() {
  return new VueRouter({
    mode: "history",
    routes: [
      { path: "/", component: Home },
      { path: "/about", component: About },
    ],
  });
}

src下添加 entry-client.js

// 用于浏览器中激活内容
import {createApp} from './app'

const {app,router} = createApp();

router.onReady(()=> {
    // 挂载以后页面就激活了称为可交互spa
    app.$mount('#app')
})

src下添加 entry-server.js

// 首屏渲染

import { createApp } from "./app";

// context由express传递进来
export default context => {
  // 返回Promise
  // 确保将来可能发生的异步数据请求
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();
    // 首屏渲染谁根据url
    router.push(context.url);
    router.onReady(() => {
      resolve(app);
    }, reject);
  });
};

这两个 js 用于生成 bundle 文件

配置 vue.config.js

// 这两个插件分别生成客户端和服务端两个bundle.json
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); // npm install vue-server-renderer
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals"); // npm install webpack-node-externals
const merge = require("lodash.merge"); // npm install 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',
    // 这允许 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 需要处理的依赖模块。
          // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
          // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
          whitelist: [/\.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: undefined
    },
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        });
      });
  }
};

# 生成 bundle 文件

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"
}
npm run build

该操作会在 dist/ 下生成 server 和 client

# 服务端渲染入口

主目录下添加 server/index.js

const express = require("express");
const fs = require("fs");

// 创建express实例
const app = express();

// 静态文件服务
app.use(express.static("../dist/client", { index: false }));

// 创建渲染器函数
const { createBundleRenderer } = require("vue-server-renderer");
const bundle = require("../dist/server/vue-ssr-server-bundle.json");

// 创建渲染器
const renderer = createBundleRenderer(bundle, {
  runInNewContext: false,
  template: fs.readFileSync("./index.html", "utf-8"),
  clientManifest: require("../dist/client/vue-ssr-client-manifest.json"),
});

// 声明路由监听
app.get("*", async (req, res) => {
  // 获取page参数
  const context = {
    title: "ssr",
    url: req.url, // 从请求对象中获取url传递下去
  };
  const html = await renderer.renderToString(context);
  res.send(html);
});

app.listen(3000, () => {
  console.log("vue ssr is on position!");
});

主目录下添加 server/index.html

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

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

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

</html>

渲染内容会被插入到 <!--vue-ssr-outlet-->,固定格式不能修改,比如不能前后加空格。<!-- vue-ssr-outlet -->

cd server && node index.js

涉及到项目的开发,建议使用 nuxt.js, 本文仅限于研究原理实现。

源码地址: https://github.com/pengxingyun/vue-ssr