☰
用三段式命名治理公共代码库:t3code的工程实践
2026/10/28 22:51:20 网站建设 项目流程

前两年我们小组接到一个看起来很普通的任务:把三个业务模块里重复出现的代码统一收拢成公共库。项目内部起的代号叫 t3code,t3 就是 Team of Three,我们正好三个人。后来做着做着,这个代号越长越丰满——它变成了一套命名规范、一套目录约定,还附带了一个生成代码的模板小工具。这篇文章想聊的就是这段经历:我们怎么把一次“整理公共代码”的杂活,变成一套别人也可以复用的工作流。如果你也在维护小团队的公共代码库,或者正被命名混乱、文件位置随缘的问题折腾,t3code 这套做法应该能给你一个参考。

1. 为什么会有 t3code:从“每人一套代码习惯”说起

1.1 起这个代号的原因

最早其实没有 t3code 这个名字。当时群里发的消息是“公共模块整理”,听起来平平无奇。直到我们第一次对齐方案,才发现三个人对“整理”的理解完全不同:一个人想建一个 utils 大目录,把所有工具函数丢进去;另一个人想按业务模块复制公共代码进去再慢慢改;还有一个人建议直接把重复代码抽成 NPM 包发布。讨论了半天,谁也没有说服谁。

后来我提出先别急着写代码,把已经存在的重复文件统计出来再决定怎么做。统计结果很有意思:三个模块里重复度最高的不是复杂业务逻辑,而是那些风格完全不同的基础代码。比如同样一个日期格式化函数,在三个模块里出现了三次,函数名分别是 formatDate、dateFmt、dayFormat。功能完全一样,就因为是三个人各自写的,命名习惯不同,导致后面接手的人根本不敢删任何一版,怕删错引用。

t3code 这个名字就是在统计完这批重复代码之后定的。t3 代表我们三个人,也是“三段式”的谐音缩写,code 则是我们最终想要的标准代码产出。它既是一个项目代号,也成了我们遵守的约定:所有公共代码的命名,都按三段式结构来,避免再出现“同一件事物有七八种叫法”的问题。

1.2 要解决的三类典型问题

t3code 要解决的第一类问题,是命名混乱。团队里每个人有自己的命名偏好:有人喜欢缩写,有人坚持全称,有人习惯用中文拼音首字母。当公共代码库由多人共同维护时,命名不一致的直接后果是检索困难。你用 formatDate 搜索,搜不到同事写的 dayFormat;你想找上传组件,结果发现它叫 UploadFile,而另一个类似功能的组件叫 FileUploader。这类问题不解决,公共库越大越难用。

第二类问题是文件位置没有规则。公共代码到底放在哪里,全靠个人记忆。有人把工具函数丢在 src/common,有人放在 src/lib,还有人直接放在 views 目录下的 helpers 文件夹里。位置分散导致两个后果:一个是重复代码难被发现,另一个是依赖关系变得混乱。为了找一个函数,IDE 的全局搜索成了高频操作,这本身就很能说明问题。

第三类问题是代码风格分层不清。UI 组件、纯工具函数、数据处理逻辑、Mock 数据,这几类代码混在一起,导致修 bug 时经常要先分析“这行代码到底属于哪一层”。公共库一旦分层不清,后续想单独做优化或替换实现就会非常痛苦。t3code 的设计目标,就是让这三类问题在动手写第一行公共代码之前就被规则约束住。

2. t3code 的核心设计:三段式命名与目录映射

2.1 T3 三段式规则:Type、Context、Task

在正式动工前,我们把命名规则定成了三段式结构:类型-上下文-动作,英文对应Type-Context-Task。这也是 t3code 里那个 3 的核心含义。类型段回答“这段代码是什么”,上下文段回答“它服务于哪个业务域”,动作段回答“它具体做了什么”。

类型段我们只保留五个固定值:ui(组件)、util(工具函数)、api(接口封装)、mock(模拟数据)、type(类型定义)。固定值的目的是限制选择范围,命名时不需要想太多,从五个词里挑一个即可。上下文段用业务模块名,比如payment、order、user,如果模块名太长就取官方缩写,但全组必须记录到 README 里。动作段是动词短语,比如format、submit、getList、parseXml,动词放最后的好处是整个名字读起来更像一句短语,比如util-order-parseXml,看一眼就知道“这是个订单模块的 XML 解析工具函数”。

