☰
t3code三层架构实战:轻量代码复用与分层约定避坑指南
2026/10/26 23:56:03 网站建设 项目流程

1. 项目缘起:一个被名字耽误的代码工具

第一次看到"t3code"这个名字,我下意识以为是某个新出的终端工具或者编辑器插件。翻了一圈社区讨论和零散的项目描述之后才反应过来,这其实是一个挺有意思的东西——它把"代码"和"极简三层结构"这两个概念揉在了一起,核心思路是用一套轻量的约定,把项目里那些重复、琐碎、容易写错的代码片段,收敛成可复用、可组合的模块。

说白了,t3code 想解决的问题很朴素:同一个项目里,同一类逻辑被反复手写,改一处漏三处。它不追求大而全的框架,也不搞复杂的抽象层,而是用"三层"这个约束,逼着开发者把代码拆成职责清晰的三个部分。这个思路听起来简单,但真正落地的时候,坑比想象中多。

我拿它在一个中小型项目里跑了两周,从最初的"这不就是换皮模板"到后来"确实省了不少重复劳动",中间踩的坑、改的配置、总结出来的经验,都在这篇里了。如果你手头有那种"业务逻辑不算复杂但重复度极高"的项目,或者你正在找一个轻量的代码组织方案,这篇应该能帮你少走点弯路。哪怕你只是好奇"三层"到底怎么分、为什么这么分,往下看也有收获。

2. 核心设计思路:为什么是"三层"而不是"两层"或"四层"

2.1 三层结构的本质:职责隔离而非物理分层

很多人一听到"三层",第一反应是 MVC、是 Controller-Service-DAO 那一套。t3code 的三层跟那个不是一回事。它的三层更偏向代码生成和复用视角的划分,我把它理解为:定义层、组合层、消费层。

  • 定义层:放的是最原子的东西——数据结构、常量、类型声明、基础工具函数。这一层的特点是"没有依赖",或者说只依赖语言本身和极少数基础库。
  • 组合层:把定义层的东西拼起来,形成有业务含义的单元。比如一个"订单金额计算"函数,它依赖定义层里的金额类型和税率常量,但自己不关心这些值从哪来。
  • 消费层:真正被外部调用的入口。API 路由、页面组件、命令行入口都属于这一层。它负责把组合层的能力暴露出去,同时处理输入输出、错误边界。

为什么是三层而不是两层?我试过把定义层和组合层合并,结果就是"工具函数"和"业务函数"混在一起,找东西全靠搜索。也试过拆成四层,多出来的那层很快就变成了"什么都往里塞"的垃圾桶。三层刚好卡在一个平衡点上:足够区分职责,又不至于让人纠结某个函数该放哪。

2.2 与常见方案的对比:它到底省了什么

方案重复代码处理学习成本适合场景
纯手写全靠复制粘贴低一次性脚本
重型框架靠框架约定高大型团队协作
代码模板靠模板引擎中固定模式生成
t3code靠三层约定+片段复用低到中中小项目、重复逻辑多

t3code 的定位很明确:它不解决"架构"问题,解决的是"同一类代码写太多遍"的问题。比如你项目里有十几个类似的表单校验、二十几个类似的 API 响应封装,t3code 的思路是让你把这些共性抽到定义层和组合层,消费层只写差异部分。

2.3 一个容易被忽略的设计取舍:约定优于配置

t3code 没有配置文件。这是它最"反直觉"也最容易被吐槽的地方。我一开始也不适应,觉得"连个 config 都没有怎么管"。但用下来发现,没有配置反而逼着你遵守目录约定:定义层就放defs/,组合层就放compose/,消费层就放entry/。目录即配置。

这个取舍的好处是:新人进来不用读一堆配置文档,看目录结构就知道代码怎么组织的。坏处是:如果你的项目结构跟约定不一致,要么改项目,要么改约定,没有中间地带。我的建议是,新项目直接按约定来,老项目先局部试点,别一上来就全量迁移。

3. 核心细节拆解:三层各自该放什么、不该放什么

3.1 定义层:只放"不会变"的东西

定义层的判断标准很简单:这个东西在未来半年内大概率不会因为业务需求变化而修改。类型声明、枚举、常量、纯函数工具,都符合这个标准。反过来,任何涉及"业务规则"的东西都不该放这里。

我踩过的一个坑:把"税率"放进了定义层,结果业务调整税率的时候,定义层被改动了,导致所有依赖它的组合层都要重新验证。后来我把税率挪到了组合层的一个配置模块里,定义层只保留"税率类型"这个声明。定义层应该是稳定的,一旦它开始频繁变动,说明你的分层出问题了。

