news 2026/9/12 7:58:57

Vue 3 响应式能力分发:Composable、Provide/Inject 与 globalProperties 选型指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 响应式能力分发:Composable、Provide/Inject 与 globalProperties 选型指南

1. 这不是“全局挂载”,而是 Vue 3 的响应式状态与能力分发体系重构

Vue 3 不再是 Vue 2 那套简单的Vue.prototype.$xxx = xxx粗暴挂载逻辑。如果你还在用“挂载全局 API”这种说法去理解 Vue 3,那本质上你还没跳出 Vue 2 的思维惯性——这会导致你在实际项目中踩坑、性能失控、调试困难,甚至写出难以维护的“伪响应式”代码。我带过 7 个中大型 Vue 3 项目,从电商后台到工业可视化平台,凡是把app.config.globalProperties当成万能胶水来用的团队,后期无一例外都重构了两次以上。核心问题在于:Vue 3 的设计哲学是“显式依赖 + 组合式分层”,而不是“隐式全局 + 任意访问”。

所谓“全局 API”在 Vue 3 中根本不存在原生概念。你看到的app.config.globalProperties是一个兼容性兜底层,它本质是为 Vue 2 迁移者准备的“安全气囊”,而非推荐路径。真正符合 Vue 3 设计意图的方案只有两个:一是基于 Composition API 的可复用组合函数(Composable),二是基于依赖注入(Provide / Inject)的层级化能力分发。前者解决“方法复用”,后者解决“跨层级状态与能力传递”。两者不是替代关系,而是协作关系——就像螺丝刀和扳手,各司其职。

举个真实例子:我们给某新能源车企做电池监控大屏时,需要在 12 个独立图表组件中调用统一的告警推送逻辑、统一的数据格式化工具、以及共享的 WebSocket 连接实例。如果用globalProperties挂载,所有组件都会持有对同一个this.$api的引用,但这个引用本身不响应数据变化,也无法被 TypeScript 精确推导类型;而改用provide/inject+useApi()组合函数后,每个图表组件只按需消费自己关心的能力,WebSocket 实例的连接状态变更会自动触发所有订阅组件更新,且 IDE 能精准提示useFormat().toPercent(value)的参数类型。这不是炫技,是工程可控性的基本门槛。

关键词“全局自定义方法”背后的真实需求,其实是“如何让业务逻辑在多个组件间安全、可追踪、可测试地复用”。而“Provide/inject 全局变量和方法”这个表述本身就有误导性——inject 并非“全局”,它严格受限于 provide 所在的组件树层级。一个在 App.vue 里 provide 的对象,在子路由组件里能拿到,在兄弟路由组件里也能拿到,但在一个完全隔离的弹窗组件(通过 createApp 动态挂载)里就完全不可见。这种“树内可见性”恰恰是 Vue 3 有意为之的约束,目的是避免状态污染和调试黑洞。

所以,这篇文章不教你“怎么挂”,而教你“为什么不能乱挂”、“在哪该用什么机制”、“怎么写才不会三个月后自己都看不懂”。接下来我会用真实项目中的配置片段、调试截图、性能对比数据,一层层拆解这三类能力分发方式的适用边界、实操陷阱和升级路径。

2. 三种能力分发机制的本质差异与选型逻辑

Vue 3 提供了三条能力分发路径,但它们解决的问题域完全不同。很多开发者混淆它们,是因为没看清 Vue 3 的底层抽象模型:响应式系统(Reactivity)组件实例生命周期(Component Instance)依赖注入上下文(Injection Context)是三个正交维度。选错机制,轻则增加 bundle 体积,重则引发内存泄漏或响应丢失。

2.1 app.config.globalProperties:兼容层,不是主干道

这是 Vue 2 迁移的“逃生舱口”,语法最像旧习惯:

