详解如何使用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 相关文章推荐
javascript 表单的友好用户体现
Jan 07 Javascript
js Dialog 实践分享
Oct 22 Javascript
Javascript根据指定下标或对象删除数组元素
Dec 21 Javascript
cookie.js 加载顺序问题怎么才有效
Jul 31 Javascript
JavaScript AOP编程实例
Jun 16 Javascript
JS实现网站菜单拖拽移位效果的方法
Sep 24 Javascript
jQuery mobile类库使用时加载导航历史的方法简介
Dec 04 Javascript
clipboard.js在移动端复制失败的解决方法
Jun 13 Javascript
JavaScript继承的特性与实践应用深入详解
Dec 30 Javascript
微信 jssdk 签名错误invalid signature的解决方法
Jan 14 Javascript
了解JavaScript函数中的默认参数
May 30 Javascript
创建nuxt.js项目流程图解
Mar 13 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 内存缓存加速功能memcached安装与用法
2009/09/03 PHP
PHPMailer使用教程(PHPMailer发送邮件实例分析)
2012/12/06 PHP
php JWT在web端中的使用方法教程
2018/09/06 PHP
yii2 在控制器中验证请求参数的使用方法
2019/06/19 PHP
jQuery 源码分析笔记(6) jQuery.data
2011/06/08 Javascript
使用JQuery和CSS模拟超链接的用户单击事件的实现代码
2012/05/23 Javascript
js获取某月的最后一天日期的简单实例
2013/06/22 Javascript
JQuery 使用attr方法实现下拉列表选中
2014/10/13 Javascript
JavaScript判断undefined类型的正确方法
2015/06/30 Javascript
js实现跨域的多种方法
2015/12/25 Javascript
学习使用grunt来打包JavaScript和CSS程序的教程
2016/01/04 Javascript
浅谈JavaScript中小数和大整数的精度丢失
2016/05/31 Javascript
jQuery调用Webservice传递json数组的方法
2016/08/06 Javascript
Node.js 实现简单小说爬虫实例
2016/11/18 Javascript
老生常谈js-react组件生命周期
2017/05/02 Javascript
JavaScript EventEmitter 背后的秘密 完整版
2018/03/29 Javascript
使用ECharts实现状态区间图
2018/10/25 Javascript
使用JavaScript保存文本文件到本地的两种方法
2019/01/22 Javascript
jQuery 隐藏/显示效果函数用法实例分析
2020/05/20 jQuery
python使用自定义user-agent抓取网页的方法
2015/04/15 Python
python使用opencv进行人脸识别
2017/04/07 Python
python OpenCV学习笔记实现二维直方图
2018/02/08 Python
mac下给python3安装requests库和scrapy库的实例
2018/06/13 Python
python实战教程之自动扫雷
2018/07/13 Python
python语言基本语句用法总结
2019/06/11 Python
python logging 日志的级别调整方式
2020/02/21 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
2020/08/28 Python
重构Python代码的六个实例
2020/11/25 Python
惠普墨西哥官方商店:HP墨西哥
2016/12/01 全球购物
Tostadora意大利:定制T恤
2019/04/08 全球购物
static全局变量与普通的全局变量有什么区别
2014/05/27 面试题
税务会计岗位职责
2015/04/02 职场文书
社区青年志愿者活动总结
2015/05/06 职场文书
2016新春团拜会致辞
2015/08/01 职场文书
2019年消防宣传标语集锦
2019/11/21 职场文书
golang定时器
2022/04/14 Golang