// defs/money.ts —— 定义层示例 export type Currency = 'CNY' | 'USD'; export interface Money { amount: number; currency: Currency; } export const CURRENCY_SYMBOL: Record<Currency, string> = { CNY: '¥', USD: '$', };

注意这里没有任何计算逻辑,只有类型和映射。计算逻辑属于组合层。

3.2 组合层:业务逻辑的主战场

组合层是三层里最"厚"的一层,也是最能体现 t3code 价值的地方。它的核心任务是:把定义层的原子能力,组合成有业务含义的单元。一个组合层函数应该满足:输入输出明确、不依赖外部状态、可单独测试。

我习惯给组合层的每个函数写一句注释,格式是"做什么 + 依赖什么 + 返回什么"。比如:

// compose/order.ts // 计算订单总价:依赖 Money 类型和税率常量,返回含税总价 export function calcOrderTotal(items: Money[], taxRate: number): Money { const subtotal = items.reduce((sum, item) => sum + item.amount, 0); return { amount: subtotal * (1 + taxRate), currency: items[0]?.currency ?? 'CNY' }; }

这里有个细节:taxRate是参数而不是从定义层直接读的。为什么?因为组合层不应该知道税率从哪来,它只负责算。税率从数据库读还是从配置读,那是消费层的事。这个边界一旦模糊,组合层就会变成"什么都管"的泥潭。

3.3 消费层:薄到极致,只做三件事

消费层最容易写胖。很多人写着写着就把业务逻辑塞进来了,结果组合层形同虚设。我的经验是,消费层只做三件事:取输入、调组合层、返输出。任何超过十行的消费层函数,都值得怀疑是不是该往下沉。

// entry/api.ts import { calcOrderTotal } from '../compose/order'; import { getTaxRate } from '../compose/config'; export async function handleOrderTotal(req: Request) { const items = await req.json(); const taxRate = await getTaxRate(); return Response.json(calcOrderTotal(items, taxRate)); }

你看,消费层没有任何计算,只是把"取税率"和"算总价"串起来。这样改业务规则的时候,只需要动组合层,消费层纹丝不动。

3.4 三层之间的依赖方向:单向,且不可逆

这是 t3code 最硬的规则:消费层可以依赖组合层,组合层可以依赖定义层,反过来绝对不行。定义层不能 import 组合层,组合层不能 import 消费层。一旦出现反向依赖,整个结构就塌了。

我见过有人为了"方便",在定义层里直接调了一个组合层的函数,结果就是循环依赖,构建工具报错,排查了半天。这条规则没有例外,如果觉得需要反向依赖,说明你的分层划分有问题,该调整的是划分方式,不是规则本身。

4. 实操落地:从零搭一个 t3code 风格的项目

4.1 目录结构:约定就是一切

t3code 没有脚手架,目录得自己建。我用的结构是这样的:

project/ ├── defs/ # 定义层 │ ├── types.ts │ ├── constants.ts │ └── utils.ts ├── compose/ # 组合层 │ ├── order.ts │ ├── user.ts │ └── config.ts ├── entry/ # 消费层 │ ├── api.ts │ └── cli.ts └── tests/ ├── compose/ └── defs/

注意tests/只测定义层和组合层,消费层不写单测(它太薄了,测它性价比低)。这个结构我用了两周,最大的感受是找文件不用想:要改类型去 defs,要改逻辑去 compose,要改入口去 entry。

4.2 参数计算实例:税率与折扣的叠加

光说结构太虚,拿一个真实场景走一遍。假设要做"订单总价 = 商品小计 × (1 + 税率) - 折扣",折扣规则是满 100 减 10。

定义层先声明类型和常量:

// defs/order.ts export interface OrderItem { price: number; quantity: number; } export const DISCOUNT_THRESHOLD = 100; export const DISCOUNT_AMOUNT = 10;

组合层写计算逻辑,注意这里把"满减"和"含税"拆成两个函数,而不是揉在一起:

// compose/order.ts import { OrderItem, DISCOUNT_THRESHOLD, DISCOUNT_AMOUNT } from '../defs/order'; export function calcSubtotal(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); } export function applyDiscount(subtotal: number): number { return subtotal >= DISCOUNT_THRESHOLD ? subtotal - DISCOUNT_AMOUNT : subtotal; } export function applyTax(amount: number, taxRate: number): number { return amount * (1 + taxRate); }

消费层串起来:

// entry/checkout.ts import { calcSubtotal, applyDiscount, applyTax } from '../compose/order'; export function checkout(items: OrderItem[], taxRate: number): number { const subtotal = calcSubtotal(items); const discounted = applyDiscount(subtotal); return applyTax(discounted, taxRate); }

