详解如何使用Vue2做服务端渲染


Posted in Javascript onMarch 29, 2017

花费了一个月时间,终于在新养车之家项目中成功部署了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流程图

详解如何使用Vue2做服务端渲染

安装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 官方演示项目的服务端实现 https://github.com/vuejs/vue-hackernews-2.0/blob/master/server.js

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。由于接口数据经常发生变化,所以没有做缓存。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery 框架使用教程 AJAX篇
Oct 11 Javascript
jQuery 树形结构的选择器
Feb 15 Javascript
js解决弹窗问题实现班级跳转DIV示例
Jan 06 Javascript
在JS中解析HTML字符串示例代码
Apr 16 Javascript
浅析JavaScript事件和方法
Feb 28 Javascript
jQuery实现切换字体大小的方法
Mar 10 Javascript
每个程序员都需要学习 JavaScript 的7个理由小结
Sep 03 Javascript
Query常用DIV操作获取和设置长度宽度的实现方法
Sep 19 Javascript
JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法示例
Jul 27 Javascript
详解JS实现简单的时分秒倒计时代码
Apr 25 Javascript
vue如何在项目中调用腾讯云的滑动验证码
Jul 15 Javascript
小程序wx.getUserProfile接口的具体使用
Jun 02 Javascript
js实现华丽的九九乘法表效果
Mar 29 #Javascript
JS简单获取当前日期时间的方法(如:2017-03-29 11:41:10 星期四)
Mar 29 #Javascript
微信小程序实现带刻度尺滑块功能
Mar 29 #Javascript
Javascript 详解封装from表单数据为json串进行ajax提交
Mar 29 #Javascript
详解如何在Vue2中实现组件props双向绑定
Mar 29 #Javascript
整理关于Bootstrap警示框的慕课笔记
Mar 29 #Javascript
node.js程序作为服务并在windows下开机自启动(用forever)
Mar 29 #Javascript
You might like
php minixml详解
2008/07/19 PHP
防止本地用户用fsockopen DDOS攻击对策
2011/11/02 PHP
php可应用于面包屑导航的递归寻找家谱树实现方法
2015/02/02 PHP
Thinkphp事务操作实例(推荐)
2017/04/01 PHP
php curl批处理实现可控并发异步操作示例
2018/05/09 PHP
因str_replace导致的注入问题总结
2019/08/08 PHP
php设计模式之职责链模式定义与用法经典示例
2019/09/19 PHP
基于jquery的用鼠标画出可移动的div
2012/09/06 Javascript
jQuery向上遍历DOM树之parents(),parent(),closest()之间的区别
2013/12/02 Javascript
基于jQuery的网页影音播放器jPlayer的基本使用教程
2016/03/08 Javascript
NodeJS与HTML5相结合实现拖拽多个文件上传到服务器的实现方法
2016/07/26 NodeJs
js调用屏幕宽度的简单方法
2016/11/14 Javascript
js序列化和反序列化的使用讲解
2019/01/19 Javascript
微信小程序五子棋游戏的悔棋实现方法【附demo源码下载】
2019/02/20 Javascript
Python实现正整数分解质因数操作示例
2018/08/01 Python
详解python读取和输出到txt
2019/03/29 Python
基于Python函数和变量名解析
2019/07/19 Python
解决echarts中饼图标签重叠的问题
2020/05/16 Python
Python多线程正确用法实例解析
2020/05/30 Python
用python对excel进行操作(读,写,修改)
2020/12/25 Python
python爬虫如何解决图片验证码
2021/02/14 Python
html5的localstorage详解
2017/05/09 HTML / CSS
英国的知名精品百货公司:House of Fraser(福来德)
2016/08/14 全球购物
优衣库美国官网:UNIQLO美国
2018/04/14 全球购物
高等教育学自荐书范文
2014/02/10 职场文书
个人培训自我鉴定
2014/03/28 职场文书
银行行长竞聘演讲稿
2014/04/23 职场文书
写给老婆的保证书
2015/02/27 职场文书
预备党员介绍人意见
2015/06/01 职场文书
拉贝日记观后感
2015/06/05 职场文书
2016年教代会开幕词
2016/03/04 职场文书
合作意向书怎么写
2019/06/24 职场文书
职工的安全责任书范文!
2019/07/02 职场文书
深入理解python协程
2021/06/15 Python
Python中可变和不可变对象的深入讲解
2021/08/02 Python
nginx代理实现静态资源访问的示例代码
2022/07/07 Servers