详解如何使用Vue2做服务端渲染
花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(ssr),并且使用上了vuex 负责状态管理,首屏加载时间从之前4g网络下的1000ms,提升到了现在500-700ms之间,ssr的优势有很多,现在让我来跟你细细道来。
技术栈
服务端:nodejs(v6.3)
前端框架 vue2.1.10
前端构建工具:webpack2.2 && gulp
代码检查:eslint
源码:es6
前端路由:vue-router2.1.0
状态管理:vuex2.1.0
服务端通信:axios
日志管理:log4js
项目自动化部署工具:jenkins
vue2与服务端渲染(ssr)
vue2.0在服务端创建了虚拟dom,因此可以在服务端可以提前渲染出来,解决了单页面一直存在的问题:seo和初次加载耗时较多的问题。同时在真正意义上做到了前后端共用一套代码。
ssr的实现原理
客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回 promise (官方是prefetch方法)来将需要的数据拿到。最后再通过
<script>window.__initial_state=data</script>
将其写入网页,最后将服务端渲染好的网页返回回去。
接下来客户端会将vuex将写入的 __initial_state__ 替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似react的 shouldcomponentupdate 的diff操作。
vue2使用的是单向数据流,用了它,就可以通过 ssr 返回唯一一个全局状态, 并确认某个组件是否已经ssr过了。
开启服务端渲染(ssr)
web框架目前我们使用的是express,之前使用过一次时间的koa来做ssr,结果发现坑很多,相关的案例太少,有些坑不太好解决,所以为了线上项目的稳定,从而选择了express。
ssr流程图
安装ssr相关
npm install --save express vue-server-renderer lru-cache es6-promise serialize-javascript vue vue-router axios
vue更新到2.0之后,作者就宣告不再对vue-resource更新,并且vue-resource不支持ssr,所以我推荐使用axios, 在服务端和客户端可以同时使用。
vue2使用了虚拟dom, 因此对浏览器环境和服务端环境要分开渲染, 要创建两个对应的入口文件。
浏览器入口文件 client-entry.js
使用 $mount 直接挂载
服务端入口文件 server-entry
使用vue的ssr功能直接将虚拟dom渲染成网页
client-entry.js 文件
import 'es6-promise/auto'; import { app, store } from './app'; store.replacestate(window.__initial_state__); app.$mount('#app');
在 client-entry.js 文件中引入了app.js, 判断如果在服务端渲染时已经写入状态,则将vuex的状态进行替换,使得服务端渲染的html和vuex管理的数据是同步的。然后将vue实例挂载到html指定的节点中。
server-entry 文件
import { app, router, store } from './app'; const isdev = process.env.node_env !== 'production'; export default context => { const s = isdev && date.now(); router.push(context.url); const matchedcomponents = router.getmatchedcomponents(); if (!matchedcomponents.length) { return promise.reject({ code: '404' }); } return promise.all(matchedcomponents.map(component => { if (component.prefetch) { return component.prefetch(store); } })).then(() => { return app; }); };
在 server-entry 文件中服务端会传递一个context对象,里面包含当前用户请求的url,vue-router 会跳转到当前请求的url中,通过 router.getmatchedcomponents( ) 来获得当前匹配组件,则去调用当前匹配到的组件里的 prefetch 钩子,并传递store(vuex下的状态),会返回一个 promise 对象,并在then方法中将现有的vuex state 赋值给context,给服务端渲染使用,最后返回vue实例,将虚拟dom渲染成网页。服务端会将vuex初始状态也生成到页面中。 如果 vue-router 没有匹配到请求的url,直接返回 promise中的reject方法,传入404,这时候会走到下方renderstream的error事件,让页面显示错误信息。
// 处理所有的get请求 app.get('*', (req, res) => { // 等待编译 if (!renderer) { return res.end('waiting for compilation... refresh in a moment.'); } var s = date.now(); const context = { url: req.url }; // 渲染我们的vue实例作为流 const renderstream = renderer.rendertostream(context); // 当块第一次被渲染时 renderstream.once('data', () => { // 将预先的html写入响应 res.write(indexhtml.head); }); // 每当新的块被渲染 renderstream.on('data', chunk => { // 将块写入响应 res.write(chunk); }); // 当所有的块被渲染完成 renderstream.on('end', () => { // 当vuex初始状态存在 if (context.initialstate) { // 将vuex初始状态以script的方式写入到页面中 res.write( `<script>window.__initial_state__=${ serialize(context.initialstate, { isjson: true }) }</script>` ); } // 将结尾的html写入响应 res.end(indexhtml.tail); }); // 当渲染时发生错误 renderstream.on('error', err => { if (err && err.code === '404') { res.status(404).end('404 | page not found'); return; } res.status(500).end('internal error 500'); }); })
上面是vue2.0的服务端渲染方式,用流式渲染的方式,将html一边生成一边写入相应流,而不是在最后一次全部写入。这样的效果就是页面渲染速度将会很快。还可以引入 lru-cache 这个模块对数据进行缓存,并设置缓存时间,我一般设置15分钟的缓存时间。
可以参考vue ssr 官方演示项目的服务端实现
axios在客户端和服务端的使用
创建2个文件用于客户端和服务端的的通信
create-api-client.js 文件(用于客户端)
const axios = require('axios'); let api; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return promise.reject(res); }, (error) => { // 网络异常 return promise.reject({message: '网络异常,请刷新重试', err: error}); }); if (process.__api__) { api = process.__api__; } else { api = { get: function(target, params = {}) { const suffix = object.keys(params).map(name => { return `${name}=${json.stringify(params[name])}`; }).join('&'); const urls = `${target}?${suffix}`; return new promise((resolve, reject) => { axios.get(urls, params).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new promise((resolve, reject) => { axios.post(target, options).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); } }; } module.exports = api;
create-api-server.js 文件(用于服务端)
const isprod = process.env.node_env === 'production'; const axios = require('axios'); let host = isprod ? 'http://yczj.api.autohome.com.cn' : 'http://t.yczj.api.autohome.com.cn'; let cook = process.__cookie__ || ''; let api; axios.defaults.baseurl = host; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return promise.reject(res); }, (error) => { // 网络异常 return promise.reject({message: '网络异常,请刷新重试', err: error, type: 1}); }); if (process.__api__) { api = process.__api__; } else { api = { get: function(target, options = {}) { return new promise((resolve, reject) => { axios.request({ url: target, method: 'get', headers: { 'cookie': cook }, params: options }).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new promise((resolve, reject) => { axios.request({ url: target, method: 'post', headers: { 'cookie': cook }, params: options }).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); } }; } module.exports = api;
由于在服务端,接口不会主动携带 cookie,所以需要在headers里写入cookie。由于接口数据经常发生变化,所以没有做缓存。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: Vue.js 60分钟快速入门教程