Egg Vue SSR 服務端渲染數據請求與asyncData

Egg Vue SSR 服務端渲染數據請求與asyncData

服務端渲染 Node 層直接獲取數據

在 Egg 項目若是使用模板引擎規範時通是過 render 方法進行模板渲染,render 的第一個參數模板路徑,第二個參數時模板渲染數據. 如以下調用方式:
javascript

async index(ctx) {
    // 獲取數據,能夠是從數據庫,後端 Http 接口 等形式
    const list = ctx.service.article.getArtilceList();
    // 對模板進行渲染,這裏的 index.js 是 vue 文件經過 Webpack 構建的 JSBundle 文件
    await ctx.render('index.js', { list });
}

從上面的例子能夠看出,這種使用方式是很是典型的也容易理解的模板渲染方式。在實際業務開發時,對於常規的頁面渲染也建議使用這種方式獲取數據沒,而後進行頁面渲染。Node 獲取數據後,在 Vue 的根 Vue 文件裏面就能夠經過 this.list 的方式拿到 Node 獲取的數據,而後就能夠進行 vue 模板文件數據綁定了。


在這裏有個高階用法,能夠直接把 ctx 等 Node 對象傳遞到 第二個參數裏面,  這個時候你在模板裏面就直接拿到 ctx 這些對象。 但這個時候就須要本身處理好 SSR 渲染時致使的 hydrate 問題,由於前端hydrate時並無 ctx 對象。
 
前端

async index(ctx) {
    // 獲取數據,能夠是從數據庫,後端 Http 接口 等形式
    const list = ctx.service.article.getArtilceList();
    // 對模板進行渲染,這裏的 index.js 是 vue 文件經過 Webpack 構建的 JSBundle 文件
    await ctx.render('index.js', { ctx, list });
}

服務端渲染 asyncData 方式獲取數據

在 Vue 單頁面 SSR 時涉及數據的請求方式,Node 層獲取數據方式能夠繼續使用,但當路由切換時(頁面直接刷新),Node 層就須要根據路由獲取不一樣頁面的數據,同時還要考慮前端路由切換的狀況,這個時候路由是不會走 Node 層路由,而是直接進行的前端路由,這個時候也要考慮數據的請求方式。


基於以上使用的優雅問題,這裏提供一種 asyncData 獲取數據的方式解決單頁面 SSR 刷新不走 SSR 問題。 Node 不直接獲取數據,獲取數據的代碼直接寫到前端代碼裏面。這裏須要解決以下兩個問題:
vue

前端路由匹配 asyncData 調用

這裏根據路由切換 url 獲取指定的路由 componet 組件,而後檢查是否有 aysncData,若是有就進行調用。調用以後,數據會放到 Vuex 的 store 裏面。java

return new Promise((resolve, reject) => {
        router.onReady(() => {
          // url 爲當前請求路由,能夠經過服務端傳遞到前端頁面
          const matchedComponents = router.getMatchedComponents(url);
          if (!matchedComponents) {
            return reject({ code: '404' });
          }
          return Promise.all(
            matchedComponents.map(component => {
              // 關鍵代碼
              if (component.methods && component.methods.asyncData) {
                return component.methods.asyncData(store);
              }
              return null;
            })
          ).then(() => {
            context.state = {
              ...store.state,
              ...context.state
            };
            return resolve(new Vue(options));
          });
        });
      });

Vue 模板定義 asyncData 方法

前端經過 Vuex 進行數據管理,把數據統一放到 store 裏面,前端經過 this.$store.state 方式能夠獲取數據,Node 和 前端均可以獲取到。webpack

<script type="text/babel">
  export default{
    computed: {
      isLoading(){
       return false;
      },
      articleList() {
        return this.$store.state.articleList;
      }
    },
    methods: {
      asyncData ({ state, dispatch, commit }) {
        return dispatch('FETCH_ARTICLE_LIST')
      }
    }
  }
</script>

 

前端 asyncData 數據統一調用

