前端代码规范落地:从命名组件到ESLint+Prettier+Husky

发布时间:2026/9/7 20:01:15
前端代码规范落地:从命名组件到ESLint+Prettier+Husky 前端代码规范光看这几个字容易联想到形式主义。可我这些年最绝望的瞬间基本都是在一个没有规范的前端项目里救火想改一个筛选条件发现同一份逻辑在三个文件里各写了一遍接口字段直接从拼音写成变量名加需求要同步改四处漏一处线上就出事。前端代码规范不是用来约束写代码的自由而是用来保证项目能持续迭代、能换人接手、能在三个月后你自己还能看懂。这篇文章我会从“垃圾代码到底贵在哪”讲起然后聊命名、组件拆分的实际原则再把 ESLint、Prettier、Husky、Commitlint 这一套落地工具链串起来最后说老项目怎么在不推翻重来的前提下一步步净化。不管你是在带团队、刚接手旧项目还是一个人维护开源项目这中间的坑和方案应该都用得上。1. 垃圾代码不是丑是贵1.1 规范先解决的是协作成本不是审美洁癖很多团队把代码规范理解成“代码风格统一”这是最浅的一层。真正让项目变慢的往往是命名混乱、职责不分、状态满天飞、功能靠复制粘贴这些并不会被缩进问题暴露出来但会在每次需求变更、每次 bug 排查里持续消耗时间。我见过最典型的情景一个页面组件几千行里面同时管着表格数据、弹窗开关、表单校验、权限判断、埋点上报。新同学接手时完全不敢动因为改任何一个变量都要全局搜索、反复确认。到最后大家只能绕着走新需求不敢往老组件里加就在边上再包一层久而久之组件外面套组件连入口都找不到。代码规范的意义是把“看起来能用”变成“以后也好改”。可读性就是可维护性可维护性就是上线效率和线上稳定性。这不是哪个团队的文化偏好而是代码一旦超过单人的复杂度后必然要付出的组织成本。1.2 垃圾代码的几个典型画像先说“垃圾代码”这个词它不只是骂人它是一种可以总结、可以量化的现象。拿我自己的经验来说最常见的四类函数越来越长、职责越叠越多。一个点击事件里先改数据、再调接口、又跳路由、还更新 store逻辑线全缠在一起。命名完全放弃沟通。接口返回什么字段前端就原样叫什么data1、res.data.list.rows[0].name这类代码到处都是。复制粘贴式实现。因为不敢动公共逻辑就在新页面里把一段代码原样粘一遍微调两行参数就完事。死代码和注释掉的代码长期堆积。留着既不删也不敢删时间长了谁也不知道这块到底还有没有在用。这类代码之所以可怕是因为它们会自我繁殖。当项目里有了第一段没人看得懂的代码后续人写新逻辑时最省事的办法就是学它而不是改它。规范要管的正是这条自我繁殖的路径。1.3 垃圾代码的成本最终落在哪些环节需求估时改一个看似很小的点因为牵扯不清越估越大排期没法信任。Bug 排查线上出问题定位原因的时间比修复时间高一个数量级。人员流动老人一走新人看代码看不懂只能继续打补丁技术债越滚越高。需求质量多个地方逻辑不一致同一个状态在不同页面的表现不一样用户感知就是“这系统怎么这么乱”。说到底垃圾代码不只是“代码丑”的问题是它直接决定了你的团队还有多少余力做真正的新功能。2. 命名、分支和提交信息把意图留在代码库里2.1 变量和函数命名先让名字把话说清楚命名这件事最容易被当成“小事”但它其实是在代码库里建立共识的第一道门槛。我给自己定的原则很简单如果一行代码需要看上下文才能猜出含义那这行代码就有改名的空间。比如布尔值的命名用is、has、can这类前缀一眼就明白不要叫flag更不要叫tmpFlag。数组变量能直接说不清是“用户列表”还是“订单列表”就不要叫data或arr叫userRows、orderList、availableOptions都行关键是看到变量名就能知道它装的是什么。函数命名应该尽量使用动词。getUserInfo、calcTotalPrice、handleSubmit都比initPage、doAction这种有信息量。尤其处理事件的函数用handleXxx开头比如handleSearchClick、handleDeleteConfirm事件意图就清清楚楚。有些团队觉得英文单词不够高级喜欢用缩写。实际上除非是项目内部约定好的领域缩写否则我宁可你写长一点也不要让后人去猜。代码是写给编译器背后的人看的不是给编译器看的。2.2 代码分支命名不要一上来就 feature-1分支命名规范最有意思的地方在于它能直接暴露一个团队的协作习惯。我接手过一些仓库分支名长这样dev2、test3、update_0628、123456。等要提测、要回溯某个功能分支合并过什么内容时完全无从查起。真正有效的主干分支命名只要一条规则类型前缀 描述遇到关联单号就带上单号。具体前缀可以按团队的流程约定一般用下面这套就够了前缀使用场景示例feat/新功能feat/user-loginfix/修 Bugfix/order-refund-errorrefactor/重构不改行为refactor/list-componentchore/构建、依赖、配置类调整chore/update-vitedocs/文档改动docs/readme-usagetest/测试相关test/utils-caseperf/性能优化perf/table-render分支描述尽量短但又不能短到没有语义。feat/user-payment就比feat/feature2强得多。也不要在分支名里写日期日期在合并记录和提交记录里都有没必要额外占用名字。2.3 提交信息Commit 是写给未来同事看的文档Git 提交信息写得烂很多团队一开始不当回事等要写 release note、要按功能回溯代码、要找“哪个 commit 引入了线上 Bug”时才会发现 git log 里全是update和fix根本没法用。业界最通用的规范是 Conventional Commits格式是type(scope): subject我的习惯是结合中文描述让类型承担“这是哪类改动”让 scope 表示“改的是哪个模块”subject 用一句话说清“为什么要这么改”。例如feat(order): 增加退款原因下拉选择 fix(cart): 修复库存校验放在提交前导致的下单失败 refactor(user): 抽出登录表单为公共组件这样提交记录每一条都可以独立阅读能够快速理解这次提交的影响范围。配合 IDE 里的 Git 面板你甚至能很轻松地从某一行代码 blame 到对应的需求背景。3. 组件和逻辑层别把页面写成大泥球3.1 组件怎么拆才算拆对了组件拆分没有唯一解但有一个很实在的判断标准如果这个组件被复制到另一个页面需要连带复制多少依赖才能工作复制过程中依赖越少说明它的内聚度越高边界越合理。反过来很多人为了“抽象”而抽象把几乎不会复用的代码硬拆成十几个小组件结果组件之间靠一堆 props 传递状态数据流比原版还绕。这种“过度组件化”和“巨型组件”其实是两个极端都会让项目失去平衡。我建议用三个维度来切功能性拆分层一个组件只解决一个领域问题。比如“订单表格”管订单展示“门店选择器”管门店选择而不是搞一个“综合业务组件”。复用频次决定边界只有可能在两个及以上地方使用的 UI才值得作为独立组件维护只在某一个页面里用到的块状结构先作为页面内部的局部组件存在就好。状态归属决定组件位置如果一个状态只影响当前 UI就别把它放到外层或全局 store 里。换句话说先让代码在“看起来整洁”和“真正能复用”之间找一个务实的位置比背再多组件设计原则都有用。3.2 业务逻辑要能从组件里搬出去光把 UI 拆细还不够。真正的“屎山”往往是业务逻辑全塞在组件生命周期里比如进页面发请求、筛选时再发一次、翻页时又写一遍 loading 状态这类过程逻辑会无限膨胀。在 React 里我们用 hooks 抽逻辑在 Vue 里我们用 composables 抽逻辑思路完全一致把“数据和请求状态”从组件视图代码里剥离出去让组件只做绑定。比如说一个通用的分页列表加载逻辑可以抽成这样function useList(api, { defaultPageSize 10 } {}) { const [list, setList] useState([]); const [loading, setLoading] useState(false); const load useCallback(async (params {}) { setLoading(true); try { const response await api(params); setList(response.rows || []); } finally { setLoading(false); } }, [api]); useEffect(() { load(); }, [load]); return { list, loading, reload: load }; }在组件里使用时就非常清爽const { list, loading, reload } useList(fetchOrderList, { defaultPageSize: 20, });以后如果你想在列表加载前加缓存、加错误重试只要改这个 hook/composable所有用到的地方都跟着改。这就是把逻辑从组件里搬出去带来的杠杆效应。3.3 全局状态要克制但跨页数据必须收口很多团队用状态管理库时容易把大量页面级状态丢到全局 store。这样短期省事时间长了会造成两个问题一是任意组件都能改全局状态数据来源不清晰二是改动一处订阅它的页面全部要重新渲染性能隐患也随之而来。我的经验分三层来处理组件内部状态能解决的问题坚决不上升到全局。父子关系状态用 props 和事件通信能少用全局就少用。多页面共享、需要长期缓存的登录态、用户信息、权限数据、基础配置才放进全局 store。真遇到跨组件共用的异步数据可以直接封装成 hook/composable在内部完成请求和缓存管理。调用方拿到的是“直接用”的接口不感知数据存在哪里以后调整实现也很方便。3.4 注释要解释“为什么”而不是把代码翻译一遍最后补一点关于注释的。垃圾代码里的注释最常见的是把代码的每个动作翻译成中文比如// 设置 loading 为 true setLoading(true);这种注释对理解毫无帮助代码本身已经表达清楚了。更有价值的注释长什么样是记录代码为什么要这么写、历史上有过什么坑// 这里不能用 includes因为 oldOptions 可能包含 null // 之前用 includes 导致空值选中的 Bug这样的注释保留的是长期维护过程中的“决策上下文”比重复描述逻辑要有价值得多。前端代码规范的最后一层其实是学会给未来读代码的人留有效信息。4. 把规范焊死在流程里ESLint、Prettier、Husky、Commitlint4.1 ESLint 不是用来教育人的是用来拦截的靠 code review 来检查每一处代码风格既不现实也不公平因为人总有漏看的时候。真正靠谱的办法是所有规则在合入之前就被工具自动拦截ESLint 就是最合适的检查器。一个基础可用的 ESLint 配置大概长这样关键点是区分“负责代码正确性的规则”和“负责代码风格安全的规则”module.exports { root: true, env: { browser: true, es2022: true, node: true, }, extends: [ eslint:recommended, plugin:prettier/recommended ], parserOptions: { ecmaVersion: 2022, sourceType: module, }, rules: { no-unused-vars: error, no-debugger: warn, no-console: warn } };如果你用的是 Vue可以额外引入eslint-plugin-vue和vue-eslint-parser如果用的是 React可以一起配eslint-plugin-react-hooks。建议不要图快直接复制一整套很重的配置规则最好一条条增补不然团队会陷入“全是报错却不知道先改哪条”的状态。4.2 Prettier 统一格式风格问题不要留给人吵架格式问题包括缩进、单双引号、分号、换行、尾逗号这类东西没有任何争论价值。项目里引入 Prettier 后所有人的编辑器在保存时都自动格式化大家都按同一份配置输出diff 会干净很多。一份常用的.prettierrc{ semi: true, singleQuote: true, printWidth: 100, trailingComma: es5 }然后把 format 脚本和 lint 脚本接在 package.json 里{ scripts: { lint: eslint src --ext .js,.jsx,.ts,.tsx,.vue --max-warnings 0, lint:fix: eslint src --fix, format: prettier --write \src/**/*.{js,jsx,ts,tsx,vue,css,scss,json,md}\ } }Prettier 与 ESLint 的关系也简单ESLint 管代码质量规则Prettier 管格式风格。两者结合时用eslint-config-prettier把 ESLint 里面和格式冲突的规则关掉避免互相打架。4.3 Husky Lint-staged提交代码前自动拦一道工具配置好了如果只靠人手动跑npm run lint很快就会被忽略。更好的做法是在 git commit 之前自动执行检查。Husky 负责挂载 git hooksLint-staged 负责只检查暂存区里改过的文件避免全仓库检查产生大量无关报错。当前版本 Husky 的初始化大概是这样npm i -D husky lint-staged npx husky init初始化后会在.husky目录里生成pre-commit文件改成npx lint-staged然后在.lintstagedrc里配置{ *.{js,jsx,ts,tsx,vue}: [eslint --fix, prettier --write], *.{css,scss,less}: [stylelint --fix], *.{json,md}: [prettier --write] }这样每次 commit 前只会处理你暂存区里的文件。假如团队有人想靠提交来“蒙混过关”一旦不满足规则git commit 就会被直接打断规则也就不再是可有可无的建议。4.4 Commitlint 管提交信息CI 管分支规则提交信息的纪律最好也让工具强制起来。装commitlint/cli和commitlint/config-conventional在.commitlintrc里写{ extends: [commitlint/config-conventional] }然后在.husky/commit-msg里执行npx --no -- commitlint --edit $1之后fix、feat这类开头之外的信息都会被拦下来。别小看这一步它能让整个团队的 git log 变成可查询、可回溯的结构化数据。分支命名这类规范放不到本地 hook 里因为团队成员的本地分支不一定跟远端一致更合适的是在 CI 上加一个 job。只要是 merge request 触发的流水线就对当前分支名做正则校验#!/usr/bin/env bash BRANCH_NAME$(git rev-parse --abbrev-ref HEAD) if [[ ! $BRANCH_NAME ~ ^(feat|fix|chore|refactor|docs|test|perf|style|release)\/ ]]; then echo 分支名不符合规范必须以 feat/、fix/、chore/、refactor/ 等开头 exit 1 fi一开始团队可能会有人不习惯但跑两周之后就会发现代码回顾时本来要花五分钟弄清楚的分支来源现在看名字就懂了。5. 老项目能救吗能但要讲究策略5.1 先定“新增代码红线”再聊存量清理接到一个已经烂了很久的老项目千万不要一上来喊着“推倒重来”。我的经验是先定一条红线从今天开始合入主干的增量代码必须通过 ESLint、Prettier、Commitlint 这套检查存量代码暂不强求。为什么让整个仓库一次性清零风险太大可能引起大量无关改动没法 review线上回归成本也极高。红线策略反而可行因为它把“必须改”的边界缩小到了正在动的地方新代码质量立刻得到控制老代码留着慢慢消化。之后再做“顺手清理”你改到哪个文件就把这个文件里明显的问题顺手修掉比如删掉无用变量、把明显重复的片段抽成函数、加上缺失的注释。每次改的金额不需要很大坚持一两个月就能看到效果。5.2 Code Review 不是找茬是一起兜底工具能把代码里“客观规则”的部分拦住但“业务理解是否准确、设计是否合理”这类问题还是得靠人。Code Review 容易走到两个极端一种是无人 review 流于形式一种是评论区变成个人审美交战。我比较推荐 pull request 里重点带着这几个问题检查检查项核心问题常见踩坑功能正确性是否覆盖异常分支只测了成功路径没看没有数据、接口报错、重复提交命名语义名字能否表达意图变量缩写太多、事件处理函数没有 handle 前缀组件边界是否把无关逻辑塞进同一层组件里混着接口请求、路由跳转、store 修改重复代码是否存在第三处复制粘贴新功能实现习惯性照抄老代码错误处理是否有 loading、错误提示、降级方案接口失败时页面空白或者无限 loading在 review 里提意见时尽量把“你不该这么写”改成“这里如果这么做下次改的时候会不会更好”。人都要面子指出问题的同时给出依据和替代方案讨论氛围就会好很多。5.3 我踩过的几个坑和最后的小提醒这套体系看着完整实际落地时会遇到不少细节问题。我把踩过的坑整理成几条给你当预排查清单Husky 不生效先检查git config core.hooksPath确认指向.husky如果初始化后.husky/pre-commit没有执行权限在类 Unix 系统里用chmod x .husky/pre-commit修一下。ESLint 和 Prettier 频繁冲突确认 extends 的最后一项是plugin:prettier/recommended它能帮你关掉 ESLint 中和 Prettier 重复的格式规则。只靠 lint-staged 会有漏网lint-staged 只会查暂存区文件如果之前已经提交的代码有问题它不会主动暴露所以必须配合 CI 上的全量检查才能保证主干干净。规则全开导致团队抵触新项目可以一开始就开“error”级规则老项目建议先把影响最严重的规则设为“warn”等团队适应后再逐步提级稳一点反而更能长久。最后分享一个我一直在用的土办法每次提交完代码自己先以“一个完全不了解这个需求的人”的身份读一遍 diff。如果读完还要去猜某个变量为什么存在、某个函数为什么这么写就说明还没达到可读标准。代码规范想守住其实不需要记住几百条规则只要每次都问自己一句“这句话写清楚了吗后面来看的人会不会卡住”就够了。