Egg Vue SSR 服务端渲染数据请求与 asyncData 方案
本文详解 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')
}
]
});
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
前端大屏自适应方案:缩放与视口单位对比
2026-10-31 18:19:27
前端大文件分片下载实现:基于localForage与Vue的完整方案
2026-10-31 16:11:47
Vue3 中使用 VueUse 实现指定区域全屏阅读功能
2026-10-31 13:55:05
基于Vue与Pinia实现页面Loading动画及请求拦截处理
2026-10-31 13:54:42
Vue封装Echarts通用组件:Vue2/Vue3 JS/TS三种模板及常用配置事件详解
2026-10-31 11:38:03
