尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue3 + TypeScript 项目命名规范:从组合式 API 到代码可维护性实践

Vue3 + TypeScript 项目命名规范:从组合式 API 到代码可维护性实践 写代码三年最怕的不是业务复杂是同事变量命名全靠当天心情。尤其项目切到 Vue3 TypeScript 之后组合式 API 把一堆变量和方法全暴露在 setup 里一个页面看下来什么data1、res2、form、getData满天飞代码评审时恨不得当场重写。我后来花了不少精力梳理了一套命名规范配合严格模式下的 TypeScript 类型检查才让项目慢慢从一个“能跑就行”的堆料工程变成了能长期维护的样子。这篇东西不是官方文档翻译是我在真实项目和代码评审里总结出来的经验。适合正在从 Options API 迁到组合式 API 的团队也适合刚接触 Vue3 TS、对命名边界还比较模糊的同学。你会看到我踩过的坑、后来定下的规则以及支撑这些规则背后的逻辑。1. Vue3 TS项目里命名为什么突然变成一件大事很多从 Vue2 过来的老手会觉得命名这种事不是靠自觉吗 Vue2 时代确实有这种空间因为 Options API 把变量、方法、计算属性都分门别类放进了各自的data、methods、computed区块。你看到一个变量叫name基本能猜到它来自data看到一个方法是handleClick也大概率知道它是事件处理函数。但 Vue3 的组合式 API 把这一切搅在一起了。所有变量、方法、计算属性全部扁平化存在于setup或script setup里本质上就是一个普通的函数作用域。你看到一个裸变量name可能是一个ref、一个reactive对象甚至是从props解构出来的值看到一个getData可能是请求封装、可能是纯函数也可能只是触发了一个事件。这种语境丢失让命名从“加分项”变成了“必答题”。1.1 组合式 API 把变量从“选项框”搬进了“执行流”Vue2 的data是一个对象天然有“状态容器”的心理暗示。Vue3 里你写const count ref(0)时count只是一个普通标识符它既没有类型标注也没有前缀说明。如果你后续在模板里不加.value页面显示不出来IDE 却不会报错——这种隐性问题如果变量本身叫countRef或明确类型排查起来就会快很多。我在实际项目里还遇到过更头疼的场景一个订单页有人在setup顶部定义了一个const form reactive({...})到模板里却用form.xxx访问另一处又在方法里写了form.value ...。命名上完全看不出来一个普通变量、一个 ref、一个 reactive全用同一个名字“裸奔”结果就是类型检查被绕过、运行时行为诡异。这背后真正的痛点是组合式 API 让作用域扁平化但扁平化的副作用就是信息密度下降。命名就成了一种弥补手段它把变量的“类型”和“行为”重新编码进名字里让读代码的人不用跳到定义处就能做初步判断。这比任何 Lint 规则都更接近解决问题的本质。1.2 TypeScript 让命名变成类型心智的一部分TypeScript 的引入理论上能帮我们锁定类型实际项目中却经常出现反效果因为类型系统能兜底大家就觉得命名随便点也能编译过、跑得通。这恰恰是本末倒置。类型系统解决的是“这个变量是什么类型”但解决不了“这个变量在业务里是什么意思”。举个例子两个开发者写同一个页面模块一个把“订单状态”命名为status另一个命名为state。类型上它俩都是string | number甚至可能是联合类型完全合法。但代码评审时你得反复上下文切换才能确认到底哪个是业务状态、哪个是加载状态。更麻烦的是如果status被赋值为pending、success而另一个state也是类似取值两个变量同时存在后来的人一旦用错类型系统根本拦不住。所以我一直跟团队强调在 TS 项目里命名是类型系统的延伸不是可选项。一个好名字能让你在 IDE 里只靠悬浮提示就能推断出变量来源和大致语义一个烂名字再严格的泛型和联合类型都救不回来。Vue3 TS 的组合本质是让我把“类型安全”和“命名可读性”当成同一件事来做。2. 变量命名建议从 ref 到 props 的完整规则变量命名是最容易起纠纷的。有人觉得ref必须带后缀有人说带后缀是画蛇添足有人说reactive应该叫data有人说恨不得全用ref。我综合了团队习惯和代码可读性最终定了一套相对平衡的规则先讲清楚每种变量类型的命名语境。2.1 ref 变量xxxRef 后缀还是裸变量名关于ref变量命名社区里吵得最凶。我试过两种极端一种是全部裸命名比如const name ref()另一种是全部加Ref后缀比如const nameRef ref()。裸命名在模板里很舒服不用写一堆后缀但回到 script 里name到底是响应式引用还是一个常量必须滚到定义处才知道。全加后缀又太啰嗦尤其在script setup里模板直接同名使用时满屏nameRef非常难看。我最终的取舍是在script setup内部定义的响应式变量用裸命名但必须严格配合类型推导和ref()的显式调用而涉及跨函数返回、跨模块传递、或者放进reactive里的引用则显式加Ref后缀表明类型。这个规则听起来简单实操里能避掉很多坑。还有一个关键场景当你需要把 ref 作为参数传给一个工具函数时必须在命名上区分“是否带 value 语义”。比如// 好的做法明确这个参数接收的是 Ref 对象 function useSearchQuery(keywordRef: Refstring) { // ... } // 容易误会的做法参数名里没有 Ref 字样 function useSearchQuery(keyword: string) { // 里面却把 Ref 对象传进来类型对不上才调试半天 }2.2 reactive 变量什么时候用怎么命名reactive是 Vue3 里处理嵌套对象响应式的主要工具但很多人用起来没有章法。我见过最糟糕的命名是const data reactive({})整个组件里所有业务字段全塞进这个data然后到处data.user.name、data.list[0].id一多起来谁看得懂data到底承载了什么我的建议是reactive变量必须带“领域名词”后缀让它看起来像一个实体或模块。比如订单模块就用const orderState reactive({...})用户信息就用const userInfo reactive({...})页面筛选条件就用const filterForm reactive({...})。不要用泛化的data、info、obj这些名字等于什么都没说。另外一个很容易忽略的规则是不要让reactive变量承担“跨模块共享”的职责。如果你发现一个reactive对象被多个组件或多个模块引用这时候它已经不适合做局部变量了应该提升到 store 或者业务模型里。命名上也要相应调整比如变成useUserStore()解构出来的结果而不是继续用userInfo这种局部命名误导使用者。2.3 props 与 emit组件对外接口的命名边界组件之间的 props 和 emits是命名上最容易失控的地方。Vue2 时代大家一般就写props: [name, value]类型全靠命名的“前半段”猜测。到了 Vue3 TS有了defineProps和defineEmits的显式类型约束命名反而更应该讲究。我习惯把 props 命名成“名词属性”尽量不带动词。比如好的userName、orderList、visible不太好的getUserName、loadOrderList、showOrNot语义胡闹props 描述的是“父组件给子组件传了什么”本质上是一个属性描述动词会让人误以为它是一个方法调用或者事件回调。除非这个 prop 本身就是函数类型的插槽语义否则不要动词化。emit 事件名则要用过去时态或完成态强调“已经发生了什么”。比如update:visible、confirm、change、submit而不是onUpdateVisible、onConfirm这类“带 on 前缀的事件名”。v-model 的双向绑定场景里update:xxx是框架约定的格式但如果你手动写事件emit(confirm, payload)比emit(onConfirm, payload)清晰得多因为on前缀通常是用来绑定监听器的不是事件名的一部分。2.4 computed 常量与普通常量的命名层次computed的命名我也见过不少坑。最常见的是把computed当普通变量命名比如const total computed(...)然后在模板里直接用total。这种写法在 Vue2 里没毛病因为computed本来就是和data平级的。但 Vue3 组合式 API 里computed本质上是一个“带有缓存功能的函数”你在script setup里写const total computed(...)和普通变量const total 100在代码上下文里几乎无法直观区分。我的建议是computed变量用“名词短语 形容词/结果语义”比如totalPrice、filteredList、canSubmit让人一看就知道它是一个计算结果。如果要进一步强调它是“机关”可以加get或use前缀但别滥用否则整个页面都是useXxx、getXxx反而分不清哪些是函数调用。普通常量呢我习惯用const MAX_COUNT 10这种全大写下划线命名和computed天然区分开。3. 方法命名建议动词、时态和语义的搭配方法的命名比变量更复杂因为方法承载了行为、时序、副作用等多种因素。Vue3 TS 项目里方法往往又分为事件处理、数据请求、通用工具、异步流程等几类。如果全用handleget泛化处理代码会显得非常平庸而且查错时毫无抓手。3.1 事件处理方法handleXxx 还是 onXxx社区里一直有handleClick和onClick的命名之争。我个人的经验是在 Vue 模板里绑定事件时用onXxx在组件内部定义处理函数时用handleXxx但这会带来一个命名冲突问题。举个例子template button clickonSave保存/button /template script setup langts function handleSave() { ... } /script这种写法的好处是模板里的clickonSave非常直观说明这是一个事件入口而handleSave在 script 里看起来像一个内部实现细节不会和onSave混淆。反过来如果你在模板里同时写clickhandleSave又在 script 里定义一个const handleSave () {}名字重复会显得很绕而且在模板里调试事件绑定时你分不清这个handleSave是模板语法还是函数调用。更稳妥的方案是事件方法统一以handle开头并且带上触发场景。比如handleSubmithandleCancelhandleInputChangehandleModalClose然后用on前缀只保留给“组件对外暴露的 emit 回调”和“props 类型的函数属性”。这样在阅读模板时clickhandleSave不会让人误以为它是一个对外事件在阅读 script 时emit(onSave)这种写法也基本不可能出现。注意emit的事件名本身不要加on但对应到组件上接收它的 props 时可以叫onSave因为那是父组件在监听的视角。3.2 数据请求方法fetch/load/get/query 怎么选后台管理系统开发里请求方法满天飞。我见过同一个项目里有人写fetchData有人写getList还有人写loadTable语义全是“拿数据”但没有任何统一约定。这种混乱在大项目里的代价非常大因为代码检索和重构时你不确定“拿订单列表”到底该搜fetchOrder、getOrderList还是loadOrders。我给大家的约定是按动作语义拆分不要混用。前缀语义场景例子fetch从远端拉取数据强调网络请求和异步过程fetchUserListget获取数据可以是缓存、本地变量或简单计算getLocalUserInfoload初始化或重新加载通常伴随页面/组件的挂载流程loadTableDataquery条件查询强调基于条件的筛选行为queryOrderByStatus实际项目里我建议优先选一个主前缀比如fetch用于所有直接调用接口的方法get用于非网络请求的取值方法load只在组件挂载或事件触发的初始化场景中使用。这样至少能让代码搜索简单很多。还要注意请求方法不要直接用动词又带async标注的困惑。比如async function getData()虽然编译没问题但读代码的人光看名字完全不知道它到底要干嘛是拿本地缓存还是走网络你不如直接写成fetchData结合 TS 的返回类型Promise...语义就很清晰了。3.3 布尔返回值方法is/has/can 的语义强化TypeScript 里返回布尔值的方法非常多比如权限判断、状态检查、数据是否存在。很多人会写checkStatus()或者getResult()类型上返回boolean但命名上一点看不出来。这在业务代码里很危险因为你调用时经常会条件判断嵌套一个语义不明的布尔返回值最容易让人误判。我建议强制在布尔方法前加is、has、can这类谓词isAdminUser(user)hasPermission(code)canEditOrder(order)如果是响应式变量则叫isXxx、hasXxx、canXxx也成立比如const isSubmitting ref(false)。关键是保持一致性只要返回值是布尔命名就必须带“是不是”“有没有”“能不能”的暗示。这个方法在 Vue3 TS 项目里特别好用因为v-ifcanEditOrder(order)比v-ifcheckOrderAuth(order)的语义强太多了模板直接可读完全不用跳进方法体去猜测。3.4 异步方法与普通方法的命名区分Vue3 TS 里async/await用得非常频繁但异步方法在命名上和普通方法往往没什么区别都是fetchData()或submitForm()。这导致一个问题调用者只看名字无法判断是否需要await处理。我个人的建议是异步方法统一用动词短语并且尽量在文档注释或类型上明确返回Promise命名不强求加Async后缀因为 Vue3 里请求几乎全是异步的加了反而冗余。但如果你在一个方法里混合了“同步检查 异步请求”的逻辑比如checkPermissionBeforeFetch()那这就不是一个纯粹的异步方法命名上要把它拆成两个一个同步判断hasPermission一个异步请求fetchData不要揉在一起。另外有个细节不要在方法名里用await这个关键字。你写await fetchData()是调用但你定义一个awaitLoadData()就非常怪。如果方法内部有大量串行异步逻辑可以用loadXxxSequentially、ensureXxxLoaded这类增强语义让读者明白它不只是单纯发起请求。4. 类型与接口命名TypeScript 的“另一半疆土”Vue3 TS 项目里命名不止针对变量和方法类型、接口、泛型的命名反而更影响项目的整体可维护性。很多时候代码报错不是因为逻辑不对而是因为类型名太过模糊导致as断言满天飞最后类型约束名存实亡。4.1 interface 与 type 的选择和命名习惯我在团队里定过一个简单规则interface用于对外结构描述type用于联合类型或工具类型。但命名上很多人同样会犯“泛化命名”的错比如interface IData、type Info这是什么没人知道。建议在interface命名中带领域名比如interface OrderEntity { id: number; status: OrderStatus; totalAmount: number; }type则通常表示一组可能值命名上用大驼峰不用加前缀比如type OrderStatus pending | paid | cancelled。不要动用I前缀TS 社区现在已经不太推荐了因为interface本身已经是类型的一种IUser和User读起来没有语义增量反而冗长。还要注意一点命名要表达“数据形态”而不是“数据来源”。比如type TableData ...这种命名很危险因为TableData听起来像是一个 UI 组件的 props可实际它是一个业务数据模型。业务模型应该用领域实体命名比如OrderRecord、UserProfile。这样在ref、reactive、computed之间流转时类型名才能保持一致。4.2 组件 Props/Emits 类型命名规范Vue3 TS 里definePropsProps()是很常见的写法。但很多人都把Props定义得极其粗糙要么直接defineProps{ visible: boolean; list: any[] }()要么搞一个interface Props然后就再也不管了。我要说的是这部分命名一旦乱掉团队协作会非常痛苦。我的做法是组件 props 类型统一叫XxxProps其中Xxx是组件名去掉后缀比如SearchFormProps。emits 类型统一叫XxxEmits同样带组件名前缀。如果 props 和业务实体强相关可以直接引用领域类型不要重复定义list: SomeEntity[]后又单独再写interface SomeList。来看一个实际例子// 好的命名 interface UserTableProps { userList: UserEntity[]; loading: boolean; } // 不好的命名 interface Props { data: any[]; loading: boolean; }后者虽然省事但你在.reading 几十个子组件时每个都叫Props别说人IDE 的悬浮提示都很容易窜。更重要的是如果props里嵌套了emits回调命名上也应该配对好比如 props 里的函数属性叫onSubmitemits 里对应事件名就叫submit不要一个叫sendData一个叫submit看起来像两个系统。4.3 泛型和工具类型的命名细节TS 项目里泛型用得多了T、K、V这种单字母命名满天飞。如果你只是写一个工具函数单字母可以接受但如果泛型参数代表了具体业务含义就一定要改成有语义的名字。比如// 不太好 function transformT(data: T): T { ... } // 更好 function transformEntityT extends Entity(entity: T): T { ... }另外对 Vue3 项目来说常见的工具类型如ref、computed、Record、Partial也要注意上下文里的命名一致性。比如你写const statusMap refRecordstring, OrderStatus({})这个statusMap就比data清晰得多。泛型的约束又反过来帮助你在写map、filter时自动得到正确的类型推断整个链路都能因为命名而受益。5. 命名之外的协作规范目录、文件、导出命名命名不建议只停留在变量和方法层面。Vue3 TS 工程里组件文件、目录结构、导出命名同样影响代码的可读性。一个项目有几十个组件、几十个 hooks如果文件名和导出名对不上再好的变量命名都会被冲淡。5.1 组件文件命名与导入命名Vue 单文件组件SFC的文件命名业内主流是 PascalCase比如UserProfile.vue、OrderTable.vue。我建议强制统一不允许小写中划线式命名为user-profile.vue因为你在模板里引用组件时可能写成UserProfile /而文件却是user-profile.vueIDE 虽然可以自动解析但检索时得花心思去拼。还有一个理由PascalCase 和组件变量的命名在 JS 里天然对应可以直接import UserProfile from /components/UserProfile.vue减少了认知负担。还有一个小细节局部组件的导入命名尽量和文件同名。别为了简洁把UserProfile缩写成UP或者Profile这样一旦搜索组件全项目同名对维护者非常友好。我自己在代码 review 时就见过有人把UserProfile /导入成User /然后发现页面渲染不对排查了半天竟然是导入名和模板名不一致这属于完全可以靠规范避免的坑。5.2 store、hooks、工具函数的命名约定Vue3 TS 项目的状态管理主流是 Pinia对应 store 的命名也有讲究。我的建议是store 文件名用小写驼峰store 内导出的 useXxxStore 函数用use 领域名比如useUserStore、useOrderStore。不要在 store 内部再导出一堆getUser、setUser这样的裸方法尽量通过storeToRefs或defineStore的setup写法保持 state 和 action 的命名语义一致。hooks组合式函数是 Vue3 中复用逻辑的主要方式。我建议 hooks 文件统一放在hooks/或composables/目录下文件名用useXxx.ts内部导出的函数名和文件名保持一致。比如useTablePagination.ts导出useTablePagination()不要导出一个paginationFunction那会让使用方一头雾水。工具函数呢建议放在utils/目录命名上尽量是“动词 名词”的通用语义比如formatDate、deepClone不要用${项目缩写}_xxx这种只有自己人才懂的简写。5.3 文件之间的命名对齐很重要项目里还有一个经常被忽略的点组件目录、页面路由、store 模块、API 接口函数的命名如果不对齐检索成本会指数级上升。比如你有一个订单管理模块目录叫order/页面文件叫OrderList.vueAPI 封装却叫getOrderDatastore 叫OrderStore看起来都“差不多”但真要搜代码你得在 5-6 个不同的命名体系里反复切换。我推荐做一次命名对齐表在团队规范里列清楚页面路由层级用什么单词、API 模块函数用什么前缀、store 叫什么、组件前缀是什么。比如订单模块统一用order作为基础词页面OrderList.vue、OrderDetail.vueAPIfetchOrderList、fetchOrderDetailstoreuseOrderStore组件名称OrderTable、OrderFormModal这样一来从路由到组件到 API所有代码都是同一个词根搜索引擎和 IDE 的引用搜索都非常精准。6. 常见问题与避坑实录这一节我在实际项目中踩了不少坑也帮同事处理过不少“命名引发的事故”。列几个高频问题配上我的排查思路。6.1 解包忘掉 ref 导致的命名混乱Vue3 里ref在模板中会自动解包但在 script 中不会。很多新手或不熟悉的人在 script 里写const name ref(hello)然后直接setTimeout(() name world, 1000)页面迟迟不更新也没有报错。这个问题表面上和命名无关但如果你一开始就遵循“裸变量名也能解包”的预设很容易写出这种误导代码。我的建议是如果你发现某个ref经常被误用成普通变量就老老实实在命名上加Ref后缀或者在定义处加一个类型标注Refstring。前者能让使用方在 IDE 提示里看到类型后者则能在编译期提醒你该用.value。说白了命名规则就是给团队里的“粗心时刻”留一张安全网。6.2 命名“为了加分”反而减分有些开发者觉得命名越“高级”越好于是写了useComposableComplexDataService这种名字或者把方法名套上多层抽象比如handleFilterTableDataWithPaginationAndSort。这种命名我见一个改一个。命名是给人读的不是给代码专家看的。我比较推崇的是“十五秒原则”一个开发者了解项目建设背景看到这个名字15 秒内能不能知道它是干什么的如果不行就说明名字里的信息过多或者过抽象了。真正的好命名不是把整个逻辑都塞进名字里而是抓住核心语义剩下的交给类型和注释。比如一个函数做了“筛选表格数据并翻页”两件事更好的做法是拆成filterTableData()和changePage()两个方法而不是合成一个filterAndChangePage()。如果确实需要一次性调用可以让外部方法叫handleTableChange()内部再调用那两个小方法。命名可以因为拆解而变得更干净而不是靠堆砌变得更“隆重”。6.3 团队规范的一致性问题Vue3 TS 项目通常是团队协作命名规范最怕的不是没有而是“有但不遵守”。这个问题很现实因为哪怕你定义了各种handle/fetch/Ref规则团队成员写代码时还是随手不一样。我的经验是先做一次代码评审专项检查把命名问题单独列出来而不是和业务逻辑混在一起讨论。比如每周花 30 分钟专门看命名重构给每个人的代码做一次“命名体检”。这种事不需要太多理论关键是形成一种团队默认命名是代码质量的一部分不是个人审美。另外可以用 ESLint 配合typescript-eslint/naming-convention规则做一部分自动检查。比如强制组件文件名、函数前缀、bool 变量的is/has前缀等。虽然这不能覆盖所有语义层面的命名问题但至少能拦住那些“变量名拼音缩写”“无意义数字后缀”这类低水平错误。6.4 常见命名问题速查表场景常见错误命名建议命名原因ref 变量传给工具函数keywordkeywordRef或类型标注Refstring避免函数内部误用普通变量reactive 对象承载多个实体dataorderState领域名词提升可读性emit 事件名onConfirmconfirmon前缀属于监听视角请求接口方法getDatafetchUserList动词加宾语语义完整布尔判断方法checkStatushasPermission返回布尔值时谓词前置类型名PropsUserTableProps避免全项目泛化命名组件文件user-profile.vueUserProfile.vue与组件导入和模板标签一致这张表是我在做代码评审时常用的一张自查清单每次新项目启动我都会把它作为初始规范的一部分发给团队成员。定期对照检查项目里命名混乱的情况会明显减少。7. 一点额外的小工具让命名规范变成可执行的约定这一节算是经验之外的附加内容。很多人看完一堆命名建议觉得“有道理但记不住全部”。我自己的做法是在项目里维护一个很短的命名约定说明文档只写关键规则不用写成几十页的规范手册。然后把这个文档链接放进 README 或者仓库根目录的CONTRIBUTING.md里。另外一个好用的方式是在代码里写几个“示范组件”或“示范 hooks”团队成员照着复制修改比读文档效率高得多。比如我会在项目里专门留一个components/_Playground/目录里面放一个命名规范的示例组件注释写明每一处变量、方法为什么这么命名。新入职的同事看一遍再上手写业务比看任何规范文档都直观。最终你会发现命名规范这件事本质上不是在管理“名字”而是在管理团队对业务的共同理解。Vue3 TypeScript 项目里的每一个变量、方法、类型、文件都是这种理解的外化。定好规则坚持执行项目会越写越顺手放任自流再强类型系统也救不回一坨“能跑但看不懂”的代码。我个人的体会是刚开始推行这套规则时团队会出现不小的摩擦因为每个人都觉得自己的命名“没问题”。但坚持几个月后回头改一个老模块时大家都会真心感谢当初的坚持。代码终究是写给人看的顺手把自己的命名习惯梳理清楚长期下来省下的时间和精力远超一开始那点适应成本。
返回列表