花费了一个月时间,终于在新养车之家项目中成功部署了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方法)来将需要的数据拿到。最后再通过

  1. <script>window.__initial_state=data</script>

将其写入网页,最后将服务端渲染好的网页返回回去。

接下来客户端会将vuex将写入的 __initial_state__ 替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似React的 shouldComponentUpdate 的Diff操作。

Vue2使用的是单向数据流,用了它,就可以通过 SSR 返回唯一一个全局状态, 并确认某个组件是否已经SSR过了。

开启服务端渲染(SSR)

Web框架目前我们使用的是express,之前使用过一次时间的koa来做SSR,结果发现坑很多,相关的案例太少,有些坑不太好解决,所以为了线上项目的稳定,从而选择了express。

SSR流程图

安装SSR相关

  1. 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 文件

  1. import 'es6-promise/auto';
  2.  
  3. import { app, store } from './app';
  4.  
  5. store.replaceState(window.__INITIAL_STATE__);
  6.  
  7. app.$mount('#app');

在 client-entry.js 文件中引入了app.js, 判断如果在服务端渲染时已经写入状态,则将vuex的状态进行替换,使得服务端渲染的html和vuex管理的数据是同步的。然后将vue实例挂载到html指定的节点中。

server-entry 文件

  1. import { app, router, store } from './app';
  2.  
  3. const isDev = process.env.NODE_ENV !== 'production';
  4.  
  5. export default context => {
  6. const s = isDev && Date.now();
  7.  
  8. router.push(context.url);
  9. const matchedComponents = router.getMatchedComponents();
  10.  
  11. if (!matchedComponents.length) {
  12. return Promise.reject({ code: '404' });
  13. }
  14.  
  15. return Promise.all(matchedComponents.map(component => {
  16. if (component.preFetch) {
  17. return component.preFetch(store);
  18. }
  19. })).then(() => {
  20. return app;
  21. });
  22. };

在 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事件,让页面显示错误信息。

  1. // 处理所有的get请求
  2. app.get('*', (req, res) => {
  3. // 等待编译
  4. if (!renderer) {
  5. return res.end('waiting for compilation... refresh in a moment.');
  6. }
  7.  
  8. var s = Date.now();
  9. const context = { url: req.url };
  10. // 渲染我们的Vue实例作为流
  11. const renderStream = renderer.renderToStream(context);
  12.  
  13. // 当块第一次被渲染时
  14. renderStream.once('data', () => {
  15. // 将预先的HTML写入响应
  16. res.write(indexHTML.head);
  17. });
  18.  
  19. // 每当新的块被渲染
  20. renderStream.on('data', chunk => {
  21. // 将块写入响应
  22. res.write(chunk);
  23. });
  24.  
  25. // 当所有的块被渲染完成
  26. renderStream.on('end', () => {
  27. // 当vuex初始状态存在
  28. if (context.initialState) {
  29. // 将vuex初始状态以script的方式写入到页面中
  30. res.write(
  31. `<script>window.__INITIAL_STATE__=${
  32. serialize(context.initialState, { isJSON: true })
  33. }</script>`
  34. );
  35. }
  36.  
  37. // 将结尾的HTML写入响应
  38. res.end(indexHTML.tail);
  39. });
  40.  
  41. // 当渲染时发生错误
  42. renderStream.on('error', err => {
  43. if (err && err.code === '404') {
  44. res.status(404).end('404 | Page Not Found');
  45. return;
  46. }
  47. res.status(500).end('Internal Error 500');
  48. });
  49. })

