VUE SSR 定制与使用心得(下)

五一归来,看到好(ji)多(ge)人在等下一篇,感觉还是挺开心的,所以赶紧整理好下篇的内容。

完善后项目

上一篇文章展示了完善后的整体设计方案, 接下来介绍完善的具体内容, 首先回顾一下设计方案

设计方案

从图上看,整个项目可以分为5大块:

Client 前端负责在页面展示后,初始化 Vue,对页面渲染和数据进行接管,以及后续的路由适配。

Server 后端负责基础 http 服务的启动,页面缓存,vue-server-render 渲染操作,以及后端路由的适配。

业务 开发业务代码的部分,包括展示逻辑,交互逻辑等

中间适配 中间适配层负责屏蔽掉业务代码在前后端执行时的差异,保证业务代码的环境通用性

构建 构建包含前端和后端 bundle 的构建,并提升开发体验

接下来会单独介绍部分模块的思路和实现细节

Hot reload

Hot reload 是在开发过程中修改代码,无需重新启动服务器即可自动编译变更的部分的技术,它可以让开发者修改代码等待编译的成本变低,达到一种流畅的开发体验。

代码实现来源于 Vue 的 SSR 示例项目 Vue-hackernews2.0,核心代码为:

const fs = require(fs) const path = require(path) const MFS = require(memory-fs) const webpack = require(webpack) const chokidar = require(chokidar) const clientConfig = require(./webpack.client.config) const serverConfig = require(./webpack.server.config) const readFile = (fs, file) => { try { return fs.readFileSync(path.join(clientConfig.output.path, file), utf-8) } catch (e) {} } module.exports = function setupDevServer (app, templatePath, cb) { let bundle let template let clientManifest let ready const readyPromise = new Promise(r => { ready = r }) const update = () => { if (bundle && clientManifest) { ready() cb(bundle, { template, clientManifest }) } } // read template from disk and watch template = fs.readFileSync(templatePath, utf-8) chokidar.watch(templatePath).on(change, () => { template = fs.readFileSync(templatePath, utf-8) console.log(index.html template updated.) update() }) // modify client config to work with hot middleware clientConfig.entry.app = [webpack-hot-middleware/client, clientConfig.entry.app] clientConfig.output.filename = [name].js clientConfig.plugins.push( new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() ) // dev middleware const clientCompiler = webpack(clientConfig) const devMiddleware = require(webpack-dev-middleware)(clientCompiler, { publicPath: clientConfig.output.publicPath, noInfo: true }) app.use(devMiddleware) clientCompiler.plugin(done, stats => { stats = stats.toJson() stats.errors.forEach(err => console.error(err)) stats.warnings.forEach(err => console.warn(err)) if (stats.errors.length) return clientManifest = JSON.parse(readFile( devMiddleware.fileSystem, vue-ssr-client-manifest.json )) update() }) // hot middleware app.use(require(webpack-hot-middleware)(clientCompiler, { heartbeat: 5000 })) // watch and update server renderer const serverCompiler = webpack(serverConfig) const mfs = new MFS() serverCompiler.outputFileSystem = mfs serverCompiler.watch({}, (err, stats) => { if (err) throw err stats = stats.toJson() if (stats.errors.length) return // read bundle generated by vue-ssr-webpack-plugin bundle = JSON.parse(readFile(mfs, vue-ssr-server-bundle.json)) update() }) return readyPromise }

head 适配

Vue SSR 官方文档虽然提供了 Title 的适配,但是实际业务中对于 head 的设置远不止于此。需要 SEO 的页面中一些 keywords,description 也是相当重要的。

这里引入 vue-meta库实现对于 head 的适配(vue-meta 地址)

在 src/app.js 加入

Vue.use(Meta, { keyName: head, // the component option name that vue-meta looks for meta info on. attribute: data-n-head, // the attribute name vue-meta adds to the tags it observes ssrAttribute: data-n-head-ssr, // the attribute name that lets vue-meta know that meta info has already been server-rendered tagIDKeyName: hid, // the property name that vue-meta uses to determine whether to overwrite or append a tag })

然后在页面的 vue 文件夹中可以增加 head 方法,返回 head 信息。

head 方法中可以调用 asyncData 中加载好的数据,以及 route 中的参数等,如下:

// index.vue export default { head() { return { title: this.$route.params.city + 标题, // someData 是 vuex 中定义的 getter meta: [{ name: description, content: 描述 + this.someData.name }], } }, }

前端错误输出

引入 friendly-errors-webpack-plugin(地址) , 可以在开发时在前端输出错误, 但是前提需要页面正常加载并且 js 成功执行过。

后端错误输出

通过修改 console 的函数和 chalk(地址))结合,输出带有颜色的信息。

引入pretty-error(地址),对 console 输出进行格式化和颜色上的区分,增加 console 的可读性。

页面缓存

页面缓存可以通过配置,根据 url 对整个页面进行缓存,包括数据和渲染。使用页面可以大幅提高页面响应速度。

页面缓存适用于变更不频繁且通用性的页面。 如首页,说明页,宣传页等。

页面缓存基于 koa 的 Middleware 实现

缓存包含两级1. lru-cache 内存缓存2. redis 缓存

实现思路:1. 通过正则分析 url,判断页面是否需要经过缓存,如果不需要则直接进入页面渲染;如果需要则进行下一步2. 需要缓存的页面,首先查找内存缓存,如果找到则返回,找不到则继续下一步3. 查找 redis 缓存,如果找到则返回,并且记录到内存缓存中,如果没找到则进入页面渲染,并将返回的页面同时保存 redis 缓存和内存缓存

User-Agent 适配

在 Vue 业务代码中会用到 user-agent 区分环境,针对某浏览器,或者 APP 内嵌页面做不同的条件渲染,但是由于运行的环境不同,获取的方式也会不同,造成工作量增加和代码的维护成本增加。

利用 Mixin,将 ua 封装成统一 api,可以减少此类不便。

const browser = uaReg => function checkBrowser() { return uaReg.test(this.userAgent) } const isAndroid = browser(/android/i) const isIOS = browser(/ios|iphone|ipad/i) const isWeChat = browser(/micromessenger/i) /** * 封装 userAgent 相关信息 */ export default process.env.VUE_ENV === server ? { computed: { userAgent() { logger.debug(userAgent, userAgent is invoke) return this.$ssrContext.userAgent }, isAndroid, isIOS, isWeChat, }, } : { computed: { userAgent() { logger.debug(userAgent, userAgent is invoke) return window.navigator.userAgent }, isAndroid, isIOS, isWeChat, }, }

在业务代码中只需要调用 this.isWeChat 即可

网络请求中 cookie 和 user-agent 透传

在一些需要 cookie 的场景中,如登录状态,需要将浏览器中的 cookie 带入到 server 端与数据接口的请求当中。

利用 axios 的拦截器 ( 其他网络库同理 ) 将 cookie 和 ua 传递到数据接口

// 透传 cookie 和 ua net.interceptors.request.use((request) => { if (process.env.VUE_ENV === server) { if (context.ctx.header.cookie && !request.ignoreCookie) { request.headers.cookie = context.ctx.header.cookie } request.headers[user-agent] = context.ctx.header[user-agent] } return request }) net.interceptors.response.use((response) => { if (process.env.VUE_ENV === server && response.headers.cookie) { context.ctx.response.headers.cookie = response.headers.cookie } return response })

目录结构调整

拆分业务和框架

目录结构调整,主要区分开业务部分和框架部分。将 vue-server-render,日志,缓存,网络适配,入口等不需要经常改变的部分单独抽离到一个文件夹,与业务代码分开,保持业务代码的纯净。

拆分页面组件和通用组件

将页面组件单独到 page 目录下,而通用组件放在 component 组件,方便管理和查找。

store 模块化

业务增加会导致 store 代码量增加,需要按照不同的业务进行模块化拆分

调整后的目录结构

├── CHANGELOG ├── README.md ├── build -- 构建相关 │ ├── setup-dev-server.js -- 开发环境自动构建脚本 │ ├── webpack.base.js -- webpack 前后端通用配置 │ ├── webpack.client.js -- webpack client 配置 │ └── webpack.server.js -- webpack server 配置 ├── dist -- 生成目标代码路径 ├── global.config.js -- 项目全局配置 ├── package.json ├── process.json -- pm2 配置 ├── src -- 项目源码 │ ├── app -- ssr 框架相关代码, 大部分情况下不需要修改 │ │ ├── WeError.js -- 通用 Error 对象 │ │ ├── app.js -- 创建 app 实例 │ │ ├── app.vue -- app 根组件 │ │ ├── entry-client.js -- client入口 │ │ ├── entry-server.js -- server 入口 │ │ ├── index.template.html -- html 模板 │ │ ├── middleware -- 中间件 │ │ └── mock-modules.js -- mock 模块, 用来替换纯浏览器端模块 │ ├── common-asserts -- 通用 css 和图片资源 │ │ ├── float │ │ ├── global │ │ └── head │ ├── components -- 通用 vue 组件 │ │ ├── bottom-app-download │ │ ├── footer │ │ ├── header │ │ └── swiper │ ├── mixin -- mixin, 消除 client 和 server 的区别, 提供统一方法 │ │ └── user-agent.js │ ├── network -- 网络模块 │ │ ├── api.js │ │ └── index.js │ ├── pages -- 页面, 每一个页面是一个文件夹, 里面可以防止页面相关的资源和页面独有的子组件 │ │ ├── index │ │ └── login │ ├── router.js -- 路由配置 │ ├── server.js -- node server 入口 │ ├── store -- vuex store, store 通过 module 管理 │ │ ├── index.js │ │ └── modules │ └── utils -- 工具, 包含 client 和 server └── yarn.lock

请求时序图

请求中如果命中缓存,则直接从缓存中拉取页面 html,直接返回到浏览器 ↑

请求中如果未命中缓存,则会正常请求数据渲染页面,并且写入缓存 ↑ 对于未开启缓存的页面,直接请求数据渲染页面,跳过查找和写入缓存的步骤 ↑

后话

抛砖引玉

通过对 SSR 的调研和定制,我们能够肯定 SSR 的优点和其对前端发展的意义。同时也了解搭建完整的 SSR 项目需要考虑的问题会比单独的服务端渲染项目和 SPA 项目要更多。

想要定制更加贴合业务需求,要做的事情远不止于此,这里只是抛砖引玉提出对于框架定制的思路和做法参考。

缺点

在最后再补充一下 SSR 的缺点:

首先 SSR 对于开发人员的技术要求会高于 SPA 项目,虽然我们做了很多工作尽量屏蔽前后端的差异,但是开发仍然需要了解自己写的每一段代码会在哪个环境下执行,以免使用了错误的平台特有的 API。存在 server,相比于 SPA 项目,这也算是一个缺点,毕竟 SPA 项目只需要静态服务器,不需要特别关心部署,监控,压力等

建议

所以说 SSR 并不是适用于所有场景,事实上也不可能有一种方案适用于所有场景。

如果项目有 SEO 需求,对用户体验有很高的要求,并且拥有一个能够了解 node 的前端团队,那么恭喜你,SSR 就是为你准备的!!!其他的情况,可能需要酌情选择了。

性能

性能测试数据以及结论后续会作为单独的文章发出,提前预告一下结果:看起来还不错

后续

SSR + PWA ? 是个很好的话题封装框架 ? 目前还欠缺拓展性,可能会引入插件机制开源 ?

参考资料&相关库

Vue SSR 官方介绍vue-metavue-hackernews 2.0webpack hot middlewarefriendly errors webpack plugin)chalkpretty errorlru-cache