Vue2的SSR并不是仅能用在express和koa中
本文以Think.js为例, 示范一下传统MVC框架如何与Vue2的服务端渲染结合
这里不提供仓库, 读者可以按照博文的命令行和给出的代码完整重现一次Vue2的SSR与Think.js搭配, 完成时会发现, SSR与MVC框架的配合一点也不复杂
初始化环境
首先全局安装Think.js,并初始化一个项目
npm install thinkjs@2 -g --registry=https://registry.npm.taobao.org --verbose
thinkjs new ./thinkjs-demo
cd thinkjs-demo
npm install
再下载博客的前台项目,项目地址在这里, 服务端渲染的前台SPA即项目中的client/front.
由于博客后台依赖mongoDB和redis,为了方便演示,后台直接使用博客线上的proxyPrefix
代理前缀来进行数据的获取,这样就不需要本地把RESTful服务器跑起来了。
因此,需要修改一下博客前台的API请求地址
修改client/front/src/store/api.js
, 将第6行的host改为如下语句
const host = typeof location === 'undefined' ?
process.env.NODE_ENV === 'production' ?
'https://smallpath.me/proxyPrefix' :
'http://localhost:8080/proxyPrefix' : '/proxyPrefix';
之后进入client/front
npm install
npm run build
现在, 把front项目根目录中的index.html和dist目录中的文件统一移动至thinkjs项目中, 移动策略如下:
front/index.html
替换thinkjs-demo/view/home/index_index.html
front/dist
目录 将front/dist中的四个文件全部粘贴到thinkjs-demo/www/static
目录中,再将静态资源front/dist/static/
中所有的文件夹也都复制到thinkjs-demo/www/static
中。
这里的thinkjs目录应该如下, 注意不要放错, 即thinkjs-demo/www/static
中不能存在名为static
的文件夹,否则非首屏渲染会出错
└─www
│ development.js
│ production.js
│ README.md
│ testing.js
│
└─static
│ client-bundle.js
│ client-vendor-bundle.js
│ server-bundle.js
│ styles.css
│
├─css
├─fonts
│ iconfont.eot
│ iconfont.ttf
│
├─img
│ iconfont.svg
│
└─js
创建SSR环境
首先安装SSR必要的依赖
npm install --save serialize-javascript vue-server-renderer lru-cache vue vue-router vuex vuex-router-sync@3.0 superagent
Thinkjs是传统的MVC框架,它有一个全局入口,用于初始化一些全局操作
在thinkjs-demo/src/common/bootstrap/global.js
中,添加如下语句
process.env.VUE_ENV = 'server';
import fs from 'fs';
import path from 'path';
const resolve = file => path.resolve(__dirname, file);
import serialize from 'serialize-javascript';
import { createBundleRenderer } from 'vue-server-renderer';
const html = (() => {
const template = fs.readFileSync(resolve('../../../view/home/index_index.html'), 'utf-8')
const i = template.indexOf('<div id=app></div>')
const style = '<link rel=stylesheet href=/static/styles.css>'
return {
head: template.slice(0, i).replace('<link rel=stylesheet href=/dist/styles.css>', style),
tail: template.slice(i + '<div id=app></div>'.length).replace(/\/dist/g, "/static")
}
})();
const bundlePath = resolve('../../../www/static/server-bundle.js');
let renderer = createRenderer(fs.readFileSync(bundlePath, 'utf-8'));
function createRenderer (bundle) {
console.log(bundlePath);
return createBundleRenderer(bundle, {
cache: require('lru-cache')({
max: 1000,
maxAge: 1000 * 60 * 15
})
})
}
global.html = html;
global.renderer = renderer;
global.serialize = serialize;
Think.js与Steam流式输出
现在到了MVC的C层了,它的controller在thinkjs-demo/src/home/controller/
目录中 其中,base.js会被其他controller继承, 因此我们可以在这里使用魔术方法, 来实现express和koa的app.use('*', ()=>{})
这种中间件的功能
'use strict';
export default class extends think.controller.base {
/**
* some base method in here
*/
__before() {
console.log('before called')
if (!renderer) {
return this.end('waiting for compilation... refresh in a moment.', 'utf-8')
}
let s = Date.now();
const context = {
path: this.http.url,
query: this.http.query,
url: this.http.url
};
const renderStream = renderer.renderToStream(context);
let firstChunk = true
this.write(html.head, 'utf-8');
renderStream.on('data', chunk => {
if (firstChunk) {
// embed initial store state
if (context.initialState) {
this.write(
`<script>window.__INITIAL_STATE__=${
serialize(context.initialState, { isJSON: true })
}</script>`, 'utf-8'
)
}
firstChunk = false
}
this.write(chunk,'utf-8')
})
renderStream.on('end', () => {
this.end(html.tail,'utf-8')
console.log(`whole request: ${Date.now() - s}ms`)
console.log('---------------')
})
renderStream.on('error', err => {
console.log(err);
})
return think.prevent();
}
}
这里使用了think.js 全局对象think, 以及controller中this.http的使用。
现在,在thinkjs-demo目录中npm start
, 本地localhost:8360的页面上就可以正确渲染出博客首屏了
注意, 这里如果点进其他链接, 没有数据是正常现象, 因为此时请求由客户端superagent发出, 在没设置跨域或代理前, 无法使用本博客的代理前缀https://smallpath.me/proxyPrefix
.
修正非首页的首屏渲染问题
但是, 非首页刷新时是会直接显示404的, 我们需要将404去除以适应SPA的H5模式。
这是SPA的H5模式的特点之一, 即要求路由由客户端接管,仅在客户端匹配到没有的路径时才由客户端应用显示一个全局404页面,服务端禁止返回404这种状态码。
将thinkjs-demo/src/home/controller/base.js
复制到thinkjs-demo/src/common/controller
文件夹中, 并修改thinkjs-demo/src/common/controller/error.js
,将class声明语句替换为:
import Base from './base.js';
export default class extends Base {
即可去除全局404错误。现在, 除了标签页并没有做服务端渲染外, 其他页面刷新后也能够正确显示数据, 这说明所有SSR已经在Think.js上正确配置了.
之后, 就是配置线上的nginx代理, 为了达到点击其他页面能直接从博客代理前缀获取数据的目的, 可以参考这样的nginx配置策略
一点思考
对于thinkjs这种传统的MVC框架, 应该统一在错误处理那里返回SSR渲染出来的界面, 因此src/home/controller/base.js
其实没有必要修改,只需要保证所有请求都能被错误处理捕获即可。
除此之外, SPA使用了服务端渲染后, 有些逻辑完全就可以放到服务端上来. 例如, 同样用Think.js写的屈屈大神的博客, 他使用了服务端获取请求的数据再异步转发给谷歌统计这种策略, 避免了国内用户使用谷歌统计不稳定的问题. 还有, 还可以将一些与RESTful语义有冲突的操作放到SSR服务端, 比如sitemap定时生成任务.
另外, 本文虽然使用了lru-cache, 但是并没上vue2 SSR的组件级缓存, 这里完全可以去除lru-cache, 并用think.js自带缓存进行无缝替代. 在进行SSR组件级缓存和不考虑缓存更新的前提下, SSR博客前台很容易达到首次访问1000毫秒而二次访问仅仅100毫秒这种访问速度. 由于博客不会经常更新, 因此组件级缓存非常适合这种场景