// main.ts const app = createApp(App) app.config.globalProperties.$http = axios.create({ baseURL: '/api' }) app.config.globalProperties.$format = { currency: (val: number) => `¥${val.toFixed(2)}` }

组件内使用:

<script setup> // 无法被 TypeScript 推导类型 console.log(this.$http.get('/user')) // ❌ this 在 setup() 中不可用 // 必须用 getCurrentInstance() 获取 proxy const { proxy } = getCurrentInstance() proxy?.$http.get('/user') // ⚠️ 类型丢失,且 proxy 可能为 null </script>

为什么它不该成为首选?

  • 类型系统断裂globalPropertiesany类型的开放对象,TypeScript 无法进行静态检查。我们在某金融项目中因$utils.formatDate()参数名拼写错误(formateDate),直到上线后用户投诉时间显示异常才被发现。
  • 响应式失效:挂载的普通对象不会被自动转为响应式。比如挂载{ loading: false },组件中修改this.$loading = true不会触发视图更新,必须手动reactive()包裹,但这样又破坏了挂载初衷。
  • Tree-shaking 失效:所有组件无论是否使用$http,都会强制引入整个 axios 实例,导致 vendor chunk 增大 120KB+(实测数据)。
  • 测试隔离困难:单元测试中 mock$http需要 patchapp.config.globalProperties,而不同测试用例间容易相互污染。

提示:仅在以下场景考虑使用 globalProperties:1)极小的纯工具函数(如isMobile());2)遗留 Vue 2 项目渐进式迁移;3)第三方库要求的挂载点(如 Element Plus 的ElMessage)。其他情况,请直接跳过。

2.2 Provide / Inject:树内能力分发,解决“跨层级共享”

Provide / Inject 的核心价值不是“全局”,而是“声明式依赖传递”。它让父组件明确告诉子组件:“我提供了这些能力,你需要就 inject,不需要就不提”。这解决了 props 层层透传的“中间人劫持”问题。

典型场景:表格组件需要排序、分页、导出功能,但这些能力由页面级容器提供,中间可能隔着 5 层嵌套组件。

// PageContainer.vue import { provide, ref } from 'vue' import { useTableStore } from '@/stores/table' export default { setup() { const store = useTableStore() // 提供响应式状态和方法 provide('tableContext', { data: store.data, pagination: store.pagination, sort: store.sort, exportData: store.exportData }) } }

子组件消费:

<!-- TableBody.vue --> <script setup> import { inject } from 'vue' const tableContext = inject('tableContext') // 类型安全:inject 返回值可标注类型 interface TableContext { data: Ref<any[]> pagination: Ref<{ page: number; size: number }> sort: (key: string) => void exportData: () => Promise<void> } const ctx = inject<TableContext>('tableContext') </script>

关键认知刷新:

  • Provide 的值可以是响应式对象(ref/reactive),也可以是普通值。但inject 拿到的是原始引用,不是副本。这意味着:ctx.data.value.push(item)会直接修改提供方的状态——这正是设计本意,不是 bug。
  • Provide 可以在任意组件层级调用,不限于根组件。我们常在 Layout 组件中 provide 用户权限信息,在 Sider 组件中 provide 菜单配置,在 Header 组件中 provide 搜索状态——形成多层能力切片。
  • Inject 支持默认值,避免空值报错:
    const ctx = inject('tableContext', { data: ref([]), sort: () => {} })

注意:Provide / Inject 不是状态管理替代品。它不解决跨路由、跨 tab 的状态同步,也不提供时间旅行调试能力。它的边界非常清晰:同一组件树内的、有明确父子关系的、需要解耦 props 透传的场景

2.3 Composable Functions:逻辑复用基石,解决“方法与状态封装”

这才是 Vue 3 的“第一公民”。一个标准的 composable 函数长这样:

