如何使用Vue2作服務端渲染

花費了一個月時間,終於在新養車之家項目中成功部署了vue2服務端渲染(SSR),而且使用上了Vuex 負責狀態管理,首屏加載時間從以前4G網絡下的1000ms,提高到了如今500-700ms之間,SSR的優點有不少,如今讓我來跟你細細道來。javascript

技術棧

服務端:Nodejs(v6.3)html

前端框架 Vue2.1.10前端

前端構建工具:webpack2.2 && gulpvue

代碼檢查:eslintjava

源碼:es6webpack

前端路由:vue-router2.1.0ios

狀態管理:vuex2.1.0git

服務端通訊:axioses6

日誌管理:log4jsgithub

項目自動化部署工具: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車服務前端團隊 微信公衆號

相關文章
相關標籤/搜索