这个拆法的好处是:折扣规则变了只改 applyDiscount,税率逻辑变了只改 applyTax,互不影响。我试过把它们写成一个函数,结果每次改规则都要重新读一遍整个函数,生怕改错地方。

4.3 实操现场记录:迁移一个老模块

我拿一个老项目里的"用户权限校验"模块做了迁移。原代码是一个 200 多行的文件,校验逻辑、常量、入口混在一起。迁移步骤:

  1. 先把所有常量(角色列表、权限映射)抽到defs/permission.ts。
  2. 再把纯校验逻辑("这个角色有没有这个权限")抽到compose/permission.ts。
  3. 最后把原来的入口函数改成只调组合层,放到entry/auth.ts。

迁移完,原文件从 200 行降到 30 行,而且组合层的校验函数可以单独测试了。迁移过程中最大的坑是:老代码里有隐式的全局状态,比如某个校验依赖了一个全局变量。这种必须先把它显式化成参数,否则组合层没法做到"无副作用"。

5. 常见问题与排查技巧实录

5.1 三层划分争议速查表

症状可能原因处理方式
定义层频繁改动业务规则混入定义层把规则下沉到组合层
组合层函数太长职责没拆干净按"输入-处理-输出"再拆
消费层超过 20 行逻辑没下沉把计算部分移到组合层
出现循环依赖反向 import检查依赖方向,调整划分
不知道某函数放哪职责边界模糊问自己"它依赖谁、被谁依赖"

5.2 独家避坑技巧

技巧一:定义层禁止 import 任何东西。这条我执行得最严。一旦定义层开始 import,它就不再"稳定"了。如果确实需要外部类型,用类型声明的方式引入,而不是直接 import 实现。

技巧二:组合层函数尽量纯。纯函数好测试、好复用、好推理。如果某个组合层函数必须访问外部资源(数据库、文件),把它拆成"取数据"和"算数据"两部分,取数据放消费层,算数据留组合层。

技巧三:消费层不写 if-else 业务分支。消费层的 if-else 只应该处理"输入格式不对"这类边界,业务分支一律下沉。我见过消费层里写了五层嵌套 if 的,那基本等于没分层。

注意:t3code 的约定不是银弹。如果你的项目本身就是"一个脚本干一件事",硬套三层只会增加负担。它的价值在"重复逻辑多、需要长期维护"的场景里才体现得出来。

5.3 一个真实的排查案例

有次构建报错,提示循环依赖。我查了半天,发现是compose/config.ts里 import 了entry/env.ts的一个常量。这就是典型的反向依赖。解决方式很简单:把那个常量挪到defs/constants.ts,两边都从定义层取。排查循环依赖的通用思路是:从报错文件出发,顺着 import 链往上找,第一个"跨层反向"的 import 就是元凶。

6. 工具选型与扩展:t3code 之外还能怎么玩

6.1 配套工具的选择逻辑

t3code 本身不管构建、不管测试,所以工具得自己配。我的选择逻辑是:构建工具选快的,测试工具选简单的,lint 工具选能强制目录约定的。

  • 构建:Vite 或 esbuild,启动快,配置少。
  • 测试:Vitest,跟 Vite 同源,配置成本低。
  • Lint:ESLint 加一条import/no-restricted-paths规则,强制"定义层不能 import 组合层"这类约束。

这条 lint 规则是我后来加的,加完之后反向依赖的问题基本绝迹了。配置大概长这样:

// .eslintrc.js rules: { 'import/no-restricted-paths': ['error', { zones: [ { target: './defs', from: './compose' }, { target: './defs', from: './entry' }, { target: './compose', from: './entry' }, ], }], }

6.2 后续可以怎么扩展

三层结构稳定之后,我试过两个扩展方向。一个是在定义层加"版本标记",给每个类型和常量标注引入版本,方便追踪变更。另一个是在组合层加"能力标签",比如给函数打上@pure、@async标签,消费层可以根据标签做不同的错误处理。这两个扩展都还在试验阶段,但思路是通的:三层是骨架,扩展是血肉,骨架稳了,血肉怎么长都不会乱。

我个人在实际操作中的体会是,t3code 最大的价值不是它提供了什么新东西,而是它用"三层"这个简单约束,把"代码该放哪"这个老问题变得可回答了。它不完美,遇到复杂项目也会有力不从心的时候,但对于那些"重复逻辑多、团队规模不大、又不想上重型框架"的场景,它确实是个务实的选择。最后分享一个小技巧:刚开始用的时候,别急着全量迁移,先拿一个模块试点,跑顺了再推广,这样踩坑的成本最低。

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

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

立即咨询