Vue.js:将new Vue().$mount()改造为可导出结构适配服务端渲染
改造Vue应用为SSR模式的最佳实践
我来帮你梳理下把现有Vue应用改成SSR的合理方案,核心思路是服务端不能复用单例实例(否则会导致跨请求的状态污染),所以我们需要把创建Vue实例、路由、Vuex Store的逻辑都封装成工厂函数,具体步骤如下:
一、先改造路由和Vuex Store为工厂函数
首先得把原来的单例路由和Store改成可复用的工厂函数,避免服务端请求之间的状态串扰:
1.1 重构路由文件(router/index.js)
原来的代码可能是直接创建并导出Router实例,现在改成导出一个创建路由的函数:
import Vue from 'vue' import Router from 'vue-router' import Home from '../views/Home.vue' Vue.use(Router) // 每次调用都返回全新的Router实例 export function createRouter() { return new Router({ mode: 'history', // 服务端部署时需要配置对应的history模式支持(比如Nginx转发) routes: [ { path: '/', name: 'Home', component: Home }, // 其他路由配置... ] }) }
1.2 重构Vuex Store文件(vuex/index.js)
同理,把单例Store改成工厂函数:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // 每次调用都返回全新的Store实例 export function createStore() { return new Vuex.Store({ state: { /* 你的状态定义 */ }, mutations: { /* 你的mutations */ }, actions: { /* 你的actions */ }, modules: { /* 你的模块 */ } }) }
二、重构main.js为可复用的createApp工厂函数
现在把原来直接创建Vue实例的代码,改成导出一个创建App的函数,每次调用都会生成全新的实例、路由和Store:
import Vue from 'vue'; import AppLayout from './theme/Layout.vue'; import { createRouter } from '../router'; import { createStore } from './vuex/index'; Vue.config.productionTip = false; // 导出核心工厂函数,返回包含app、router、store的对象 export function createApp() { const router = createRouter(); const store = createStore(); // 用render函数渲染根组件,这是SSR推荐的写法(替代原来的...AppLayout) const app = new Vue({ router, store, render: h => h(AppLayout) }); return { app, router, store }; }
说明:原来的
new Vue({ router, ...AppLayout, store })是把AppLayout的选项合并到Vue实例中,虽然客户端可行,但SSR中用render: h => h(AppLayout)更规范,因为.vue文件的默认导出是组件选项,通过h函数渲染更符合Vue的组件化逻辑。
三、编写客户端入口文件(entry-client.js)
把原来main.js中的挂载逻辑移到这个文件,专门处理客户端的初始化:
import { createApp } from './main'; const { app, router, store } = createApp(); // 如果服务端预取了状态,同步到客户端Store if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); } // 等待路由就绪后再挂载,避免路由跳转异常 router.onReady(() => { app.$mount('#app'); });
四、编写服务端入口文件(server-entry.js)
这个文件是服务端渲染的入口,需要导出一个函数,接收请求上下文并返回Vue实例:
import { createApp } from './main'; // 服务端入口函数,接收请求上下文(包含url、req、res等信息) export default function(context) { return new Promise((resolve, reject) => { const { app, router, store } = createApp(); // 设置当前请求的路由路径 router.push(context.url); // 等待路由就绪,确保所有异步组件和路由钩子执行完成 router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // 没有匹配到路由时返回404 if (!matchedComponents.length) { return reject({ code: 404 }); } // 调用匹配组件的asyncData方法(如果有的话),预取页面数据 Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }); } })).then(() => { // 把预取的状态存入上下文,服务端渲染时会注入到HTML中 context.state = store.state; resolve(app); }).catch(reject); }, reject); }); }
关键注意点
- 为什么必须用工厂函数?:服务端是多用户并发环境,单例的Vue实例、路由或Store会导致不同请求之间的状态污染,所以必须为每个请求创建全新的实例。
- 异步数据预取:如果你的页面需要异步数据,要在组件中定义
asyncData方法(接收store和route参数),服务端会在渲染前调用这些方法预取数据,然后同步到客户端。
内容的提问来源于stack exchange,提问作者Boris K
相关产品推荐
相关产品推荐

