Egg Vue SSR 服务端渲染数据请求与 asyncData 方案

作者:小布丁时间:2026-10-24 02:54:13浏览:438

本文详解 Egg 框架下 Vue 服务端渲染(SSR)的数据获取策略。对比传统 render 传参与高阶 asyncData 模式,重点阐述如何利用 Vue-Router 钩子与 Vuex 解决单页应用刷新及前端路由切换时的数据加载问题,确保 Node 层与前端数据同步。

Egg Vue SSR 服务端渲染数据请求与 asyncData 方案

Node 服务端直接获取数据

Egg 项目使用模板引擎时,通过 render 方法渲染,其首个参数为模板路径,第二个参数为渲染数据。调用示例如下:


async index(ctx) {
  // 获取数据,可以是从数据库,后端 Http 接口 等形式
  const list = ctx.service.article.getArtilceList();
  // 对模板进行渲染,这里的 index.js 是 vue 文件通过 Webpack 构建的 JSBundle 文件
  await ctx.render('index.js', { list });
}

上述示例展示了典型的模板渲染方式,常规页面开发建议采用此方式获取数据并渲染。Node 层获取数据后,在 Vue 根文件中通过 this.list 即可获取,进而完成 Vue 模板的数据绑定。

存在一种高阶用法,可将 ctx 等 Node 对象传递至 render 的第二个参数,从而在模板中直接访问 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 方式获取数据

服务端渲染 asyncData 方式获取数据

Vue 单页面 SSR 涉及数据请求时,Node 层获取数据的方式可继续保留。但当发生路由切换(页面直接刷新)时,Node 层需根据路由获取不同页面数据,同时需兼顾前端路由切换场景。此时路由不经过 Node 层,而是直接进行前端路由,故需考虑数据请求策略。

为解决上述优雅性问题,提供一种 asyncData 获取数据的方式以解决单页面 SSR 刷新不走 SSR 的问题。Node 层不直接获取数据,而是将获取数据的代码直接写入前端代码中。此处需解决以下两个问题:

前端路由匹配 asyncData 调用

前端路由匹配 asyncData 调用

根据路由切换后的 url 获取指定的路由 component 组件,随后检查是否存在 asyncData。若存在则进行调用,调用后的数据将存入 Vuex 的 store 中。


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 方法

Vue 模板定义 asyncData 方法

前端通过 Vuex 进行数据管理,将数据统一存放于 store 中。前端可通过 this.$store.state 方式获取数据,Node 端与前端均可获取该数据。


<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 数据统一调用

在服务端调用 asyncData 时可解决单页面 SSR 刷新问题。那么在前端切换路由时,由于不经过服务端路由,数据应如何处理?

在 Vue 单页面实现中,通常使用 Vue-Router。此时可借助 Vue-Router 提供的 afterEach 钩子进行统一数据请求,直接调用 Vue 模板中定义的 asyncData 方法。代码如下:


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

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')
   }
  ]
 });
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章