小心!Pinia 可能已变成第二个 localStorage

作者:技术拾光

时间:2026-06-02

浏览:299

# 一段你大概率写过的代码 写的时候挺顺手的,对吧?但三个月后你盯着那堆字段,一个都不敢删——因为根本不知道谁在依赖它们。半年后页面已经重写了新版,这个 store 还稳稳地躺在项目里,唯一的原因是:**你也不确定它是不是真的没人用了**。 这还真不是 Pinia 的锅。这其实是一个结构性的老问题—

# 一段你大概率写过的代码 写的时候挺顺手的,对吧?但三个月后你盯着那堆字段,一个都不敢删——因为根本不知道谁在依赖它们。半年后页面已经重写了新版,这个 store 还稳稳地躺在项目里,唯一的原因是:**你也不确定它是不是真的没人用了**。 小心!Pinia 可能已变成第二个 localStorage 这还真不是 Pinia 的锅。这其实是一个结构性的老问题——**复杂页面的状态,根本就没有一个明确的归属**。 --- ## 前端状态的三层分布
┌─────────────────────────────────────────────────┐
│  应用级状态                                      │
│  用户信息 / 权限 / 主题 / 路由                  │  ← Pinia
│  生命周期:跟应用一起活,通常不销毁              │
├─────────────────────────────────────────────────┤
│  页面级状态                                      │
│  筛选条件 / 表格分页 / 弹窗 / 轮询 / 草稿        │  ← Page Scope
│  生命周期:跟页面可见性走,离开/销毁时回收        │      长期被忽视的中间层
├─────────────────────────────────────────────────┤
│  组件级状态                                      │
│  输入框 / UI 局部态 / 私有交互                  │  ← ref / reactive
│  生命周期:跟组件实例走                          │
└─────────────────────────────────────────────────┘
中间这一层长期被忽视。它规模太大,塞不回组件;又不够全局,不该污染 Pinia。于是它常年处于无家可归的尴尬境地——要么被塞进 Pinia 变成第二个 localStorage,要么分散在一堆 `ref` 里靠 `provide/inject` 互相摸黑握手,要么写一堆 `watch + onBeforeUnmount` 勉强拼凑出一个手工版的“页面作用域”。 --- ## 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)
页面进入时创建,页面运行时承载所有副作用,页面离开时一次性回收。**`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. 页面组件里使用



### 3. 子组件不需要 import




子组件**不需要知道父页面用的是哪个 scope 定义**。统一 `injectPageScope()`,零耦合。 --- ## 几个能直接看出价值的细节 ### Auto Bridge:$route/$router 不用手动传 Vue 2 时代 `vue-page-store` 通过 `$vm` 隐式持有组件实例,scope 内可以 `this.$vm.$route.query`。Vue 3 没有这个传统,如果硬要保留体验,会写成这样:
//  用户每个页面都要这么写
const orderScope = useOrderScope({
  $route: useRoute(),
  $router: useRouter(),
})
这个方案团队考虑过,然后否决了——**框架的复杂度该由框架吃,不是用户**。所以 `vue-page-scope` 默认行为是:
//  直接这样写
const orderScope = useOrderScope()

// scope 内部 enter / actions / watch 直接用
this.$route.query
this.$router.push('/...')
实现细节:框架内部通过 `getCurrentInstance().proxy.$route` 桥接,**不 import vue-router**,装了就用,没装就 noop。如果你用的是微前端或自研路由,还能显式覆盖:
const orderScope = useOrderScope({
  $route: microAppRoute,    // 显式注入优先级高于 auto bridge
  $router: microAppRouter,
  $user: useUserStore(),    // 也可以注入任意 composables
})
**常用门,框架自己开;特殊门,用户再给钥匙。** ### $loading 自动追踪
actions: {
  async search() {
    const res = await api.getOrders(...)
    this.$source.response = res
  }
}

  搜索

不需要包装器,不需要手写 `loading.value = true / finally { loading.value = false }`。返回 Promise 的 action 自动追踪,带并发计数,防止先返回的请求提前关 loading。 ### 一次销毁,全部回收 页面销毁(或路由切走的 keep-alive 解绑)时:
effectScope.stop()
   ↓
所有 watch / computed 释放
$setInterval 清理
plugin destroy 钩子触发
事件总线清空
**这才是最值钱的部分**。不需要在 `onBeforeUnmount` 里手动 `clearInterval` / 手动 `stopWatch()` / 手动 `unsubscribe()`,你只要把副作用注册进 scope,scope 销毁就替你一并清理。 异步安全也是默认的:scope 销毁后 async action 还在 pending,await 完赋值不会崩溃,也不会触发任何渲染——因为 watcher 已经全部 stop。 --- ## 为什么用 effectScope Vue 3.2 加的 `effectScope` 是为这种“作用域响应式容器”专门设计的:
const scope = effectScope(true)  // detached,不被父 scope 收编

scope.run(() => {
  const state = reactive({ keyword: '' })
  watch(() => state.keyword, () => { /* ... */ })
})

scope.stop()  // 所有 watch / computed 一行释放
Pinia 内部用的也是 `effectScope`,只是它的 scope 跟 Pinia 实例走,不跟页面走。 `vue-page-scope` 做的事很简单:**把这个能力提升到页面维度**。每个 page scope 是一个独立的 `effectScope(true)`,在 setup 内创建,通过 `onBeforeUnmount` 统一释放。 --- ## 它和 Pinia 是什么关系 不是替代关系,是分层关系。
Pinia          → 应用级(用户、权限、主题、跨页面共享)
Page Scope     → 页面级(筛选、表格、弹窗、轮询、草稿)
ref / reactive → 组件级(输入框、局部 UI 态)
复杂后台项目里,把这三层混在一起是常见状况;把它们分清楚是治理的开始。 --- ## 演进:从 vue-page-store 到 vue-page-scope `vue-page-scope` 不是凭空冒出来的。它的前身是 2025 年开始写、迭代到 v0.5 的 `vue-page-store`(Vue 2)。 写到 v0.5 的时候才意识到:这个库一直在做的事情并不是“管理状态”,而是在管理一个**完整的页面作用域**——state / source / getters / actions / watch / 生命周期 / 定时器 / 事件总线 / plugin,全部绑定在页面可见性生命周期上。 Vue 3 重新实现的时候,把这个发现显性化,起了新名字:`vue-page-scope`。
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
**叙事同步,版本不同步。根接上,身份证重新办**。所以 `vue-page-scope` 从 `0.1.0` 起步,不续编 `vue-page-store` 的 v0.5。 --- ## 当前进展 `vue-page-scope@0.1.0` 已发布,包含: - `definePageScope` / `useXxxScope` / `injectPageScope` 完整核心 API - Auto bridge + explicit injection 两层路由集成 - Plugin 协议(跨 Vue 2 / Vue 3 一致) - TypeScript 类型(支持注入字段精确类型推导) - ESM + CJS 双产物 - keep-alive 双响炮去重、init 抛错自毁、stale write 异步安全
npm install vue-page-scope
import { definePageScope, injectPageScope } from 'vue-page-scope'
--- ## 写在最后 这个库不大。如果你的项目里没有复杂后台、没有 keep-alive、没有 5+ 弹窗共存、没有多 tab 配置页——你不需要它。Pinia + 组件 state 完全够用。 但如果你正在维护一个**字段越塞越多、不敢删任何东西、页面已经销毁但定时器还在 console 里刷的 Pinia store**——它可能是你要找的中间层。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章