// composables/useApi.ts import { ref, onUnmounted } from 'vue' import axios from 'axios' export function useApi() { const loading = ref(false) const error = ref<string | null>(null) const request = async <T>(config: AxiosRequestConfig) => { loading.value = true error.value = null try { const res = await axios.request<T>(config) return res.data } catch (e) { error.value = e.response?.data?.message || '请求失败' throw e } finally { loading.value = false } } // 自动清理副作用 onUnmounted(() => { // 可在此取消未完成的请求 }) return { loading, error, request } }

组件中使用:

<script setup> import { useApi } from '@/composables/useApi' const { loading, error, request } = useApi() const loadData = async () => { const data = await request({ url: '/users' }) console.log(data) } </script> <template> <button @click="loadData" :disabled="loading"> {{ loading ? '加载中...' : '加载数据' }} </button> <div v-if="error">{{ error }}</div> </template>

为什么它是首选?

  • 类型即文档:函数签名useApi(): { loading: Ref<boolean>, request: <T>(...) => Promise<T> }比任何注释都清晰。
  • 作用域隔离:每次调用useApi()都创建独立的loadingref,A 组件的 loading 状态绝不会影响 B 组件。
  • 可组合性useApi()可以内部调用useAuth()useCache(),形成能力链:
    export function useUserApi() { const { token } = useAuth() const { request } = useApi() return { getUser: (id: string) => request({ url: `/users/${id}`, headers: { Authorization: `Bearer ${token.value}` } }) } }
  • Tree-shaking 友好:未使用的 composable 不会进入最终 bundle。Webpack 分析显示,采用 composable 后,API 相关代码体积比 globalProperties 方案减少 68%。

3. 实操细节:从零搭建企业级能力分发体系

现在我们落地一个真实场景:构建一个支持多环境、可中断、带缓存的 API 调用体系,并将其能力分发到全站组件。这不是玩具 demo,而是我在某 SaaS 平台生产环境跑了一年半的方案。

3.1 环境感知的 API 工厂:动态 baseURL 与拦截器

硬编码 baseURL 是灾难源头。我们通过 Vite 环境变量注入 + 运行时检测构建灵活工厂:

// utils/apiFactory.ts import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' // 根据 NODE_ENV 和部署路径动态生成 base URL const getBaseURL = (): string => { if (import.meta.env.PROD) { // 生产环境:读取 window.__ENV__.API_BASE_URL(由 nginx 注入) if (typeof window !== 'undefined' && window.__ENV__?.API_BASE_URL) { return window.__ENV__.API_BASE_URL } // 回退到相对路径 return '/api' } // 开发环境:Vite 代理配置 return import.meta.env.VUE_APP_API_BASE_URL || 'http://localhost:3000/api' } // 创建 axios 实例工厂 export const createApiClient = (config: Partial<AxiosRequestConfig> = {}): AxiosInstance => { const instance = axios.create({ baseURL: getBaseURL(), timeout: 10000, ...config }) // 请求拦截器:添加 token instance.interceptors.request.use( (config) => { const token = localStorage.getItem('auth_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) // 响应拦截器:统一错误处理 instance.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // 清理登录态,跳转登录页 localStorage.removeItem('auth_token') window.location.href = '/login' } return Promise.reject(error) } ) return instance }

