搭建自己的 Server Side Render
Vue 实例的服务端渲染
- 使用 vue-server-renderer 插件完成 vue 实例的服务端渲染
- 使用 express 创建一个 node 服务器
- fs 模块读取 html 模板
const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const renderer = require('vue-server-renderer').createRenderer({
template: fs.readFileSync('./index.template.html', 'utf-8')
})
const server = express()
server.get('/', (req, res) => {
const app = new Vue({
template: `
<div id="app">
<h1>{{ message }}</h1>
</div>
`,
data: {
message: 'hahah'
}
})
renderer.renderToString(app, {
title: 'hello world!',
meta: `
<meta name="description" content="hello world">
`
}, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error.')
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(html)
}
})
})
server.listen(3000, () => {
console.log('server running at port 3000.')
})
html 模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{{ meta }}}
<title>{{ title }}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
- 注意这里的占位符
<!--vue-ssr-outlet-->两边不要有空格,否则渲染失败。 - 如果要渲染 html 标签,需要使用三个大括号。
- 解决页面乱码问题,最好的解决方式就是在请求头中添加 meta 标签,同时在响应头中设置 Content-type
构建配置
基本思路
服务端渲染就是将 Vue 实例渲染成纯静态的 HTML 字符串然后发送给客户端。所以,Vue 中的交互功能在客户端是不能实现的,比如说,@click 点击事件,v-model 数据双向绑定,这种的通过服务端渲染之后发送给客户端渲染的方式是无效的。因为服务端渲染完发送给客户端的是纯字符串,客户端负责完成的只是将这个 html 字符串进行渲染,至于 Vue 应用中的行为是没有的。