三段之间统一用横线连接,整体使用小驼峰加横线混合风格:全小写前缀加小驼峰动作词,如util-order-parseXml。不用点号、不用斜杠、不用下划线,是为了保证跨平台文件系统兼容,也避免在单元格或命令行里复制路径时被特殊字符干扰。

这套规则看起来简单,但它真正起作用的地方在于限制了“自由发挥空间”。命名时的人为判断被压缩到最小,剩下的五个类型、一个模块名、一个动作词,任何一个熟悉规则的人都能快速推导出某个文件应该叫什么,从哪里找。对三个人协作来说,这个统一性的价值远大于命名本身的优雅程度。

2.2 目录映射与全局约定

命名规则只解决了一半问题,另一半是目录结构。我们给 t3code 定的目录映射原则是:命名即路径,路径即命名。一个文件叫什么名字,它放在哪个目录下,这两件事必须能互相推导出来。

对应关系是这样规定的:任何公共代码都放在仓库的packages/t3code目录下面,然后按类型分一层子目录。ui开头的文件固定放在packages/t3code/ui/下,util开头的放在packages/t3code/util/下,其他类型同理。类型子目录里再按上下文开一个文件夹,比如ui下面会有ui/payment/、ui/order/、ui/user/,每个模块的组件落到各自文件夹里。

这样做有一个额外好处:代码 review 时可以按照路径快速判断改动范围。一次 PR 如果同时改了util/order/和ui/payment/,就能很明显地看出跨模块改动的风险,reviewer 会格外多加注意。把规则写到命名的层面,比在规范文档里写十句“注意分层”要有效得多,因为命名是写代码时一定会做的事,而读规范文档不是。

目录映射还有一层补充约定:不允许出现单独的common或shared目录。这两个词看着方便,实际是最容易养出“垃圾堆”的地方。所有代码必须落到具体类型和具体上下文目录里,如果没有合适的模块目录,就先建目录再放文件。用早期的 t3code 经验来说,这个约束至少避免了公共库变成文件坟场。

3. 实战:用 t3code 落地一个业务模块

3.1 初始化项目结构与模板

理论规则定了,接下来是落地。我们的第一批试点是支付模块,因为支付模块里的重复代码最多:金额格式化、订单状态映射、支付方式图标组件、接口封装,还有测试用的 Mock 数据。这些内容天然适合做 t3code 的第一批公共代码。

初始化时,我们的目录结构大致是这样:

packages/t3code/ ├── package.json ├── README.md ├── ui/ │ └── payment/ ├── util/ │ └── payment/ ├── api/ │ └── payment/ ├── mock/ │ └── payment/ └── type/ └── payment/

package.json里维护了一个简单的依赖项列表,专门用来存放公共代码依赖的基础库,比如日期处理、请求库、UI 框架。同时我们在包名上直接定为@team/t3code,通过包管理器的 workspace 功能让三个业务模块引用它。

下一步是准备模板文件。模板的作用不是生成页面,而是生成“符合 t3code 规范的骨架文件”。我们准备了四个模板:工具函数模板、组件模板、接口封装模板、Mock 数据模板,每个模板都预留了类型、上下文、动作三个占位符。用 Node.js 写了一个简单的生成脚本,脚本接收几个参数后自动创建对应目录和文件。

模板不追求开箱即用,它的核心价值是保证所有公共代码的初始状态一致。文件头注释、函数签名、导出方式、测试用例的放置,这些容易被忽略的细节都由模板兜住。后续开发只需补全实现,不需要关心这个文件长得像不像公共风格。

3.2 为支付模块生成三件套

我们以“金额格式化工具函数”为例,走一遍完整过程。按照 t3code 规则,它的类型是util,上下文是payment,动作是formatAmount,所以最终文件名为util-payment-formatAmount.ts,放在packages/t3code/util/payment/目录下。

生成脚本的用法是这样:

node scripts/t3code/generate.js --type util --context payment --task formatAmount

