开发规范与源码级实践指南)
Vuetify 共享组合式函数Shared Composables开发规范与源码级实践指南【免费下载链接】vuetify Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify导读Vuetify 的核心组件库由数百个共享组合式函数composables驱动例如useDisplay、useLocale、useRounded等。本文以仓库中 .claude/rules/composables.md 这份团队开发规范为骨架结合packages/vuetify/src/composables/与packages/vuetify/src/util/下的真实源码系统讲解共享组合式函数的设计约束、两种标准形态Props 驱动形态与插件形态、作用域与 SSR 注意事项、公共 API 发布纪律及测试要求。读完本文你将能写出与 Vuetify 核心组件风格一致、可被 API 生成器自动收录、可被多个组件安全复用的高质量组合式函数。一、为什么需要共享组合式函数规范共享组合式函数被众多组件同时消费——一个签名或响应式处理上的小失误会顺着组件树扩散到所有使用者。因此规范首先强调两件事组件专属逻辑放组件旁边仅被单个组件使用的逻辑应写成组件目录下的局部 composable见components.md的约定而不是塞进共享目录。共享逻辑优先复用在动手写新函数之前先检索src/composables与src/util确认仓库里是否已有现成实现。一个典型印证是 VHover.tsx它自身只有 hover 相关的薄薄一层逻辑而modelValue的双向绑定交给useProxiedModel来自/composables/proxiedModel、延迟开关交给makeDelayProps/useDelay来自/composables/delay两者都是共享组合式函数。这正是复用优先原则的日常形态。vuetify/v0的进入通道src/util/v0.ts规范特别指出vuetify/v0Vuetify 4.x 迭代中引入的工具库只能通过 v0.ts 这一入口进入核心代码。调用点一律import from /util新增 v0 导出也追加到该文件禁止在调用点直接import from vuetify/v0。从源码看v0.ts 目前再导出了isArray、isBoolean、isElement、isFunction、isNull、isNullOrUndefined、isNumber、isObject、isString、isSymbol、isThenable、isUndefined、findMatchRanges等工具并把range以createRange的名字导出——注释解释了原因range会在VPagination、VRating、VSlider等多个组件中遮蔽局部变量。这既是集中入口也是命名治理。二、形态一Props 驱动形态Prop-driven shape大多数共享组合式函数采用这种形态一个 props 工厂 一个use函数use函数接收props与组件名。规范给出了标准模板// Utilities import { computed } from vue import { getCurrentInstanceName, propsFactory } from /util // Types export interface ExampleProps { example?: boolean | string } // Composables export const makeExampleProps propsFactory({ example: [Boolean, String], }, example) export function useExample ( props: ExampleProps, name getCurrentInstanceName(), ) { const exampleClasses computed(() props.example ? ${name}--example : []) return { exampleClasses } }2.1propsFactory的底层实现propsFactory定义在 propsFactory.ts其核心机制是柯里化第一次调用传入 props 定义与来源标识source返回一个可接受defaults的函数第二次调用传入默认值后为每个 prop 注入default字段并统一打上source标记export function propsFactory PropsOptions extends ComponentObjectPropsOptions (props: PropsOptions, source: string) { return Defaults extends PartialKeysPropsOptions {}( defaults?: Defaults ): AppendDefaultPropsOptions, Defaults { return Object.keys(props).reduceany((obj, prop) { const isObjectDefinition isObject(props[prop]) const definition isObjectDefinition ? props[prop] : { type: props[prop] } if (defaults prop in defaults) { obj[prop] { ...definition, default: defaults[prop] } } else { obj[prop] definition } if (source !obj[prop].source) { obj[prop].source source } return obj }, {}) } }关键收益有两个类型收窄AppendDefault等类型工具会根据defaults推导出更精确的 prop 类型。源码注释中的例子很直观——提供{ foo: a }默认值后props.foo的类型从string | undefined收窄为string来源追溯source标记让 api-generator 能定位该 prop 的出处文档这正是共享 props 只描述一次机制见第五节的实现基础。2.2 签名规范props或MaybeRefOrGetter绝不解构规范对use函数的签名有严格要求接收props或MaybeRefOrGetter在computed/toRef内部用toValue读取保证同时兼容响应式与非响应式输入绝不在函数签名中解构 propsfunction useX({ foo })是被禁止的——解构会丢失响应式破坏 props 的响应性传递。真实实现可参考 rounded.ts它同时演示了两种输入形态的兼容写法export function useRounded ( props: RoundedProps | RefRoundedValue, name getCurrentInstanceName(), ): RoundedData { const roundedClasses computed(() { const rounded isRef(props) ? props.value : props.rounded const tile isRef(props) ? false : props.tile const classes: string[] [] // ... }) const roundedStyles computedCSSProperties(() { const rounded isRef(props) ? props.value : props.rounded // ... }) return { roundedClasses, roundedStyles } }useRounded能接收整份 props 或单个Ref内部用isRef分支取值返回的RoundedData类型也明确声明了roundedClasses: Refstring[]与roundedStyles: RefCSSProperties。2.3 返回值规范refs 普通对象命名加前缀规范明确三条返回约定返回普通对象元素是 refs绝不用reactive——reactive会带来代理与解包的心智负担组件侧需要的是可单独解构的 refs命名加组合式函数前缀如exampleClasses、exampleStyles。原因很实际组件往往并排解构多个组合式函数的返回值前缀能避免命名冲突、让来源一目了然修饰类遵循${name}--x约定其中name是组件名如v-btn--rounded。useRounded中${name}--rounded就是这一约定的直接体现useDisplay生成的${name}--mobile同理。三、形态二插件形态Plugin shape全局服务display、theme、locale 等采用另一种形态。它们的生命周期绑定在 Vuetify 实例上而非单个组件createExample(options)在 framework.ts 的createVuetify中被调用实例通过app.provide(ExampleSymbol, example)注入到全局useExample()用inject取回实例。规范给出的模板export const ExampleSymbol: InjectionKeyExampleInstance Symbol.for(vuetify:example) export function useExample () { const example inject(ExampleSymbol) if (!example) throw new Error([Vuetify] Could not find example injection) return example }3.1 真实案例locale与displaylocale.ts 是插件形态的完整范本。它定义LocaleSymbol: InjectionKeyLocaleInstance RtlInstance Symbol.for(vuetify:locale)createLocale(options)构造实例选择外部 adapter 或内置的createVuetifyAdapteruseLocale()注入并校验export function useLocale () { const locale inject(LocaleSymbol) if (!locale) throw new Error([Vuetify] Could not find injected locale instance) return locale }display.ts 则展示了插件形态的复杂性createDisplay(options, ssr)内部用watchEffect根据window.innerWidth与阈值xs: 0, sm: 600, md: 840, lg: 1145, xl: 1545, xxl: 2138推算全部断点布尔值注册resize监听并在onScopeDispose中移除同时它还向外暴露makeDisplayProps/useDisplay供组件按 Props 驱动形态消费。一个组合式函数同时承载两种形态是 Vuetify 中常见的设计。3.2 注册与注入的完整链路framework.ts 中createVuetify的install阶段把所有全局服务挂到 Vue 应用上app.provide(DefaultsSymbol, defaults) app.provide(RootDefaultsSymbol, defaults) app.provide(DisplaySymbol, display) app.provide(ThemeSymbol, theme) app.provide(IconSymbol, icons) app.provide(LocaleSymbol, locale) app.provide(DateOptionsSymbol, date.options) app.provide(DateAdapterSymbol, date.instance) app.provide(GoToSymbol, goTo)整个createVuetify运行在一个effectScope内unmount()调用scope.stop()即可释放全部全局副作用——这是插件形态服务优雅销毁的关键设计。四、实例、作用域与 SSR 注意事项这是规范中偏硬核的部分涉及 4 条必须遵守的纪律4.1 需要 vm 时用getCurrentInstance(useExample)组合式函数需要访问组件实例vm时必须从/util引入getCurrentInstance并传入自身名称。getCurrentInstance.ts 的实现会让名字出现在报错信息里export function getCurrentInstance (name: string, message?: string) { const vm _getCurrentInstance() if (!vm) { throw new Error([Vuetify] ${name} ${message || must be called from inside a setup function}) } return vm }配套的getCurrentInstanceName()还会把组件名转成 kebab-case——这就是useExample(props, name getCurrentInstanceName())中默认组件名参数的来源。4.2 条件生效的副作用用useToggleScopeuseToggleScope(source, fn)用于仅在某个条件为真时才存在的副作用如 hover 时的监听器。其实现位于 toggleScope.ts核心是利用 Vue 的effectScopewatch(source, ...)监听布尔源为true时创建effectScope并scope.run(fn)变回false时scope.stop()并置空内部所有 effect 一并销毁支持带reset参数的fn可手动重启作用域组件卸载时通过onScopeDispose兜底停止。4.3 监听器与观察器必须在onScopeDispose中清理凡是addEventListener、observe、定时器等长生命周期副作用都要在onScopeDispose中成对移除。范例就在 display.tsif (IN_BROWSER) { window.addEventListener(resize, updateSize, { passive: true }) onScopeDispose(() { window.removeEventListener(resize, updateSize) }, true) }4.4 浏览器 API 用IN_BROWSER/SUPPORTS_*守卫SSR 环境下window、document不存在所有浏览器 API 访问都必须守卫。globals.ts 集中提供了这些常量export const IN_BROWSER typeof window ! undefined export const SUPPORTS_INTERSECTION IN_BROWSER IntersectionObserver in window export const SUPPORTS_TOUCH IN_BROWSER (ontouchstart in window || window.navigator.maxTouchPoints 0) export const SUPPORTS_EYE_DROPPER IN_BROWSER EyeDropper in window export const SUPPORTS_MATCH_MEDIA IN_BROWSER matchMedia in window isFunction(window.matchMedia)注意其巧妙的短路结构IN_BROWSER为false时后续的IntersectionObserver in window根本不会执行天然规避了 SSR 抛错。五、公共 API 与发布纪律共享组合式函数是 Vuetify 对外 API 的一部分规范从四个层面约束其发布流程5.1composables/index.ts是唯一公共出口composables/index.ts 顶部注释写得很直白PUBLIC INTERFACES ONLY。目前只导出useDate、useDefaults、useDisplay、useGoTo、useLayout、useLocale、useRtl、useTheme、useHotkey、useMask、createRulesPlugin、useRules。这里导出的成员会被 api-generator 收录进官方 API 文档。5.2 内部代码禁止走 barrel与公共出口相反内部代码必须直接import from /composables/example永远不走composables/index.ts这个 barrel。这样既能避免循环依赖也保证了公共出口的纯粹性——只有需要公开的成员才出现在那里。5.3 新增公开成员要登记new-in.json向某个公共组合式函数新增对外暴露的成员时需同步更新 packages/docs/src/data/new-in.json让文档站点的 New in 模块能及时呈现新能力。5.4 破坏性变更必须走next分支修改公共组合式函数的返回结构或参数属于破坏性变更breaking change只能提交到next分支随大版本发布禁止在维护分支偷偷改签名——这保护了所有下游组件与第三方库的兼容性。5.5 共享 props 的文档只描述一次来自共享组合式函数的 props其 API 描述集中放在 api-generator 的 locale 文件中例如 packages/api-generator/src/locale/en/ 下的example.json而不是在每个组件的 json 里重复维护。这正是propsFactory的source标记见 2.1发挥作用的地方——api-generator 依靠它把组件 props 与共享来源的描述关联起来一处编写、全局生效。六、测试要求所有共享组合式函数都必须有单元测试放在packages/vuetify/src/composables/__tests__/目录下文件命名为example.spec.ts。仓库现有测试覆盖了大部分组合式函数rounded.spec.ts、border.spec.ts、elevation.spec.ts、size.spec.ts、tag.spec.ts——Props 驱动形态的类名/样式推导测试display.spec.browser.ts、goto.spec.browser.tsx、resizeObserver.spec.browser.tsx、scroll.spec.browser.tsx——依赖浏览器环境的测试使用.browser后缀与普通单元测试分离theme.spec.ts、defaults.spec.ts、icons.spec.ts、validation.spec.ts——插件形态服务的状态与提供/注入行为测试proxiedModel.spec.ts、delay.spec.ts、group.spec.ts、list-items.spec.ts、filter.spec.ts等——被大量组件消费的基础能力测试。测试文件与实现一一对应既验证了规范中签名与返回结构的稳定性也为后续重构提供了安全网。七、把规范落到组件一个完整示例以 VHover.tsx 为例把本文全部要点串起来看export const makeVHoverProps propsFactory({ disabled: Boolean, modelValue: { type: Boolean, default: null }, ...makeDelayProps(), }, VHover) export const VHover genericComponentVHoverSlots()({ name: VHover, props: makeVHoverProps(), setup (props, { slots }) { const isHovering useProxiedModel(props, modelValue) const internal shallowRef(false) const { runOpenDelay, runCloseDelay } useDelay(props, value { internal.value value if (!props.disabled) { isHovering.value value } }) // ... }, })这里可以看到共享的makeDelayProps/useDelay直接复用无需重写延迟逻辑useProxiedModel承担 v-model 桥接组件自己的 props 通过propsFactory组合...makeDelayProps()而不是复制粘贴组件名VHover作为source标记传入。整个组件保持轻薄这正是共享组合式函数规范想要达到的最终效果。结语Vuetify 的共享组合式函数体系并非玄学而是一套边界清晰的工程契约复用优先控制重复两种形态统一结构作用域与 SSR 纪律保证健壮公共 API 发布流程守护兼容性测试目录兜底质量。遵循这份规范你既能为 Vuetify 贡献风格一致的核心代码也能在自己的 Vue 组件库中复刻这套经过大规模实战验证的组合式函数设计模式。【免费下载链接】vuetify Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考