ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端代码规范落地指南:命名、目录、分支与自动化检查实践

前端代码规范落地指南:命名、目录、分支与自动化检查实践 前端代码规范这四个字我几年前刚带团队时觉得是最虚的东西。“代码能跑就是胜利规范是团队大牛用来互掐的工具吧”直到我在一个项目里接手了别人遗留的代码同页面里三种请求方式混用、组件命名全靠拼音缩写、改动一个按钮的效果要翻十几层模板……我才明白大部分所谓“垃圾代码”不是能力问题是团队没有把规范当成工程约束来落地。这篇不聊高大上的方法论只讲我在实际开发里沉淀下来的一整套“前端代码规范”覆盖命名、目录、分支、提交、自动化检查以及几个高频踩坑点适合正在带团队或想改掉同事“自由风格”的前端同学参考。1. 命名规范先让代码在读者眼里“顺”起来1.1 文件、变量与组件的命名规则我见过最多也最头疼的垃圾代码不是业务写不对而是命名乱七八糟。比如同一个 Vue 项目里有人用user-profile.vue有人用userProfile.vue还有人干脆叫page3.vue后面接手的人找组件全靠全局搜索。先确定文件与组件命名的一套基线普通功能文件、页面组件文件用小写加中划线kebab-case比如user-profile.vue、cron-expression-picker.vue。文件名一旦统一编辑器里的模糊搜索命中率会高很多。组件内部name属性或类名用大驼峰PascalCase比如UserProfile、CronExpressionPicker。这样在 Vue DevTools、React DevTools 和错误堆栈里能直接对应到文件。共享的基础 UI 组件建议加前缀比如 BaseButton、BaseInput、BaseModal。看到前缀你就知道它是跨业务复用的原语组件不是某个页面的临时产物。普通变量、函数名用小驼峰比如fetchUserList、isSubmitting。目录如src/shared/utils可以维持小写。变量命名里面有一些自己定的硬规矩布尔值变量尽量用is、has、can开头例如isLoading、hasPermission、canSubmit别人看到命名就能猜到它是true/false不用回头去看let flag 0。事件处理函数统一用handle开头还是on开头同一个项目里只选一种。我个人倾向在模板里绑定用onClick、onSubmit这种外部语义在组件内部实现用handleClick、handleSubmit。避免一会儿clickHandle一会儿handleDetail的混乱。接口返回的数据不要直接起res、data这种泛化名字。后端 response 里通常有明确语义比如userInfo、pageResult就算真的要保留 data也应当包一层const { list, total } data解构。说句实在话命名规范不需要列几百条团队真正要守住的是“一致性”和“可推断性”。当我看到一个变量叫isVisible我心里就有预期它是个布尔值看到一个函数叫getUserProfile我就会预期它返回一个用户资料对象。一切违背直觉的命名都是垃圾代码的温床。1.2 事件、样式与状态类的命名细节除了函数和变量很多团队容易忽略事件名与样式状态类的规范。在 Vue 组件里自定义事件强烈建议用 kebab-case并且加上动词或更新前缀例如update:visible、change-value。不要只发一个onSure这种事件接收方根本不知道这个事件会在什么时候冒出来。React 中自定义事件较少但如果是第三方库或者事件总线也建议用module:eventName风格比如user:logout、cart:updated避免事件互相撞车。CSS/BEM 或 Tailwind 都无所谓但要统一状态类命名。我经常看到classactive在不同组件里被反复使用结果很容易出现“只要一加 active按钮就变蓝色”的鬼故事。自己写组件样式的时候状态类建议统一使用is-、has-语义比如.is-active、.is-disabled、.has-error。在模板里尽量不要手写一大段三元字符串拼 class而是用对象语法或 classnames 库集中管理这样状态类一目了然。2. 目录与组件边界避免代码变成“文件垃圾场”2.1 按业务功能组织和切分目录很多从零起步的前端项目目录结构是最容易烂掉的前端团队习惯把所有页面组件都塞进src/components下理由是“方便复用”。但做了几个月后你会发现这里躺着登录弹窗、营销 Banner、订单表格、甚至还有一小段全局数据请求组件之间互相 import 到飞起谁也说不清哪些该归谁。更建议的方式是按业务领域切分而不是按组件类型切分。一张可以直接抄的主目录形态如下src/ app/ # 全局应用配置、路由、状态入口 shared/ # 跨模块复用的基础组件、hooks、工具函数 modules/ # 按业务域划分的功能模块 user/ api.ts components/ views/ store.ts order/ api.ts components/ views/ store.ts每一个业务模块内部包含自己的 api、组件和页面业务流量从“登录”进来后只会访问modules/user和modules/order相关代码跨模块复用一律走shared。这样做的核心价值是隔离和收敛改用户模块时不会误伤订单模块删除某个业务模块时也能安全地整体删除。实际落地过程中不要急着大动干戈。存量项目里可以先建立新的目录约定后续新需求按这个结构插进去再利用碎片时间把src/components里确实跨业务复用的组件移动到src/shared/components把只属于某业务域的组件移到对应的modules下。我曾经用一个“三个月迁移计划”改造类似老项目每次只搬一个业务相关文件风险远低于那种“周末花一天重构所有目录”的激进方案。2.2 组件的极简分层UI组件、容器组件与业务组件组件的拆分粒度也是代码规范里相当重要的一环。很多同学的困惑是“到底拆多细才算合理”我的经验是把组件分成三层基础 UI 组件只负责展示和交互状态不感知业务数据。比如BaseButton、BaseTable、DatePicker接收 props发出事件内部最多有一点自身状态。容器/业务组件负责从接口或状态库里取数、下发事件。比如“用户列表页”而它底下的行操作按钮可以拆成独立业务组件。页面视图层组合以上两层承载路由跳转和页面编排。规范要点是禁止基础 UI 组件内部直接调用业务 API。我看过一个团队把BaseTable写成了内置请求数据并自带分页的“神仙组件”初看似乎方便结果每个页面的接口字段和数据结构都不一样组件代码里全是if (props.type order)这样的判断硬生生把通用组件变成了维护黑洞。组件粒度判断可以给一个简单阈值如果一个文件超过 400~500 行并且它同时做了请求、状态处理和高度复杂的模板渲染就应该考虑拆件了。拆件的目标是让每个组件只关心一件事。很多代码维护困难不是不够抽象而是抽象错了方向。3. 分支命名与提交规范把烂代码挡在合入前3.1 一套不太重但实用的分支命名模型前端代码规范不只停留“代码长得什么样子”也包括代码以什么流程进入主干。没有分支规范的团队我最常见到的场景是所有人直接在 dev 分支开发上线前发现 dev 分支混杂着 4 个未完成需求互相之间有半截联调代码谁也不敢随便删。我自己在多数项目上使用的是“单主干 短期特性分支”的轻量模型核心规则如下分支类型命名示例何时使用主干分支main/master保持随时可发布集成分支develop日常集成可选功能分支feature/order-list-refactor新功能、需求改造缺陷修复fix/login-session-expireBug 修复临时发布release/1.4.0准备发布紧急修复hotfix/server-error-500线上紧急修复关于特性分支需要保持一个习惯分支名同时包含需求标识例如feature/CRM-114-user-export。如果没有 Jira 这类系统也可以用日期加业务含义比如feature/20260612/order-sorting。分支名可以被 Git 历史保存很久将来翻 log 时看到一串feature/fix2只会让人一头雾水。3.2 Commit Message 别再用“fix bug”蒙混过关提交信息看起来是小事但它直接决定你合入代码后是否能快速定位和回滚。垃圾提交信息有几种典型画风update修改了代码fixaaa当项目出了问题你需要从几十个update中找出哪一个是改了登录逻辑的效率极低。我推行过一套接近 Conventional Commits 的轻量提交格式type(scope): subject # 示例 feat(order): 新增订单详情导出按钮 fix(user): 修复登录超时后没有跳转登录页 refactor(shared): 抽离 BaseModal 通用弹窗组件 perf(table): 使用虚拟滚动优化大数据量渲染 style(button): 统一按钮 disabled 状态样式 docs(readme): 补充启动命令说明 test(utils): 增加日期格式化单测用例可以配合commitlint在提交时校验 type 和 subject 是否合规不满足直接报错。这里有一个来自实操的建议不要为了“让历史看起来很好看”而强迫每个 commit 都拆得非常细。合理的节奏是“一个逻辑单元一个 commit”比如“修复订单状态显示错误”和“移除调试代码”分开提交这样即使后续要 revert 某个具体问题也不会丢掉无关改动。代码评审场景里提交规范的价值同样巨大。如果 MR 里出现 30 个文件全改成fix评审人根本无从下手。统一 type scope 之后评审人打开 MR 先看提交历史就能理解从主干到分支发生了什么评审效率肉眼可见提升。4. 用自动化工具守住底线别用自觉对抗遗忘4.1 ESLint Prettier Stylelint 各司其职代码规范必须“机器可执行”不能只靠口头约定。不同成员在没有工具约束的时候总会“忘掉”规范所以自动化是让规范真正落地的核心。经常有朋友问我ESLint 和 Prettier 到底选哪个这俩根本不是二选一的关系我用一句话解释Prettier 管排版单引号还是双引号、结尾是否有分号、缩进是 2 空格还是 4 空格。它把你的代码格式化成统一风格。ESLint 管代码质量未使用变量、隐式 any、方法复杂度太高、直接修改 props 等等。前端项目里需要同时安装这两者并让它们互相配合。ESLint 通过eslint-plugin-prettier或额外的禁用规则把格式类检查交给 Prettier 去做。 一个简单的配置文件可以是{ semi: false, singleQuote: true, trailingComma: all, printWidth: 100 }// .eslintrc.cjs module.exports { root: true, extends: [ eslint:recommended, plugin:vue/vue3-recommended, vue/typescript/recommended, prettier ], rules: { no-console: process.env.NODE_ENV production ? warn : off, no-debugger: process.env.NODE_ENV production ? error : off, typescript-eslint/no-explicit-any: warn, vue/no-unused-components: error } }这里不需要贴出完整配置但建议在提交前借助husky和lint-staged做一次增量检查只 lint 本次改动的文件避免老项目里几百个历史报错阻塞开发。核心命令大致是# package.json scripts { lint: eslint . --ext .vue,.js,.ts,.jsx,.tsx, lint:fix: eslint . --fix, format: prettier --write \./**/*.{vue,ts,js,json,css}\ }Stylelint 则用来规范 CSS 类名顺序、颜色值是否使用变量、是否存在无效属性等。如果你的团队使用 Tailwind可能不太需要 Stylelint但当你用 CSS Module 或普通 scss 时它仍然能拦住很多低级错误。4.2 用单测与复杂度检查兜住重构风险代码规范另一道防线是测试和复杂度约束。不需要所有前端项目都追求 100% 覆盖率但核心业务逻辑金额计算、日期范围校验、权限判断、复杂状态转换应该至少保留基础单测。我在项目中用 Vitest Vue Test Utils但它也适用于 React只要把配对的依赖换一下就行。举一个简单的完整测试示例import { describe, it, expect } from vitest import { getPaginationParams } from ../utils/pagination describe(getPaginationParams, () { it(page 为 0 时返回第一页, () { expect(getPaginationParams(0, 20)).toEqual({ page: 1, pageSize: 20 }) }) it(pageSize 超过上限时会被重置, () { expect(getPaginationParams(1, 10000)).toEqual({ page: 1, pageSize: 100 }) }) })这类测试不是目的而是为了保护“被公共方法引用的代码”在后续重构时不会悄悄破坏语义。工具层面还可以开启 ESLint 的复杂度规则rules: { complexity: [warn, { max: 10 }] }当一个函数复杂度过高时IDE 会给出警告。这避免了出现“几百行 if-else 写在一个函数里、维护者改一个分支影响十个场景”的陷阱。复杂度检查只是一个可量化信号重点在于逼着大家把大函数拆小让单个函数的职责与规模都处于可控范围。5. 反模式与性能实践把规范从“表面整洁”推向“真正可靠”5.1 “序列化后比较”这个坏味道讨论完了命名和流程再回来看一些具体代码层面的常见垃圾代码。比如很多前端在判断对象是否变化时习惯用JSON.stringify一把梭const isDiff JSON.stringify(prev) ! JSON.stringify(current)简单场景下它很有效但大对象、循环引用或函数字段会被JSON.stringify静默忽略而且当对象层级很深、体积很大时这个操作会占用大量主线程时间。我见过一个项目在 Vue 的 watch 里每秒对一棵巨大树结构做序列化比较结果用户操作表单时掉帧明显。性能优化并不是否定工具本身而是提醒自己“工具要用对场景”。如果只是判断属性是否改变更合理的做法是显式比较关键字段或使用 lodash 的isEqual同样不适合超深超大对象或在 UI 变更不频繁时增加节流。// 不要为了比较而序列化整棵状态树 const isFormDirty computed(() { return form.name ! origin.name || form.email ! origin.email })这里不需要背面试八股只需要在代码评审时加一条凡是出现JSON.stringify都问一下它的目的、性能与边界。很多看不见的界面卡顿都是这类“顺手写法”堆积出来的。5.2 用 Web Worker 处理大文件上传不拖垮 UI另一个更偏性能工程的例子是大文件上传。我们在页面里选择一个大文件后通常要计算 MD5、读取分片信息。如果在主线程里直接对动辄几十 MB 的文件做二进制读取和哈希页面会直接卡死用户点击“取消”都没有反应。前端可以借助 Web Worker 把这类 CPU 密集任务放到后台线程。我做了一个“上传插件内部使用 Worker 计算分片校验值”的实现大致思路如下Worker 侧self.onmessage (event) { const { file, chunkSize } event.data const total Math.ceil(file.size / chunkSize) const chunks [] for (let i 0; i total; i) { const start i * chunkSize const end Math.min(file.size, start chunkSize) // 可以在 worker 内计算 hash 或读取 arrayBuffer chunks.push({ index: i, start, end }) } self.postMessage({ chunks }) }主线程侧const worker new Worker(new URL(../workers/file-split.worker.ts, import.meta.url)) worker.postMessage({ file, chunkSize: 5 * 1024 * 1024 }) worker.onmessage (e) { const { chunks } e.data uploadChunksWithConcurrency(file, chunks) } function uploadChunksWithConcurrency(file: File, chunks: number[]) { const pool 3 let index 0 async function workerTask() { while (index chunks.length) { const chunk chunks[index] // 上传每个分片 await uploadChunk(file, chunk) } } const tasks Array.from({ length: pool }, () workerTask()) return Promise.all(tasks) }这里需要注意几个细节1 是并发数不要开太大否则浏览器单域名并发限制会把普通接口请求堵死2 是每个分片上传失败要有重试3 是进度通知主线程时不能每个分片都触发一次渲染建议用“每 3 个分片上报一次”或借助 requestAnimationFrame/节流合并 UI 更新。6. 与后端配合、团队落地与面试表达规范不是“前端自嗨”6.1 从接口契约到类型安全前端代码规范还经常被人忽略的一环是对接口层的治理。没有规范的团队里会看到满屏散落的any可能是后端没给类型也可能是前端图省事直接把 response 里的一坨 JSON 塞进全局状态后续所有下游属性查询全靠猜。我现在的习惯是如果有 OpenAPI/Swagger尽量引入类型生成工具把接口定义转换成 TypeScript 类型如果没有完整文档至少要在项目的api目录下集中定义接口函数和响应类型。以 Vue 项目为例可以在自身模块内写清晰类型export interface UserProfile { id: number name: string avatar: string role: admin | user } export interface UserListResult { list: UserProfile[] total: number page: number } export function fetchUserList(params: PageParams): PromiseUserListResult { return http.get(/api/users, { params }) }这种“前后端边界用类型描述清楚”的方式能避免很多字段拼写错误和 mock 与真实接口不一致的问题。你可能会说这是后端该做的但前端团队完全可以自己推动一套工具链“你的代码规范”在接口层依然有效。6.2 新人落地与面试场景中的“规范表达”最后聊聊团队规范和面试展示。代码规范最终的落地要靠流程也要靠人。我们团队曾经试行过一份特别长的规范文档从换行到 npm 包命名都写上结果没人认真看。后来我们把文档简化成三份新人第一天要读的《上手清单》启动方式、目录结构、命名规则、提交规范。日常开发标准依赖统一、禁止随意改公共配置、接口必须走 api 层。Code Review Check List每次 MR 需要确认的 8 项内容比如是否有调试代码、是否处理异常状态、事件名是否语义化、是否有 500 行以上的大组件。面试时很多同学被问到“前端开发 skills”或者“代码规范”时只会回答“统一用 prettier / eslint”。这当然没错但很难和其他人拉开差距。真正加分的回答方式是讲流程讲取舍先说“我们团队怎么划分模块和组件边界”。再说“我们用什么工具保证规范自动生效”。最后拿一次实际案例说明“因为某个规范我们修复了一个很难排查的线上问题”。前端面试题里常出现代码可维护性相关的问题本质就是考察候选人是否有工程化经验。与其背各种库的 API不如把“一个 bug 是怎么从命名混乱与流程缺失里产生的”讲出来。面试官很吃这一套因为这说明你真的带过或参与过较重的前端项目。代码规范说到底不是为了约束而约束。它减少的是“这个函数到底改了什么”的困惑是“为什么这个按钮样式和别人不一样”的扯皮是上线前临时回滚时找不到分支的恐慌。这些东西短期不产生业务价值但长期来看是每个前端工程师真正能沉淀下来的职业资产。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进