脚本执行后会创建对应的目录和文件,并自动填充模板内容。工具函数模板的初始内容大概长这样:

/** * @t3.util 统一工具函数 * @context payment 支付业务域 * @task formatAmount 格式化金额 * * 使用说明:金额统一以分为单位存储,展示时格式化为元。 */ export function formatAmount(value: number): string { // TODO: 按业务规则实现格式化 return String(value); }

模板固定了文件头注释、函数导出语法、入参出参类型,剩下的工作就是填充TODO部分。实测下来,这个过程非常快,因为命名不用想,目录不用找,连注释都有了雏形。

接着生成支付方式图标的组件文件,规则是ui-payment-payMethodIcon.tsx。组件模板里预置了 props 的类型定义和默认导出结构。Mock 数据文件则命名为mock-payment-orderList.ts,里面预生成了一份包含订单状态、支付方式和金额字段的数组骨架。这三件套生成完之后,支付模块的公共代码基础就在一个下午内搭好了,而且风格肉眼可见地一致。

3.3 生成逻辑与关键代码

生成脚本的核心逻辑并不复杂,我做成了一个简单的映射关系:解析输入的三个参数,拼出合法文件名,检查是否重复,然后写入模板。关键点落在两处:一个是文件名冲突检测,另一个是模板字符串的替换。

文件名冲突检测的代码大概长这样:

// scripts/t3code/generate.js const path = require('path'); const fs = require('fs'); function resolveTarget(type, context, task) { const kebabTask = task .replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()) .replace(/^-/, ''); const fileName = `${type}-${context}-${kebabTask}`; const dir = path.join(process.cwd(), 'packages/t3code', type, context); const fullPath = path.join(dir, fileName); return { dir, fullPath, fileName }; } function ensureUnique(fullPath) { if (fs.existsSync(fullPath)) { throw new Error(`t3code: 文件已存在,请检查是否重复:${fullPath}`); } } module.exports = { resolveTarget, ensureUnique };

小驼峰转横线的逻辑需要注意边界情况。第一次写的时候直接用了正则replace(/[A-Z]/g, ...),结果遇到formatXMLAmount这种连续的缩写时就乱了。后面加了判断,只在遇到大写且在非首字母位时才转换。这类细节不落地跑一次很难发现,属于典型的“规则看上去简单,实现时全是边界”。

模板替换用的是最朴素的字符串替换,不引入模板引擎,因为生成的文件数量不大,而且模板里的占位符只有三个。引入模板引擎反而会带来转义问题。等将来模板复杂到需要循环和条件判断时,再考虑升级也不迟,过早抽象会让脚本本身失去可读性。

4. 踩坑记录与排查技巧实录

4.1 最容易被忽略的命名冲突

t3code 跑起来后遇到的第一个坑是命名冲突,而且发生在两个人完全不相关的工作流里。同事 A 在订单模块建了一个util-order-getStatusText,同事 B 在订单模块也想写一个“获取订单状态文案”的函数,但他起的动作词是getOrderStatus。生成脚本只检查完整文件名是否重复,因为两个文件名不同,所以都成功生成了,内容却高度重叠。

这个问题的根源在于动作段可以换一种说法来表达同一个意思。仅靠生成脚本检查文件是否存在,根本挡不住这种语义重复。我们后来加了一道人工 review 流程:每次新增公共代码前,先在目录下看一遍同模块已有的文件列表。这一步效率很低,但确实有效。

再往后我们干脆利用 IDE 的文件搜索,在提交前按模块名扫一遍目录,把功能相似的文件标记出来合并。实测下来,只要养成“新增前先看列表”的习惯,重复建设概率能下降一半以上。自动化的重复代码检测工具也能做这事,但当时为了不引入太重的基础设施,先用人工流程扛住了。

4.2 模板脚本在不同平台的兼容性问题

第二个坑出现在 Windows 环境下。团队里有人用 macOS,有人用 Windows,生成脚本里用path.join拼路径,本来以为没有问题,直到有人在 Windows 上执行脚本后,生成的文件路径出现了反斜杠。这直接影响文件名里的横线规则吗?不影响文件名,但影响的是输出提示信息和团队统一路径描述。

真正的问题来自文件名本身的大小写。Windows 的文件系统不区分大小写,macOS 默认也不区分,但 Linux 区分。如果某个人在 Windows 上生成了util-Order-getList的文件,提交到 Git 仓库后,在 Linux 的 CI 环境中就很可能和另一个文件util-order-getList同时存在,造成模棱两可的引用。

解决方式是在生成脚本里强制将所有参数转为小写,规则改成“类型段、上下文段一律小写,动作段保持小驼峰”。动作段里用户本来就会写成小驼峰,问题不大。这个坑告诉我们:命名规范必须考虑最严格的文件系统环境,不能只在大家的开发机上看起来没问题。

4.3 团队协作中的落地阻力

技术问题都能靠代码解决,真正的阻力来自习惯变化。三个人里有一位老同事,已经写了五年的代码,让他把习惯的getStatus改成getStatusText,把放文件的习惯从src/utils改到packages/t3code/util/,一开始非常抗拒。这很正常,命名习惯是个人编码风格的一部分,突然被统一会让人觉得自己在失去控制权。

我们的处理方式很朴素:不搞强制考核,而是把 t3code 的收益做成一次公开演示。挑了一个所有人都熟悉的模块,让两个人分别按旧习惯和 t3code 规则去查找同一个工具函数,计时对比。结果旧习惯因为文件位置不固定,花了三分钟才找到;t3code 规则下,直接按路径推导定位,十秒钟不到。这个演示之后,最反对的人也无话可说。

还有一个技巧是让命名规则有“逃生舱”:如果某个场景确实不适合三段式命名,允许提出来,由集体讨论后决定是否特例。设立逃生舱的价值不是真的要用它,而是让大家感觉到规则不是冰冷的锁链,“经过讨论打破规则”比“无条件服从规则”更能让人接受。

5. t3code 能带走的东西和个人体会

5.1 可复用清单

如果只看 t3code 的产出,具体的文件列表只对当时的项目有意义。但它的方法论可以拆出来带走,我认为最有价值的是三张清单。

第一张是命名规则清单:类型、上下文、动作三段式,固定类型词表,统一分隔符,强制小写前缀。这套规则可以应用到任何多人协作的代码项目里,不只限于前端,后端接口命名、数据库表名前缀、消息队列的 topic 命名,都可以用同一套思路。

第二张是目录映射清单:命名即路径,路径即命名,不允许出现模糊的 common 目录。建议任何项目在约定目录结构时,都先问一句“从目录位置能不能反推出这个文件是干什么的”;如果不行,说明目录粒度还需要再拆。

第三张是模板化生成清单:公共代码不只是共享文件,还要共享文件的初始形态,包括文件头注释、导出方式、参数类型风格。只要保证每个新文件长着同一副“骨架”,后续维护就轻松很多。这三张清单离开了 t3code 项目本身,完全可以作为新项目公共代码治理的起步工具。

5.2 我的小心得

踩过这么多坑之后,我最大的体会是:代码命名规范这件事,不能用“建议”的方式落地。你越是说“大家尽量统一一下”,越没人当回事。必须把它变成文件名、目录结构、生成脚本这些客观存在的约束,才能从根本上减少讨论成本。命名是程序员每天都在做的动作,把规则嵌在这个高频动作里,比贴在 Wiki 上有效一百倍。

另外,公共代码库的大小要控制住。t3code 最终沉淀的公共代码并没有越滚越大,因为我们在每次新增前都会问同一个问题:这段代码放进公共库,是减少了重复,还是增加了未来改动的负担?公共代码的维护成本是复利计算的,今天收进来一段没想清楚的代码,未来站在它上面写的每一行新代码都会继承问题。所以宁可让公共库瘦一点,也不要让它变成收容站。

我们后来把 t3code 的规则扩展到了接口命名和 Git 分支前缀上,效果同样明显。你可以试一下:从一个小模块开始,把“类型-上下文-动作”这个三段式命名套到你的文件上,再搭配同名同路径的目录约定,坚持两周,再回头看代码检索时间是不是降了一截。别的我不敢保证,至少“这三个文件的命名终于能对上了”这件事,一定会让你的代码库安静很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询