☰
t3code三层代码复用规范:从原子层到场景层的落地实践
2026/10/28 6:33:07 网站建设 项目流程

1. 从"t3code"这个代号说起:它到底指什么

第一次看到"t3code"这个词,很多人会一头雾水。它不像"React""Vue"那样有明确的官方文档,也不像某个大厂的开源项目那样自带光环。我在几个技术社群里蹲了一段时间,发现大家对这个词的用法其实分成了好几派:有人把它当成一个轻量级的代码片段管理工具,有人拿它指代某种"三层代码结构"的实践方法论,还有人干脆把它当作一个内部项目的代号。不管哪种理解,核心都指向同一件事——用更小的粒度去组织、复用和交付代码。

我个人的判断是,t3code 更像是一个"约定"而非"框架"。它不强制你装什么依赖,也不要求你改构建流程,而是通过一套命名和分层习惯,让代码在团队里流动得更顺畅。这一点很关键,因为大多数团队真正缺的不是工具,而是"大家按同一套规矩写代码"的默契。t3code 的价值恰恰在于它足够轻,轻到你可以今天就落地,明天就见效。

这篇文章适合三类人看:一是手上有一堆重复代码片段、每次都要复制粘贴的开发者;二是团队里负责定规范、但推不动重型框架的技术负责人;三是对"代码复用"这件事有兴趣、想找一套低门槛方案的独立开发者。我会从概念拆解、目录设计、实操步骤、踩坑记录几个角度,把 t3code 这套东西讲透,让你看完就能在自己项目里试起来。

需要先说明的是,t3code 目前没有权威的官方定义,下面讲的内容是基于社区常见用法和我自己实践后的合理归纳。如果你所在团队对它有别的理解,完全可以在我的基础上做调整,核心思路是通用的。

2. t3code 的三层结构:为什么是"三"而不是"二"或"四"

2.1 三层划分的底层逻辑

t3code 这个名字里的"t3",最主流的解读是"tier 3",也就是三层。这三层分别是:原子层(atom)、组合层(compose)、场景层(scene)。听起来有点像原子设计(Atomic Design),但 t3code 更偏向代码逻辑而非 UI 组件,这是它和 Brad Frost 那套方法论最大的区别。

为什么偏偏是三层?我试过两层,也试过四层,最后发现三层是一个"刚刚好"的平衡点。两层的问题是粒度过粗,原子和场景混在一起,改一个按钮文案可能牵动整个页面逻辑;四层的问题是层级太多,开发者每次写代码都要想"这个该放哪一层",认知负担反而上升。三层的好处是:原子层只管"一件事怎么做",组合层只管"几件事怎么串",场景层只管"这个业务场景要什么"。职责边界清晰,但又不会细到让人抓狂。

举个具体例子。假设你要做一个"用户登录后展示欢迎语"的功能。在 t3code 里,原子层会有一个formatUserName函数,只负责把用户对象转成可读的名字;组合层会有一个buildWelcomeMessage,调用原子层的函数并拼接文案;场景层则是LoginWelcome组件或模块,负责在登录成功后调用组合层并渲染。每一层只依赖下一层,不跨层调用,这是 t3code 的铁律。

2.2 三层之间的依赖方向

依赖方向必须是单向的:场景层 → 组合层 → 原子层。反过来绝对不行。我见过太多项目在原子层里直接读全局状态、调接口,结果原子层变得又重又难测。t3code 要求原子层是"纯"的——给定输入,必有确定输出,不碰网络、不碰存储、不碰全局变量。

这条规则听起来简单,执行起来需要纪律。我的做法是在项目里加一个 ESLint 规则,禁止原子层目录下的文件 import 网络请求库或状态管理库。一开始会有人抱怨"这样写太麻烦",但两周之后大家就习惯了,而且测试覆盖率明显上去了。因为原子层纯函数太好测了,一个输入一个断言,写完就跑。

组合层可以调用原子层,也可以做轻量的数据编排,但同样不建议直接发请求。它的定位是"把原子拼成有意义的小单元"。场景层才是真正对接业务的地方,接口调用、路由跳转、状态订阅都放这里。这样分层之后,你会发现一个有趣的现象:业务逻辑的变更 90% 只影响场景层,组合层和原子层几乎不动。这就是 t3code 最实在的收益。

