在 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 }); }
在 Vue 單頁面 SSR 時涉及數據的請求方式,Node 層獲取數據方式能夠繼續使用,但當路由切換時(頁面直接刷新),Node 層就須要根據路由獲取不一樣頁面的數據,同時還要考慮前端路由切換的狀況,這個時候路由是不會走 Node 層路由,而是直接進行的前端路由,這個時候也要考慮數據的請求方式。
基於以上使用的優雅問題,這裏提供一種 asyncData 獲取數據的方式解決單頁面 SSR 刷新不走 SSR 問題。 Node 不直接獲取數據,獲取數據的代碼直接寫到前端代碼裏面。這裏須要解決以下兩個問題:
vue
這裏根據路由切換 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)); }); }); });
前端經過 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 調用時,能夠解決單頁面 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
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();
// 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') } ] }); }