Pinia在你的项目中可能已沦为第二个localStorage

作者:袖梨 2026-06-02

前端开发中,你是否遇到过这样的困境:随着业务迭代,Pinia store逐渐变成了第二个localStorage?本文将揭示状态管理的三层架构,并介绍Page Scope这一创新解决方案。


一段你大概率写过的代码

// 某个后台业务页面的 Pinia store
export const useOrderListStore = defineStore('orderList', {
  state: () => ({
    keyword: '',
    page: 1,
    pageSize: 20,
    selectedIds: [],
    deleteDialogVisible: false,
    deleteConfirmLoading: false,
    detailDrawerVisible: false,
    currentDetailId: null,
    columnsConfig: [],
    tempEditDraft: null,
    pollTimer: null,           //  这条尤其刺眼
    lastFetchedAt: null,
    activeTab: 'basic',
  }),
})

开发时看似合理,三个月后却不敢删除任何字段,半年后页面重构但store仍保留,因为无法确认是否还有组件依赖这些状态。

你项目里的 Pinia,可能已经成了第二个 localStorage

这并非Pinia的缺陷,而是前端状态管理的结构性问题:复杂页面的状态缺乏明确归属。


前端状态的三层分布

┌─────────────────────────────────────────────────┐
│  应用级状态                                      │
│  用户信息 / 权限 / 主题 / 路由                  │  ← Pinia
│  生命周期:跟应用一起活,通常不销毁              │
├─────────────────────────────────────────────────┤
│  页面级状态                                      │
│  筛选条件 / 表格分页 / 弹窗 / 轮询 / 草稿        │  ← Page Scope
│  生命周期:跟页面可见性走,离开/销毁时回收        │      长期被忽视的中间层
├─────────────────────────────────────────────────┤
│  组件级状态                                      │
│  输入框 / UI 局部态 / 私有交互                  │  ← ref / reactive
│  生命周期:跟组件实例走                          │
└─────────────────────────────────────────────────┘

页面级状态常被忽视:它规模过大不适合组件,又不具备全局性不应污染Pinia。于是这些状态往往无家可归,要么被塞进Pinia变成持久化存储,要么通过ref和provide/inject勉强传递,要么手动实现页面作用域。


Page Scope 是什么

核心概念:为复杂页面创建隔离的响应式容器。

                  ┌────────────────────────┐
                  │   Page Component       │
                  │   (Owner: setup 内)    │
                  └───────────┬────────────┘
                              │
                              │ useOrderScope()
                              ▼
       ┌───────────────────────────────────────────┐
       │     PageScope  (基于 Vue 3 effectScope)    │
       │                                            │
       │   source        state        getters       │
       │   actions       watch        $loading      │$setInterval  event bus    $route 桥接   │
       │   plugins (任意外部扩展)                   │
       │                                            │
       └─────────────────────┬─────────────────────┘
                             │
                             │ 页面离开 / 销毁
                             ▼
                  effectScope.stop()
              ↓ 自动回收所有响应式副作用 ↓
        (watch / computed / $setInterval / plugin watchers)

Page Scope随页面创建而初始化,运行期间管理所有副作用,页面销毁时通过effectScope.stop()自动清理所有响应式依赖。


真实代码长什么样

1. 定义一个 Page Scope

// scopes/order-list.ts
import { definePageScope } from 'vue-page-scope'export const useOrderScope = definePageScope('orderList', {
  // 页面输入 / 接口原始返回
  source: () => ({
    response: null,
    query: {},
  }),  // 业务状态
  state: () => ({
    keyword: '',
    page: 1,
    selectedIds: [],
    deleteDialogVisible: false,
  }),  // 派生计算
  getters: {
    list()  { return this.$source.response?.list || [] },
    total() { return this.$source.response?.total || 0 },
    hasSelection() { return this.selectedIds.length > 0 },
  },  // 业务方法 —— 返回 Promise 的 action 自动追踪 $loading
  actions: {
    async search() {
      const res = await api.getOrders({
        keyword: this.keyword,
        page: this.page,
      })
      this.$source.response = res
    },
  },  // 一次性初始化(拉字典、注册等)
  init() {
    this.loadDictOptions()
  },  // 每次页面可见时执行(keep-alive 切回也会)
  enter() {
    this.$source.query = this.$route.query   // ← 直接用 $route,见下文 auto bridge
    this.search()
    this.$setInterval(() => this.search(), 5000)  // ← 页面级定时器
  },  // 离开时 $setInterval 自动清,通常不需要写
  leave() {},
})