上面是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 文件(用于客户端)

  1. const axios = require('axios');
  2. let api;
  3.  
  4. axios.defaults.timeout = 10000;
  5.  
  6. axios.interceptors.response.use((res) => {
  7. if (res.status >= 200 && res.status < 300) {
  8. return res;
  9. }
  10. return Promise.reject(res);
  11. }, (error) => {
  12. // 网络异常
  13. return Promise.reject({message: '网络异常,请刷新重试', err: error});
  14. });
  15.  
  16. if (process.__API__) {
  17. api = process.__API__;
  18. } else {
  19. api = {
  20. get: function(target, params = {}) {
  21. const suffix = Object.keys(params).map(name => {
  22. return `${name}=${JSON.stringify(params[name])}`;
  23. }).join('&');
  24. const urls = `${target}?${suffix}`;
  25. return new Promise((resolve, reject) => {
  26. axios.get(urls, params).then(res => {
  27. resolve(res.data);
  28. }).catch((error) => {
  29. reject(error);
  30. });
  31. });
  32. },
  33. post: function(target, options = {}) {
  34. return new Promise((resolve, reject) => {
  35. axios.post(target, options).then(res => {
  36. resolve(res.data);
  37. }).catch((error) => {
  38. reject(error);
  39. });
  40. });
  41. }
  42. };
  43. }
  44.  
  45. module.exports = api;

create-api-server.js 文件(用于服务端)

  1. const isProd = process.env.NODE_ENV === 'production';
  2.  
  3. const axios = require('axios');
  4. let host = isProd ? 'http://yczj.api.autohome.com.cn' : 'http://t.yczj.api.autohome.com.cn';
  5. let cook = process.__COOKIE__ || '';
  6. let api;
  7.  
  8. axios.defaults.baseURL = host;
  9. axios.defaults.timeout = 10000;
  10.  
  11. axios.interceptors.response.use((res) => {
  12. if (res.status >= 200 && res.status < 300) {
  13. return res;
  14. }
  15. return Promise.reject(res);
  16. }, (error) => {
  17. // 网络异常
  18. return Promise.reject({message: '网络异常,请刷新重试', err: error, type: 1});
  19. });
  20.  
  21. if (process.__API__) {
  22. api = process.__API__;
  23. } else {
  24. api = {
  25. get: function(target, options = {}) {
  26. return new Promise((resolve, reject) => {
  27. axios.request({
  28. url: target,
  29. method: 'get',
  30. headers: {
  31. 'Cookie': cook
  32. },
  33. params: options
  34. }).then(res => {
  35. resolve(res.data);
  36. }).catch((error) => {
  37. reject(error);
  38. });
  39. });
  40. },
  41. post: function(target, options = {}) {
  42. return new Promise((resolve, reject) => {
  43. axios.request({
  44. url: target,
  45. method: 'post',
  46. headers: {
  47. 'Cookie': cook
  48. },
  49. params: options
  50. }).then(res => {
  51. resolve(res.data);
  52. }).catch((error) => {
  53. reject(error);
  54. });
  55. });
  56. }
  57. };
  58. }
  59.  
  60. module.exports = api;

由于在服务端,接口不会主动携带 cookie,所以需要在headers里写入cookie。由于接口数据经常发生变化,所以没有做缓存。

如果您想了解更多最新前端技术,请关注 AutoHome车服务前端团队 微信公众号

