Vuetify 源码代码可读性规范:注释、命名、函数与抽象设计的实战指南

原创2026-09-18 17:40:06833 阅读
文章标签:前端UI组件

Vuetify 源码代码可读性规范:注释、命名、函数与抽象设计的实战指南

导读

本文以 Vuetify 仓库中的 code-readability.md 规范文档为主体,系统拆解 Vuetify 在长期维护中沉淀下来的代码可读性约定:注释什么该写、什么绝不写,变量与函数如何命名,函数声明与箭头函数如何取舍,以及何时应该(或不应该)抽离抽象。文中所有结论均结合仓库内的真实源码(如 VMenu、forwardRefs)逐一印证,读者读完既能掌握一套可直接套用的编码风格,也能理解这套风格在 Vuetify 源码库中落地时的具体样貌。

前置纪律:先跑 lint,再自我审查

规范的适用边界非常清晰——它针对的是 Vuetify 源码目录(packages/vuetify/src/**,即文档 frontmatter 中 paths 声明的作用范围)。规范的执行顺序是:

  1. 先在 packages/vuetify 目录下运行 pnpm lint:fix;
  2. 再对照本文列出的可读性清单审查自己的 diff;
  3. 只改动本次变更真正需要的行,禁止顺手重命名、重排或重排格式(no drive-by renames, reordering or reformatting)。

这条"只碰必要的行"的纪律,保证了代码评审 diff 的最小化,也让后续几条规范都能在明确的边界内执行。

从仓库的 package.json 可以看到 lint 与 lint:fix 两个脚本的真实构成:

"lint": "concurrently -n \"tsc,eslint\" --kill-others-on-fail \"tsgo -p tsconfig.checks.json --noEmit --pretty\" \"eslint src -f codeframe --max-warnings 0\"",
"lint:fix": "concurrently -n \"tsc,eslint\" \"tsgo -p tsconfig.checks.json --noEmit --pretty\" \"eslint --fix src\""

即 lint 由两路并行任务组成:一路是 tsgo(TypeScript 编译器)以 tsconfig.checks.json 做类型检查;另一路是 ESLint 对 src 目录做检查,且 --max-warnings 0 表示任何 warning 都视为失败。lint:fix 则是在类型检查的同时用 eslint --fix 自动修复。仓库的 ESLint 还通过 eslint-local-rules.cjs 用 glob.sync('./scripts/rules/*') 动态加载了 scripts/rules 下的一批自定义规则(如 jsx-prop-casing、sort-imports 等),这些由 ESLint 强制的内容在本文规范中一律不重复。

注释:默认不写,写就写"代码读不出来的原因"

规范的核心态度非常鲜明:注释默认一个都不写。尤其是 AI 生成的注释,绝大多数都是在复述下一行代码在做什么,评审时通常会被直接删掉。

什么时候才值得写一条注释?只有当"原因无法从代码本身读出来"时才写:

  • 浏览器怪癖:某个写法是为了绕开特定浏览器的行为差异;
  • 顺序约束:这两行必须按这个顺序执行,颠倒会出问题;
  • 坑:如果后人"简化"了这段代码就会踩坑。

写法要求是一行、措辞直白(One line, plain wording),不解释"是什么",只解释"为什么"。

绝对禁止的注释

规范明确列出四类永不出现的注释:

禁止类型 反例
复述代码 // set open to false
叙述意图或历史 // fixed the bug where…、// new approach
引用 issue 或链接 // see #12345、GitHub URL(这些应写在 commit message 里)
给内部函数加 JSDoc ——

一个例外:import 分组头要保留

唯一需要主动保留的注释是文件顶部的 import 分组头——// Components、// Composables、// Utilities、// Types。这是 Vuetify 文件级的既定约定,新代码不得删除。

在真实的组件源码中可以清晰看到这套分组。以 VMenu.tsx 为例,文件开头依次是 // Styles、// Components、// Composables、// Utilities、// Types 五个分组头,每个分组下方集中对应的 import。这一约定让一个动辄引入二三十个依赖的组件文件保持极强的可扫描性。

命名:变量、参数、函数一律不缩写

规范的第一条命名铁律:变量、参数、函数中不使用缩写。文档给出的正反示例:

// ❌
const idx = items.indexOf(item)
const opts = { ...defaults, ...options }
function handleKeydown (e: KeyboardEvent) {}

// ✅
const index = items.indexOf(item)
const merged = { ...defaults, ...options }
function onKeydown (e: KeyboardEvent) {}

注意 idx、opts 这类缩写被明确否决,而全写 index、merged 是正解;handleKeydown 需要改为 onKeydown(见下文事件处理命名)。

允许的例外

  • 短 lambda 参数没问题:items.map(v => v.value)、(a, b) => a - b 这类单字符箭头函数参数可以保留;
  • 既定名称保持不变:e 代表事件、vm、以及元素引用上的 El 后缀(如 activatorEl、contentEl);
  • 老代码不成为新代码的借口:util 目录下的旧代码仍在使用 val/cb/obj 这类缩写,不要模仿它们。在 forwardRefs.ts 中仍能看到 getDescriptor (obj: any, key)、const val = Reflect.get(ref.value, key) 这样的历史写法,这正是规范特意点名的"不要照抄"对象。

在组件语境里"语境即命名"

规范进一步要求:当上下文足以消除歧义时,优先使用单词。例如在 VMenu 内部就用 open 而不是 isMenuOpen——因为组件本身就是"菜单",open 语义已经完整。这是组件级命名与模块级命名的重要差异:越靠近局部上下文,越应该精简。

事件处理器统一 on<Action> 前缀

事件处理函数的命名固定为 on<Action>,永远不用 handle<Action>。这一点与上面的正反例一致,也和 Vue 模板中 @click="onClick"、@keydown="onKeydown" 的事件绑定习惯天然对齐。

在 VMenu.tsx 中能直接看到这一约定的落地:function onKeydown (e: KeyboardEvent) 与 function onActivatorKeydown (e: KeyboardEvent) 都是标准的 on<Action> 命名,同时兼顾了"单词优先"(没有写成 onMenuKeydown)。

函数:优先函数声明,而非 const 箭头

在 setup 与模块作用域中,函数一律使用函数声明而不是 const 箭头函数:

// ❌
const toggle = () => { … }

// ✅
function toggle () { … }

选择函数声明的原因很实际:它具备**提升(hoisting)**能力,可以在定义之前被引用,且天然拥有自己的名字(便于调试器显示、便于递归与解构),也不会与 const 的"声明后不可变"语义产生混淆。在 VMenu.tsx 中,onKeydown、onActivatorKeydown 以及内部大量辅助函数都遵循了 function 声明的写法。

抽象:能内联就不抽,能派生就不同步

抽象过度是大型组件库最常见的腐化来源,规范用四条规则划出了边界:

1. 单一调用点不建 helper

如果一个辅助函数只有一个调用点,直接内联它,不要为了抽象而抽象。多一层间接就多一层心智负担,而它没有解决任何重复问题。

2. 写新 helper 前先搜 util 与 composables

动手写新工具函数之前,先 grep src/util 和 src/composables。Vuetify 在这两个目录沉淀了大量现成能力——比如 composables 下的 forwardRefs、useRender、propsFactory 等——重复实现不仅浪费,还会导致同一语义在不同文件里出现两套实现,破坏可维护性。

3. 禁止"只转发参数"的包装

不要写一个仅仅重命名或原样转发参数的包装函数。function doX (a, b) { return doY(a, b) } 这种包装没有增加任何语义价值,只会让调用链多一跳。

4. 用派生代替同步

优先用 toRef / computed 表达"派生数据",而不是用 watch 去监听一个 ref、再把值写进另一个 ref。前者是声明式的数据流——状态变化自动传导、永远一致;后者是命令式的"手动同步"——需要手动维护写入时机,还容易引入额外的一次渲染或无限循环风险。凡是"B 可以由 A 计算得出"的关系,都应该用 computed 表达。

规范全景速查表

维度 规则要点 正例 / 反例
前置流程 先 pnpm lint:fix 再自审 diff,只动必要行 不做顺手改名、重排
注释 默认不写;只写代码读不出的原因;一行直白 ✅ 浏览器怪癖/顺序约束;❌ 复述代码、叙述历史、引 issue、内部 JSDoc
import 分组头 // Components、// Composables 等必须保留 VMenu.tsx
命名 不缩写;短 lambda 可留;e/vm/El 后缀是既定名 ✅ index/merged/onKeydown;❌ idx/opts/handleKeydown
上下文命名 语境明确时用单词 open 优于 isMenuOpen(VMenu 内部)
事件处理 统一 on<Action>,禁用 handle<Action> VMenu.tsx
函数声明 setup 与模块内用 function 声明 ❌ const toggle = () => …
抽象边界 单调用点内联;先 grep util/composables;禁转发包装;用 computed/toRef 派生而非 watch 同步 forwardRefs.ts 为既有工具佐证

结语

这份 code-readability.md 规范虽然篇幅不长,却精准地刻画了 Vuetify 源码库的代码气质:注释克制、命名直白、函数声明统一、抽象审慎。四条约束之间相互呼应——正是因为命名足够清晰,才不需要注释复述;正是因为函数是声明式提升的,才让 on<Action> 风格在组件内自由流动;正是因为强制先检索 util 与 composables,才让 forwardRefs 这类既有工具真正被复用而非被复制。无论你是向 Vuetify 提交代码,还是在自己的组件库中借鉴这套风格,都可以把本文的速查表当作一份可直接落地的可读性基线。

登录后查看全文
vuetify