Vue 服務端渲染業務入門實踐

keuo9813 7年前發布 | 30K 次閱讀 Vue.js Vue.js開發

背景

最近, 產品同學一如往常笑嘻嘻的遞來需求文檔, 縱使內心萬般拒絕, 身體倒是很誠實。 接過需求,好在需求不復雜, 簡單構思 后決定用Vue, 得心應手。 切好圖, 挽起袖子準備擼代碼的時候, SEO同學不知何時已經站到了背后。

"聽說你要用Vue?"

"恩..."

"SEO考慮了嗎?整個SPA出來,網頁的SEO咋辦?"

"奧..."

換以前, 估計只能無奈的換個實現方式, 但是Vue 2.0時代的到來, 給你多了一種可能。 你可以對SEO工程師說:用Vue沒問題!

想必,很多前端同學都有類似這樣的經歷, 為了SEO,只能放棄得心應手的框架。 SEO(Search Engine Optimization)顧名思義就是一系列為了提高 網站收錄排名,吸引精準用戶的方案。 這么看來,SEO確實是有舉足輕重的作用。 不過,好消息是,Vue2.0的發布為SEO提供了可能, 這就是SSR(serve side render)。

說起SSR,其實早在SPA (Single Page Application) 出現之前,網頁就是在服務端渲染的。服務器接收到客戶端請求后,將數據和模板拼接成完整的頁面響應到客戶端。 客戶端直接渲染, 此時用戶希望瀏覽新的頁面,就必須重復這個過程, 刷新頁面. 這種體驗在Web技術發展的當下是幾乎不能被接受的,于是越來越多的技術方案涌現,力求 實現無頁面刷新或者局部刷新來達到優秀的交互體驗。 比如Vue:

- 在客戶端管理路由,用戶切換路由,無需向服務器重新請求頁面和靜態資源,只需要使用 ajax 獲取數據在客戶端完成渲染,這樣可以減少了很多不必要的網絡傳輸,縮短了響應時間。

- 聲明式渲染(告訴 vue 你要做什么,讓它幫你做),把我們從煩人的DOM操作中解放出來,集中處理業務邏輯。

- 組件化視圖,無論是功能組件還是UI組件都可以進行抽象,寫一次到處用。

- 前后端并行開發,只需要與后端定好數據格式,前期用模擬數據,就可以與后端并行開發了。

- 對復雜項目的各個組件之間的數據傳遞 vue  - Vuex 狀態管理模式

缺點大家自然猜到了, 對,主要的一點就是不利于SEO,或者說對SEO不友好。 來看下面兩張圖;

SPA頁面的源代碼

下圖SSR頁面的源代碼

上面兩張圖就是使用了傳統單頁應用和SSR的頁面源代碼, 第一張圖中,很明顯頁面的數據都是通過Ajax異步獲取,然而搜索引擎度娘家的爬蟲看到這樣空曠的源碼并不會絲毫留戀. 相反,通過服務端渲染的頁面,就有很多對于爬蟲來講有效的連接. 畢竟度娘一家獨大,看來服務端渲染確實有探究的必要了。

vue 的服務端渲染是怎么回事?

先看一張Vue官網的服務端渲染示意圖

從圖上可以看出,ssr 有兩個入口文件,client.js 和 server.js, 都包含了應用代碼,webpack 通過兩個入口文件分別打包成給服務端用的 server bundle 和給客戶端用的 client bundle. 當服務器接收到了來自客戶端的請求之后,會創建一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 文件,并且執行它的代碼, 然后發送一個生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 之后,會和服務端生成的DOM 進行 Hydration(判斷這個DOM 和自己即將生成的DOM 是否相同,如果相同就將客戶端的vue實例掛載到這個DOM上, 否則會提示警告)。

怎么實現?

知道了Vue服務端渲染的大致流程,那怎么用代碼來實現呢?

1. 創建一個 vue 實例

2. 配置路由,以及相應的視圖組件

3. 使用 vuex 管理數據

4. 創建服務端入口文件

5. 創建客戶端入口文件

6. 配置 webpack,分服務端打包配置和客戶端打包配置

7. 創建服務器端的渲染器,將vue實例渲染成html

  • 首先我們來創建一個 vue 實例

// app.js

import Vue from 'vue';
import router from './router';
import store from './store';
import App from './components/app';   

let app = new Vue({
    template: '<app></app>',
    base: '/c/',
    components: {
        App
    },
    router,
    store
});

export {
    app,
    router,
    store
}</code></pre> 

