详解如何使用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 相关文章推荐
根据出生日期自动取得星座的js代码
Jul 20 Javascript
解析John Resig Simple JavaScript Inheritance代码
Dec 03 Javascript
JS获取并操作iframe中元素的方法
Mar 21 Javascript
JavaScript中for..in循环陷阱介绍
Nov 12 Javascript
jquery获取颜色在ie和ff下的区别示例介绍
Mar 28 Javascript
smartcrop.js智能图片裁剪库
Oct 14 Javascript
JS实现双击屏幕滚动效果代码
Oct 28 Javascript
Jquery调用iframe父页面中的元素及方法
Aug 23 Javascript
jQuery中DOM节点删除之empty与remove
Jan 20 Javascript
vue filters的使用详解
Jun 11 Javascript
微信小程序组件传值图示过程详解
Jul 31 Javascript
JS中的继承操作实例总结
Jun 06 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面向对象
2012/02/22 PHP
php实现简单文件下载的方法
2015/01/30 PHP
php7 错误处理机制修改实例分析
2020/05/25 PHP
js关闭浏览器窗口及检查浏览器关闭事件
2013/09/03 Javascript
jquery插件开发之实现md5插件
2014/03/17 Javascript
jQuery解析XML与传统JavaScript方法的差别实例分析
2015/03/05 Javascript
javascript实现仿IE顶部的可关闭警告条
2015/05/05 Javascript
AngularJS中的$watch(),$digest()和$apply()区分
2016/04/04 Javascript
详解JavaScript中Hash Map映射结构的实现
2016/05/21 Javascript
Jquery获取当前城市的天气信息
2016/08/05 Javascript
详解AngularJS2 Http服务
2017/06/26 Javascript
详解jquery选择器的原理
2017/08/01 jQuery
微信小程序的日期选择器的实例详解
2017/09/29 Javascript
Bootstrap Table 搜索框和查询功能
2017/11/30 Javascript
实例分析JS与Node.js中的事件循环
2017/12/12 Javascript
基于webpack4搭建的react项目框架的方法
2018/06/30 Javascript
通过webpack引入第三方库的方法
2018/07/20 Javascript
Angular异步变同步处理方法
2018/08/13 Javascript
vue 中Virtual Dom被创建的方法
2019/04/15 Javascript
微信小程序实现弹出菜单动画
2019/06/21 Javascript
antd多选下拉框一行展示的实现方式
2020/10/31 Javascript
python编程实现希尔排序
2017/04/13 Python
python实现数据导出到excel的示例--普通格式
2018/05/03 Python
python爬取哈尔滨天气信息
2018/07/14 Python
基于TensorBoard中graph模块图结构分析
2020/02/15 Python
如何实现在jupyter notebook中播放视频(不停地展示图片)
2020/04/23 Python
如何将anaconda安装配置的mmdetection环境离线拷贝到另一台电脑
2020/10/15 Python
python3.9.1环境安装的方法(图文)
2021/02/02 Python
家乐福巴西网上超市:Carrefour巴西
2016/10/31 全球购物
介绍一下Linux文件的记录形式
2013/09/29 面试题
2014年驻村干部工作总结
2014/11/17 职场文书
2015年大学班级工作总结
2015/04/28 职场文书
创业计划书之密室逃脱
2019/11/08 职场文书
CSS几步实现赛博朋克2077风格视觉效果
2021/06/16 HTML / CSS
详解Java七大阻塞队列之SynchronousQueue
2021/09/04 Java/Android
Win11怎么解除儿童账号限制?Win11解除微软儿童账号限制方法
2022/07/07 数码科技