资讯详情

资讯详情

前端命名规范:变量、函数、组件与文件命名最佳实践

刚接手一个维护了很久的前端项目时最让我头疼的不是复杂的业务逻辑而是一堆看似合法却毫无信息量的名字data1、tmp、flag、aa()、temp2.vue。这些标识符每个都能通过编译每个都能正常运行但当你需要修改它们时就进入了漫长的猜谜环节。今天分享的内容是我们团队最终沉淀下来的一套前端命名规范覆盖变量、函数、组件、文件四个维度目标是让项目里不再出现“看到名字要猜半天”的情况。这套标准不绑定具体框架Vue、React、原生 TypeScript 项目都可以按需裁剪后直接用。如果你正在写个人项目或者正想给团队立一套可执行的规范这篇应该值得你花几分钟看完。1. 命名混乱的真实代价一次 20 分钟的排查1.1 一个flag引发的谜题先说一个具体的例子。某个模块里有一段逻辑页面进入时请求订单数据请求成功后展示列表失败时展示空状态。接手的人看到代码里有三个变量flag、flag2和result。第一个flag在初始化函数里被设为true第二个flag2在异步回调里被设为false第三个result一会儿是数组一会儿是null。我当时为了搞清楚“现在到底处于什么状态”打了不下十个断点来回对比各个函数的执行顺序最后花了约二十分钟才理顺。把名字改成isListRefreshing、hasLoadedOrders、orderList之后整个函数几乎不用注释就能读懂了。这样的场景在真实项目里一点都不罕见。变量本身没有语法错误错的只是它传递的语义信息太少。排查问题的大多数时间并不是花在“改代码”上而是花在“理解这段代码原本想干什么”上。命名就是驱动理解的最关键线索一个模糊的名字会直接把后来者带进沟里。1.2 命名规范本质上是沟通约定有人认为命名规范是“美感问题”是“洁癖”优先级不高。我的看法正好相反它是实打实的沟通成本问题。一个项目少则几万行多则几十万行代码写一次可能只要几分钟但这段代码会被 review 一次、被维护者阅读几十次甚至上百次。好的命名让每次阅读都少猜一次坏的命名让每次阅读都多堵一次。统一的命名标准还有一个隐形价值减少团队讨论成本。如果没有标准每次 Code Review 都可能因为“这个名字好不好”争论几分钟有了标准之后只需要对照规范说“这个违反了第几条”讨论就结束了。这也是为什么规范一定要写成文字而不是只停留在口头约定——没有文字过两周就有人开始自由发挥。规范的本质不是审美而是可预期的约定大家统一说同一种语言代码的认知负担才会真正降下来。2. 变量命名先统一“数据是什么”再谈风格2.1 普通变量从data1到latestOrderList普通变量是出现频率最高的一类命名也是混乱的重灾区。我给团队定的第一条原则是命名必须回答“这个数据是什么”而不是“这个数据是什么类型”。data、info、list这类词没有回答“什么”所以是典型的无效命名。data1、data2更糟它们连“哪个数据”都没说清楚还引入了序号这个额外维度序号一旦多了根本分不清谁是谁。再往后一层命名还要尽量带上“限定信息”。如果是接口返回的用户数组userList比users更精确也比res.data.list强一万倍如果是当前选中的订单currentOrder比order多了一层限定在较长的函数里不容易产生歧义。变量名建议使用完整单词或约定好的缩写不要自己发明缩写。比如btn也许还可以接受bt就没人看得懂。大小写方面普通变量统一使用 camelCase。TypeScript 的对象类型、React 组件这种构造函数使用 PascalCase接口返回的字段如果是 snake_case在边界处转换一次不要让user_id这种风格渗透到组件内部。对象属性命名同样遵循 camelCase和变量保持一致。如果从后端拿到的是user_id这种字段在接口层做一次 DTO 转换让业务代码始终使用userId后续 ESLint 规则也更容易统一落地。2.2 布尔值、集合与映射前缀和后缀的固定约定布尔变量比较特殊它的值只有true和false必须在名字里直接体现判断语义。我推荐使用is、has、can、should、need这类助动词开头isLoading、isSubmitting、hasPermission、canEdit、shouldRender。看到is你知道是“是否”看到has你知道是“有没有”看到can你知道是“能不能”。这种助动词前缀等于给布尔变量盖了个章调用者不需要再猜。不要用flag这种无意义名更不建议用反向布尔比如isNotLoggedIn。虽然它语法正确但使用时必然出现if (!isNotLoggedIn)这种双重否定的代码读起来要转两个弯。写成isLoggedIn之后取反就是if (!isLoggedIn)思路顺得多。这点在 Code Review 里很常见值得专门写进规范。数组和集合的命名我推荐在语义名词后加List或直接用复数。比如userList、orderList如果强调去重后的集合可以用userSet但这种情况不多见。映射表用xxxMap或xxxRecord后缀比如statusTextMap表示状态码到文案的映射orderIdMap表示 id 到订单的映射。看到后缀调用者就知道它是数组还是 Map这在很大程度上避免了类型混乱。类型推荐形式正例反例普通数据camelCase语义名词 限定词latestOrderListdata1布尔值is / has / can / should 前缀isSubmittingflag数组复数或 List 后缀userListarr映射表Map / Record 后缀statusTextMapobj常量UPPER_SNAKE_CASEAPI_BASE_URLapiHost2.3 常量、临时变量与缩写细节里面藏着大坑跨模块共享的配置常量我建议统一用 UPPER_SNAKE_CASE比如API_BASE_URL、MAX_RETRY_COUNT、DEFAULT_PAGE_SIZE。这类常量是全局的“定死”的值大写在视觉上就能跟普通变量区分开。模块内部不可变的配置对象或枚举映射用 camelCase 也不会出大问题因为它的作用域小上下文足够清晰。TypeScript 的enum成员用 PascalCase如enum OrderStatus { Pending, Paid, Cancelled }这样在 IDE 提示里更整洁。临时变量是另一个容易翻车的地方。循环里用i、j是公认的例外可以保留除此之外尽量少用tmp。如果临时变量的生命周期只有两三行currentItem也比tmp清楚得多。实际上很多tmp在使用中会被传进函数、存进数组慢慢变成全局状态名字却一直是“临时”等到排查时已经说不清它是谁了。缩写也要有度。btn、img、conf这类在行业里已经约定俗成的缩写可以用但自创缩写要尽量避免。尤其是拼音缩写yhxx、btList这种名字除了作者本人当时可能明白三个月后连作者自己都得猜。宁可名字长一点也不要让语义丢失。命名是写给未来维护者看的而那个未来维护者大概率就是忘记上下文的你自己。3. 函数命名动词前缀把调用点变成一句话3.1 高频动词前缀及其语义边界函数命名最实用的方法是建立一个团队内部的“动词前缀字典”。动词不是越多越好而是要固定下来字典里有的动词别再自创同义词。比如已经有fetch就不要再写getRemoteList、queryData混着来否则调用者每次都要猜它们的差异。前缀语义边界示例get从内存、缓存或已加载数据中同步读取getUserById(id)fetch发起异步请求从远程获取数据fetchUserList()load在初始化阶段拉数据并写入状态loadInitialData()find按条件查找可能返回 undefinedfindUserByEmail(email)compute基于已有数据做同步计算computeTotalPrice(items)create / build构造新对象或新实例buildOrderDraft()save / update / delete对持久层或状态做写操作updateUserProfile()handle绑定在 UI 事件上的处理函数handleSubmitClick()to / format格式转换或序列化formatDate()、toPascalCase()validate校验并返回结果validateInputValue()函数名应该是一个动作短语能够让人在调用点直接理解“它在做什么”。我经常用一个比喻函数名是菜单上的菜名看到“鱼香肉丝”你就知道主料是什么看到“蚂蚁上树”你还要去问服务员。我们的目标是让函数名都尽量是“鱼香肉丝”而不是“蚂蚁上树”。3.2 事件处理与回调on和handle的分工关于事件处理团队里最容易争论的是on和handle到底怎么用。我的建议是内部实现函数用handleXxx对外暴露的回调接口用onXxx。比如组件内部点击保存按钮时写handleSave把它作为 props 传给子组件时叫onSave。这样一眼就能看出onSave是这个组件对外提供的钩子handleSave是组件自己响应交互的逻辑。Vue 项目中自定义组件向外 emit 事件时也可以用onXxx作为 prop 名模板里的事件监听clickhandleSave保持内部实现是handle风格。如果全项目都写成onSaveClick或handleSaveSubmit也没问题关键是统一。一旦定了就不要再出现一半onXXX一半handleXXX混着用的状态那才是真正让团队崩溃的地方。还有一类容易混淆的是“改变状态”和“读取状态”。如果函数会修改外部变量或状态建议用set、update、save等明确写出写操作如果它只是计算并返回结果用get、compute这种纯函数语义。纯函数与副作用函数的边界一旦清晰调用者就知道调它会不会引发状态变化排查 bug 时能省不少事。3.3 函数命名的四个反模式函数命名的反面例子值得专门列出来说说。第一种是完全没有动作语义的名字aaa()、fn()、deal()。deal这个词在英文里太抽象没有具体指向不如直接用handle、process或者更具体的submit、parse。第二种是拼音缩写yhList、chuliClick看起来省事但对外籍同事或未来接手的任何人都是灾难。第三种是动词太泛doTask、processData、handler这类基本等于没说。一个叫processData的函数可以处理任何数据也可以什么都不处理它没有传递给使用者任何有效信息应该拆成parseResponse、transformUserList这种具体描述。第四种是一个函数里塞了多个动作loadAndRenderAndNotify。这种名字等于在说“我什么都干”但什么都干恰恰让调用者很难判断它返回什么、副作用是什么。更好的做法是拆成loadData、renderList、notifyResult三个函数在外面用一个流程函数组装它们。命名不只是起名它还倒逼我们设计更合理的函数拆分。4. 组件命名按层级与颗粒度给名字4.1 基础、业务、页面三个层级的命名范式组件命名要遵循一个原则看到组件名就知道它属于哪一层、是做什么的。我习惯把组件分成三层基础组件、业务组件、页面组件。基础组件是通用 UI 元素如BaseButton、BaseInput、AppHeader、AppFooter。它们放在components/base目录改动影响面大所以命名要低调、通用不携带具体业务语义。业务组件与具体业务强相关如OrderCard、UserProfileCard、PaymentStatusBadge名字里直接带模块名这样任何人在 IDE 里看到OrderCard就会立刻猜到它是跟订单卡片相关的组件。页面组件是路由级别的如OrderListPage、UserCenterView建议统一加Page或View后缀方便在 IDE 中与普通组件区分。对于高阶组件业界约定用with前缀如withLoading、withAuthHooks 一定用use前缀如useAuth、useOrderDetail。这些不是装饰而是让人一看到调用方式就能猜出它是个什么东西。with开头的函数返回组件use开头的函数使用 Hooks 语法这是行业里形成的默契不建议打破。4.2 组件文件与组件名强一致组件文件名的最大问题是不一致。有的文件叫order-card.vue有的叫OrderCard.vue甚至同一个项目里userCard.jsx和User-Card.tsx混杂。组件名在代码里是 PascalCase文件名就应该和它强一致OrderCard.vue导出OrderCard。这样做的好处是import 时不会出现“导入的名字和文件名对不上”的情况IDE 跳转也更直接。Vue SFC 里export default { name: OrderCard }也要和文件名保持一致。这个name在 devtools 调试时会显示出来如果不一致调试时会出现“明明在打开OrderCard.vuedevtools 里却显示OrderListCard”这种奇怪的状况给自己增加不必要的困惑。React 函数组件虽然没有强制要求displayName等于文件名但组件名本身和文件名保持同构同样能降低心智负担。4.3 props、事件与 hooks组件接口的可预知性props 的命名同样要遵守变量规则尽量语义化。布尔 props 用isXxx、hasXxx、canXxx前缀事件回调 props 用onXxx。给组件传对象时不要叫data按内容叫user、order、config都要好得多。组件内部不要把外部传入的 props 直接改名成data再各处使用这会切断可追踪性——从父组件到子组件同一个数据应该始终叫同一个名字。Vue 3 中事件名在emits里声明后模板中用 kebab-case 监听逻辑上要和回调名保持一致React 中自定义组件事件统一用onVerb结构。这些细节看似很多其实都指向同一个原则接口要让调用者无法猜错。你写下一个OrderCard :orderorder submithandleSubmit /读者甚至不需要翻组件定义就能大概知道这个组件接收什么、触发什么。5. 文件与目录命名让 import 路径也“自解释”5.1 不同文件类型的命名规则文件命名的混乱通常发生在工具文件和组件文件混在一起的时候。我的建议是组件文件、页面文件统一用 PascalCase例如OrderCard.vue、UserCenterPage.vue工具函数、纯逻辑模块统一用小写开头的 kebab-case 或 camelCase例如format-date.ts、getUserStatus.ts类型定义文件用xxx.model.ts或xxx.d.ts常量集中文件用constants.ts。有时保留框架默认约定也很重要。比如 Vue 官方风格指南允许 kebab-case 文件名React 社区则普遍用 PascalCase。如果你在维护一个已有项目不必推翻原有习惯但要在文档里明确“项目内必须二选一”并且老代码逐步对齐。最怕的是同一个目录里三种风格并存那 import 语句会非常混乱搜索文件也要多试好几次才能命中。5.2 目录结构按类型聚合还是按领域聚合目录结构我强烈建议优先按领域模块聚合而不是按文件类型堆叠。纯按类型分的目录早期很清爽components/、utils/、hooks/各放一类。但业务增长后一个订单功能会散落在components/orderCard、hooks/useOrder、utils/orderStatus、views/orderList等多个位置改一个需求要在三四个目录间跳转这本身就违背了“高内聚”的原则。按领域聚合之后订单所有相关代码放在features/order下内部再分components、hooks、utils内聚性会好很多。下面是一个我比较常用的结构src/ app/ components/ base/ ui/ features/ auth/ order/ components/ hooks/ utils/ OrderListPage.tsx shared/ constants/ types/ styles/目录命名建议统一 kebab-case。features下的模块名也要和路由 path 保持可对应关系比如order对应/order/...这样从 URL 到目录、到文件都有一个可预测的路径。新加入的成员只要知道业务模块名就能顺着路径找到对应代码而不是在全局搜索框里碰运气。5.3 资源文件、路由路径与测试文件静态资源文件同样值得统一图片、图标用 kebab-case如icon-arrow-left.svg、bg-home2x.png不要出现IMG_2024.png和微信图片_xxx.jpg这种直接扔进仓库的资源名。资源文件的“可搜索性”也很重要命名里带上用途关键词后续替换素材时才能快速定位。路由文件方面path 统一 kebab-case/user-center而不是/userCenter动态段用:orderId页面组件名与路由 chunk 名可以约定OrderCenterPage和webpackChunkName: order-center对应调试时看 network 面板会省很多时间。测试文件放在被测文件同目录用.test.ts或.spec.tsx后缀命名对应被测文件useAuth.test.ts、OrderCard.spec.tsx。这个约束一旦形成跑测试时定位文件就能比手更快。6. 让规范真正落地的三个抓手6.1 用 ESLint 强制命名规则再好的规范如果只写在文档里执行效果都会打大折扣。所以我会用 ESLint 把一部分命名规则变成自动化检查。最基础的是camelcase规则强制变量和函数使用 camelCase。如果项目里用 TypeScript可以再加typescript-eslint/naming-convention它能针对变量、函数、类型、枚举成员分别设置格式。// .eslintrc.cjs module.exports { rules: { camelcase: [error, { properties: never }], id-length: [warn, { min: 2, exceptions: [i, j, k, x, y, z] }], typescript-eslint/naming-convention: [ error, { selector: variable, format: [camelCase, UPPER_CASE] }, { selector: function, format: [camelCase] }, { selector: typeLike, format: [PascalCase] }, { selector: enumMember, format: [PascalCase] }, ], }, }注意camelcase的properties: never表示不强制对象属性名因为接口返回的数据有时会带 snake_case 字段。如果你希望对象属性也统一可以改成properties: always但要做好边界转换。id-length用来拦截a、b、tmp这种过短变量名循环变量除外。配合husky和lint-staged在提交前自动跑一次约定就真正变成了项目基础设施——代码根本走不进提交记录命名问题在源头就被拦住了。6.2 Code Review 里的命名检查清单自动化规则能拦一部分问题比如大小写和长度但“命名是否有语义”这种事机器很难判断还得靠人。我在团队里用的是一张很简单的检查清单不复杂但很实用检查点通过标准常见反例变量名一眼能看出语义data2、flag、tmp布尔变量is / has / can / should 开头loginStatus、judge函数名动词开头且动词与实现一致fn()、doSomething组件名PascalCase能看出层级和用途card1、Item文件名与组件名或导出名一致order-card.vue里导出OrderCard缩写使用全称或团队词典内缩写yhxx、btReview 时不必面面俱到每次重点过一遍表格中的关键项就能拦住八成命名问题。我个人的习惯是看到反面例子就在评论里直接贴出“建议改成xxx”而不是只写一句“命名不好”。给出明确预期答案讨论效率会高很多。6.3 旧项目渐进式改造从最痛的地方开始老项目改造最大的误区是“今天开始全面重命名”。这样做很容易引发大规模合并冲突还会把 git 历史搅乱因为每次重命名都是一次全文件 diff后续排查问题时 blame 也失去了意义。我的建议是新代码从今天起强制按新规范写旧代码按“痛感排序”逐步改。优先重命名那些高频修改、每次需求都要打开的文件核心页面组件、公共工具函数、状态管理模块。改动方式上先在 IDE 里用 rename symbol 批量改引用再手动检查。把“重命名”和“行为修改”分成两次提交这样出问题时可以快速定位是命名重构引入的问题还是业务逻辑本身变了。还有一条心得文档一定要短。写一本厚规范没人看写一张速查表大家会用。把常见正例、反例、禁用词列成表格贴到 README 或团队空间新人加入时十分钟就能看完而不是翻五十页手册。我自己在多个项目里试过让规范自然生长也试过强制推行最后发现最好的状态是“半自动约束 轻量审查”。半自动靠 ESLint轻量审查靠 Code Review 清单。只要团队里没人想当那个 PR 上看不懂命名的人规范就会慢慢变成默认值。最后再分享一个小建议当你看到一段命名很混乱的代码不要只说“命名不好”而是直接说出“它应该叫什么名字”。把这个预期答案写下来往往比讨论原则更有用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →