zoukankan      html  css  js  c++  java
  • 如何使用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。由于接口数据经常发生变化,所以没有做缓存。

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

  • 相关阅读:
    ajax简单案例
    jquery中的数据传输
    java-Reflect
    Factory Method 和AbstractFactory
    Singleton
    英语六级口语备考指南
    ACM信息汇总
    jquery练习
    char可不可以存汉字
    信息安全
  • 原文地址:https://www.cnblogs.com/wangshiyang/p/6565624.html
Copyright © 2011-2022 走看看