1. 一套命名规范为什么值得单独开会讨论接手一个前端项目最容易让人血压升高的时刻往往不是遇到复杂的业务逻辑而是打开一个目录看到index.vue、index2.vue、list.vue、List.vue、userList.vue、user-list.vue、UserList.vue全部挤在同一个文件夹里。更让人难受的是这些文件名还都能正常构建、正常运行谁也不会报错但接手的人会在半小时内产生强烈的抵触情绪。OMS 这类业务系统尤其容易出现这种状况模块多、页面多、表单多、权限多业务堆叠之后文件数量会迅速膨胀到几百上千个命名一旦失控后续每次查找和改动都变成额外负担。OMS也就是订单管理系统它的特点决定了它对命名的要求比普通展示型项目更严格。一套订单系统里会同时存在订单、售后、退款、发货、库存、结算、权限、角色、字典、报表等十几个业务域每个业务域又会有列表页、详情页、编辑页、弹窗、抽屉、状态枚举、格式化方法、请求接口。这些内容如果只靠“看得懂”来命名短期能撑住等团队成员换了两批之后代码就会开始变得难读。我在几个中后台项目里反复验证过一件事命名规范不是为了让代码更符合审美而是在项目规模跨过某个临界点后降低沟通和检索成本的基础设施。Vue3 的出现让这件事变得更复杂也更有必要。组合式 API 把逻辑从选项式结构里解放出来setup里可以写状态、写方法、写监听、写生命周期代码组织自由度大幅提升随之而来的问题是命名约束没了。选项式时代data、methods、computed、watch这些选项天然给了一部分分类命名混一混还不至于太乱到了setup里一个const变量可以是响应式数据、可以是工具方法、可以是计算属性光看声明形式已经判断不出它属于哪一类。约束少了规范就得从人的约定里补回来。这篇内容面向的是正在维护或即将启动 Vue3 中后台系统的开发者尤其是订单、库存、结算这类业务模块较多的项目。不管你是刚接手一个老项目的初级开发还是准备给团队制定约定的技术负责人下面的内容都能直接拿去用。我会把命名拆到文件、组件、变量、类型、样式、事件、接口这几个层面每个层面都说明白为什么这么定而不是只给一份看起来正确的清单。规范这种东西只给结论不给理由团队成员是不会遵守的。2. 文件与目录先定好项目中各文件应该放在哪再讨论文件叫什么2.1 目录按业务域切分不再按技术类型切分很多老项目沿用了views、components、api、utils这样的技术分类项目小的时候挺好用业务一多就废了。找一个售后订单的详情页得先猜它在views/order/afterSale还是views/afterSale/order猜错两三次就开始怀疑人生。OMS 这类系统我建议目录的第一层就按业务域切技术类型放到业务域内部。src/ views/ order/ 订单域 list/ 列表 detail/ 详情 components/ 订单域私有组件 after-sale/ 售后域 settlement/ 结算域 system/ 系统管理用户、角色、字典 components/ 全局通用组件 hooks/ 全局组合式函数 api/ 接口定义 types/ 全局类型 constants/ 全局常量与枚举这样切的好处很实际一个新需求落到“售后域”开发只需要在views/after-sale里找找不到再看公共目录路径判断从“猜”变成“推导”。跨域复用的组件才往上提到全局components这一条要卡死否则所有人都会图省事往全局扔最后全局目录又会变成新的问题来源。判断标准很简单只有一个业务域在用就放在该业务域的components里两个以上业务域在用才考虑提升到全局。提升时还要想一想这两处用法是否真的一致还是只是长得像。2.2 文件名统一使用 kebab-case组件名统一使用 PascalCase文件系统这块的坑跨平台协作的团队都踩过。macOS 和 Windows 默认不区分大小写Linux 区分。本地开发时UserList.vue和userList.vue能共存推到构建机上就可能出现引用找不到的问题。所以文件名必须统一一种写法我选 kebab-case全小写加短横线最稳。文件类型命名格式示例说明页面目录kebab-caseafter-sale、order-detail多单词必须带连字符单文件组件PascalCaseOrderStatusTag.vue文件名与组件名保持一致组合式函数camelCaseuse前缀useOrderList.ts文件与导出函数同名类型声明PascalCaseOrderDetail.ts一个文件一个主类型常量枚举camelCase 或 kebab-caseorder-status.ts导出用大写常量工具函数camelCaseformatAmount.ts动词开头有个细节值得单独说单文件组件用 PascalCase 文件名但在模板里引用时用 kebab-case。这是 Vue 官方风格指南里推荐的做法原因是 PascalCase 文件名在编辑器里辨识度更高而模板里 kebab-case 更符合 HTML 的书写习惯也能避免大小写敏感问题。团队里经常有人问“到底用哪个”答案是两边各用各的不冲突。2.3 页面文件不要命名为index.vue这是我个人最坚持的一条。index.vue在编辑器标签页里全是同名切文件全靠记忆位置搜索时也排在很后面。订单列表页就叫OrderList.vue详情页就叫OrderDetail.vue。有人会说目录已经表达了语义文件名重复没必要但从实际使用体验看标签页可读、全局搜索可命中这两点带来的收益远大于多打几个字符的成本。顺带提一个容易忽略的场景动态路由和import()懒加载。如果页面文件都叫index.vue打包产物里的 chunk 名会非常难辨认排查线上加载问题时基本只能靠 sourcemap。文件名有语义chunk 名就有语义这是白送的排错便利。3. 组件命名Base、The、业务前缀三套体系要分清3.1 全局基础组件加Base前缀项目里总会沉淀出一批与业务无关的组件比如按钮、输入框封装、日期选择、空状态、加载骨架。这类组件加Base前缀好处是目录里一眼能分出来哪些是“基础设施”哪些是“业务组件”。BaseTable.vue、BaseDialog.vue、BaseEmpty.vue排序时也会自然聚在一起。Base前缀还有一层隐性约束既然叫 Base就代表它不能依赖任何业务逻辑。我见过BaseTable.vue里引入订单接口的写法这等于把基础设施和业务做了绑定其他业务域想复用时就得改代码。所以这条前缀不只是命名还带了一个设计约束团队 review 时可以直接拿它当检查点。3.2 唯一实例组件加The前缀页头、侧边栏、全局弹窗容器这类一个页面只出现一次的组件用The前缀。TheHeader.vue、TheSidebar.vue、TheGlobalDialog.vue。这个约定的价值在于看到The开头就知道它不该被复用多次也不需要传一堆 props 做差异化。如果没有这个约定很容易出现Header.vue被复制三份、每份改一点的情况。3.3 业务组件用业务域前缀不用动词开头OMS 里的业务组件比如订单状态标签、金额展示、售后进度条命名建议用业务域前缀OrderStatusTag.vue、OrderAmountText.vue、AfterSaleProgress.vue。为什么不建议用ShowOrderStatus.vue这种动词开头因为组件是名词性的东西它描述的是“一个什么东西”而不是“做一个什么动作”。动词开头容易和工具函数混淆也不利于按业务域归类。组件内部的name选项在 Vue3 里虽然可选但我建议显式写上且与文件名完全一致。调试工具里的组件树、KeepAlive的include、递归组件引用都依赖这个名字名字不一致会带来一些很难排查的问题。4. 变量与方法用前缀把“它是什么”写在名字里4.1 响应式状态用名词布尔值必须有is、has、can前缀组合式 API 里最乱的就是状态声明。const list ref([])这种写法到处都是但list到底是什么列表订单列表还是商品列表在setup里上下文还清楚抽到hooks里之后就完全失忆。规范上我要求状态变量必须是“业务域 名词”的完整表达// 推荐 const orderList refOrderItem[]([]) const orderTotal ref(0) const isLoading ref(false) const hasSelectedRows ref(false) const canSubmit computed(() orderList.value.length 0 !isLoading.value) // 不推荐 const list ref([]) const total ref(0) const loading ref(false) const selected ref(false)布尔值加is、has、can前缀这条看起来是小事实际收益很大。模板里写v-ifloading和v-ifisLoading的差别不在于多打两个字符而在于读到loading时你要停顿一下判断它是布尔还是数据读到isLoading时语义直接就出来了。has用于“是否拥有某项数据”can用于“是否具备某项操作权限或条件”这三个前缀把布尔值的语义分成了三档比统一用is更准确。4.2 方法用动词开头事件处理函数加handle或on前缀方法的命名规则相对成熟动词开头动宾结构。fetchOrderList、submitOrder、resetQueryForm、exportOrderExcel。请求类方法统一用fetch或get别一会儿get、一会儿query、一会儿load、一会儿request同一个动作四种叫法搜索的时候要试四遍。事件处理这块我习惯在组件内部用handleXxx在 props 的事件声明里用onXxx// 组件内部的事件处理 function handleRowClick(row: OrderItem) {} function handlePageChange(page: number) {} // 对外暴露的事件defineEmits const emit defineEmits{ (e: submit, payload: OrderForm): void (e: cancel): void }()这个区分对应了“内部消化”和“向外传递”两种职责读代码时看到handle就知道是本地逻辑看到on就知道是外部传进来的回调或者对外抛的事件。4.3 组合式函数统一use前缀返回值用对象不用数组hooks目录下的文件统一use开头useOrderList.ts、useTableSelection.ts、usePermission.ts导出的函数名与文件名一致。返回值我强烈建议用对象不要用数组// 推荐解构时按名取值 export function useOrderList() { const orderList refOrderItem[]([]) const isLoading ref(false) const fetchOrderList async () { /* ... */ } return { orderList, isLoading, fetchOrderList } } // 不推荐数组解构顺序错了不报错 return [orderList, isLoading, fetchOrderList]数组返回的问题很隐蔽调用方按顺序解构哪天你在中间插了一个返回值所有调用方全部错位而且 TypeScript 都不一定拦得住。对象解构按名字取加字段不影响已有调用重命名也能被类型系统发现。4.4 常量与枚举SCREAMING_SNAKE_CASE加业务域前缀// constants/order-status.ts export const ORDER_STATUS { PENDING: pending, PAID: paid, SHIPPED: shipped, COMPLETED: completed, CANCELLED: cancelled, } as const export type OrderStatus typeof ORDER_STATUS[keyof typeof ORDER_STATUS]用as const配合派生类型比手写枚举更灵活值改一处类型自动跟着变。常量名全大写加下划线业务域前缀ORDER_用来防止不同域的状态常量重名比如ORDER_STATUS和AFTER_SALE_STATUS放在同一个文件里也不会冲突。5. 类型与接口I前缀早就该放弃了用后缀区分职责5.1 类型命名不用I前缀直接用名词TypeScript 早期社区流行IUser、IOrder这种匈牙利式前缀现在基本可以放弃了。原因很实际类型名在编辑器里的辨识度靠颜色和位置就够了I前缀占字符还不携带新信息。更麻烦的是接口和类混用时IUser和User并存会让人不知道该用哪个。直接User、Order、OrderDetail清爽。5.2 用后缀表达类型职责我的习惯是用后缀区分不同类型的职责这套后缀比前缀好用得多后缀用途示例无后缀核心业务实体Order、User、ProductParams请求入参OrderListParams、OrderCreateParamsResult、Response接口返回OrderListResult、LoginResultForm表单数据OrderForm、AfterSaleApplyFormVO、DTO与后端约定的视图或传输对象OrderDetailVOProps、Emits组件入参与事件OrderTablePropsState局部状态聚合OrderFormState这套后缀的好处是看到一个类型名就能判断它是给哪一层用的也能判断它能不能直接拿去发请求。OrderForm是表单态直接提交大概率要转换OrderCreateParams才是可提交的入参。把这两者分开能省掉很多“表单里多了个字段导致后端报错”的问题。5.3 枚举值与后端字符串对齐不做二次翻译OMS 对接后端时状态字段通常是字符串或者数字编码。这里有个常见错误操作前端定义一套语义化的枚举然后在转换函数里和后端编码互相映射。代码写起来很爽一旦后端加了一个状态前端忘了同步页面就会显示空白。// 推荐常量值跟后端编码完全一致语义只在 key 上体现 export const ORDER_STATUS { PENDING: 10, PAID: 20, SHIPPED: 30, } as const // 不推荐前端自定义值再做一层映射 export const ORDER_STATUS { PENDING: pending, } as const // 然后需要 statusMap: { pending: 10 }把语义放在 key 上值老老实实跟后端对齐少一层映射就少一处可能不同步的地方。这个做法我在三个项目里推过最初都有人觉得“值不好看”但线上出过一次状态漏配的问题之后就没人再提了。6. 样式、事件与接口容易被忽略但最容易出问题的几个命名内容6.1 样式类名用 BEM配合scoped使用Vue3 单文件组件默认scoped理论上类名冲突概率不高但一旦用了:deep()或者全局样式冲突就来了。BEM 虽然不是唯一解但在中后台项目里足够稳.order-list { } .order-list__header { } .order-list__item { } .order-list__item--active { }块名与组件名对应OrderList.vue的块就是order-list。这条对应关系定下来之后样式文件里出现一个不知道属于谁的类名基本能判定是野样式。另一点要注意不要在模板里堆内联style尤其是带动态计算的内联样式排查样式问题时极难定位。6.2 自定义事件名用 kebab-case不用动词过去式defineEmits里的事件名Vue3 官方建议用 kebab-case因为模板里绑定对大小写不敏感updateValue和updatevalue可能被当成同一个。事件名建议用“动作”而非“动作完成态”submit、cancel、row-click、page-change不要写submitted、clicked。事件表达的是“发生了某个动作”由父组件决定后续处理。const emit defineEmits{ (e: row-click, row: OrderItem): void (e: page-change, page: number, size: number): void (e: selection-change, rows: OrderItem[]): void }()第三个参数用对象好处最大的时候是在事件数量超过五六个之后。参数列表超过三个调用方就开始懒得看类型声明了改成对象传参可读性会好很多。6.3 接口函数的命名与 URL 对齐api目录下的函数命名我建议“动作 资源”的结构并且与 URL 保持一定对应关系// api/order.ts export function getOrderList(params: OrderListParams) {} export function getOrderDetail(orderId: string) {} export function createOrder(data: OrderCreateParams) {} export function updateOrderStatus(orderId: string, status: OrderStatus) {} export function exportOrderList(params: OrderListParams) {}对应关系定下来之后后端给一份接口文档前端写api层基本是机械劳动不需要每次纠结叫什么。另外强烈建议api层只做请求不做数据加工。我见过在api函数里顺手把返回的列表字段从records改成list的写法一开始省事后来接口返回结构调整全项目搜索records找不到任何结果因为已经被藏进api层了。6.4 一个容易引发冲突的操作组件名和接口函数重名这个坑值得单独列出来。OrderDetail.vue组件和getOrderDetail函数本身不冲突但如果把组件命名为OrderDetail同时在同一个文件里import { OrderDetail } from /api/order如果接口层也导出叫OrderDetail的类型就会撞名。解决方式是靠后缀约定组件用 PascalCase 无后缀接口用动词开头类型用VO、Params等后缀三套体系错开之后重名概率大幅下降。7. 一次真实的重构过程从混乱命名到可维护结构7.1 改造前的状态说一个我参与过的实际场景。某订单系统运行了两年多views目录下有 200 多个.vue文件其中 40 多个叫index.vue30 多个叫list.vue或List.vue混用还有newPage.vue、temp.vue、test2.vue这种临时名字一直留到了生产环境。hooks目录里的函数有useList、getOrderData、orderFetch三种风格并存。最直接的影响是新同事上手第一周基本在问路径问完第二周又忘。改造的切入点不是先定规范文档而是先做一次命名普查。用一个脚本扫出所有文件名、所有导出函数名、所有use开头的函数统计重复率和风格分布把问题量化出来。这一步很关键因为它能把“我觉得乱”变成“40 个同名文件、3 种命名风格”说服力完全不同。7.2 分阶段改造的顺序改造顺序上有讲究不能一把梭。我的顺序是先文件、再目录、再变量、最后样式。原因是文件移动对业务逻辑零影响风险最低先把看得见的东西理顺团队会建立起信心变量改名涉及逻辑需要配合测试样式最后动因为它影响面最广但优先级最低。具体做法是每个业务域单独一轮。第一轮只重命名文件把index.vue改成OrderList.vue这类具名文件同步改路由引用跑一遍构建确认没问题。第二轮调整目录结构把散落在views根下的业务文件收进业务域目录。第三轮才是变量和方法改名这一轮要格外小心改一个跑一次页面。7.3 重构中遇到的真实问题有两个问题值得记下来。第一个是路由配置的name与组件名不一致。改文件名时只改了文件没改路由name结果KeepAlive的include全部失效页面缓存莫名其妙丢了。排查时以为是KeepAlive用法问题查了很久才发现是name没同步。所以改文件名时路由name、组件name、KeepAlive的白名单这三处必须一起改可以列成一张检查清单贴在重构任务里。第二个是工具函数被多个业务域引用改名引发连锁修改。有个formatDate被 60 多处引用团队决定改成formatDateTime以明确语义结果漏改了动态拼接的引用比如通过字符串拼出来的方法调用上线后才发现。教训是大范围改名别靠手工搜索替换用带类型检查的 IDE 重命名功能改完跑一遍全量类型检查比肉眼扫靠谱得多。7.4 改造后的收益改造完成后最直观的变化是路径查找时间从“问人”变成“推导”。全局搜索的命中率也上来了搜AfterSale能精准找到相关文件因为不再有一堆index.vue稀释结果。还有一个意外收益新功能的文件放置位置变得有共识了以前每个需求都要讨论一下放哪现在按业务域和类型两个维度一推就定了需求评审时能省掉一部分扯皮。8. 让规范真正落地检查、工具与团队约定8.1 用 ESLint 把能自动化的部分自动化规范写在文档里没人看写进 lint 才有人执行。文件命名可以用eslint-plugin-filename-rules或者项目里自定义的脚本在 CI 阶段校验组件名可以用vue/component-definition-name-casing、vue/multi-word-component-names这几条规则兜住。变量命名的语义部分 lint 管不了但大小写风格能管typescript-eslint/naming-convention配一下camelCase用于变量、PascalCase用于类型、UPPER_CASE用于常量风格层面就统一了。{ rules: { vue/component-definition-name-casing: [error, PascalCase], vue/multi-word-component-names: error, typescript-eslint/naming-convention: [ error, { selector: variable, format: [camelCase, UPPER_CASE] }, { selector: typeLike, format: [PascalCase] }, { selector: interface, format: [PascalCase], custom: { regex: ^[A-Z][a-zA-Z0-9]*$, match: true } } ] } }配置的时候注意别一刀切UPPER_CASE得给常量留着PascalCase得给组件导入留着否则会冒出一堆误报团队第一天就把它关掉了。lint 规则上线前先在存量代码上跑一遍看误报率误报多的规则要么调参数要么先设成warn。8.2 Code Review 只盯三类命名Review 时逐条对规范不现实我只盯三类新增的页面文件是否具名、新增的组合式函数是否use开头且返回对象、新增的布尔变量是否有语义前缀。这三类覆盖了最容易复发的命名区域其余交给 lint。这样 Review 成本可控也不会因为“规范太多”而被抵触。8.3 规范文档要写“反例”规范文档里只写推荐写法团队记不住写上反例和反例带来的具体问题记得住。比如“不要用index.vue因为编辑器标签页无法区分、全局搜索命中率低、chunk 名无语义”这三条理由比一句“建议具名”有用得多。文档放在仓库里用 Markdown 写跟代码一起 review比放在内部 wiki 上更容易保持更新。还有一点经验规范不要一次定太多条。第一版控制在 20 条以内覆盖文件和变量这两块最高频的区域跑三个月再补。定 60 条的结果通常是全部被忽略因为没人记得住也没人有精力在 review 时逐条核对。8.4 新人上手时的引导方式新同事进项目第一周不要求他背规范而是让他做一件具体的事给一个已有业务域补一个列表页。目录结构、文件命名、变量命名、接口函数命名这四件事在补页面的过程中会全部走到做一遍比看十遍文档有效。做完之后由老同事 review 一次把不一致的地方指出来这一轮下来基本就形成了肌肉记忆。我试过几次效果比开会讲规范好得多而且新人也更容易接受因为他是在做具体的事而不是在听抽象的要求。最后分享一个我在实际项目里养成的习惯每接一个新需求先花两分钟想清楚这个功能落在哪个业务域、涉及哪几类文件、每类文件叫什么名字写进需求卡片里再动手。这两分钟能省掉后面返工改名的麻烦也让 review 的人一眼看出命名是否符合规范。命名这件事想在前面的成本永远低于事后重构的成本。