什么是ssr

在这里插入图片描述

进阶1:把vue实例变成html

用renderToString()把vue实例挂载的内容变成html,返回给客户端

// 安装
npm i vue-server-renderer express -D
// nodejs服务器
const express = require("express");
const Vue = require("vue");

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

// 创建渲染器
// 是一个工厂函数:一个函数返回一个对象
const renderer = require("vue-server-renderer").createRenderer();

const page = new Vue({
  data: { title: "哈哈哈" },
  template: "<div>{{title}}<div> hello, vue ssr! </div></div>"
});

app.get("/", async (req, res) => {
  try {
    const html = await renderer.renderToString(page);
    console.log(html);
    res.send(html);
  } catch (err) {
    res.status(500).send("服务器内部错误");
  }
});

app.listen(3000, () => {
  console.log("服务器启动成功");
});

进阶2:结合vue-router来访问不同页面

src 
├── components 
│      ├── Detail.vue 
│      └── Index.vue 
├── App.vue 
├── app.js # 通用入口 
├── entry-client.js # 客户端入口,仅运行于浏览器 
└── entry-server.js # 服务端入口,仅运行于服务器
// 安装 
npm install webpack-node-externals lodash.merge -D

// 脚本配置
npm i cross-env -D
// 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" 
	}
// vue.config.js
// webpack插件
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",
    // 这允许 webpack 以 Node 适用方式处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: TARGET_NODE ? "node" : "web",
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此处告知 server bundle 使用 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
        });
      });
  }
};

// router.js
import Vue from 'vue'
import Router from 'vue-router'

import Index from '@/components/Index'
import Detail from '@/components/Detail'

Vue.use(Router)

// 这里为什么不导出一个router实例哪?
// 每次用户请求都需要创建新router实例,如果用户请求多次都用一个实例会造成数据污染
export default function createRouter() {
    return new Router({
        mode: 'history',
        routes: [
            {path: '/', component: Index},
            {path: '/detail', component: Detail},
        ]
    })
}
// server/index.js
/* eslint-disable no-console */
// nodejs服务器
const express = require("express");
const fs = require('fs')

// 创建express实例和vue实例
const app = 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 renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template: fs.readFileSync('../public/index.temp.html', 'utf-8'), // 宿主模板文件
    clientManifest
})

// 中间件处理静态文件请求
app.use(express.static('../dist/client', {index: false})) // 为false是不让它渲染成dist/client/index.html
// app.use(express.static('../dist/client'))

// 前端请求什么我都不关心,所有的路由处理交给vue
app.get("*", async (req, res) => {
  try {
      const context = {
          url: req.url,
          title: 'ssr test'
      }
    // console.log(req.url);
    const html = await renderer.renderToString(context); // 之前接收vue实例,现在接收上下文
    // console.log(html);
    res.send(html);
  } catch (error) {
    console.log(error);
    res.status(500).send("服务器内部错误");
  }
});

app.listen(3000, () => {
  console.log("渲染服务器启动成功");
});

代码参考

vue-server-renderer源码解读

  • serverBundle(生成的vue-ssr-server-bundle.json):提供sourceMap,里面包含了源代码编译后的所有内容,将用于服务端渲染输出html的正文。
  • clientManifest(生成的vue-ssr-client-manifest.json):注入首次渲染需要加载script标签。

renderToString(context)的时候就可以根据context的url,去serverBundle提供的sourceMap,匹配搭配出要渲染的内容和客户端Bundle提供的script内容。(程序想要成为可交互的spa就必须要用client bundle)
在这里插入图片描述

参考

Logo

前往低代码交流专区

更多推荐