如何使用Vue2做服务端渲染的更多相关文章

  1. Vue2服务端渲染

    Vue2 服务端渲染 花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(SSR),并且使用上了Vuex 负责状态管理,首屏加载时间从之前4G网络下的1000ms,提升到了现在500 ...

  2. 用prerender-spa-plugin插件Vue项目优化SEO做ssr服务端渲染及预渲染

    今天在做公交的时候没干,用手机看看文章,偶然发现了一个关于Vue优化seo的文章,我先是在Vue的官方文档看了一篇关于Vue做SEO优化的文章. 上面提到了nuxt.js这个框架,这个框架我做过一个小 ...

  3. 详解react/redux的服务端渲染:页面性能与SEO

        亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)   react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...

  4. 6.前端基于react,后端基于.net core2.0的开发之路(6) 服务端渲染(SSR)

    0.源码地址 https://gitee.com/teambp/ScaffoldClient  这个地址下载对应源码. 1.服务端渲染是啥? 就是在服务器进行页面渲染(废话),当页面展示后,显示的就是 ...

  5. 【redux】详解react/redux的服务端渲染:页面性能与SEO

        亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)   react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...

  6. React 服务端渲染最佳解决方案

    最近在开发一个服务端渲染工具,通过一篇小文大致介绍下服务端渲染,和服务端渲染的方式方法.在此文后面有两中服务端渲染方式的构思,根据你对服务端渲染的利弊权衡,你会选择哪一种服务端渲染方式呢? 什么是服务 ...

  7. 基于vue的nuxt框架cnode社区服务端渲染

    nuxt-cnode 基于vue的nuxt框架仿的cnode社区服务端渲染,主要是为了seo优化以及首屏加载速度 线上地址 http://nuxt-cnode.foreversnsd.cngithub ...

  8. react+laravel与服务端渲染的几点思考

    一.前后端完全分离 1.用React.js做MVC中的V,剩下的交给Laravel 2.Laravel用来做API接口开发. 3.好处:实现了前后端开发的分离,从而加快前后端开发效率.另外若是多端的如 ...

  9. [Next] 服务端渲染知识补充

    渲染 渲染:就是将数据和模版组装成 html 客户端渲染 客户端渲染模式下,服务端把渲染的静态文件给到客户端,客户端拿到服务端发送过来的文件自己跑一遍 js,根据 JS 运行结果,生成相应 DOM,然 ...

随机推荐

  1. jquery checkbox全选 获取值

    <style> table { line-height:35px; }</style> <div align="left" style="m ...

  2. 2017年学习的三个CSS新特性

    这是翻译的一篇文章,原文是:3 New CSS Features to Learn in 2017,翻译的不是很好,如有疑问欢迎指出. 新的一年,我们有一系列新的东西要学习.尽管CSS有很多新的特性, ...

  3. Fiddler 模拟请求的操作方法

    此文记录使用Fidder Web Debugger工具,模拟请求的操作步骤! 首先简述一下fiddler的使用: 1.下载安装Fidder抓包工具. 2.打开fiddler发现有左边的栏有请求的url ...

  4. Hadoop权威指南:通过FileSystem API读取数据

    Hadoop权威指南:通过FileSystem API读取数据 [TOC] 在Hadoop中,FileSystem是一个通用的文件系统API 获取FileSystem实例的几个静态方法 public ...

  5. Hadoop权威指南:通过distcp并行复制

    Hadoop权威指南:通过distcp并行复制 distcp是一个分布式复制程序,改程序可以从Hadoop文件系统间复制大量数据,也可以将大量的数据复制到Hadoop中 distcp的典型应用是在两个 ...

  6. [个人翻译]GitHub指导文件(GitHub Guides[Hello World])

    [个人翻译]GitHub指导文件(GitHub Guides[Hello World]) Mirage_j个人翻译,欢迎转载,最好标明出处http://www.cnblogs.com/mirageJ/ ...

  7. 浅谈Django的Q查询以及AngularJS的Datatables分页插件

    使用Q查询,首先要导入Q模块: from django.db.models import Q 可以组合使用&,|操作符用于多个Q的对象,产生一个新的Q对象,Q对象也可以用~操作符放在前面表示否 ...

  8. pch 文件

    PCH的文件的用途:      在实际的项目开发中,如果很多地方都在使用某个类的头文件,很多地方都在使用同一个”宏”的时候:很多地方用到了NSLog()函数, 在app发布的时候,想清除掉时,此时就需 ...

  9. bzoj4554: [Tjoi2016&Heoi2016]游戏 二分图匹配

    4554: [Tjoi2016&Heoi2016]游戏 Description 在2016年,佳缘姐姐喜欢上了一款游戏,叫做泡泡堂.简单的说,这个游戏就是在一张地图上放上若干个炸弹,看 是否能 ...

  10. 传输控制协议TCP

    TCP主要特点: (1)面向连接 (2)只能是点到点 (3)可靠的传输协议 (4)全双工通信,各自各自的读写缓冲区,应用层吧数据交给发送缓冲区,tcp合适就发送了,接受的时候tcp写入接受缓冲区,应用 ...