- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
createSharedComposable是 VueUse 提供的 State 类工具函数,它能把任意一个普通 composable 包装成"多组件共享"版本:同一个 Vue 实例(App)内的所有调用方复用同一份状态,且只注册一次副作用(如事件监听器)。在 airi 这个基于 Vue 3 + Vite 构建的多端 monorepo(Web / Capacitor / Electron)中,它非常适合用于鼠标位置、视口尺寸、媒体查询等"全局性、只应初始化一次"的状态,同时其 SSR 自动降级机制又保证了服务端渲染场景下的请求隔离。读完本文,你将掌握createSharedComposable的用法、底层行为边界、与createGlobalState/createInjectionState的选型差异,以及它在 airi 仓库中的真实应用切入点。
为什么需要 createSharedComposable
在 Vue 中,一个 composable 被多个组件调用时,通常意味着每个调用方都会各自执行一遍函数体。对于像useMouse、useWindowSize、useMediaQuery这类依赖全局事件监听器的 composable,这会造成重复监听、资源浪费,而且各组件拿到的ref是不同实例,状态无法互通。
createSharedComposable解决的就是这个问题:它接收一个 composable 函数,返回一个新的共享版本。首次调用时执行真正的函数体并缓存返回值,后续所有调用方直接拿到同一份缓存结果:
import { createSharedComposable, useMouse } from '@vueuse/core' const useSharedMouse = createSharedComposable(useMouse) // CompA.vue const { x, y } = useSharedMouse() // CompB.vue - will reuse the previous state and no new event listeners will be registered const { x, y } = useSharedMouse()如官方注释所述(见 .agents/skills/vueuse-functions/references/createSharedComposable.md),CompB.vue中的调用会复用CompA.vue已有的状态,并且不会再注册新的事件监听器——这是它和普通 composable 最本质的区别。
SSR 环境下的自动降级(重要边界)
createSharedComposable在文档中有一条醒目的警告(WARNING),这是使用前必须理解的行为:
当在SSR环境中使用时,
createSharedComposable会自动回退到非共享版本。这意味着在 SSR 中,每次调用都会创建全新的实例,以避免跨请求状态污染。
这条设计的价值在于:服务端渲染时同一个 App 模块可能被多个并发请求复用,若 composable 状态缓存在模块级作用域,请求 A 写入的状态就可能泄漏给请求 B。自动回退让 SSR 下每次调用都是干净的独立实例,保证请求间隔离;而客户端渲染(CSR)下则维持共享语义。这也意味着共享能力只保证在同一个运行时实例内成立,不要把createSharedComposable当作跨页面、跨 App 的持久化存储手段。
与其他 State 类工具的选型
在 airi 仓库的技能库 .agents/skills/vueuse-functions/SKILL.md 中,createSharedComposable被归入State类别,并与下面两个函数并列,三者共同构成 VueUse 的"全局状态三件套",但适用场景不同:
| 函数 | 核心语义 | 典型场景 |
|---|---|---|
createSharedComposable | 让任意 composable 在多个调用方间共享状态与副作用 | 全局传感器类 composable(鼠标、视口、媒体查询) |
createGlobalState | 把状态提升到全局作用域,跨 Vue 实例复用 | 跨多个 Vue app 实例的 store(内存版 / localStorage 持久化版) |
createInjectionState | 通过 provide / inject 在组件树内传递状态 | 需要显式 Provider 边界、支持默认值与注入 key 的 store |
与 createGlobalState 的对比
createGlobalState的定位是"把状态保持在全局作用域,使其可跨 Vue 实例复用",它把状态存放在模块级全局变量中,不绑定任何组件实例的生命周期(见 .agents/skills/vueuse-functions/references/createGlobalState.md):
// store.ts import { createGlobalState } from '@vueuse/core' import { computed, shallowRef } from 'vue' export const useGlobalState = createGlobalState( () => { // state const count = shallowRef(0) // getters const doubleCount = computed(() => count.value * 2) // actions function increment() { count.value++ } return { count, doubleCount, increment } } )它还可以直接与useStorage组合,实现 localStorage 持久化:
// store.ts import { createGlobalState, useStorage } from '@vueuse/core' export const useGlobalState = createGlobalState( () => useStorage('vueuse-local-storage', 'initialValue'), )而createSharedComposable走的是"缓存调用结果"的路线,对带副作用、需要随组件作用域清理的 composable 更友好,它不会自己维护持久化,职责更单一。
与 createInjectionState 的对比
createInjectionState则面向组件树内的依赖注入:它返回[useProvideXxx, useXxx]两个函数,前者在 Provider 组件中创建并提供状态,后者在子树组件中注入(见 .agents/skills/vueuse-functions/references/createInjectionState.md)。它还支持自定义injectionKey和defaultValue,以控制未提供状态时的行为。当你需要"状态只在特定子树内共享、且能显式控制作用域边界"时,应优先选择createInjectionState;而"全 App 无差别共享一次"则用createSharedComposable即可。
在 airi 仓库中的实际应用切入点
airi 是一个同时面向 Web、Electron(stage-tamagotchi)与移动端(stage-pocket)的 Vue 3 多包仓库,各应用与 UI 包都通过 pnpm catalog 统一依赖 VueUse(见 pnpm-workspace.yaml:@vueuse/core: ^14.4.0、@vueuse/shared: ^14.4.0)。这意味着技能库中描述的createSharedComposable能力在整个仓库内开箱即用。
仓库中对useMouse的典型调用出现在 apps/stage-web/src/pages/index.vue 与 apps/stage-web/src/pages/index.vue#L78:
import { breakpointsTailwind, useBreakpoints, useMouse } from '@vueuse/core' const { x: mouseX, y: mouseY } = useMouse() const cursorPosition = computed(() => ({ x: mouseX.value, y: mouseY.value, }))这里的useMouse会在每次组件挂载时向window注册mousemove(以及可选的touchmove、scroll)监听。如果同一页面中多个组件都需要鼠标坐标(例如交互区域、悬浮层、跟随光标的小部件),直接各自调用useMouse()就会重复注册监听器。将其包装为共享版本:
// composables/use-shared-mouse.ts import { createSharedComposable, useMouse } from '@vueuse/core' export const useSharedMouse = createSharedComposable(useMouse)之后任意组件(如stage-layouts的InteractiveArea、stage-ui的各类弹层组件)调用useSharedMouse(),都共享同一组x/yref,且全局只存在一套事件监听,副作用成本从 O(n) 降为 O(1)。
类似地,airi 的 UI 层大量使用这类"全局传感器" composable,同样是createSharedComposable的天然候选:
- packages/stage-ui/src/components/misc/profile-switcher-popover.vue 中使用
useElementBounding、useElementSize、useWindowSize; - packages/stage-ui/src/components/scenarios/dialogs/model-selector/reports/live2d/modal.vue 与 packages/stage-ui/src/components/scenarios/dialogs/model-selector/tachieReportModal.vue 中使用
useMediaQuery('(min-width: 768px)')判断桌面端; - packages/stage-ui/src/components/scenarios/chat/components/voice-composer.vue 中使用
useWindowSize、useLocalStorage、useNow、onLongPress等。
例如useMediaQuery('(min-width: 768px)')会在多个弹窗与页面中反复出现,每次调用都会创建一个新的MediaQueryList监听;若用createSharedComposable(useMediaQuery)包装,所有组件共用同一个查询结果与监听,同时由于useMediaQuery内部使用 Vue 的响应式作用域管理,共享后生命周期仍受首个调用方的组件作用域约束(在 SPA 中该组件通常常驻,共享实例自然存活到 App 卸载)。
值得说明的是:airi 仓库自身的业务状态层大量采用 Pinia store(例如 apps/stage-web/src/pages/index.vue 中的useBackgroundStore、useSettings、useVoiceStore配合storeToRefs使用)。Pinia 本身已具备"单例 store + 跨组件共享"的能力,因此createSharedComposable更适合的是无状态管理需求的传感器/浏览器 API 类 composable,而不是替代 Pinia 管理业务状态。
类型声明与使用约束
根据原文档的 Type Declarations(见 .agents/skills/vueuse-functions/references/createSharedComposable.md),其签名非常简洁:
export type SharedComposableReturn<T extends AnyFn = AnyFn> = T /** * Make a composable function usable with multiple Vue instances. * * @see https://vueuse.org/createSharedComposable * * @__NO_SIDE_EFFECTS__ */ export declare function createSharedComposable<Fn extends AnyFn>( composable: Fn, ): SharedComposableReturn<Fn>几个值得注意的约束点:
- 返回值类型与被包装函数完全一致:
SharedComposableReturn<Fn> = T,即返回的函数类型就是传入的 composable 类型,调用方无需任何类型断言,解构出的ref、computed类型与直接调用原函数完全等价。 @__NO_SIDE_EFFECTS__注解:该注解用于帮助打包器(如 Vite/Rollup 的 tree-shaking)识别函数无模块级副作用,仅在调用时产生效果,从而允许在未使用时被安全摇树移除。- 泛型约束
AnyFn:入参必须是函数;AnyFn在 VueUse 中定义为接受任意参数、返回任意值的函数类型,因此无论是() => {...}还是带参 composable 均可包装。 - 被包装函数内部需要依赖 Vue 的响应式作用域:从 VueUse 的实现逻辑可以推断,共享实例的创建依赖首次调用时的作用域(如组件
setup或effectScope),因此共享的生命周期与首个调用方绑定;若在首个调用方卸载前其他组件仍在引用,缓存依然有效,但彻底释放仍取决于首次调用方的作用域清理。
最佳实践与注意事项
综合原文档警告、类型声明与 airi 仓库的工程实践,在使用createSharedComposable时建议遵循以下准则:
- 只包装"全局唯一语义"的 composable:鼠标坐标、视口尺寸、断点、网络状态、在线状态等本就属于全局信息的函数适合共享;而依赖组件 props、局部 DOM ref 的 composable 不应共享。
- 共享的是状态与副作用,不是隔离边界:所有调用方读写的是同一组
ref,任何一方修改都会影响全局,需要确认这种语义符合业务预期。 - SSR 下不要依赖共享语义:服务端渲染时它会自动降级为每次新建实例,因此不要把"跨调用方共享"作为 SSR 环境下的硬性假设,跨请求状态应交给请求级上下文或数据库/缓存层。
- 与 Pinia 的分工:airi 中业务状态(背景、设置、语音等)由 Pinia 管理;
createSharedComposable应聚焦于浏览器 API 与传感器类 composable,避免与 store 层职责重叠。 - 优先用共享版本替代裸调用:在 .agents/skills/vueuse-functions/SKILL.md 中,
createSharedComposable的 Invocation 规则为AUTO——只要场景适用(多个组件需要同一全局传感器状态),应当优先采用共享封装而非逐个裸调useMouse/useMediaQuery等,以减少重复监听、提升运行时性能与代码可维护性。
简而言之,createSharedComposable是 VueUse 在"状态共享"维度上最轻量的一把钥匙:不需要引入 store、不需要设计 Provider 树,只需一行包装即可让任意 composable 具备全 App 级去重共享能力;理解它的 SSR 降级与作用域绑定语义后,就能在 airi 这类多端 Vue 仓库中安全、高效地使用它。
- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
相关推荐
VueUse createSharedComposable 源码解析:让组合式函数跨组件、跨 Vue 实例共享状态
VueUse createSharedComposable 源码解析:让组合式函数跨组件、跨 Vue 实例共享状态 createSharedComposable
前端airi 前端状态共享实践:深入 VueUse createSharedComposable 与多 Vue 实例下的组合式函数复用
airi 前端状态共享实践:深入 VueUse createSharedComposable 与多 Vue 实例下的组合式函数复用 在 airi 这样一个同时包
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染airi 项目中的 useArrayUnique 实战指南:VueUse 响应式数组去重 Composable 的用法与原理
airi 项目中的 useArrayUnique 实战指南:VueUse 响应式数组去重 Composable 的用法与原理 本篇技术指南聚焦 VueUse 的
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考