☰
t3code:轻量级跨项目代码复用与三层架构实践指南
2026/10/27 23:14:01 网站建设 项目流程

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): Promise<User>; logout(): Promise<void>; getCurrentUser(): Promise<User | 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.ts)

export interface User { id: string; name: string; email: string; } export interface UserService { getUserById(id: string): Promise<User | null>; searchUsers(keyword: string): Promise<User[]>; updateUser(id: string, data: Partial<User>): Promise<User>; }

默认实现(domain/services/impl/default-user-service.ts)

import { User, UserService } from '../user-service'; import { httpClient } from '@base/utils/http'; export class DefaultUserService implements UserService { async getUserById(id: string): Promise<User | null> { const response = await httpClient.get(`/api/users/${id}`); return response.data || null; } async searchUsers(keyword: string): Promise<User[]> { const response = await httpClient.get('/api/users', { params: { q: keyword }, }); return response.data || []; } async updateUser(id: string, data: Partial<User>): Promise<User> { const response = await httpClient.put(`/api/users/${id}`, data); return response.data; } }

注意这里的httpClient是从@base/utils/http引入的,属于基础层的能力。领域层依赖基础层是允许的,方向正确。

应用层容器(application/project-a/container.ts)

import { 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.ts)

import { 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": "zhangsan@example.com", "created_at": "2024-01-01T00:00:00Z" }

但前端视图需要的格式是:

{ "id": "123", "name": "张三", "email": "zhangsan@example.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里配moduleNameMapper:

module.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,只打包用到的部分。

第二,领域层的服务实现要懒加载。如果某个服务只在特定页面用到,可以在容器里用动态 import:

export 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字段,每次做结构审计时更新这个日期。这样你一眼就能看出距离上次审计过了多久,提醒自己该做检查了。这个字段不参与任何构建逻辑,纯粹是给自己看的。

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

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

立即咨询