和我們以前寫的vue實例差別不大,但是我們不會在這里將app mount到DOM上,因為這個實例也會在服務端去運行,這里直接將 app 暴露出去。

  • 配置 vue 路由

  import Vue from 'vue';
  import VueRouter from 'vue-router';

  import IndexView from '../views/indexView';
  import ArticleItems from '../views/articleItems';

  Vue.use(VueRouter);

  const router = new VueRouter({
      mode: 'history',
      base: '/c/',
      routes: [
          {
              path: '/:alias',
              component: IndexView
          }, {
              path: '/:alias/list',
              component: ArticleItems
          }
      ]
  });

注意這里的 base,在服務端傳遞 path 給 vue-router 的時候要注意去掉前面的 '/c/',否則會匹配不到。

  • 創建視圖組件,這里我們使用單文件組件,下面是 indexView.vue 文件的實例代碼

<template>
      <div class="content">
          <course-cover :class-data="classData[0]"></course-cover>
          <article-items :article-items="articleItems"></article-items>
      </div>
  </template>

  <script>
      import courseCover from '../components/courseCover.vue';
      import articleItems from '../components/articleItems';

      export default {
          computed: {
              classData() {
                  return this.$store.state.courseListItems;
              },
              articleItems() {
                  return this.$store.state.articleItems;
              }
          },
          components: {
              courseCover,
              articleItems
          },
          // 服務端獲取數據
          fetchServerData ({ state, dispatch, commit }) {
              let alias = state.route.params.alias;

              return Promise.all([
                  dispatch('FETCH_ZT', { alias }),
                  dispatch('FETCH_COURSE_ITEMS'),
                  dispatch('FETCH_ARTICLE_ITEMS')
              ])
          },
          // 客戶端獲取數據
          beforeMount() {
              return this.$store.dispatch('FETCH_COURSE_ITEMS');
          }
      }
  </script>

這里我們暴露一個 fetchServerData 方法用來在服務端渲染時做數據的預加載,具體在哪調用,下面會講到。 beforeMount 是vue的生命周期鉤子函數,當應用在客戶端切換到這個視圖的時候會在特定的時候去執行,用于在客戶端獲取數據。

  • 使用 vuex 管理數據,vue2.0 的服務端官方推薦使用 STORE 來管理數據,和1.0相比 api 有一些調整

  import Vue from 'vue';
  import Vuex from 'vuex';
  import axios from 'axios';

  Vue.use(Vuex);

  let apiHost = 'http://localhost:3000';

  const store = new Vuex.Store({
      state: {
          alias: '',
          ztData: {},
          courseListItems: [],
          articleItems: []
      },
      actions: {
          FETCH_ZT: ({ commit, dispatch, state }, { alias }) = {
              commit('SET_ALIAS', { alias });
              return axios.get(`${apiHost}/api/zt`)
                          .then(response => {
                              let data = response.data || {};
                              commit('SET_ZT_DATA', data);
                          })
          },
          FETCH_COURSE_ITEMS: ({ commit, dispatch, state }) => {
              return axios.get(`${apiHost}/api/course_items`).then(response => {
                  let data = response.data;
                  commit('SET_COURSE_ITEMS', data);
              });
          },
          FETCH_ARTICLE_ITEMS: ({ commit, dispatch, state }) => {
              return axios.get(`${apiHost}/api/article_items`)
                          .then(response => {
                              let data = response.data;
                              commit('SET_ARTICLE_ITEMS', data);
                          })
          }
      },
      mutations: {
          SET_COURSE_ITEMS: (state, data) => {
              state.courseListItems = data;
          },
          SET_ALIAS: (state, { alias }) => {
              state.alias = alias;
          },
          SET_ZT_DATA: (state, { ztData }) => {
              state.ztData = ztData;
          },
          SET_ARTICLE_ITEMS: (state, items) => {
              state.articleItems = items;
          }
      }
  })

  export default store;

state 使我們應用層的數據,相當于一個倉庫,整個應用層的數據都存在這里,與不使用vuex的vue應用有兩點不同:

-  Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。

-  Vuex 不允許我們直接對 store 中的數據進行操作。改變 store 中的狀態的唯一途徑就是顯式地提交(commit) mutations。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

action 響應在view上的用戶輸入導致的狀態變化,并不直接操作數據,異步的邏輯都封裝在這里執行,它最終的目的是提交 mutation 來操作數據。 mutation vuex 中修改store 數據的唯一方法,使用 commit 來提交。

  • 創建服務端的入口文件 server-entry.js

// server-entry.js
    import {app, router, store} from './app';

    export default context => {

        const s = Date.now();
        router.push(context.url);
        const matchedComponents = router.getMatchedComponents();
        if(!matchedComponents) {
            return Promise.reject({ code: '404' });
        }

        return Promise.all(
            matchedComponents.map(component => {
                if(component.fetchServerData) {
                    return component.fetchServerData(store);
                }
            })
        ).then(() => {
            context.initialState = store.state;
            return app;
        })
    }

