构建同构渲染
构建流程
-
客户端动态交互功能
服务端渲染只是把 vue 实例处理为纯静态的 html 片段返回给客户端,对于 vue 实例当中需要客户端动态交互的功能本身没有提供
... const app = new Vue({ template: ` <div id="app"> <h1>{{ message }}</h1> <h2>客户端动态交互</h2> <div> <input v-model="message"> </div> <div> <button @click="onClick">点击测试</button> </div> </div> `, data: { message: '拉钩' }, methods: { onClick () { console.log('hello world') } } }) ...
返回到客户端的页面是没有交互功能的

- 基本思路

源代码 --> webpack 打包 --> Node Server 服务
此时,我们应用中只有 Server entry 服务端入口,只能处理服务端渲染,想要实现服务端渲染内容拥有动态交互能力,还需要 Client entry 客户端入口,用于处理客户端渲染,接管服务端渲染内容,激活成一个动态页面
Server entry 通过 webpack 最终打包成 Server Buner,主要用于做服务端渲染(SSR)
Client entry 通过 webpack 最终打包成 Client Buner,发送给浏览器,用于接管服务端渲染好的静态页面,对他进行激活成一个动态的客户端应用
源码结构
根据这幅图实现同构应用,既要实现服务端渲染,也要实现客户端渲染能够处理客户端动态交互
一个基本项目可能像是这样:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
-
使用 webpack 的源码结构重新设置代码结构
创建 src 目录,添加 App.vue 和 app.js、entry-client.js、entry-server.js,具体请查看 webpack 的源码结构
-
通过 webpack 打包构建,真正完成同构应用
安装依赖
npm i vue vue-server-renderer express cross-env
安装开发依赖
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin
配置文件及打包命令
-
初始化 webpack 打包配置文件,具体见github
build ├── webpack.base.config.js # 公共配置 ├── webpack.client.config.js # 客户端打包配置文件 └── webpack.server.config.js # 服务端打包配置文件
-
在 npm scripts 中配置打包命令
"scripts": { "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js", "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js", "build": "rimraf dist && npm run build:client && npm run build:server" },
-
启动服务
-
通过
yarn build
打包生成客户端和服务端文件 -
具体实现参考Bundle Renderer 指引
-
替换 server.js 中 createRender 为 createBundleRenderer,接收 serverBundle、template 和 clientManifest 为参数,启动服务
... const serverBundle = require('./dist/vue-ssr-server-bundle.json') const template = fs.readFileSync('./index.template.html', 'utf-8') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { template, clientManifest, }) ...
此时在浏览器端获取不到 dist 下的 js 文件,是因为没有在服务器当中有 dist 中的资源传递给客户端,挂载中间件 server.use('/dist')
image-20210326082507311.png
-
挂载处理静态资源中间件
const express = require('express') // 得到express实例 const server = express() // 挂载处理静态资源中间件 //dist应该和客户端打包的出口的publicPath保持一致 使用express.static处理静态资源 // 当请求到/dist开头的资源时,使用express.static尝试在./dist目录下查找并返回 server.use('/dist', express.static('./dist'))
此时重新刷新浏览器,js 文件正常加载,客户端交互功能正常
解析渲染流程
服务端渲染如何输出
-
从路由着手,当客户端请求后匹配路由,调用 renderer 渲染器的 renderer.renderToString 方法
-
renderToString 将 vue 实例渲染为字符串发送给客户端,但是 renderToString 中并没有 vue 实例,vue 实例是从哪来的呢?
-
renderer 是通过 createBundleRenderer 创建,传入的 serverBundle 对应
vue-ssr-server-bundle.json
文件,是通过 server.entry.js 构建出来的结果文件{ "entry": "server-bundle.js", // 入口 "files": {...}, // 所有构建结果资源列表 "maps": {...} //源代码 source map 信息 }
-
renderer 在渲染时会加载 serverBundle 的入口,得到 entrry-server.js 中创建的 vue 实例
-
对此 vue 实例进行渲染,并将渲染的结果注入到 template 模板当中,最终将 template 模板发送到客户端
客户端渲染如何接管并激活
-
客户端需要将打包的 js 脚本注入到页面当中,是怎么做的呢?
-
在 createBundleRenderer 中,配置的 clientManifest,对应
vue-ssr-client-mainfest.json
文件,是客户端打包资源的构建清单{ "publicPath": "/dist/", "all": ["app.5c8c7bcfd286b41168f3.js", "app.5c8c7bcfd286b41168f3.js.map"], "initial": ["app.5c8c7bcfd286b41168f3.js"], "async": [], "modules": {} } /* publicPath:访问静态资源的根相对路径,与 webpack 配置中的 publicPath 一致 all:打包后的所有静态资源文件路径 initial:页面初始化时需要加载的文件,会在页面加载时配置到 preload 中 async:页面跳转时需要加载的文件,会在页面加载时配置到 prefetch 中 modules:项目的各个模块包含的文件的序号,对应 all 中文件的顺序;moduleIdentifier和 和all数组中文件的映射关系(modules对象是我们查找文件引用的重要数据),实际作用是当客户端在实际运行的时候,加载的模块用到哪些资源,vue 会根据此信息去加载那些资源 */
-
当客户端加载完成后,客户端的 js 是如何工作的呢?
详细请查看客户端激活