2.3 和常见分层方案的对比

方案层数主要面向学习成本适合团队规模
MVC3请求-响应低任意
原子设计5UI 组件中中大型
Clean Architecture4+领域逻辑高大型
t3code3代码片段复用低2-20 人

从表里能看出来,t3code 的定位很明确:它不是要替代 Clean Architecture 那种重型架构,而是给中小团队一套"够用就好"的复用规范。你不需要画一堆圈圈图,也不需要引入依赖注入容器,只要按三层放文件就行。这也是我在小团队里最愿意推它的原因——推行成本低,见效快。

3. 目录怎么摆:一套可以直接抄的 t3code 结构

3.1 基础目录骨架

说了半天理念,落到实操第一步就是目录怎么建。我试过好几种摆法,最后稳定下来的结构是这样的:

src/ atoms/ format/ formatUserName.ts formatDate.ts validate/ isEmail.ts isPhone.ts calc/ calcDiscount.ts compose/ user/ buildWelcomeMessage.ts buildUserProfile.ts order/ buildOrderSummary.ts scenes/ login/ LoginWelcome.tsx loginService.ts order/ OrderDetail.tsx orderService.ts

这个结构的关键在于:atoms 按"动作类型"分,compose 按"业务域"分,scenes 按"页面/流程"分。为什么 atoms 不按业务域分?因为原子函数往往是跨业务复用的,formatDate既可能用在订单里,也可能用在用户资料里,按业务域分反而会造成重复。而 compose 和 scenes 天然带业务属性,按域分更直观。

3.2 命名约定比目录更重要

目录只是容器,真正让 t3code 跑起来的是命名约定。我总结了几条硬规矩,团队里执行下来效果不错:

  • 原子层函数一律用动词开头:formatXxx、validateXxx、calcXxx、parseXxx。看到名字就知道它干什么。
  • 组合层函数一律用buildXxx或composeXxx开头,暗示它是由多个原子拼出来的。
  • 场景层文件用业务名词命名,比如LoginWelcome、OrderDetail,不加动词前缀。
  • 每一层的文件名和导出的主函数名保持一致,方便全局搜索。

这几条规矩看起来琐碎,但省下的沟通成本很可观。以前 code review 时经常要问"这个函数放哪层合适",现在看名字基本就能判断。如果名字里带build,那它就不该出现在 atoms 目录里;如果名字是format开头却放在 scenes 里,那大概率是放错地方了。

3.3 用 index 文件做"层内出口"

每个层目录下放一个index.ts,统一导出该层对外的接口。这样做有两个好处:一是上层引用时路径短,import { formatUserName } from '@/atoms'比一长串相对路径清爽;二是可以控制"哪些是公开的,哪些是内部的"。有些辅助函数只在层内用,就不导出,避免被上层误用。

注意:index 文件不要做任何逻辑,只做 re-export。我见过有人在 index 里写初始化代码,结果 import 顺序一变就出 bug,这种坑没必要踩。

3.4 和构建工具的配合

t3code 本身不依赖任何构建工具,但配合路径别名会舒服很多。以 Vite 为例,在vite.config.ts里加几行:

import { defineConfig } from 'vite' import path from 'path' export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src'), '@atoms': path.resolve(__dirname, 'src/atoms'), '@compose': path.resolve(__dirname, 'src/compose'), '@scenes': path.resolve(__dirname, 'src/scenes'), }, }, })

配好之后,跨层引用一眼就能看出依赖方向。如果某个文件里出现了@scenes被@atoms引用的情况,review 时立刻就能发现违规。这种"让错误显眼"的设计,比写十页规范文档都管用。

4. 落地实操:从零搭一个 t3code 小项目

4.1 先写原子层:从最笨的函数开始

落地 t3code 最忌讳一上来就设计大而全的结构。我的建议是从你手上最重复的那段代码开始。比如你发现项目里到处在拼用户显示名,有的地方用昵称,有的地方用真名,有的地方还要加前缀。那就先写一个原子函数:

// src/atoms/format/formatUserName.ts export interface UserLike { nickname?: string realName?: string prefix?: string } export function formatUserName(user: UserLike): string { const base = user.nickname || user.realName || '匿名用户' return user.prefix ? `${user.prefix}${base}` : base }

这个函数没有任何依赖,输入输出都是纯数据。写完之后,把所有散落各处的拼接逻辑替换成调用它。这一步做完,你就已经尝到 t3code 的甜头了——改一次规则,全项目生效。

4.2 再搭组合层:把原子串成业务小单元

原子层稳定之后,开始抽组合层。还是用户场景,假设登录成功后要展示一句欢迎语,格式是"早上好,张三,欢迎回来"。这里面涉及时间判断和名字格式化两个原子能力,组合层负责把它们串起来:

// src/compose/user/buildWelcomeMessage.ts import { formatUserName } from '@atoms/format/formatUserName' export function buildWelcomeMessage(user: UserLike, now: Date): string { const hour = now.getHours() const greeting = hour < 12 ? '早上好' : hour < 18 ? '下午好' : '晚上好' return `${greeting},${formatUserName(user)},欢迎回来` }

注意这里我把now作为参数传进来,而不是在函数内部new Date()。这是 t3code 组合层的一个小技巧:把"不确定的东西"参数化,这样测试时就能固定时间,断言结果稳定。很多人写组合层时习惯直接读系统时间,结果测试写起来很痛苦,这是可以避免的。

4.3 最后接场景层:对接真实业务

场景层是唯一允许"不纯"的地方。它负责调接口、拿数据、处理加载和错误状态,然后把数据交给组合层加工,最后渲染。以 React 为例:

// src/scenes/login/LoginWelcome.tsx import { useEffect, useState } from 'react' import { buildWelcomeMessage } from '@compose/user/buildWelcomeMessage' import { fetchCurrentUser } from './loginService' export function LoginWelcome() { const [message, setMessage] = useState('') useEffect(() => { fetchCurrentUser().then((user) => { setMessage(buildWelcomeMessage(user, new Date())) }) }, []) return <div className="welcome">{message}</div> }

到这里,一个最小的 t3code 闭环就跑通了。原子层管格式化,组合层管拼接,场景层管数据获取和渲染。三层各司其职,任何一层要改,影响范围都可控。

4.4 验证分层是否真的生效

搭完之后怎么验证分层有没有白做?我有个简单的自测方法:尝试只改原子层,看组合层和场景层要不要动。如果改一个formatUserName的规则,组合层和场景层完全不用改,说明分层是干净的。如果发现组合层里也藏了一份格式化逻辑,那就说明抽取得不彻底,得回去补。

另一个验证方法是看测试。原子层的测试应该是纯函数测试,不需要 mock 任何东西;组合层的测试最多 mock 一下原子函数;场景层的测试才需要 mock 接口。如果原子层测试里出现了jest.mock('axios')这种代码,那基本可以断定分层被破坏了。

5. 踩过的坑:t3code 落地时最容易翻车的几个地方

5.1 原子层越写越"胖"

最常见的翻车方式,是原子层慢慢变成了"什么都能干"的杂物间。一开始只是formatDate,后来有人往里加fetchUserAndFormat,再后来有人加formatDateAndSaveToLocal。原子层一旦开始碰 IO,整个分层就塌了。

我的应对办法是给原子层定一条硬指标:单个原子函数不超过 20 行,且不出现 await。出现 await 就说明它在做异步操作,那它就不该待在原子层。这条规则执行起来有点机械,但确实有效。20 行以内的纯函数,想塞私货也塞不进去。

5.2 组合层变成"第二场景层"

另一个高频坑是组合层偷偷调接口。开发者觉得"反正组合层也是业务逻辑,调个接口怎么了",结果组合层变得依赖网络环境,测试跑不起来,复用也无从谈起。组合层的定位应该是"纯数据的编排",它接收数据、加工数据、返回数据,不负责获取数据。