server.js 返回一個函數,該函數接受一個從服務端傳遞過來的 context 的參數,將 vue 實例通過 promise 返回。 context 一般包含 當前頁面的url,首先我們調用 vue-router 的 router.push(url) 切換到到對應的路由, 然后調用 getMatchedComponents 方法返回對應要渲染的組件, 這里會檢查組件是否有 fetchServerData 方法,如果有就會執行它。

下面這行代碼將服務端獲取到的數據掛載到 context 對象上,后面會把這些數據直接發送到瀏覽器端與客戶端的vue 實例進行數據(狀態)同步。

context.initialState = store.state`

創建客戶端入口文件 client-entry.js

// client-entry.js
    import { app, store } from './app';
    import './main.scss';
    store.replaceState(window.__INITIAL_STATE__);
    app.$mount('#app');

客戶端入口文件很簡單,同步服務端發送過來的數據,然后把 vue 實例掛載到服務端渲染的 DOM 上。

  • 配置 webpack

// webpack.server.config.js
    const base = require('./webpack.base.config'); // webpack 的通用配置
    module.exports = Object.assign({}, base, {
        target: 'node',
        entry: './src/server-entry.js',
        output: {
            filename: 'server-bundle.js',
            libraryTarget: 'commonjs2'
        },
        externals: Object.keys(require('../package.json').dependencies),
        plugins: [
            new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
            })
        ]
    })

注意這里添加了 target: 'node' 和 libraryTarget: 'commonjs2',然后入口文件改成我們的 server-entry.js, 客戶端的 webpack 和以前一樣,這里就不貼了。

  • 分別打包服務端代碼和客戶端代碼

因為有兩個 webpack 配置文件,執行 webpack 時候就需要指定 --config 參數來編譯不同的 bundle。 我們可以配置兩個 npm script

   "packclient": "webpack --config webpack.client.config.js",
    "packserver": "webpack --config webpack.server.config.js"

然后在命令行運行

   npm run packclient
    npm run packserver

就會生成兩個文件 client-bundle.js 和 server-bundle.js

  • 創建服務端渲染器

// controller.js

  const serialize = require('serialize-javascript');
  // 因為我們在vue-router 的配置里面使用了 `base: '/c'`,這里需要去掉請求path中的 '/c'
  let url = this.url.replace(/\/c/, '');
  let context = { url: this.url };
  // 創建渲染器
  let bundleRenderer = createRenderer(fs.readFileSync(resolve('./dist/server-bundle.js'), 'utf-8'))
  let html = yield new Promise((resolve, reject) => {
      // 將vue實例編譯成一個字符串
      bundleRenderer.renderToString(
          context,   // 傳遞context 給 server-bundle.js 使用
          (err, html) => {
              if(err) {
                  console.error('server render error', err);
                  resolve('');
              }
              /**
               * 還記得在 server-entry.js 里面 `context.initialState = store.state` 這行代碼么?
               * 這里就直接把數據發送到瀏覽器端啦
              **/
              html += `<script>
                          // 將服務器獲取到的數據作為首屏數據發送到瀏覽器
                          window.__INITIAL_STATE__ = ${serialize(context.initialState, { isJSON: true })}
                      </script>`;
              resolve(html);
          }
      )
  })

  yield this.render('ssr', html);

  // 創建渲染器函數
  function createRenderer(code) {
      return require('vue-server-renderer').createBundleRenderer(code);
  }

在 node 的 views 模板文件中只需要將上面的 html 輸出就可以了

// ssr.html
    {% extends 'layout.html' %}
    {% block body %}
        {{ html | safe }}
    {% endblock %}

    <script src="/public/client.js"></script>

這樣,一個簡單的服務端渲染就結束了,限于篇幅,詳細的代碼請 參考  Github代碼庫 。

https://github.com/pangz1/vue-ssr

小結

整個demo包含了:

- vue + vue-router + vuex 的使用

- 服務端數據獲取

- 客戶端數據同步以及DOM hydration。

沒有涉及:

- 流式渲染

- 組件緩存

對Vue的服務端渲染有更深一步的認識,實際在生產環境中的應用可能還需要考慮很多因素。

選擇Vue的服務端渲染方案,是情理之中的選擇,不是對新技術的盲目追捧,而是一切為了需要。 Vue 2.0的SSR方案只是提供了一種可能,多了一種選擇,框架本身在于服務開發者,根據不同的場景選擇不同的方案,才會事半功倍。

 

 

來自:http://mp.weixin.qq.com/s/rClP45Eng4vlI887wY5fdw

 

 本文由用戶 keuo9813 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!