普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月14日首页

在 Vue 2.6 微前端架构中,我们为什么放弃了 Vuex 管理页面状态?

2026年3月14日 13:01

在 Vue 2.6 微前端架构中,我们为什么放弃了 Vuex 管理页面状态

背景:一个越来越"重"的页面

我们团队用 single-spa 搭了一套微前端架构,主技术栈是 Vue 2.6 + Element UI。系统里有不少复杂页面——转化漏斗分析详情、行为数据分析仪表盘、事件流程分析……这类页面的共同特点是:

  • 组件层级深,一个页面拆成 10+ 子组件很常见
  • 组件间通信频繁,筛选条件变了、Tab 切了、日期选了,好几个组件要同步响应
  • 状态生命周期跟页面走,进来要初始化,离开要清干净

一开始我们用 Vuex 管这些状态,很快就发现不对劲。

Vuex 管页面状态,哪里不对?

第一个问题:状态残留。 用户从漏斗详情页跳到事件分析页,再跳回来,Vuex 里上一次的筛选条件还在。你说用 beforeDestroy 里手动 reset?可以,但每个页面都要写一遍,写漏了就是 bug。

第二个问题:命名空间膨胀。 每个复杂页面一个 Vuex module,funnelDetail/setFilterseventAnalysis/setFiltersbehaviorDashboard/setFilters……全局 store 越来越臃肿,而这些 module 99% 的时间都不需要存在。

第三个问题:Vuex 的仪式感太重。 改一个状态要经过 commit → mutation → state,对于页面内部的交互状态来说,这个链路完全多余。筛选条件变了就该直接改,不需要走 mutation 审计。

我们试过的其他方案

provide / inject——只能传数据,不能传事件。组件 A 想通知组件 B "筛选变了,你该刷新了",provide/inject 做不到。

全局 EventBus($micRootBus ——我们微前端里有一个全局事件总线。但拿它做页面内通信,三个致命问题:

// 1. 命名冲突:漏斗详情和事件分析都有 filter:change
this.$micRootBus.$emit('filter:change', filters) // 谁的 filter?

// 2. 内存泄漏:每个 $on 都要手动 $off,页面销毁时漏一个就泄漏
beforeDestroy() {
  this.$micRootBus.$off('funnelDetail:filter:change', this.handler1)
  this.$micRootBus.$off('funnelDetail:tab:change', this.handler2)
  this.$micRootBus.$off('funnelDetail:date:change', this.handler3)
  // ... 8 个地方全要清,漏一个就寄
}

// 3. 边界模糊:事件扩散到全局,debug 时不知道谁在监听

组件 data + props 层层传递——5 层组件传一个筛选条件,中间 3 层只是当传话筒。经典的 props drilling 地狱。

每个方案都差点意思。我们需要的是一个页面级别的运行时上下文——状态、通信、副作用,全部限定在当前页面的作用域里,页面销毁时一键回收。

于是我们造了 vue-page-store

核心思路很简单:用一个隐藏的 Vue 实例承载响应式 state + computed getters,再加一个闭包隔离的事件总线,生命周期绑定在一起。

npm install vue-page-store

定义一个页面级 Store

import { definePageStore } from 'vue-page-store'

export const useFunnelStore = definePageStore('funnelDetail', {
  state: () => ({
    filters: { dateRange: [], platform: '' },
    loading: false,
    funnelSteps: [],
  }),

  getters: {
    isReady() {
      return !this.loading && this.funnelSteps.length > 0
    },
  },

  actions: {
    async fetchData() {
      this.loading = true
      try {
        this.funnelSteps = await api.getFunnelSteps(this.filters)
      } finally {
        this.loading = false
      }
    },
  },
})

API 风格完全对齐 Pinia:state / getters / actions,用过 Pinia 的人零学习成本。

组件中使用

const store = useFunnelStore()

// 直接读
store.filters
store.isReady

// 直接改
store.filters = newFilters

// 调 action
store.fetchData()

// 批量更新
store.$patch({ loading: true, filters: newFilters })

没有 commit,没有 mutation,没有 mapState。直接属性访问,直接赋值。

页面内通信:作用域隔离的事件

这是 vue-page-store 和 Pinia 最大的区别。我们内置了一个页面作用域级的事件总线

// 组件 A —— 发射事件
store.$emit('filter:change', newFilters)

// 组件 B —— 监听事件
const off = store.$on('filter:change', (filters) => {
  this.applyFilters(filters)
})

重点来了: _listeners 是闭包内的私有变量,每个 store 实例独立一份。 漏斗详情的 filter:change 和事件分析的 filter:change 完全隔离,互不干扰。

为什么不拆成独立的 EventBus?因为生命周期要跟 store 绑定。$destroy 的时候自动清空所有 listeners:

store.$destroy = () => {
  // 清空事件 —— 不会泄漏
  Object.keys(_listeners).forEach(key => delete _listeners[key])
  // 销毁 Vue 实例 —— 回收 watchers
  vm.$destroy()
  // 移除注册 —— 下次进来是全新的
  storeRegistry.delete(id)
}

调用方(子组件)只需要注入 store 就能通信,不需要感知全局 Bus,不需要手动 $off,不需要加命名前缀。

页面销毁:一行代码全部回收

// 页面根组件
beforeDestroy() {
  useFunnelStore().$destroy()
}

state、getters、watchers、事件监听——全部清干净。下次进这个页面,又是一个全新的 store。

它不是 Pinia 的替代品

这一点必须说清楚。vue-page-store 解决的是 Vuex / Pinia 覆盖不到的那个中间地带:

Vuex Pinia vue-page-store
作用域 全局 全局 页面级
生命周期 应用级 应用级 页面级($destroy 回收)
事件通信 内置 emit/emit/on(作用域隔离)
Vue 2.6 支持 ⚠️ 需 @vue/composition-api ✅ 原生支持
适合管什么 用户信息、权限、全局配置 同左 复杂页面内部状态

推荐组合:Vuex 管全局,vue-page-store 管页面。 各管各的,互不干扰。

声明式 watch:页面级副作用的自动管理

除了状态和事件,页面里还有一类东西需要管理——副作用。比如"查询时间范围变了,自动判断是否按小时查询":

export const useFunnelStore = definePageStore('funnelDetail', {
  state: () => ({ /* ... */ }),

  getters: {
    isQueryByHour() {
      const range = this.filters?.dateRange
      return (new Date(range[1]) - new Date(range[0])) / 3600000 <= 24
    },
  },

  watch: {
    'isQueryByHour'(val) {
      if (!val) this.tabTime = 'hour'
    },
  },
})

声明式写法,定义的时候绑上去,$destroy 的时候跟着 Vue 实例一起销毁。不需要手动 $watch 再手动 unwatch

实现原理:100 行代码

核心实现非常简单,整个库不到 200 行,核心逻辑 100 行出头:

  1. new Vue({ data: { $$state }, computed }) —— 一个隐藏的 Vue 实例,承载响应式和 computed
  2. Object.defineProperty 代理 —— 把 state 和 getters 暴露到 store 对象上
  3. 闭包内的 _listeners 对象 —— 作用域隔离的事件总线
  4. storeRegistry Map —— 保证同一个 id 只有一个实例

没有黑魔法,没有额外依赖,gzip 后不到 3KB。

最后

如果你也在用 Vue 2.6 + 微前端架构,遇到了页面级状态管理的痛点,可以试试:

npm install vue-page-store

Vue 3 项目推荐用 Pinia,这个库专为 Vue 2.6 场景设计。

如果对你有帮助,欢迎 star ⭐️,有问题直接提 issue。

❌
❌