如果你发现组合层函数需要传 token、需要处理 loading,那它八成放错层了。正确的做法是把接口调用留在场景层,组合层只接收已经拿到的数据。

5.3 跨层引用导致的循环依赖

三层结构最怕的就是循环依赖:atoms 引 compose,compose 引 scenes,scenes 又引 atoms。一旦成环,构建工具会报一堆莫名其妙的错,排查起来很痛苦。预防办法除了前面说的 ESLint 规则,还可以在 CI 里加一个依赖检查脚本,用madge或dependency-cruiser扫一遍,发现跨层反向引用就直接 fail。

npx madge --circular --extensions ts,tsx src/

这条命令跑一次就能看出有没有环。我一般把它挂在 pre-commit 钩子里,提交前自动跑,省得推到远端才发现。

5.4 过度抽象:为了分层而分层

最后一个坑比较隐蔽:有些人学了 t3code 之后,恨不得把每个if都抽成原子函数。结果项目里全是isXxx、hasXxx这种一行函数,读代码时要在十几个文件之间跳来跳去,反而更累。分层是为了降低认知负担,不是为了增加文件数量。

我的经验法则是:一个逻辑如果只在一个地方用,且不超过 5 行,就先别抽。等它出现第二次、第三次的时候再抽也不迟。t3code 是"按需分层",不是"强制分层"。这一点想清楚,能省下大量无谓的重构。

6. 让 t3code 真正跑起来的几个配套习惯

6.1 用代码模板降低"放哪层"的决策成本

分层规范推不动,往往不是因为大家不认同,而是因为每次写代码都要想"这个放哪"。解决办法是给编辑器配代码片段模板。以 VS Code 为例,配一个atom片段,输入atom就自动生成原子层的文件骨架:

{ "t3code atom": { "prefix": "atom", "body": [ "export function ${1:formatSomething}(${2:input}: ${3:InputType}): ${4:string} {", " $0", "}" ] } }

同理再配compose和scene两个片段。这样新建文件时,骨架自动带出来,放哪层、怎么命名,模板已经帮你定好了。决策成本一降,规范执行率立刻上去。

6.2 在 code review 里只盯三件事

review 时不用逐行看,只盯三个点:依赖方向对不对、命名符不符合约定、原子层有没有碰 IO。这三点过了,基本就没大问题。我试过在团队里推行"三问 review 法",平均每个 PR 的 review 时间反而缩短了,因为大家知道该看什么,不用漫无目的地读。

6.3 定期做"层内清理"

每隔一两个迭代,花半小时扫一遍各层目录,看看有没有放错的文件、有没有重复的原子函数、有没有已经没人用的组合函数。这种小清理比大重构温和得多,也不会影响业务进度。我一般把它安排在迭代末尾的"技术债时间"里,一次清一点,积少成多。

6.4 新人上手时先给一张"地图"

新同事入职时,别急着让他读代码,先给他画一张三层地图,标清楚每层放什么、依赖方向是什么、命名怎么起。然后让他从改一个原子函数开始,逐步过渡到组合层、场景层。这样一周之内他就能独立按 t3code 写代码,比扔给他一堆文档有效得多。

7. 关于 t3code 的一些个人体会

我用 t3code 这套东西大概有一年多,最大的感受是:它治的不是代码,是习惯。很多团队代码乱,不是因为技术不行,而是因为没有一个"大家都认"的摆放规则。t3code 提供的正是这么一套规则,简单到不需要培训,严格到能挡住大部分随意发挥。

当然它也不是万能的。如果你的项目是纯算法、几乎没有业务编排,那三层结构可能显得多余;如果你的团队已经有成熟的分层规范,也没必要为了 t3code 而改。它的最佳适用场景,是那种"代码量在涨、重复在变多、但还没到需要重型架构"的中小项目。在这个区间里,t3code 的性价比很高。

最后分享一个小技巧:如果你不确定某个函数该放哪层,就问自己一句——"它需要知道业务吗?"不需要,放原子层;需要一点,放组合层;需要很多,放场景层。这一句话,基本能解决 80% 的分层纠结。剩下的 20%,写多了自然就有感觉了。

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

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

立即咨询