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"
]
}
- 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 导出函数,并在每次渲染中重复调用此函数。
- 修改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 }
}
- 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>
- 执行npm run build
可以看到生成的dist包
- 执行npm run s-serve 启动node 服务
至此 Vue 2.X 集成SSR 基础部分就完成啦, 源码链接 此外还有如何实现开发模式下热更新的内容,具体可以查看这篇文章