关键细节:

  • getBaseURL()优先读取运行时注入的window.__ENV__,这是为了应对同一套代码部署在多个客户环境(如customer-a.comcustomer-b.com)时,避免重新构建。Nginx 配置示例:
    location / { add_header X-Env "{'API_BASE_URL': 'https://api.customer-a.com'}"; # 在 index.html 中注入 sub_filter '<head>' '<head><script>window.__ENV__ = $sent_http_x_env;</script>'; }
  • 拦截器中不直接调用router.push(),因为 axios 是独立模块,不应强依赖 Vue Router。错误处理交给上层 composable 或组件决定。

3.2 可中断的请求 Hook:AbortController 实战

Vue 2 时代用cancelToken,Vue 3 应拥抱标准 AbortController:

// composables/useRequest.ts import { ref, onUnmounted } from 'vue' import { createApiClient } from '@/utils/apiFactory' export interface RequestOptions<T> { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: any params?: any // 是否启用 abort 控制 abortable?: boolean } export function useRequest<T>() { const loading = ref(false) const error = ref<string | null>(null) const data = ref<T | null>(null) // 存储当前请求的 AbortController let controller: AbortController | null = null const execute = async (options: RequestOptions<T>) => { loading.value = true error.value = null data.value = null // 创建新的 AbortController controller = new AbortController() try { const response = await createApiClient().request<T>({ url: options.url, method: options.method || 'GET', data: options.data, params: options.params, signal: options.abortable ? controller.signal : undefined }) data.value = response.data return response.data } catch (e) { if (e.name === 'AbortError') { console.log('请求已被取消') return null } error.value = e.response?.data?.message || '请求失败' throw e } finally { loading.value = false } } // 暴露取消方法 const cancel = () => { if (controller) { controller.abort() controller = null } } // 组件卸载时自动取消 onUnmounted(() => { cancel() }) return { loading, error, data, execute, cancel } }

实操心得:

  • onUnmounted中调用cancel()是必须的!否则组件销毁后请求仍在进行,可能触发已销毁组件的data.value = xxx,导致 Vue 报错Cannot set property 'value' of null
  • abortable选项默认为false,因为并非所有请求都需要取消(如提交表单后立即跳转,取消无意义)。我们只在搜索框防抖请求、图表轮询等场景开启。
  • AbortController在 iOS 12.2+ 和所有现代浏览器中支持良好,无需 polyfill。

3.3 响应式缓存策略:基于 ref 的内存缓存

对于不频繁变更的配置类接口(如字典项、菜单列表),我们实现简易 LRU 缓存:

// composables/useCachedRequest.ts import { ref, computed } from 'vue' import { useRequest } from './useRequest' import { createApiClient } from '@/utils/apiFactory' // 内存缓存 Map:key 为请求 URL,value 为 { data, timestamp } const cacheMap = new Map<string, { data: any; timestamp: number }>() // 缓存有效期:5 分钟 const CACHE_TTL = 5 * 60 * 1000 export function useCachedRequest<T>(key: string, ttl: number = CACHE_TTL) { const { loading, error, data, execute } = useRequest<T>() // 从缓存读取 const cached = computed(() => { const item = cacheMap.get(key) if (item && Date.now() - item.timestamp < ttl) { return item.data } return null }) // 封装执行逻辑 const fetch = async () => { // 先尝试读缓存 if (cached.value) { data.value = cached.value return cached.value } // 缓存失效,发起请求 const result = await execute({ url: key }) if (result) { // 写入缓存 cacheMap.set(key, { data: result, timestamp: Date.now() }) } return result } return { loading, error, data: computed(() => cached.value || data.value), fetch, // 强制刷新 refresh: () => { cacheMap.delete(key) return fetch() } } }

避坑指南:

  • 缓存 key 必须唯一且稳定。我们约定 key 为GET:/api/dict/status这种格式,包含 method 和 url,避免 POST 和 GET 同 url 冲突。
  • cacheMap是全局单例,但useCachedRequest每次调用返回独立的响应式对象,保证组件间状态隔离。
  • 不做持久化缓存(如 localStorage),因为配置变更后前端无法感知,会导致脏数据。真正的解决方案是服务端加 ETag 或 Cache-Control。

3.4 Provide/Inject 的工程化封装:Context Provider 组件

直接在 setup 中 provide 显得零散。我们创建<ApiProvider>组件统一管理:

<!-- components/ApiProvider.vue --> <script setup lang="ts"> import { provide, reactive } from 'vue' import { useApi } from '@/composables/useApi' import { useUserApi } from '@/composables/useUserApi' // 创建顶层 API 上下文 const apiContext = reactive({ ...useApi(), user: useUserApi(), // 可扩展其他领域 API order: useOrderApi() }) provide('apiContext', apiContext) </script> <template> <slot /> </template>

在 main.ts 中包裹根组件:

// main.ts import { createApp } from 'vue' import App from './App.vue' import ApiProvider from './components/ApiProvider.vue' const app = createApp({ render: () => ( <ApiProvider> <App /> </ApiProvider> ) })

子组件中 inject:

<script setup> import { inject } from 'vue' const apiContext = inject('apiContext') // 类型安全注入 interface ApiContext { loading: Ref<boolean> request: <T>(config: any) => Promise<T> user: ReturnType<typeof useUserApi> } const ctx = inject<ApiContext>('apiContext') </script>

为什么需要这个封装层?

  • 启动时机控制ApiProvider可在mounted钩子中预加载基础数据(如用户信息、权限菜单),避免首屏白屏。
  • 错误边界隔离:在ApiProvider内部用<Suspense>包裹,当 API 初始化失败时,可统一 fallback 到维护页面。
  • 调试友好:在 Vue Devtools 中,apiContext作为独立的 provide 节点显示,比散落在各处的 provide 更易追踪。

4. 全链路调试与性能优化实战

再完美的设计,没有调试手段和性能验证都是空中楼阁。以下是我在生产环境沉淀的排查清单。

4.1 响应式依赖图谱可视化:揪出“幽灵依赖”

Vue 3 的响应式依赖是隐式的,有时组件更新了,但你不知道是谁触发的。我们用effectScope+ 自定义 hook 构建依赖追踪:

// composables/useTrace.ts import { effectScope, reactive, toRaw } from 'vue' export function useTrace<T>(name: string, source: T) { const scope = effectScope() const traced = scope.run(() => reactive(toRaw(source))) as T // 在控制台打印依赖关系 console.group(`[TRACE] ${name}`) console.log('Traced object:', traced) console.log('Dependencies:', Object.keys(traced)) console.groupEnd() // 清理时释放 scope onUnmounted(() => scope.stop()) return traced }

在关键组件中使用:

<script setup> import { useTrace } from '@/composables/useTrace' import { useUserApi } from '@/composables/useUserApi' const userApi = useUserApi() // 追踪 userApi 的响应式属性 const tracedUserApi = useTrace('UserApi', userApi) </script>

调试技巧:

  • 打开 Chrome DevTools → Console,搜索[TRACE]即可定位所有追踪点。
  • 结合 Vue Devtools 的 “Reactivity” 标签页,点击响应式对象右侧的👁️图标,查看谁在监听它。
  • 发现某个ref被 20 个组件监听?说明它被过度共享,应该拆分为更细粒度的 state。

4.2 请求性能分析:量化评估每种方案

我们用 Performance API 记录真实耗时:

// utils/performance.ts export const performanceMetrics = { // 记录 API 请求耗时 recordApiTime: (url: string, duration: number) => { if (performance.mark && performance.measure) { performance.mark(`api-start-${url}`) setTimeout(() => { performance.mark(`api-end-${url}`) performance.measure(`api-${url}`, `api-start-${url}`, `api-end-${url}`) }, duration) } }, // 获取指标 getApiMetrics: () => { const entries = performance.getEntriesByType('measure') return entries.filter(e => e.name.startsWith('api-')) } }

useRequest中集成:

// composables/useRequest.ts import { performanceMetrics } from '@/utils/performance' const execute = async (options: RequestOptions<T>) => { const start = Date.now() // ... 请求逻辑 const end = Date.now() performanceMetrics.recordApiTime(options.url, end - start) }

实测数据对比(某订单列表页):

方案首屏加载时间内存占用请求并发数可维护性
globalProperties + axios2.8s142MB8★☆☆☆☆(类型混乱)
Provide/Inject + composable1.9s98MB5★★★★☆(依赖清晰)
纯 composable(按需引入)1.6s85MB3★★★★★(零耦合)

结论:纯 composable 方案性能最优,但 Provide/Inject 在需要跨多层共享状态时不可替代。两者结合才是正解。

4.3 内存泄漏检测:识别“悬挂的 ref”

Vue 3 的 ref 本质是 JavaScript 对象,不当持有会导致内存泄漏。我们用 Chrome 的 Memory Tab 检测:

  1. 打开 DevTools → Memory → Take heap snapshot
  2. 操作页面(如打开关闭弹窗 10 次)
  3. 再次 Take heap snapshot
  4. 切换到 Comparison 视图,筛选RefImpl

常见泄漏模式:

  • 定时器未清除setInterval(() => { count.value++ }, 1000)在组件卸载后仍在运行。解决方案:onUnmounted(() => clearInterval(timer))
  • EventBus 未解绑:使用 mitt 时,emitter.on('event', handler)后忘记emitter.off('event', handler)。解决方案:在onUnmounted中统一解绑。
  • 闭包持有组件实例:在 composable 中定义函数并捕获props,而props持有组件实例引用。解决方案:用toRefs(props)解构,或确保函数不引用props

我们曾在一个实时监控组件中发现,每打开一次,内存中就多一个RefImpl实例,原因是 WebSocket 的onmessage回调中引用了props.id。修复后,内存曲线变为平稳直线。

4.4 TypeScript 类型安全加固:从 any 到精确推导

inject默认返回any,这是最大隐患。我们建立类型守卫:

// types/inject.d.ts import { InjectionKey, inject } from 'vue' // 定义注入键的类型 declare module '@vue/runtime-core' { interface ComponentCustomProperties { $api: ReturnType<typeof useApi> } } // 安全的 inject 函数 export function safeInject<T>( key: InjectionKey<T> | string, defaultValue?: T ): T { const value = inject(key, defaultValue) if (value === undefined) { throw new Error(`Injection key "${key}" is not provided`) } return value } // 使用示例 const apiContext = safeInject<ApiContext>('apiContext')

类型实践要点:

  • 所有 provide 的 key 必须是InjectionKey<T>类型,而非字符串字面量。这样 TypeScript 能校验 key 与 value 类型匹配。
  • shims-vue.d.ts中扩展ComponentCustomProperties,让this.$api在 Options API 中也有类型提示。
  • 对于复杂嵌套对象,用DeepReadonly<T>防止意外修改:
    provide('config', readonly(configValue)) // configValue 是 reactive 对象

5. 常见问题速查表与独家避坑技巧

以下是我在 12 个项目中踩过的坑,整理成可速查的表格。这些问题在官方文档中几乎不提,但每个都足以让你加班到凌晨。

问题现象根本原因解决方案验证方式
inject()返回undefined,但provide()已调用Provide 和 Inject 不在同一组件树层级。常见于createApp().mount()动态挂载的弹窗、Tooltip 组件使用getCurrentInstance()?.appContext获取当前应用上下文,在动态组件中手动 provide:
```ts
const app = createApp(Popup)
app._context = getCurrentInstance()?.appContext
Composable 中的ref在组件卸载后仍被修改,报错Cannot set property 'value' of nullonUnmounted钩子未正确注册,或异步操作未取消在 composable 中显式管理生命周期:
ts<br>const stop = watchEffect(() => { /* 逻辑 */ })<br>onUnmounted(() => {<br> stop()<br> controller?.abort()<br>})<br>
onUnmounted中添加console.log('unmounted'),确认钩子是否执行
Provide 的响应式对象在子组件中修改,父组件无更新Provide 的值是reactive()创建的,但子组件 inject 后直接赋值ctx.data = newData(破坏响应式链接)必须通过.valueObject.assign()修改:
ts<br>// ✅ 正确<br>ctx.data.value = newData<br>// ✅ 正确<br>Object.assign(ctx.data, newData)<br>// ❌ 错误<br>ctx.data = newData<br>
在父组件中监听data的变化,确认 setter 是否触发
TypeScript 提示Property 'xxx' does not exist on type 'ComponentCustomProperties'shims-vue.d.ts中未正确定义$api类型,或类型文件未被识别确保shims-vue.d.tssrc目录下,且tsconfig.jsoninclude包含src/**/*;类型定义必须用declare module '@vue/runtime-core'删除node_modules/.vite重启开发服务器,检查 VS Code 是否有类型提示
页面切换时,Provide 的状态被重置Provide 在路由组件中调用,而路由组件被keep-alive缓存,但 provide 逻辑在setup中重复执行将 provide 逻辑移到beforeRouteEnter导航守卫,或在onActivated中检查是否已 providesetup中添加console.log('provide executed'),切换路由观察输出频率

独家避坑技巧:

  • “Provide 陷阱”规避法:永远不要在setup()中直接provide('key', reactive(obj))。先创建const context = reactive({ ... }),再provide('key', context)。这样 inject 时拿到的是同一响应式对象引用,而非新创建的 proxy。

  • Composable 的命名规范:以use开头,动词+名词结构,如useApiuseFormuseWebSocket。避免apiHookapiUtil等模糊命名,这会让团队成员无法快速识别其用途。

  • 全局状态的“冷热分离”:将状态分为“热状态”(频繁变更,如 loading)和“冷状态”(极少变更,如用户基本信息)。热状态用 composable 独立管理,冷状态用 Pinia 存储,避免响应式系统负担过重。我们在某项目中将用户权限数组从reactive改为readonly(ref()),首屏渲染性能提升 35%。

  • 错误边界的最小化:不要在根组件用<ErrorBoundary>包裹全局。而是在每个业务模块(如 Dashboard、Report、Settings)内部设置边界。这样某个模块崩溃不会导致整个应用白屏,运维同学能精准定位故障模块。

最后分享一个小技巧:在vite.config.ts中添加插件,自动检查未使用的 composable:

// vite-plugin-unused-composable.ts export default function unusedComposablePlugin() { return { name: 'unused-composable', transform(code, id) { if (!id.endsWith('.ts') || !id.includes('composables/')) return // 分析 import 语句和调用位置 // 若无调用,则在构建时警告 if (!code.includes('use')) { console.warn(`⚠️ Composable ${id} 未被任何组件使用`) } } } }

这让我们在迭代中及时清理废弃代码,保持代码库健康度。毕竟,最好的全局 API,就是你根本不需要全局 API。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 7:56:26

Espresso 核心原理:UI线程协同协议与IdlingResource实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 7:54:41

STM32F103 Standby模式深度解析与可靠唤醒实践

简介&#xff1a;本资源是一套面向嵌入式初学者与STM32项目开发者的低功耗实战例程&#xff0c;聚焦STM32F103系列单片机的Standby&#xff08;待机&#xff09;模式应用&#xff0c;适用于电池供电、便携设备等对功耗敏感的物联网终端开发场景。压缩包共73个文件&#xff0c;含…

作者头像 李华
网站建设 2026/9/12 7:54:19

SpringBoot电商平台实战:库存管理与订单状态机设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 7:53:22

ARM Cortex-M嵌入式AI静态评测:从语法层到硅片层的四重穿透法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 7:52:57

One API 的 OpenAI 渠道如何接入 Cloudflare AI Gateway

One API 的 OpenAI 渠道如何接入 Cloudflare AI Gateway 【免费下载链接】one-api LLM API 管理 & 分发系统&#xff0c;支持 OpenAI、Azure、Anthropic Claude、Google Gemini、DeepSeek、字节豆包、ChatGLM、文心一言、讯飞星火、通义千问、360 智脑、腾讯混元等主流模型…

作者头像 李华