
1. 项目缘起与整体设计思路1.1 为什么会有 t3code 这个想法最早接触 t3code 这个概念是在一个内部工具链的讨论里。当时团队面临一个很具体的问题多个小项目之间共享的代码片段越来越多复制粘贴导致维护成本急剧上升但上完整的 monorepo 又太重构建和发布流程会变得非常复杂。t3code 就是在这样的背景下被提出来的——它不是一个具体的框架或库而是一种组织代码的思路核心目标是用最小的结构成本实现跨项目的代码复用与统一管理。我把它理解成一种“轻量级代码共享协议”。你可以把它想象成一个中央厨房各个门店项目不需要自己备菜而是从中央厨房t3code 仓库统一取用标准化的半成品门店只负责最后的烹饪和出品。这样做的好处是配方统一、口味一致而且改一处就能影响所有门店。t3code 这个命名本身也很有意思。t3 通常指代“top 3”或者“tier 3”在社区语境里往往暗示一种精简、聚焦的层级划分。code 则直白地指向代码本身。合在一起它传递的信号是只保留最核心的三层代码结构其余全部外置。这个理念在实际落地时确实能解决很多团队“想复用又怕耦合”的纠结。1.2 核心设计原则三层分离与单向依赖t3code 的骨架建立在三个明确的层级上我把它总结为“三层分离、单向依赖”。这三层分别是基础层Base Layer存放与业务无关的纯工具函数、类型定义、常量枚举。这一层不允许依赖任何上层模块也不允许引入业务逻辑。它的存在意义是提供最稳定的地基。领域层Domain Layer存放与业务领域相关的模型、服务接口、数据转换逻辑。它可以依赖基础层但不可以依赖应用层。这一层是 t3code 的核心价值所在因为它承载了可复用的业务能力。应用层Application Layer存放具体项目的入口、路由、页面组装逻辑。它只能依赖领域层和基础层不能反向被依赖。这种单向依赖的好处非常明显当你修改基础层的一个工具函数时影响范围是可控的因为你知道没有任何上层逻辑会反向渗透进来。我试过在一个中型项目里强行打破这个规则让领域层直接引用了应用层的配置对象结果两周后做重构时发现领域层的单元测试完全没法独立运行因为测试环境里根本没有应用层的上下文。这个坑让我后来在多个项目里都严格执行单向依赖检查。1.3 与常见方案的对比为什么不用 monorepo 或 npm 包很多人会问既然要共享代码为什么不直接上 monorepo或者把公共部分发成 npm 包我在这两种方案上都踩过坑下面这张表是我个人的经验总结方案优点缺点适用场景monorepo统一版本、统一构建、调试方便仓库体积大、CI 时间长、权限管理复杂大型团队、长期维护的核心产品npm 包版本清晰、按需安装发版流程繁琐、调试需要 link、版本碎片化稳定的通用工具库t3code 模式结构轻、复用快、调试直接需要约定纪律、缺乏强制约束中小团队、快速迭代的多项目t3code 的定位介于两者之间。它不追求 monorepo 的全面管控也不像 npm 包那样需要严格的版本管理。它更像是一种“约定大于配置”的协作方式大家把可复用的代码放到约定的目录里通过相对路径或路径别名直接引用。这种方式在项目数量不多、团队规模不大的时候效率极高。但这里有一个关键前提必须有代码审查机制来保证三层结构不被破坏。我见过太多团队一开始约定得很好后来因为赶进度有人直接把业务逻辑塞进了基础层导致整个结构逐渐腐化。所以 t3code 能不能跑起来技术只占三成纪律占七成。1.4 目录结构的标准模板基于上面的设计原则我整理了一套可以直接抄作业的目录结构。这套结构在三个不同规模的项目里验证过基本不需要大改t3code-root/ ├── base/ │ ├── utils/ │ ├── types/ │ └── constants/ ├── domain/ │ ├── models/ │ ├── services/ │ └── transformers/ ├── application/ │ ├── project-a/ │ ├── project-b/ │ └── shared-entry/ ├── scripts/ │ ├── check-layer.js │ └── sync-base.js └── t3code.config.json这个结构里base、domain、application是三个核心目录。scripts放的是辅助脚本比如层级依赖检查、基础层同步等。t3code.config.json用来配置路径别名和检查规则。我特别想强调scripts/check-layer.js这个文件。它是我在第三个项目里才加上的之前两个项目都靠人工 review结果都出现了不同程度的层级穿透。这个脚本的逻辑很简单遍历所有源文件解析 import 语句如果发现base目录下的文件引用了domain或application或者domain引用了application就直接报错并退出。把它挂到 CI 的 pre-commit 钩子上基本能杜绝 90% 以上的违规。2. 核心细节解析与实操要点2.1 基础层的边界划定什么该放什么不该放基础层是 t3code 的地基它的纯净度直接决定了整个结构的稳定性。我在实际项目里总结了一条判断标准如果一个函数或类型脱离当前业务语境后仍然有意义它就可以放基础层如果它必须结合某个业务场景才能理解那就应该放领域层。举个例子formatDate这个函数它接收一个时间戳和一个格式字符串返回格式化后的日期。这个函数在任何项目里都能用所以放基础层没问题。但如果是formatOrderDate它内部隐含了“订单日期需要特殊处理比如超过 24 小时要显示‘昨天’”这样的业务规则那就必须放领域层。另一个常见的误区是把常量放错位置。比如MAX_RETRY_COUNT 3如果这个重试次数是网络请求的通用配置放基础层没问题。但如果是“订单支付重试次数”那就应该放领域层因为它是业务规则的一部分。我见过一个项目把所有的常量都堆在基础层的constants目录里结果后来业务调整支付重试次数从 3 改成 5开发人员改了基础层却意外影响了另一个项目的消息推送重试逻辑。这就是边界不清导致的连锁反应。注意基础层不允许引入任何第三方业务相关的依赖。比如你不能在基础层里 import 一个 UI 组件库的 Button因为 Button 是视图层的概念不属于基础能力。2.2 领域层的服务设计接口与实现的分离领域层是 t3code 里最灵活也最容易写乱的一层。我的经验是领域层一定要做接口与实现的分离。具体来说每个业务能力都先定义一个接口然后再提供至少一个默认实现。这样做的好处是应用层可以面向接口编程将来替换实现时不需要改动应用层代码。以用户认证为例领域层可以这样设计// domain/services/auth-service.ts export interface AuthService { login(credentials: Credentials): PromiseUser; logout(): Promisevoid; getCurrentUser(): PromiseUser | null; } // domain/services/impl/default-auth-service.ts export class DefaultAuthService implements AuthService { // 具体实现 }应用层在组装时只需要从领域层拿到AuthService接口然后注入具体的实现。如果将来要换成另一种认证方式只需要在领域层新增一个实现类应用层几乎不用改。这种设计还有一个隐藏好处单元测试变得非常容易。因为领域层的服务依赖的是接口测试时可以轻松 mock 掉外部依赖。我在一个项目里做领域层测试时覆盖率能稳定在 85% 以上就是因为接口隔离做得好。但这里有一个实操要点接口的定义要稳定实现的变更要频繁。我见过有人把接口和实现放在同一个文件里结果每次改实现都要动接口文件导致 import 路径频繁变动。正确的做法是接口放在domain/services/下实现放在domain/services/impl/下两者物理隔离。2.3 应用层的组装逻辑依赖注入的轻量实现应用层是 t3code 的出口它负责把基础层和领域层的能力组装成可运行的项目。这里最核心的技术点是依赖注入。很多前端项目习惯直接 import 具体实现但在 t3code 模式下应用层应该只依赖接口具体实现通过一个统一的容器来注入。我通常会在应用层放一个container.ts文件内容大致如下// application/project-a/container.ts import { DefaultAuthService } from ../../domain/services/impl/default-auth-service; import { AuthService } from ../../domain/services/auth-service; const container { authService: new DefaultAuthService() as AuthService, // 其他服务 }; export default container;然后在页面或组件里这样使用import container from ./container; const user await container.authService.getCurrentUser();这种写法看起来多了一层但实际收益很大。当你要在测试环境替换成 mock 服务时只需要改container.ts一个文件。当你要在另一个项目里复用同样的领域层能力时也只需要复制一份container.ts并调整实现即可。提示容器文件不要写得太复杂保持“一个服务一个属性”的扁平结构。我见过有人把容器写成了多层嵌套的对象结果找服务时要写container.domain.auth.service既难记又容易出错。2.4 路径别名的配置与维护t3code 的目录结构比较深如果全部用相对路径会出现../../../../base/utils这种难以阅读的引用。所以路径别名几乎是必须的。我通常在tsconfig.json或vite.config.ts里配置三个别名{ compilerOptions: { paths: { base/*: [base/*], domain/*: [domain/*], app/*: [application/project-a/*] } } }这样在代码里就可以写成import { formatDate } from base/utils/date清晰很多。但路径别名有一个坑不同构建工具的配置方式不一样。TypeScript 的paths只影响类型检查不影响实际打包。如果你用 Vite还需要在vite.config.ts里配resolve.alias如果用 Webpack需要配resolve.alias如果用 Jest还需要在jest.config.js里配moduleNameMapper。我建议把这四个地方的配置写成一个共享的 JSON 文件然后各个工具都从这个文件读取避免改了一处忘了另一处。另外路径别名的顺序也很重要。如果base/*和domain/*有重叠构建工具会按配置顺序匹配。我一般把最具体的别名放在最前面比如app/*放在base/*之前避免误匹配。3. 实操过程与核心环节实现3.1 从零搭建 t3code 结构的完整步骤假设你现在有一个空目录想从零开始搭建 t3code 结构下面是我实际操作的步骤记录。整个过程大概需要 20 分钟前提是你已经装好了 Node.js 和 npm。第一步初始化项目mkdir t3code-demo cd t3code-demo npm init -y npm install typescript vite --save-dev npx tsc --init这一步没什么特别的就是创建一个标准的 TypeScript 项目。tsc --init会生成一个默认的tsconfig.json后面我们会修改它。第二步创建三层目录mkdir -p base/utils base/types base/constants mkdir -p domain/models domain/services/impl domain/transformers mkdir -p application/project-a mkdir -p scripts这里我特意把services和services/impl分开就是为了强制接口与实现的物理隔离。transformers目录用来放数据转换逻辑比如把后端返回的 DTO 转成前端需要的 ViewModel。第三步配置路径别名修改tsconfig.json在compilerOptions里加入{ baseUrl: ., paths: { base/*: [base/*], domain/*: [domain/*], app/*: [application/project-a/*] } }然后在vite.config.ts里同步配置import { defineConfig } from vite; import path from path; export default defineConfig({ resolve: { alias: { base: path.resolve(__dirname, base), domain: path.resolve(__dirname, domain), app: path.resolve(__dirname, application/project-a), }, }, });第四步编写层级检查脚本在scripts/check-layer.js里写入以下内容const fs require(fs); const path require(path); const LAYER_ORDER [base, domain, application]; const violations []; function checkFile(filePath) { const content fs.readFileSync(filePath, utf-8); const currentLayer LAYER_ORDER.find(layer filePath.includes(/${layer}/)); if (!currentLayer) return; const importRegex /from\s[](\w)\//g; let match; while ((match importRegex.exec(content)) ! null) { const importedLayer match[1]; const currentIndex LAYER_ORDER.indexOf(currentLayer); const importedIndex LAYER_ORDER.indexOf(importedLayer); if (importedIndex currentIndex) { violations.push(${filePath} 违规引用了 ${importedLayer} 层); } } } function walkDir(dir) { const files fs.readdirSync(dir); files.forEach(file { const fullPath path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { walkDir(fullPath); } else if (file.endsWith(.ts) || file.endsWith(.tsx)) { checkFile(fullPath); } }); } walkDir(path.resolve(__dirname, ../)); if (violations.length 0) { console.error(层级依赖违规); violations.forEach(v console.error( - ${v})); process.exit(1); } else { console.log(层级依赖检查通过); }这个脚本的逻辑是根据文件路径判断它属于哪一层然后解析 import 语句如果发现引用了更底层的模块就记录为违规。注意这里的LAYER_ORDER是[base, domain, application]索引越小层级越低。importedIndex currentIndex表示引用了比自己更低的层级这是不允许的。第五步在 package.json 里加入检查命令{ scripts: { check-layer: node scripts/check-layer.js, prebuild: npm run check-layer, build: vite build } }这样每次构建前都会自动检查层级依赖有问题直接中断构建。3.2 一个完整的领域层服务实现示例为了让你更直观地理解 t3code 的写法我写一个完整的用户服务示例。这个示例包含接口定义、默认实现、以及应用层的调用方式。接口定义domain/services/user-service.tsexport interface User { id: string; name: string; email: string; } export interface UserService { getUserById(id: string): PromiseUser | null; searchUsers(keyword: string): PromiseUser[]; updateUser(id: string, data: PartialUser): PromiseUser; }默认实现domain/services/impl/default-user-service.tsimport { User, UserService } from ../user-service; import { httpClient } from base/utils/http; export class DefaultUserService implements UserService { async getUserById(id: string): PromiseUser | null { const response await httpClient.get(/api/users/${id}); return response.data || null; } async searchUsers(keyword: string): PromiseUser[] { const response await httpClient.get(/api/users, { params: { q: keyword }, }); return response.data || []; } async updateUser(id: string, data: PartialUser): PromiseUser { const response await httpClient.put(/api/users/${id}, data); return response.data; } }注意这里的httpClient是从base/utils/http引入的属于基础层的能力。领域层依赖基础层是允许的方向正确。应用层容器application/project-a/container.tsimport { DefaultUserService } from domain/services/impl/default-user-service; import { UserService } from domain/services/user-service; export const container { userService: new DefaultUserService() as UserService, };应用层页面调用application/project-a/pages/user-list.tsimport { container } from ../container; export async function renderUserList(keyword: string) { const users await container.userService.searchUsers(keyword); // 渲染逻辑 return users.map(u li${u.name}/li).join(); }这个链路非常清晰应用层调用容器容器注入领域层的实现领域层依赖基础层的 HTTP 工具。每一层各司其职没有越界。3.3 基础层工具函数的编写规范基础层的工具函数虽然简单但也有一些容易忽略的规范。我总结了三条第一条纯函数优先。基础层的函数尽量不要有副作用输入相同则输出相同。比如formatDate(timestamp, format)就是纯函数而saveToLocalStorage(key, value)就有副作用。有副作用的函数不是不能放基础层但要格外小心因为它们会影响调用方的状态。第二条参数校验要前置。基础层被所有上层依赖一旦出错影响面很大。所以参数校验一定要做而且要在函数入口处做。比如export function formatDate(timestamp: number, format: string): string { if (typeof timestamp ! number || isNaN(timestamp)) { throw new Error(formatDate: timestamp 必须是有效数字); } if (typeof format ! string || format.length 0) { throw new Error(formatDate: format 必须是非空字符串); } // 格式化逻辑 }第三条不要引入业务概念。基础层的函数命名和参数命名都要保持业务中立。比如calculateDiscount这个名字就带有业务色彩应该改成applyRate或者multiply。我见过一个项目的基础层里有一个isVipUser函数后来另一个项目没有 VIP 概念这个函数就完全没法复用只能复制一份改个名字。这就是业务概念污染基础层的典型后果。3.4 数据转换层的设计DTO 与 ViewModel 的桥接在 t3code 的领域层里我通常会单独划出一个transformers目录专门放数据转换逻辑。这一层的作用是隔离后端接口和前端视图让两边可以独立变化。举个例子后端返回的用户数据可能是这样的{ user_id: 123, user_name: 张三, email_address: zhangsanexample.com, created_at: 2024-01-01T00:00:00Z }但前端视图需要的格式是{ id: 123, name: 张三, email: zhangsanexample.com, createdAt: 2024-01-01 }这时候就需要一个转换函数// domain/transformers/user-transformer.ts import { User } from domain/models/user; export function toUser(dto: any): User { return { id: dto.user_id, name: dto.user_name, email: dto.email_address, createdAt: dto.created_at.split(T)[0], }; }这样做的好处是如果后端字段名变了只需要改转换函数视图层完全不用动。同样如果视图层需要新的字段也只需要在转换函数里加不会影响后端接口。提示转换函数一定要做空值保护。我踩过的坑是后端某个字段偶尔返回 null转换函数直接报错导致整个页面白屏。后来我在每个转换函数里都加了默认值处理比如dto.user_name || 未知用户。4. 常见问题与排查技巧实录4.1 层级依赖检查报错怎么办层级检查脚本报错是最常见的问题通常有以下几种情况情况一基础层引用了领域层。这是最严重的违规。排查方法是看报错信息里指出的文件路径找到对应的 import 语句。解决方式通常是把被引用的领域层代码下沉到基础层或者把基础层的调用逻辑上移到领域层。我遇到过一次是基础层的日志工具引用了领域层的配置对象后来把配置对象改成了通过参数传入基础层只负责格式化日志不关心配置来源。情况二领域层引用了应用层。这种情况通常是因为领域层需要读取某个应用级别的配置。正确的做法是把配置定义成接口放在领域层应用层实现这个接口并注入。比如领域层需要知道当前环境是开发还是生产可以定义一个EnvironmentProvider接口应用层提供具体实现。情况三路径别名配置错误导致误报。有时候 import 语句写的是相对路径但脚本只检查了开头的别名导致漏报或误报。我的建议是统一使用路径别名禁止在跨层引用时使用相对路径。可以在 ESLint 里加一条规则来强制这一点。下面这张表是我整理的常见报错与解决方案速查报错信息可能原因解决方案base 层引用了 domain 层基础层混入了业务逻辑下沉代码或上移调用domain 层引用了 application 层领域层依赖了应用配置定义接口应用层注入找不到模块 base/utils路径别名未配置检查 tsconfig 和构建工具配置循环依赖警告同层内互相引用提取公共部分到更底层4.2 路径别名在测试环境失效的排查路径别名在开发环境正常但跑 Jest 测试时报Cannot find module base/utils这是非常高频的问题。原因是 Jest 不认识 TypeScript 的paths配置需要单独在jest.config.js里配moduleNameMappermodule.exports { moduleNameMapper: { ^base/(.*)$: rootDir/base/$1, ^domain/(.*)$: rootDir/domain/$1, ^app/(.*)$: rootDir/application/project-a/$1, }, };注意这里的顺序app要放在base前面因为正则匹配是从上到下执行的。如果base在前面app开头的路径可能会被误匹配。还有一个坑是ts-jest的配置。如果你用ts-jest它默认会读取tsconfig.json的paths但有时候版本不兼容会导致读取失败。我的经验是显式在jest.config.js里配moduleNameMapper不要依赖ts-jest的自动读取。4.3 基础层同步到多个项目的实操技巧t3code 的一个典型场景是基础层需要同步到多个独立项目。如果每个项目都复制一份基础层改一处就要同步多处很容易漏。我的做法是用一个简单的同步脚本#!/bin/bash # scripts/sync-base.sh SOURCE_DIR./base TARGET_PROJECTS(../project-b ../project-c) for target in ${TARGET_PROJECTS[]}; do if [ -d $target ]; then rsync -av --delete $SOURCE_DIR/ $target/base/ echo 已同步到 $target else echo 警告$target 不存在跳过 fi done这个脚本用rsync做增量同步--delete参数会删除目标目录里多余的文件保证两边完全一致。但这里有一个风险如果目标项目在基础层里加了自定义文件会被直接删掉。所以我在实际使用时会先跑一次--dry-run确认没有意外删除后再正式执行。注意同步脚本不要放在 CI 里自动执行因为不同项目的发布节奏不一样。我建议把它做成手动触发的命令由负责人在确认基础层稳定后再执行同步。4.4 领域层服务测试的 mock 策略领域层的服务通常依赖外部接口测试时需要 mock。我的策略是在容器层面 mock而不是在服务内部 mock。具体做法是// domain/services/impl/__tests__/default-user-service.test.ts import { DefaultUserService } from ../default-user-service; import { httpClient } from base/utils/http; jest.mock(base/utils/http, () ({ httpClient: { get: jest.fn(), put: jest.fn(), }, })); describe(DefaultUserService, () { it(getUserById 应该返回用户数据, async () { (httpClient.get as jest.Mock).mockResolvedValue({ data: { id: 1, name: 测试用户 }, }); const service new DefaultUserService(); const user await service.getUserById(1); expect(user?.name).toBe(测试用户); }); });这种写法的好处是mock 的是基础层的 HTTP 工具而不是领域层服务本身。这样测试覆盖的是领域层的真实逻辑只是把外部依赖替换掉了。我见过有人直接 mock 整个UserService结果测试跑过了但领域层的逻辑一行都没执行这种测试没有意义。4.5 性能与构建体积的优化经验t3code 结构本身不会带来性能问题但如果基础层和领域层写得不好会导致构建体积膨胀。我总结了几个优化点第一基础层按需导出。不要在base/index.ts里export * from ./utils这样会把所有工具函数都打包进去。应该让使用方直接引用具体文件比如import { formatDate } from base/utils/date。这样构建工具可以做 tree-shaking只打包用到的部分。第二领域层的服务实现要懒加载。如果某个服务只在特定页面用到可以在容器里用动态 importexport const container { get userService() { return import(domain/services/impl/default-user-service) .then(m new m.DefaultUserService()); }, };但这样会让调用方变成异步需要权衡。我的经验是核心服务直接实例化边缘服务用懒加载。第三定期检查循环依赖。循环依赖不仅会导致构建报错还会让 tree-shaking 失效。可以用madge这个工具来检查npx madge --circular --extensions ts ./domain如果发现循环依赖通常是因为两个服务互相引用。解决方式是把公共部分提取到第三个文件或者用事件机制解耦。4.6 团队协作中的纪律维护t3code 能不能长期跑下去技术只占一部分更重要的是团队纪律。我在三个团队里推行过 t3code总结下来最有效的三条规则是规则一新人入职第一周必须通过层级检查。我会让新人在本地跑一次npm run check-layer确保他们理解三层结构。这比写十页文档都管用。规则二代码审查时优先看 import 语句。我审查 PR 时第一眼不是看业务逻辑而是看 import 路径。如果发现跨层引用直接打回。这个习惯坚持三个月团队就会形成肌肉记忆。规则三每季度做一次结构审计。用脚本统计各层的文件数量和依赖关系如果发现基础层文件数异常增长或者领域层出现了不该有的工具函数就及时清理。我见过一个项目的基础层从 20 个文件涨到 80 个就是因为没人做审计大家都往里塞东西。提示审计脚本可以很简单就是统计各层文件数并输出趋势图。关键是让团队看到结构的变化形成视觉上的提醒。4.7 从 t3code 迁移到其他架构的注意事项如果你的项目发展到一定阶段需要从 t3code 迁移到 monorepo 或微前端有几个点需要提前准备第一保持接口稳定。t3code 的领域层接口设计得越稳定迁移成本越低。因为迁移时只需要把实现替换掉接口可以原样保留。第二记录依赖关系。在迁移前用工具生成一份完整的依赖图明确哪些模块依赖哪些模块。这样迁移时可以按依赖顺序逐步搬移避免一次性大爆炸。第三基础层优先迁移。基础层是最独立的迁移风险最小。先把基础层搬到新架构里验证没问题后再搬领域层最后搬应用层。我试过反过来操作先搬应用层结果因为基础层还没就位应用层完全跑不起来调试了两天才发现问题。第四保留回滚方案。迁移过程中一定要保留旧结构的完整备份并且确保新旧结构可以并行运行一段时间。我通常会在迁移期间让两个结构同时存在通过配置开关切换等新结构稳定运行两周后再删除旧结构。4.8 常见问题速查表问题现象可能原因排查步骤解决方案构建时报模块找不到路径别名未配置检查 tsconfig 和构建工具同步配置所有工具测试时报模块找不到Jest 未配 moduleNameMapper检查 jest.config.js添加映射规则层级检查误报相对路径未识别查看报错文件的具体 import统一改用路径别名循环依赖警告同层服务互相引用用 madge 检查提取公共部分或事件解耦构建体积过大基础层全量导出检查 index.ts改为按需导出同步基础层后报错目标项目有自定义文件对比同步前后差异用 dry-run 预检5. 个人实操体会与后续扩展方向5.1 我在三个项目里踩过的真实坑第一个项目里我把 t3code 的目录结构搭好了但没写层级检查脚本。结果三个月后基础层里出现了getUserToken这样的函数领域层里出现了renderUserPage这样的视图逻辑。整个结构名存实亡。后来我花了两天时间做重构把违规的代码一个个归位才恢复秩序。这件事让我明白没有自动化检查的架构约定等于没有约定。第二个项目里我用了路径别名但只在tsconfig.json里配了忘了配 Vite。开发环境因为 Vite 有默认的 TypeScript 支持居然能跑起来但构建时直接报错。更坑的是CI 环境用的是另一套构建配置报错信息还不一样。我排查了半天才发现是别名配置不一致。从那以后我养成了一个习惯每加一个路径别名就在一个 checklist 里打三个勾——tsconfig、构建工具、测试工具。第三个项目里我遇到了基础层同步的问题。当时有四个项目共享基础层我用脚本同步但有一次同步时目标项目正在开发一个新功能基础层的变更和他们的本地修改冲突了导致代码丢失。后来我改成了手动同步加代码审查虽然慢一点但安全很多。自动化不是万能的涉及代码合并的场景人工确认永远是最可靠的。5.2 这个结构还能怎么扩展t3code 的三层结构是一个起点不是终点。根据项目的发展可以在几个方向上扩展方向一增加“适配层”。当项目需要对接多个外部系统时可以在基础层和领域层之间加一个适配层专门处理外部接口的差异。适配层依赖基础层被领域层依赖这样领域层就不需要关心外部系统的具体协议。方向二引入“特性开关”。在应用层加一个特性开关模块用来控制不同项目启用哪些领域层能力。这样同一个领域层可以服务多个项目每个项目按需开启功能避免不必要的代码打包。方向三拆分基础层为多个子包。当基础层变得很大时可以按功能拆成base-utils、base-types、base-http等子包每个子包独立版本管理。这样不同项目可以按需引用减少不必要的依赖。方向四增加代码生成。领域层的接口和模型如果有规律可以用代码生成工具自动生成。比如从后端 API 文档生成 TypeScript 类型定义减少手写代码的工作量和出错概率。5.3 给准备尝试 t3code 的朋友的几句实在话如果你看完这篇文章想在自己的项目里试试 t3code我有几句实在话想说。第一不要一上来就搞全套。先从基础层和领域层的分离开始把最常用的工具函数和业务服务放进去跑通一个完整的链路。等这个链路稳定了再考虑加检查脚本、路径别名、同步机制。我见过有人第一天就把所有工具配齐结果第二天就因为配置冲突放弃了。第二纪律比技术重要。t3code 的技术门槛很低但纪律要求很高。如果你的团队没有代码审查习惯或者大家习惯“先跑起来再说”那 t3code 可能不适合你。强行推行只会增加沟通成本。第三定期回顾结构。我每个月会花半小时看一下各层的文件变化如果发现某一层异常膨胀就及时调整。这个习惯让我在三个项目里都保持了结构的健康度。第四不要为了架构而架构。t3code 解决的是“多项目代码复用”的问题。如果你只有一个项目或者项目之间几乎没有共享代码那用 t3code 就是过度设计。我见过一个单人项目用了 t3code结果每次改代码都要在三个目录之间跳来跳去效率反而降低了。最后分享一个小技巧在t3code.config.json里加一个lastReviewDate字段每次做结构审计时更新这个日期。这样你一眼就能看出距离上次审计过了多久提醒自己该做检查了。这个字段不参与任何构建逻辑纯粹是给自己看的。