2. 页面组件里使用

<script setup>
import { useOrderScope } from '../scopes/order-list'// 必须在 setup 内调用,该组件成为 scope 的 owner
// 不需要传 $route / $router —— 框架自动桥接
const orderScope = useOrderScope()
script><template>
  <input v-model="orderScope.keyword" />
  <button
    :loading="orderScope.$loading.search"
    @click="orderScope.search"
  >
    搜索
  button>
  <p>共 {{ orderScope.total }} 条p>
template>

3. 子组件不需要 import


<script setup>
import { injectPageScope } from 'vue-page-scope'const scope = injectPageScope()  // ← 自动拿到父级页面的 scope
script><template>
  <input v-model="scope.keyword" />
template>

子组件通过injectPageScope()自动获取父级页面scope,无需耦合具体scope实现。


几个能直接看出价值的细节

Auto Bridge:route/route / route/router 不用手动传

传统方案需要手动注入路由实例,而Page Scope通过auto bridge自动集成路由系统:

const orderScope = useOrderScope({
  $route: microAppRoute,    // 显式注入优先级高于 auto bridge
  $router: microAppRouter,
  $user: useUserStore(),    // 也可以注入任意 composables
})

$loading 自动追踪

异步action自动追踪loading状态,无需手动维护:

"scope.$loading.search" @click="scope.search">
  搜索

一次销毁,全部回收

页面销毁时自动清理所有副作用:

effectScope.stop()
   ↓
所有 watch / computed 释放
$setInterval 清理
plugin destroy 钩子触发
事件总线清空

为什么用 effectScope

Vue 3.2引入的effectScope专为管理响应式作用域设计:

const scope = effectScope(true)  // detached,不被父 scope 收编scope.run(() => {
  const state = reactive({ keyword: '' })
  watch(() => state.keyword, () => { /* ... */ })
})scope.stop()  // 所有 watch / computed 一行释放

Page Scope将这一能力提升到页面维度,每个scope都是独立的effectScope实例。


它和 Pinia 是什么关系

两者互补而非替代:

Pinia          → 应用级(用户、权限、主题、跨页面共享)
Page Scope     → 页面级(筛选、表格、弹窗、轮询、草稿)
ref / reactive → 组件级(输入框、局部 UI 态)

清晰划分这三层是复杂项目状态治理的关键。


演进:从 vue-page-store 到 vue-page-scope

从Vue 2时代的vue-page-store演进而来,核心改进:

vue-page-store (Vue 2)           vue-page-scope (Vue 3)
──────────────────────           ───────────────────────
"页面级 Store"                   "页面级 Scope"
hidden Vue instance              effectScope(true)
hook:mounted 黑魔法              setup lifecycle hooks
bindTo(vm) 显式绑定              owner 模型自动绑定
$vm 逃生口                       auto bridge + injection

当前进展

[email protected]已发布,主要特性:

  1. 完整的核心API体系
  2. 自动路由桥接与显式注入
  3. 跨版本插件协议
  4. 完善的TypeScript支持
  5. 双模块格式支持
  6. keep-alive优化与错误处理
npm install vue-page-scope
import { definePageScope, injectPageScope } from 'vue-page-scope'

Page Scope为复杂前端应用提供了优雅的状态管理方案,特别适合具有多弹窗、keep-alive等复杂交互场景的项目。通过明确的状态分层,它能有效解决Pinia store膨胀和状态泄漏问题,让前端架构更加清晰可维护。

相关文章

精彩推荐