上图是 Vue Server Side Render 的源码结构
SSR 应用需要有两个 webpack 打包的入口一个是 Server entry 另一个是 Client entry ,分别打包生成 Server bundle 和 Client bundle 。
Server bundle 就是用于服务端渲染,如果想要服务端渲染的内容在客户端拥有动态交互能力就需要 Client bundle 用于客户端渲染,接管服务端渲染的内容激活成一个动态页面。这就是整个同构应用的实现流程。
源代码结构
同构应用的实现主要是分为两个部分,一部分是源代码的组织以及打包,另一部分就是将打包后的代码在服务端运行。
入口文件完成之后接下来就是使用 webpack 打包,将服务端不能识别和运行的代码以及文件处理成可以在服务端直接运行的代码。
安装依赖
- 生产依赖
yarn add vue vue-server-renderer express cross-env
| 包 | 说明 |
|---|---|
| vue | Vue.js 核心库 |
| vue-server-renderer | Vue 服务端渲染工具 |
| express | 基于 Node 的 Web 服务框架 |
| cross-env | 通过 npm scripts 设置跨平台环境变量 |
- 开发依赖
yarn add --dev 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-template-compiler friendly-errors-webpack-plugin
| 包 | 说明 |
|---|---|
| webpack | webpack 核心包 |
| webpack-cli | webpack 的命令行工具 |
| webpack-merge | webpack 配置信息合并工具 |
| webpack-node-externals | 排除 webpack 中的 Node 模块 |
| @babel/core | babel 相关工具 |
| @babel/plugin-transform-runtime | babel 相关工具 |
| @babel/preset-env | babel 相关工具 |
| babel-loader | babel 相关工具 |
| vue-loader | 处理 .vue 资源 |
| vue-template-compiler | 处理 .vue 资源 |
| css-loader | 处理 css 资源 |
| url-loader | 处理图片资源 |
| file-loader | 处理字体资源 |
| rimraf | 基于 Node 封装的一个跨平台 rm -rf 工具 (通过它可以在命令行中做一些删除操作,打包之前清除原有的 dist) |
| friendly-errors-webpack-plugin | 友好的 webpack 错误提示 |
配置文件及打包命令
初始化 webpack 打包配置文件
# build
# webpack.base.config.js # 公共配置
# webpack.client.config.js # 客户端打包配置文件
# webpack.server.config.js # 服务端打包配置文件
配置打包命令
"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 构建应用,然后使用 node 执行 server.js 启动服务。这时候项目启动起来了,访问网页还会有问题就是交互功能依然不能正常使用,这是因为并没有把打包后的 dist 目录开放出来,请求 js 资源文件失败。解决方法就是开放 dist 文件,在服务器中将 dist 文件开放出来。
处理方法,在 server 挂载一个处理静态资源的中间件.
const server = express()
server.use('/dist', express.static('./dist'))
当访问 ‘/dist’ 开头的资源的时候,使用 express.static 中间件尝试去 dist 目录中查找对应的资源并返回。
然后应用就启动成功了,客户端的 vue 也能正常工作了。
解析渲染流程
服务端渲染
如何生成网页?
当客户端发起请求以后匹配到路由在服务端调用 renderToString() 方法将 Vue 实例完成渲染并返回给客户端。
Vue 实例的渲染关键就在服务端渲染的 serverBundle 中,这个 json 文件中包含了服务端渲染你的入口(entry),以及入口文件的代码(files),以及入口文件代码的 sourcemap (maps)
客户端渲染
如何接管服务端渲染的静态网页并激活为可以交互的应用?
显然,客户端需要将服务端返回的结果挂载到模板中并渲染。这些的关键就是 clientManifest ,它是客户端打包资源的构建清单,清单中描述了客户端构建出的资源相关的信息。
| key | 说明 |
|---|---|
| publicPath | 打包资源的出口(存放目录) |
| all | 客户端打包生成的所有资源文件 |
| initial | sercerrender 渲染的时候会把这个里面的资源自动的注入到 template 中 |
| async | 存储异步资源的资源信息(比如在代码中加载的异步组件、异步的 js 模块等) |
| modules | 原始模块的依赖信息 |
详细解释以及注意事项,请看官方文档
开发模式下的自动化配置(含热更新)
前面完成的构建过程只能满足项目的生产模式下的构建和打包,如果是再开发模式下就需要每次更改项目代码之后都去手动的运行脚本重新构建,这种模式下的开发显然是不合理的。所以需要单独的配置开发模式的构建过程,使其可以自动地监听文件的变化,自动的构建项目。
实现开发模式构建的关键是 server.js 中的 renderer 方法,这个方法是通过接收服务端和客户端打包的结果使用 createBundleRenderer 方法创建出来的渲染函数。在生产模式下,只需要使用打包结果去创建就可以了,但是在开发模式下每次更新代码之后都需要重新打包生成最新的资源文件,并且用这个最新的资源文件生成最新的 renderer 方法进行渲染。
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
const resolve = file => path.resolve(__dirname, file)
module.exports = (server, callback) => {
let ready
const onReady = new Promise(r => ready = r)
// 监视构建 -> 更新 renderer
let template
let serverBundle
let clientManifest
const update = () => {
if (template && serverBundle && clientManifest) {
callback(serverBundle, template, clientManifest)
ready()
}
}
// 监视构建 template -> 调用 update -> 更新 renderer 渲染器
const templatePath = resolve('../index.template.html')
template = fs.readFileSync(templatePath, 'utf-8')
update()
// fs.watch\fs.watchFile\第三方包 chokidar
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler, {
logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
})
// 存在内存中
// 编译结束的钩子函数
serverCompiler.hooks.done.tap('server', () => {
serverBundle = JSON.parse(
// 在 devMiddleware 的文件系统中读取文件
serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json', 'utf-8'))
)
update()
})
// 存在disk中
// serverCompiler.watch({}, (err, states) => {
// if (err) throw err
// if (states.hasErrors()) return
// serverBundle = JSON.parse(
// // 这里不能用 require 去加载 json 文件,因为 require 是由缓存的。
// fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json', 'utf-8')) // 默认是 buffer
// )
// console.log(serverBundle)
// update()
// })
// 监视构建 clientManifest -> 调用 update -> 更新 renderer 渲染器
const clientConfig = require('./webpack.client.config')
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新的一个客户端脚本
clientConfig.entry.app
]
clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
})
// 存在内存中
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json', 'utf-8'))
)
update()
})
server.use(hotMiddleware(clientCompiler, {
log: false //关闭它本身的日志输出
}))
// 将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问
server.use(clientDevMiddleware)
return onReady
}
1765

被折叠的 条评论
为什么被折叠?



