项目代号 t3code是我们团队内部发起的一项编码工程化实践。在公司里做过几年开发的同学应该都有体会代码写得好不好很多时候不是技术栈的问题而是规范和习惯的问题。t3code 要做的就是把团队里分散在各处的编码约定、提交规范、Review 规则沉淀成一套可落地的方案并用工具把关键环节卡住让每个人都用同一套标准写代码、交代码、审代码。这篇文章我打算把 t3code 从立项到推行的完整过程拆开讲适合正在被代码风格不统一、提交信息混乱、Review 流于形式这些问题困扰的团队负责人和一线工程师参考。你能看到我们是怎么设计这套体系的、哪些环节必须配置工具卡住、哪些地方只需要定规矩就行以及落地期间踩过的坑和最后怎么补的。内容全部来自实际项目方案可以直接抄。1. 项目起源与整体设计思路1.1 为什么叫 t3code以及它到底是个什么项目先说说这个名字。t3code 不是官方的什么标准缩写纯粹是我们团队内部起的代号。T3 在当时的语境里指三个关键词Team、Three tiers、Traceability也就是“团队、三层架构、可追溯”。后面对应的 code 不用多解释就是代码工程本身。合在一起t3code 表达的意思是以一个十人左右的研发小组为单位把业务代码、规范约束、过程记录三条线打通让每次代码变更都有据可查、有规可依。这个项目本质上不是写某个业务功能也不是造一个新框架而是做“团队编码基础设施”。它包含四块内容编码规范文档集、脚手架生成器、Git 提交约束、CI 质量门禁。你可以把它理解成一套组合拳单独拿出任何一块都不新鲜但把它们串在一起效果远大于各管各的。当年立项的直接导火索是团队里一位同事在 Review 时发现同一个接口的命名在三个模块里出现了三种写法分别是getUserInfo、fetchUserData、queryMemberById。功能相似、语义相近但叫法完全对不上导致后面接手维护的同事不得不反复翻代码确认它们是不是同一个东西。这种问题靠嘴上说“大家注意统一”是解决不了的必须靠制度和工具。1.2 我们真正想解决的几个核心问题把痛点列出来之后目标反而清晰了。t3code 要解决的不是“代码丑不丑”这种审美问题而是四个直接影响协作效率和代码可维护性的实际问题。第一个问题是命名和目录结构没有共识。新人入职后光搞明白“代码该放哪个目录、文件该叫什么名字”就要花掉两周时间。每个老员工有自己的习惯新项目又往往复制旧项目当模板结果就是每个子项目的结构都不太一样。我们需要一份团队内部公认的目录规范并且把它固化到模板里新项目一键生成就是标准结构。第二个问题是提交信息完全不可读。翻 Git 日志的时候满屏都是update、fix、modify、提交根本看不出这次改动是为了修 bug 还是加功能更看不出影响范围。等出问题要回滚版本时几乎只能靠猜。我们需要让提交信息具备结构化格式一眼能看出类型、范围和目的。第三个问题是Code Review 经常走过场。Review 变成“点赞大会”代码能不能合并不取决于质量而取决于人际关系。我们需要的不是更严厉的惩罚而是一份具体的、可勾选的检查清单让 Review 的人有话可说、有据可查。第四个问题是老代码无人愿碰、新代码无章可循。存量代码不是一天能改完的但如果新代码继续乱写这份债只会越欠越多。所以 t3code 的原则是新代码必须达标老代码优先保证可读性改动渐进式清理而不是搞一刀切式的大重构。1.3 方案选型背后的三个关键取舍做这个项目之前我也调研过市面上一些规范和工具比如各种 lint 工具、commit 规范工具、脚手架生成器都挺成熟。但真正落地的时候最大的难点不是工具不够而是“多而杂”。我们最终做了三个取舍这里展开说下原因。第一个取舍是不追求大而全只抓住影响最大的环节。网上很多工程化方案动不动就是几十条规范、上百个插件看着专业实际落地时团队成员根本记不住最后成了摆设。我们把规范分成“强制类”和“建议类”两类。强制类只保留少数几条命名规则、提交格式、文件结构、禁止事项这些用工具卡死。建议类比如“函数不超过 50 行”“注释要写清原因”进 Review 清单但不进工具避免误杀。第二个取舍是优先用社区成熟工具少造轮子。t3code 项目里我们自己写的代码只有两个部分项目脚手架和命名检查脚本。其余像 ESLint、Commitlint、Husky、Stylelint 这些全部用社区方案配置出来的。这样做的好处是文档多、问题有人踩过、招聘新人时学习成本低。自己造轮子一时爽维护起来就是无底洞。第三个取舍是规范文档必须和工具联动互相对照。我们吃过一个亏规范文档写了一大堆工具里却只配置了一半结果是文档和实际检查不一致团队成员反而更迷糊。后来定的规矩是文档里标注“已工具化”的条目必须能在检查脚本里找到对应实现工具里出现的每一条规则也必须在文档里有出处。两者不一致就是 bug要当天修复。2. 核心规范设计与实操要点2.1 目录结构与命名规范小细节里的大讲究目录结构是 t3code 里第一个落地的规范也是新人最容易踩坑的地方。我们团队主要做业务系统既有前端也有后端服务所以目录规范分了两套但核心思想一致按“业务域”组织代码而不是按“技术类型”组织代码。拿后端服务举例。很多项目的目录是这么分的controllers、services、models、utils。这种按技术分层的方式在项目小的时候很直观但项目一大改一个订单功能要同时打开四五个目录来回跳。我们改成了按业务域划分src/ order/ controller.js service.js repository.js schema.js tests/ user/ controller.js service.js repository.js schema.js tests/ common/ logger.js response.js errors.js这个结构的核心逻辑是一个业务域的所有代码聚在一起改功能时只需要进一个目录。common目录只放真正跨域共享的基础件比如日志、统一响应格式、错误定义不允许放业务逻辑。这条规范我们花了很大力气才维持住因为总有同事图省事把跟用户相关的工具函数直接塞进common时间一长common就变成垃圾堆了。命名规范方面我们只定了四类硬规则文件命名统一用小驼峰或短横线不同技术栈分别规定组件和类名用大驼峰函数和变量用小驼峰常量用全大写下划线。规则看着简单但效果出奇地好。以前代码里userInfo、UserInfo、user-info混着用现在工具会直接报错省掉了大量 Review 时的口水战。2.2 Git 提交信息规范如何让日志变得可读可追溯提交信息这件事很多团队完全靠自觉结果就是前文提到的update、fix满天飞。t3code 采用了 Conventional Commits 的结构化提交格式每一条提交信息都包含类型、可选范围、主题三部分。类型我们精简为八种feat新功能、fix修 bug、docs文档、style格式调整不影响逻辑、refactor重构不新增功能也不修 bug、perf性能优化、test测试相关、chore构建或辅助工具变动。范围用/分隔写到业务域级别比如feat(order): 新增订单导出功能。这里有一个容易被忽略的细节scope 要控制在业务域粒度。一开始有人把 scope 写得很细比如feat(order/controller): xxx后来我们发现 scope 太细反而没有意义因为一次提交往往横跨 controller、service、repository。后来统一到业务域特殊跨域改动用common一目了然。为了让这个规范真正生效我们用 commitlint 加 husky 做了三层拦截。第一层是 husky 的commit-msg钩子在本地提交时直接校验第二层是 CI 里的格式检查第三层才是人工 Review。前两层自动化拦截能挡住 90% 的不合规提交人工 Review 只需要关注剩下的特殊情况。配置示例放在后面的实操章节。2.3 Code Review 检查清单把“走过场”变成“有分量”Review 走过场的核心原因是评审人不知道看什么、说什么。t3code 做了一份十项的检查清单每次提交 PR 时自动附在描述里评审人逐项打勾。这几项包括功能是否与需求描述一致、是否有对应测试且测试是否覆盖关键分支、命名是否符合 t3code 规范、错误处理路径是否完整、是否引入了不必要的依赖、是否有明显性能隐患、数据库变更是否包含回滚方案、日志输出是否包含足够的上下文、是否存在复制粘贴的重复代码、改动是否超出了本需求的范围。这份清单里我最想强调两点。第一点是**“改动是否超出需求范围”**。很多 PR 混着需求改动和顺手改动比如改一个订单 bug 时顺手把支付模块的格式重新排版了。一旦这种 PR 合进去出了问题查 blame 时完全对不上需求上下文。我们要求一次 PR 只做一件事顺手改动单独提 PR 或者明确标注出来。第二点是**“错误处理路径是否完整”**。业务代码最容易出问题的不是正常路径而是异常路径。调外部接口有没有超时处理读文件时文件不存在怎么办数据库写入失败是否会导致数据不一致Review 清单把这类问题摆在明面上评审人必须回答“是或否”不能留空。3. 实操过程与核心环节实现3.1 从零搭建项目脚手架生成规范化的初始结构t3code 的核心产出物之一是我们的脚手架工具create-t3code用 Node.js 写的命令行工具。新建项目时执行一条命令就能生成带规范目录结构、预配好 lint 和 commitlint 的项目骨架省掉手工搭建和复制粘贴旧模板的麻烦。实现思路并不复杂就是在模板目录里放好所有基础文件然后根据用户输入的参数做文件名的替换和依赖的安装。核心代码大概长这样#!/usr/bin/env node import { prompt } from inquirer; import { copyFile, mkdir, writeFile } from fs/promises; import { execSync } from child_process; import path from path; const questions [ { type: input, name: projectName, message: 项目名称 }, { type: list, name: platform, message: 选择平台, choices: [web, service, cli] }, { type: confirm, name: needDocker, message: 是否生成 Dockerfile, default: true } ]; const result await prompt(questions); const templateDir path.resolve(__dirname, ../templates/${result.platform}); const targetDir path.resolve(process.cwd(), result.projectName); await mkdir(targetDir, { recursive: true }); await copyTemplate(templateDir, targetDir, result.projectName); await writeFile( path.join(targetDir, README.md), # ${result.projectName}\n\n由 t3code 脚手架生成目录规范见 docs/STRUCTURE.md\n ); execSync(cd ${targetDir} npm install, { stdio: inherit });脚手架里除了基础文件还默认放入了.eslintrc、.commitlintrc、.prettierrc、husky配置。架子搭好后团队不需要再自己配一遍也能保证每个新项目从第一天起就符合规范。模板生成只是第一步更关键的是后续的维护机制模板文件要跟着规范同步更新不然就又是一次性产物。这里有个实操心得脚手架模板一定要有版本号。我们后来给模板加了t3code.version字段项目生成时会记录下来。当规范升级时通过这个版本号判断老项目是否需要跑一遍迁移脚本。没有版本号的模板过半年根本不知道哪些项目是旧结构升级就无从谈起。3.2 用 husky 和 commitlint 卡住提交规范提交规范的落地靠的不是嘴是钩子。我们在项目里配置了 husky并在commit-msg钩子里调用 commitlint 做校验。配置文件长这样// .commitlintrc.json { extends: [commitlint/config-conventional], rules: { type-enum: [2, always, [ feat, fix, docs, style, refactor, perf, test, chore ]], scope-case: [2, always, lower-case], subject-case: [0], subject-empty: [2, never], type-empty: [2, never] } }# .husky/commit-msg npx --no -- commitlint --edit $1这个配置的坑点在于subject-case一定要设为[0]也就是不限制主题大小写。我们最初用默认配置结果同事写中文主题时没事写英文主题时首字母大写被拦截报错信息提示得也不够友好大家一度以为是工具坏了。后来才明白不少英文单词本身就是大小写敏感的比如iOS、API强制小写反而别扭。husky 还有一个容易踩的坑是安装方式。老版本的 husky 使用package.json里的husky字段配置新版本改用.husky/目录下的脚本文件。很多网上的老教程还在讲husky: { hooks: { ... } }这种写法在新版本里根本不会生效。建议直接看官方文档或安装时用npx husky-init自动生成目录结构比自己手写靠谱得多。3.3 CI 质量门禁让不合规的代码根本合不进主干本地钩子能挡住大部分问题但没法挡住所有问题。比如开发者本地配置了--no-verify跳过钩子或者不同机器上的 lint 版本不一致导致结果不同又或者有人直接通过网页端合并了分支根本不会触发本地钩子。所以 CI 这道质量门禁必须有。我们的 CI 流程分三个阶段安装依赖、质量检查、构建验证。质量检查阶段跑三个命令npm run lint、npm run test、npm run check:name。最后一个check:name是 t3code 项目自己写的脚本专门扫描新增文件是否违反命名规范。任何一步失败CI 直接红PR 无法合并。以 GitLab CI 为例核心配置如下quality-gate: stage: test script: - npm ci - npm run lint - npm run test -- --coverage - npm run check:name rules: - if: $CI_PIPELINE_SOURCE merge_request_event artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml这里要提醒一句CI 里的依赖安装一定要用npm ci而不是npm install。npm install会根据 package-lock 的差异调整依赖版本可能导致 CI 环境和本地环境不一致出现“本地能过、CI 挂了”的灵异事件。npm ci严格按照 lock 文件安装保证可复现。CI 还有个容易遗漏的细节命名检查脚本要只检查改动文件不要全量扫描。我们的 v1 版本脚本是全量扫描的项目文件多了之后一次要跑几分钟很影响开发体验。后来改成通过git diff --name-only获取本次变更的文件列表再对列表做检查几秒钟就能跑完#!/bin/bash # scripts/check-name.sh FILES$(git diff --name-only --diff-filterACMR HEAD~1..HEAD) for FILE in $FILES; do node scripts/check-name.js $FILE done3.4 lint 规则如何配置既严又不烦lint 这块我们内部讨论得最多因为 lint 规则设置得太严会让人产生“又被工具卡了”的抵触情绪设得太松又形同虚设。t3code 最终定的配置原则是能自动修复的规则全开需要人工改动的规则慎重开。ESLint 的配置示例// .eslintrc.js module.exports { root: true, env: { browser: true, es2022: true }, extends: [eslint:recommended, plugin:prettier/recommended], parserOptions: { ecmaVersion: latest, sourceType: module }, rules: { no-unused-vars: error, prefer-const: error, no-var: error, eqeqeq: [error, always], curly: error, max-depth: [warn, 4], max-lines-per-function: [warn, 200] } };我把max-depth和max-lines-per-function设为 warn而不是 error。原因是这类“复杂度类”规则指标超标不代表代码一定有 bug可能只是写法的风格问题。如果直接设为 error经常出现明明功能很清晰、只是因为函数长了几个分支就被拦截的情况很打击积极性。设为 warn 之后开发者能看到提示但不会被卡住Review 时由人来判断是否要拆分。还有一个心得配了 prettier 之后不要再配一堆跟格式相关的 ESLint 规则。ESLint 负责代码质量Prettier 负责代码格式两者职责分离。之前我们在 ESLint 里配了indent、quotes、semi等规则结果和 Prettier 冲突每次保存文件时两个工具互相打架最后全删了只留plugin:prettier/recommended世界瞬间清净。4. 落地过程与问题排查实录4.1 规则冲突规范之间打架往往是定义边界没划清t3code 推行后遇到的第一类典型问题是不同规范之间互相冲突。印象最深的一个案例是关于“空行”的规则。我们在代码风格规范里写了“函数之间空一行”但 Prettier 在默认配置下不会强制函数之间的空行导致 lint 没有报错、Review 却被打回的情况。后来排查下来问题出在规范文档描述不精确。“函数之间空一行”这个表述没有区分“顶层函数声明”和“函数内部语句块”。Prettier 对顶层函数声明之间的换行是遵循原作者的空行但内部语句块之间的空行则有强规则。我们最后把这条规范改成顶层函数声明之间保留一个空行语句块之间仅按控制流需要加空行交给 Prettier 管理不再单独检查。这个案例给我的启发是规范文档里每个条款都要写清楚“归工具管”还是“归人管”。归工具管的交给自动检查归人管的写进 Review 清单。两边都管等于两边都不管最容易出问题。4.2 老代码适配策略不能一刀切也不能全部纵容t3code 推行到存量代码库时团队里出现了两派意见。一派主张借这个机会把所有代码全部重写另一派主张动存量代码有风险只约束新代码。这两个方向我都不太赞成。全量重写的问题很现实业务不是技术债很多老代码里藏着说不清的业务逻辑重写过程中稍有不慎就会改变行为出线上事故。完全不改的问题也很明显存量代码和规范脱节两套标准并存时间一长规范就形同虚设。我们的办法是“三个一批”存量代码选一批高频变动的模块先做规范化改造标记一批暂时不动但明确技术债属性并写进迭代计划新增代码一律强制走 t3code 规范。选高频变动模块的标准是看 Git 提交历史过去三个月里提交最多的文件优先改因为这些文件是活代码改了之后价值最大一次都不动的死代码不如留着不动。这个“三个一批”的策略实质是承认技术债清理是渐进式的不追求“毕其功于一役”。团队没有因为改造而停摆存量代码也在按节奏向规范靠拢。半年后再看那批优先改造的模块基本都不再有命名和结构问题新代码也非常干净。4.3 格式化工具引发的“虚假冲突”和它背后的协作隐患有一次团队多人并行开发时几乎每个人都遇到了同一个 PR 合并冲突而且冲突点都在文件的空白、换行符、缩进这些格式无关紧要的地方真正的代码改动反而被淹没在里面。查下来发现问题不在业务改动而在于团队成员本地的 IDE 配置不一致有人用 Prettier 默认配置单引号、无分号、LF 换行有人用旧配置双引号、分号、CRLF 换行还有人装了和 Prettier 冲突的其他格式化插件。保存文件时大家互相覆盖格式产生了大量无意义的格式差异Diff 界面一片混乱。思路很清晰格式事务必须统一收口。我们在项目根目录配置了统一的.prettierrc和.editorconfig并在安装脚本里强制团队使用.husky/pre-commit钩子执行lint-staged在提交前统一格式化。同时宣布一条规定本地 IDE 里的格式化工具要么跟随 Prettier 配置要么直接禁用。# .husky/pre-commit npx lint-staged// package.json 中的 lint-staged 配置 { lint-staged: { *.{js,ts,vue,jsx,tsx}: [prettier --write, eslint --fix], *.{json,md,yml,yaml}: [prettier --write] } }这个方案上线后格式冲突几乎归零。这里有个经验值得分享多人协作下格式问题不是审美问题是协作效率问题。没有统一的格式化工具每浪费一分钟在无意义的 Diff 上都是对团队协作效率的间接伤害。4.4 团队推行阻力与沟通策略工具管机器人管人t3code 落地的第一个月意见最大的其实是团队里的资深工程师。他们不是反对规范本身而是反感“被工具管着”的感觉。有位同事的原话至今记得“我代码写得没问题为什么还要被一个脚本拦住”后来解决问题的关键不是把规则调松而是把话讲透。我在一次周会上专门摊开讲了三个例子第一个例子是同一个接口三种命名的维护成本第二个例子是一次提交信息含糊导致回滚时花了两个小时确认版本范围第三个例子是命名检查脚本在 CI 里帮团队拦截了多少次低级错误。这之后反对声音基本消失了。规范能不能推行下去很多时候不是规则本身的问题而是团队有没有在“为什么要有这条规矩”上达成共识。这个体会我写进 t3code 的落地经验里工具的职责是执行规则但人的职责是解释规则。任何规范落地先讲清楚为什么再给工具顺序不能反。4.5 典型问题速查表把 t3code 推行过程中遇到的高频问题整理成了一张表方便团队排查问题时快速对号入座。现象可能原因解决方案提交时 commitlint 报错但本地能过本地配置与 CI 不一致或工具版本不同用npm ci并核对.commitlintrc是否在版本控制内PR 显示大量格式冲突本地 Prettier 配置不统一或 IDE 插件冲突统一.prettierrc配置 lint-staged 统一格式化CI lint 报错但本地不报node_modules版本漂移删掉 node_modules改用npm ci新项目目录结构和模板不一致手工复制旧模板弃用复制统一使用create-t3code脚手架团队成员跳过 husky 钩子--no-verify被使用或 husky 安装不完整将检查逻辑同步到 CICI 是最后防线husky 重新安装规范文档和工具实际检查不一致文档更新但工具未同步建立“文档工具联动”规则不一致按 bug 处理老代码文件很多lint 报错几百个存量问题累积用“三个一批”策略渐进处理新代码强制走规范这张表不是一次写成的是在推行过程中边遇到问题边补的。项目跑到第三个月大部分成员碰到问题第一反应已经不是私聊问人而是先查这张表。5. 复盘与后续扩展方向t3code 从立项到稳定运行差不多花了三个月。头一个月订规范、搭工具第二个月在几个重点项目上试点第三个月才在团队全面铺开。现在回头看有几个环节如果重来一遍我会调整做法。5.1 如果重来我会提前布局的三件事第一件规范文档应该从第一天就放在仓库里而不是挂在 Confluence 或 Wiki 上。和代码放在一起才能保证文档和工具同步演进也能在代码 Review 时直接引用规范链接效率高得多。第二件脚手架模板应该先小范围试用而不是直接在全部团队铺开。我们用create-t3code生成的第一个项目是团队内部的一个工具系统不是核心业务线这样试错成本低。等模板稳定了再推广到正式项目。模板和规范一样都需要一个磨合期。第三件从一开始就引入覆盖率趋势图。t3code 在 CI 里收集了测试覆盖率数据但没有从一开始就在 MR 面板上展示趋势导致早期团队对覆盖率的关注不够。后来加了 MR 覆盖率对比之后明显能看到大家对测试的态度从“写了就行”变成了“得保持不降”。这个机制建议所有团队尽早加上。5.2 后续可扩展的方向我在实际使用中的体会t3code 现在运行得比较稳了我自己的感觉是这套体系还可以向两个方向延伸。一个方向是往“依赖治理”延伸把依赖升级、漏洞扫描、许可证合规做成类似 t3code 的规范流程另一个方向是往“架构守护”延伸用类似 ArchUnit 的工具去守护模块边界防止业务域之间的依赖腐化。不过扩展之前有个前提条件必须满足团队已经从心里认可这套流程而不是当成走过场。工具能守住下限但工程文化的上限还是要靠人的共识托底。这也是我在这个项目里最大的体会。