在服務端 asyncData 調用時,能夠解決單頁面 SSR 刷新問題,那直接在前端切換路由時因不走服務端路由,那數據如何處理?


在 Vue 單頁面實現時,一般都會使用 Vue-Router,這個時候能夠藉助 Vue-Router 提供 afterEach 鉤子進行統一數據請求,能夠直接調用 Vue 模板定義的 asyncData 方法。代碼以下:ios

const options = this.create(window.__INITIAL_STATE__);
const { router, store } = options;
router.beforeEach((route, redirec, next) => {
  next();
});
router.afterEach((route, redirec) => {
  if (route.matched && route.matched.length) {
    const asyncData = route.matched[0].components.default.asyncData;
    if (asyncData) {
      asyncData(store);
    }
  }
});

最後貼上能夠用的完整代碼,請根據實際須要進行修改, 實際可運行例子見 https://github.com/easy-team/egg-vue-webpack-boilerplate/tree/feature/green/spa git

  • Vue 頁面初始化統一封裝
import Vue from 'vue';
import { sync } from 'vuex-router-sync';
import './vue/filter';
import './vue/directive';

export default class App {
  constructor(config) {
    this.config = config;
  }

  bootstrap() {
    if (EASY_ENV_IS_NODE) {
      return this.server();
    }
    return this.client();
  }

  create(initState) {
    const { index, options, createStore, createRouter } = this.config;
    const store = createStore(initState);
    const router = createRouter();
    sync(store, router);
    return {
      ...index,
      ...options,
      router,
      store
    };
  }

  client() {
    Vue.prototype.$http = require('axios');
    const options = this.create(window.__INITIAL_STATE__);
    const { router, store } = options;
    router.beforeEach((route, redirec, next) => {
      next();
    });
    router.afterEach((route, redirec) => {
      console.log('>>afterEach', route);
      if (route.matched && route.matched.length) {
        const asyncData = route.matched[0].components.default.asyncData;
        if (asyncData) {
          asyncData(store);
        }
      }
    });
    const app = new Vue(options);
    const root = document.getElementById('app');
    const hydrate = root.childNodes.length > 0;
    app.$mount('#app', hydrate);
    return app;
  }

  server() {
    return context => {
      const options = this.create(context.state);
      const { store, router } = options;
      router.push(context.state.url);
      return new Promise((resolve, reject) => {
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents();
          if (!matchedComponents) {
            return reject({ code: '404' });
          }
          return Promise.all(
            matchedComponents.map(component => {
              if (component.asyncData) {
                return component.asyncData(store);
              }
              return null;
            })
          ).then(() => {
            context.state = {
              ...store.state,
              ...context.state
            };
            return resolve(new Vue(options));
          });
        });
      });
    };
  }
}
  • 頁面入口代碼
// index.js
'use strict';
import App from 'framework/app.js';
import index from './index.vue';
import createStore from './store';
import createRouter from './router';

const options = { base: '/' };

export default new App({
  index,
  options,
  createStore,
  createRouter,
}).bootstrap();
  • 前端 router / store 定義
// store/index.js

'use strict';
import Vue from 'vue';
import Vuex from 'vuex';

import actions from './actions';
import getters from './getters';
import mutations from './mutations';

Vue.use(Vuex);

export default function createStore(initState = {}) {

  const state = {
    articleList: [],
    article: {},
    ...initState
  };

  return new Vuex.Store({
    state,
    actions,
    getters,
    mutations
  });
}

// router/index.js

import Vue from 'vue';

import VueRouter from 'vue-router';

import ListView from './list';

Vue.use(VueRouter);

export default function createRouter() {
  return new VueRouter({
    mode: 'history',
    base: '/',
    routes: [
      {
        path: '/',
        component: ListView
      },
      {
        path: '/list',
        component: ListView
      },
      {
        path: '/detail/:id',
        component: () => import('./detail')
      }
    ]
  });
}
相關文章
相關標籤/搜索