ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3 Composables 设计模式实战:以 JavaScript + JSDoc 构建类型安全的组合式函数

Vue 3 Composables 设计模式实战:以 JavaScript + JSDoc 构建类型安全的组合式函数 Vue 3 Composables 设计模式实战以 JavaScript JSDoc 构建类型安全的组合式函数【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills组合式函数Composables是 Vue 3 中复用有状态逻辑的核心机制而纯 JavaScript 项目中如何写出既结构规范又具备完整类型提示的组合式函数则是一门值得深入的技术课题。本篇指南以 claude-skills 仓库中vue-expert-js技能的参考文档 composables-patterns.md 为主体结合该技能其他参考文档与仓库结构系统讲解从基础组合式函数、响应式 API 选型、生命周期封装、共享单例状态到异步取消的完整模式集。读完本文你将掌握一套可直接复制到真实 Vue 3 纯 JS 项目中的组合式函数设计范式并学会用 JSDoc 补齐类型安全让代码在无 TypeScript 的前提下依然可被编辑器、ESLint 与 Agent 精确理解。文档定位这套模式在项目中的角色composables-patterns.md是 claude-skills 仓库中 vue-expert-js 技能 的五份核心参考文档之一。该技能专门面向「只用 JavaScript、不用 TypeScript」构建 Vue 3 应用的场景其工作流明确规定用 JSDoc 的typedef、param、returns、type注解实现完整类型覆盖并用eslint-plugin-jsdoc校验覆盖率随后以 Vitest 验证。组合式函数正是这套工作流的核心产物——技能要求「每个公开函数都标注param和returns、复杂对象结构用typedef定义、响应式变量用type注解」。因此本参考文档中的每一个模式示例都是「组合式函数设计」与「JSDoc 类型标注」两种能力的融合体阅读时建议与同技能下的 jsdoc-typing.md 对照学习。基础组合式函数结构以 useToggle 为例组合式函数的第一个设计要点是统一的结构约定函数命名以use开头、内部通过ref创建响应式状态、返回一个包含状态与操作方法的对象。文档给出的最小完整示例是useToggle// composables/useToggle.js import { ref } from vue /** * typedef {Object} UseToggleReturn * property {import(vue).Refboolean} value * property {() void} toggle */ /** * param {boolean} [initialValuefalse] * returns {UseToggleReturn} */ export function useToggle(initialValue false) { const value ref(initialValue) const toggle () { value.value !value.value } return { value, toggle } }这段代码体现了三个值得固化的习惯返回类型的显式声明通过typedef {Object} UseToggleReturn定义返回对象的完整形状再以returns {UseToggleReturn}标注函数返回值。这正是 vue-expert-js 技能「每个公开函数都要有param和returns」约束的直接落地。参数默认值语义param {boolean} [initialValuefalse]中的方括号表示可选参数false表示默认值这与 JSDoc 的可选参数语法完全一致与export function useToggle(initialValue false)的实现互相印证。.value的语义边界toggle内部通过value.value !value.value修改响应式状态对外返回的仍是value本身一个Refboolean调用方在模板中会被自动解包在脚本中则通过.value访问。从仓库的 SKILL.md 核心工作流 可以印证这套约定的完整闭环设计结构含 JSDoc 类型注解→ 用script setup实现 → 用 ESLint JSDoc 插件校验类型覆盖 → 用 Vitest 验证。也就是说参考文档中的每个组合式函数都是「类型标注 逻辑实现」双重交付的模板而不是可以省略注解的示意代码。ref 与 reactive 的选择按数据的形状决定文档给出的第二条准则是「响应式 API 按数据形状选型」。完整示例与注解如下import { ref, reactive, toRefs, toValue } from vue // Use ref for: primitives, reassignable values, composable returns /** type {import(vue).Refnumber} */ const count ref(0) // Use reactive for: complex objects with nested properties /** type {{ email: string, password: string }} */ const form reactive({ email: , password: }) // Convert reactive to refs for destructuring const { email, password } toRefs(form) // Unwrap ref or return plain value /** param {number | import(vue).Refnumber} maybeRef */ function double(maybeRef) { return toValue(maybeRef) * 2 }逐个拆解其中的决策点ref()用于原始值与可整体重新赋值的值。count是一个数字且组合式函数返回的响应式状态通常以Ref形式对外暴露便于调用方解构后依然保持响应性。reactive()用于具有嵌套属性的复杂对象。表单这类「多字段、深层结构」的数据用reactive声明后内部所有嵌套属性都自动具备响应性无需像ref那样逐个访问.value。toRefs()解决「解构丢失响应性」问题。直接const { email } form会把email变成一个普通字符串之后对该变量的修改不再触发视图更新toRefs(form)会把每个属性转换成独立的Ref解构后依然与源对象保持响应式连接。toValue()是 Vue 3.3 引入的通用解包工具。它接受一个Ref或普通值传入Ref时返回其内部值传入普通值时原样返回。上面的double(maybeRef)因此既能接受double(4)也能接受double(count)这让组合式函数的参数设计可以「同时兼容响应式和非响应式输入」。同样的选型原则在仓库同技能的其他文档中反复出现composition-api.md 参考文档 以 TypeScript 版本演示了同一组 API 的用法二者可互为对照而 jsdoc-typing.md 则补充了用typedeftype为reactive表单声明完整类型结构的写法。生命周期钩子的组合式封装事件监听与异步安全组合式函数的一大优势是能在函数体内直接调用生命周期钩子把「注册 清理」的配对逻辑封装成可复用的单元。文档给出了两个典型场景。场景一封装事件监听// composables/useEventListener.js import { onMounted, onUnmounted, toValue } from vue /** * template {keyof WindowEventMap} K * param {K} event * param {(ev: WindowEventMap[K]) void} handler * param {EventTarget | import(vue).RefEventTarget} [targetwindow] */ export function useEventListener(event, handler, target window) { onMounted(() toValue(target).addEventListener(event, handler)) onUnmounted(() toValue(target).removeEventListener(event, handler)) }这个例子把template泛型注解也用上了template {keyof WindowEventMap} K约束了事件名必须是浏览器内置事件类型handler 的参数类型随之被推导为对应的事件对象。注意target同时接受EventTarget或RefEventTarget配合toValue(target)在挂载时解包——这正好呼应上文「参数兼容响应式与非响应式输入」的设计思想。场景二生命周期感知的异步状态「组件卸载后再更新状态」是 Vue 应用中常见的隐患。文档给出的useAsyncState用标志位加onUnmounted优雅地规避了这个问题// Lifecycle-aware async (prevents state updates after unmount) import { ref, onUnmounted } from vue export function useAsyncState(fn) { const data ref(null) const loading ref(false) let isMounted true onUnmounted(() { isMounted false }) async function execute() { loading.value true try { const result await fn() if (isMounted) data.value result } finally { if (isMounted) loading.value false } } return { data, loading, execute } }其原理是模块闭包中的isMounted初始为true组件卸载时onUnmounted将其置为false。当异步的fn()在卸载之后才 resolve 时isMounted为false于是跳过data.value result避免了对已卸载组件内部状态的无意义写入。finally中的同样守卫保证了loading状态也不会被卸载后的残留任务错误地翻转。在 vue-expert 的 composition-api 参考文档 中可以看到完整的生命周期钩子清单onMounted、onUpdated、onUnmounted、onErrorCaptured等其中onUnmounted被明确标注为「清理定时器、监听器」的归宿与本文的两个例子完全一致。共享状态单例模块级 ref当多个组件需要共享同一份状态时组合式函数天然支持「单例」模式把ref声明在模块作用域而非函数体内所有调用useNotifications()的组件共享同一份状态。// composables/useNotifications.js import { ref, readonly } from vue // Module-level state singleton shared across all components /** type {import(vue).RefArray{id: string, message: string}} */ const notifications ref([]) export function useNotifications() { /** param {string} message */ function notify(message) { const id Date.now().toString() notifications.value.push({ id, message }) setTimeout(() dismiss(id), 5000) } /** param {string} id */ function dismiss(id) { notifications.value notifications.value.filter(n n.id ! id) } return { notifications: readonly(notifications), notify, dismiss } }这个模式有三个要点值得吸收单例的机制notifications定义在模块顶层模块只被加载一次因此无论多少组件调用useNotifications()拿到的都是同一个数组引用。用readonly()保护只读边界对外暴露的是readonly(notifications)组件只能通过notify/dismiss两个方法修改状态杜绝了外部直接 push、篡改共享数据的散弹式副作用。封装业务规则自动生成 id、5 秒后自动消失setTimeout(() dismiss(id), 5000)这些业务逻辑被收敛在组合式函数内部组件侧只关心「发起通知」和「手动关闭」。与「单例」相对的另一极是工厂函数模式若把ref放进函数体内、返回新的实例那么每个组件就拥有独立的副本。文档末尾的速查表明确区分了两者「Module-level ref → Singleton shared stateFactory function → New instance per component」。选择依据很简单——需要跨组件同步就声明在模块级需要组件隔离就声明在函数体内。当共享状态进一步复杂化时state-management.md 提供的 Pinia setup store 语法正是对这种模式的工程化扩展store 内同样用ref声明状态、computed声明派生数据。带取消的异步组合式函数AbortController 全流程请求竞态与内存泄漏是异步 UI 的两大顽疾。文档给出的useCancellableFetch用AbortController给出了完整解法// composables/useCancellableFetch.js import { ref, onUnmounted } from vue export function useCancellableFetch() { const data ref(null) const error ref(null) const loading ref(false) /** type {AbortController | null} */ let controller null /** param {string} url */ async function execute(url) { controller?.abort() controller new AbortController() loading.value true error.value null try { const res await fetch(url, { signal: controller.signal }) data.value await res.json() } catch (e) { if (/** type {Error} */ (e).name ! AbortError) { error.value /** type {Error} */ (e) } } finally { loading.value false } } onUnmounted(() controller?.abort()) return { data, error, loading, execute } }逐层分析它的防御机制连续请求的竞态防护每次execute开头先controller?.abort()中止上一次未完成的请求再创建新的AbortController。这意味着用户快速触发多次请求时只有最后一次的结果会被采纳前序请求被主动作废。AbortError 的静默处理catch中判断e.name ! AbortError才写入error。被我们主动中止的请求会抛出AbortError这属于「预期内的取消」而非「真实错误」因此不污染错误状态。代码里的/** type {Error} */ (e)是 JSDoc 类型断言帮助编辑器正确识别错误对象类型。组件卸载时的兜底取消onUnmounted(() controller?.abort())确保组件卸载瞬间仍有未完成请求时立即中止避免请求回调访问已销毁的组件状态。至此本文生命周期部分的三个技巧onUnmounted清理、isMounted守卫、AbortController取消恰好构成了一套完整的异步安全体系监听类副作用用钩子配对清理无取消机制的异步用标志位守卫可取消的网络请求用 AbortController 主动中止。模式速查表一表掌握选型决策文档在末尾给出了精炼的速查表这是整个组合式函数设计决策的浓缩完整保留如下PatternUse Caseref()Primitives, values passed to/from composablesreactive()Objects with nested reactivitytoRefs()Destructure reactive while keeping reactivitytoValue()Unwrap ref or return plain valueModule-level refSingleton shared stateFactory functionNew instance per componentonUnmountedCleanup timers, listeners, abort controllers这张表可以直接作为组合式函数设计的决策清单先判断数据形状原始值还是嵌套对象选ref/reactive需要解构保持响应性就用toRefs函数入参要兼容 ref 和普通值时用toValue状态共享范围决定单例还是工厂最后检查所有注册的副作用是否在onUnmounted中清理。用 JSDoc 把类型安全拉满组合式函数的进阶标注composables-patterns.md 中的示例已经展示了typedef、param、returns、template、type、import(vue).RefT等多种标注。同技能的 jsdoc-typing.md 则把这些技巧系统化这里选取与组合式函数直接相关的三个进阶用法泛型返回useFetch// composables/useFetch.js import { ref, watchEffect, toValue } from vue /** * template T * typedef {Object} UseFetchReturn * property {import(vue).RefT | null} data - Fetched data * property {import(vue).RefError | null} error - Error if any * property {import(vue).Refboolean} loading - Loading state * property {() Promisevoid} refresh - Refetch data */ /** * Composable for fetching data * template T * param {string | import(vue).Refstring} url - URL to fetch * param {RequestInit} [options] - Fetch options * returns {UseFetchReturnT} */ export function useFetch(url, options {}) { /** type {import(vue).RefT | null} */ const data ref(null) /** type {import(vue).RefError | null} */ const error ref(null) /** type {import(vue).Refboolean} */ const loading ref(false) async function refresh() { loading.value true error.value null try { const response await fetch(toValue(url), options) if (!response.ok) { throw new Error(HTTP error: ${response.status}) } data.value await response.json() } catch (e) { error.value /** type {Error} */ (e) } finally { loading.value false } } watchEffect(() { refresh() }) return { data, error, loading, refresh } }template T让data的类型随调用上下文自动推断url参数同时接受字符串或Refstring并通过toValue解包与 useEventListener 的 target 参数设计一脉相承watchEffect则让url变化时自动重新拉取数据。可配置选项对象useLocalStorage// composables/useLocalStorage.js import { ref, watch } from vue /** * template T * typedef {Object} UseLocalStorageOptions * property {(value: T) string} [serialize] - Custom serializer * property {(value: string) T} [deserialize] - Custom deserializer */ /** * Reactive localStorage composable * template T * param {string} key - Storage key * param {T} defaultValue - Default value if key not found * param {UseLocalStorageOptionsT} [options] - Options * returns {import(vue).RefT} */ export function useLocalStorage(key, defaultValue, options {}) { const serialize options.serialize ?? JSON.stringify const deserialize options.deserialize ?? JSON.parse /** type {import(vue).RefT} */ const data ref(defaultValue) // Load from storage const stored localStorage.getItem(key) if (stored) { try { data.value deserialize(stored) } catch { data.value defaultValue } } // Persist on change watch(data, (value) { localStorage.setItem(key, serialize(value)) }, { deep: true }) return data }这里展示了「自定义序列化函数」的选项注入模式默认用JSON.stringify/JSON.parse调用方可通过options.serialize/options.deserialize覆盖且typedef把选项函数的签名精确到参数与返回值IDE 悬停即可看到完整契约。跨文件共享类型在纯 JS 项目中复杂类型常被多个组合式函数共享。jsdoc-typing.md 推荐把共享类型集中到types.js用「导出空对象」的方式帮助 IDE 解析// types.js - Shared type definitions /** * typedef {Object} User * property {number} id * property {string} name * property {string} email * property {UserRole} role */ /** * typedef {admin | user | guest} UserRole */ // Export empty object for IDE import support export const Types {}其他文件通过/** typedef {import(./types.js).User} User */引入组合式函数内部即可使用/** type {import(vue).RefUser | null} */标注状态。这种「类型模块」的组织方式让组合式函数的类型定义可复用、可维护是纯 JS 项目中替代.d.ts的轻量方案。组合式函数的可测性设计设计良好的组合式函数同样易于测试。vue-expert-js 技能的 testing-patterns.md 专门演示了如何在组件测试中 mock 组合式函数——这反向印证了组合式函数的一个核心设计约束必须通过模块导出且返回结构要可被vi.spyOn().mockReturnValue()完整替换// Header.test.js import { describe, it, expect, vi } from vitest import { mount } from vue/test-utils import { ref, computed } from vue import Header from ./Header.vue import * as useAuthModule from /composables/useAuth describe(Header, () { it(shows login button when logged out, () { vi.spyOn(useAuthModule, useAuth).mockReturnValue({ user: ref(null), isLoggedIn: computed(() false), login: vi.fn(), logout: vi.fn() }) const wrapper mount(Header) expect(wrapper.find([data-testlogin-btn]).exists()).toBe(true) }) it(shows user menu when logged in, () { vi.spyOn(useAuthModule, useAuth).mockReturnValue({ user: ref({ id: 1, name: John }), isLoggedIn: computed(() true), login: vi.fn(), logout: vi.fn() }) const wrapper mount(Header) expect(wrapper.find([data-testuser-menu]).exists()).toBe(true) }) })这段测试揭示的组合式函数设计启示是返回值应保持「状态用 ref、派生值用 computed、操作用普通函数」的稳定结构。mock 之所以能精确还原useAuth的返回形状正是因为真实实现遵循了本文第一部分useToggle演示的统一约定——这也正是「模式」的价值所在约定一致替换才无痛。技能工作流「测试失败则回到组合式函数修正逻辑或注解再重跑」的闭环也建立在组合式函数返回结构稳定可断言的前提上。实战落地把这套模式嵌入开发工作流在 claude-skills 的 vue-expert-js 技能语境下这套组合式函数模式的完整使用流程是设计阶段按「数据形状」选型ref/reactive按「共享范围」决定单例还是工厂先写typedef定义返回契约实现阶段用script setup不带langts在组件中引入组合式函数需要 ES 模块时用.mjs扩展名参考文档与 SKILL.md 中useCounter.mjs示例的完整标注格式校验阶段运行 ESLint eslint-plugin-jsdoc检查每个公开 API 的注解完整性缺失或格式错误的注解需在继续前修复测试阶段用 Vitest Vue Test Utils 验证如vi.spyOnmock 组合式函数、flushPromises()等待异步渲染测试不绿则回到组合式函数修正逻辑或注解。仓库对技能文档的质量约束也在保障这套模式的稳定性scripts/validate-skills.py会校验技能的 YAML 元数据完整性、Core Workflow 步骤数恰好为 5 步与上述流程对应、reference 文档的相对路径是否可解析、以及是否残留非标准标题头。也就是说你在 composables-patterns.md 中看到的每一个模式都处于一套被脚本化校验保障的文档体系之内可以直接作为团队内部知识库或 Agent 技能参考使用。小结回顾 composables-patterns.md 的核心脉络组合式函数的本质是把「响应式状态 生命周期 副作用清理」打包成可复用的函数单元。基础结构解决「怎么写」ref/reactive 选型解决「用什么响应式容器」生命周期封装解决「何时注册与清理」模块级 ref 解决「状态共享范围」AbortController 解决「异步取消」。配合 JSDoc 的typedef/template/type体系纯 JavaScript 项目也能获得接近 TypeScript 的开发体验——这正是 claude-skills 的 vue-expert-js 技能试图交付的完整方案。把这五类模式沉淀为团队内共享的组合式函数库是 Vue 3 应用中长期可控、可维护、可测试的可靠路径。【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表