t3code 是我给一个全栈项目起的内部代号,名字看起来简单,背后实际是一整套基于 T3 Stack 的技术组合拳——TypeScript、Tailwind CSS、tRPC、Next.js、Prisma、NextAuth。很多人第一次听到 T3 时会以为只是某个新框架,其实它是一套“怎么把现代全栈开发组合在一起”的工程方案。我用 t3code 这个代号把它落地成一套可复用的脚手架和业务代码,目标只有一个:从数据库表一路到页面按钮,整条数据链路全程类型安全,中间不再有 any 和手写接口类型这种软肋。
如果你正被“后端改了字段,前端报错但编译器没发现”这类问题折磨,或者准备用 Next.js 做全栈应用却不知道数据库、API、认证、样式这几层怎么组织,这篇内容应该能帮上忙。下文从技术选型原理讲到初始化步骤、核心代码链路、部署踩坑,大部分都是我在 t3code 里真实跑过的方案和教训。它不是框架文档的复述,而是把一个真实项目的落地过程拆给你看。
1. t3code 到底是什么:T3 这套组合拳的定位与边界
1.1 从项目名说起:T3 不是框架,是组合方式
先解释一下名字。T3 最初来自 t3.gg 上 Theo 分享的一套个人偏好技术栈,包含 TypeScript、Tailwind、tRPC、Turbo,后面随着社区实践逐渐稳定成:Next.js 作为应用框架、TypeScript 作为语言底座、tRPC 做 API 层、Prisma 做数据层、NextAuth 做认证、Tailwind 做样式。T3 的核心理念是“类型安全优先”——每一层的数据结构都由编译器在开发期帮你校验,而不是等上线后用户报错。
t3code 这个代号实际上是两层含义:一层是“用 T3 组织代码”,另一层是“沉淀一套有业务真实度的代码样板”。因为官方脚手架 create-t3-app 生成出来的只是空壳骨架,真正的业务复杂度要自己填。我把用户系统、数据统计、后台管理这类常见模块塞进骨架里,才有了一堆值得记录的实战细节。
1.2 这套方案解决了我在全栈开发里最痛的问题
我早期做全栈项目是一套典型的三段式:React 写前端、Node 写 API、SQL 写数据库。最耗时的从来不是写功能,而是同步模型。后端改一个字段,前端类型定义要手动改,API 文档要改,联调时又发现 JSON 里某个键拼错了。小项目还能忍,代码量一上来到处是隐患。
t3code 的核心价值就是把这个“同步模型”的过程省略掉。Prisma 的 schema 是唯一事实来源,数据库表结构从那里生成;tRPC 的 router 定义即接口,前端调用时自动拿到完整类型推导;NextAuth 的 session 类型一路贯通到服务端和客户端。你改一个 Prisma 字段后,跑一遍类型检查,所有受影响的位置全部报错,改完就完事。这个体验不是爽一点,是省掉了一整类 bug。
1.3 谁适合拿这套方案,谁的场景需要谨慎
根据我的实践,这几类项目用 t3code 的思路收益最大:
- 内部管理系统、后台面板、SaaS 业务后台:数据模型明确、认证需求普遍,tRPC 自底向上的类型安全能把开发效率拉满。
- 中小型全栈应用:团队本来就不大,不想维护独立的 BFF 层或 GraphQL 服务。
- 想学习现代全栈最佳实践的开发者:一套代码把前后端连接方式完整展示出来。
但也要说清楚边界。如果你的应用需要被大量第三方客户端调用,或者需要开放的公共 API 给外部开发者用,tRPC 这种内部 RPC 协议并不合适,不如直接上 REST 或者 GraphQL。另外如果团队后端以 Java/Go 为主,前端只是消费方,那 T3 组合的收益也会减弱。t3code 最适合的是“一个团队同时掌握前后端,自己消费自己接口”的场景。
2. 技术栈五项拆解:每个库都在前端链路里扮演什么角色
2.1 Next.js + TypeScript:应用底座与类型血液
Next.js 在 t3code 里承担的是路由、渲染、服务端能力整合。它提供 App Router 后,服务端组件和客户端组件能混用,这让 tRPC 的调用策略更灵活——页面首屏可以直接在服务端调数据,交互部分再交给客户端。
TypeScript 是整个方案的地基,但我更想强调的是 strict 模式。create-t3-app 默认开启严格模式,并且开启了noUncheckedIndexedAccess。这个配置看起来严苛,实际救过我很多次:访问数组或对象字段时,TS 会强制你处理undefined的可能性。前期写代码会觉得繁琐,后期重构时才发现每一条约束都在帮你兜底。
2.2 tRPC:把 API 调用变成类型安全的过程调用
tRPC 是 t3code 里最亮眼的一环。它的思路和 GraphQL 相反:GraphQL 试图统一所有客户端协议,tRPC 则完全放弃通用性,只服务“自家前端调用自家后端”这一场景。没有 schema 定义文件,没有 codegen,router 里写什么函数,前端 import 过来就是什么函数。
用一句话给没接触过的读者解释:普通 REST 请求的过程是“前端发 HTTP、后端解析路径、处理后返回 JSON、前端再手写类型接住”;tRPC 的过程是“前端调用一个函数,参数类型直达后端,返回类型原路返回”。中间那层 HTTP 细节由 tRPC 自动处理,你不需要写接口文档,因为类型本身就是文档。
2.3 Prisma + 数据库 Schema:数据层的唯一事实来源
Prisma 在 t3code 里负责连接数据库和定义模型。它的 schema 文件是一个可读性很好的 DSL,比如:
model User { id String @id @default(cuid()) email String @unique name String? role Role @default(USER) posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } enum Role { USER ADMIN }这段定义同步解决了几个问题:数据库表结构、TypeScript 类型、API 操作入口(Prisma Client)。我强烈建议无论项目多小,都用 Prisma Migrate 管理表结构变更,而不是直接在数据库里改表。这样所有环境(本机、预览、生产)的表结构都能保持一致。
2.4 NextAuth.js:认证与会话的类型闭环
NextAuth 在 t3code 里负责登录认证和会话管理。它默认支持 Google、GitHub 这类 OAuth 提供商,也支持 Credentials 账号密码登录。关键操作是把它的 session 类型和 tRPC 上下文打通,让每个请求处理函数里都能拿到“当前用户”的强类型数据。
这里有一条我自己走过的弯路:默认情况下 NextAuth 的类型只包含user.id、user.email这几个基础字段。如果你在数据库里给 User 加了role字段,必须手动扩展类型声明,否则session.user.role在你代码里永远是 undefined 且不报错。这个细节后面踩坑部分还会细说。
2.5 Tailwind CSS:样式层不拖后腿
Tailwind 在技术栈里看起来最不起眼,但它的价值在于让样式层不成为类型安全链路的短板。原子类的方式让我在写 UI 时基本不离开 TSX 文件,不用来回切 CSS 文件,也不会有样式变量和组件状态不同步的问题。配合clsx或tailwind-merge做条件类名拼接,代码很干净。
选 Tailwind 还有个现实原因:t3code 里大量展示型页面(表格、列表、表单)都是高频重复的 UI,Tailwind 的原型速度明显比手写 CSS 快,配合 shadcn/ui 这类组件库能直接进入业务逻辑开发阶段。
2.6 表格:技术栈职责一览
| 技术 | 角色 | 核心价值 | 如果没有它 |
|---|---|---|---|
| TypeScript | 语言层 | 全链路类型推导 | 端到端类型安全无从谈起 |
| Next.js | 应用层 | 路由、SSR、Server Actions | 只能分成前后端两个项目 |
| tRPC | API 层 | 函数式 RPC 强类型 | 接口联调回到手写类型的时代 |
| Prisma | 数据层 | schema 生成类型与迁移 | 数据库变更不可追踪 |
| NextAuth | 认证层 | 登录与会话管理 | 自己造轮子,安全风险高 |
| Tailwind | 样式层 | 原子 CSS 快速实现 UI | 样式开发拖慢整体节奏 |
3. 从零初始化 t3code:一条能直接照抄的命令行路线
3.1 创建项目与模板选择
第一步是初始化项目。官方脚手架命令是:
npx create-t3-app@latest执行后会出现一组交互问答,主要选项是:
Next.js版本(最新 App Router)TypeScript or JavaScript:选 TypeScripttailwind:选上trpc:选上database:PrismanextAuth:登录认证选上(如果应用不需要登录可以关掉,但 t3code 默认开启)eslint / prettier:建议都选上
我用的是app router版本,因为 App Router 的 Server Component 能和 tRPC 的 server caller 配合,实现首屏性能优化。如果团队还停留在 Pages Router 的旧习惯,建议直接迁移,不要再开新项目用旧模式。
3.2 环境变量:最容易被忽略的启动开关
项目生成后,根目录会有一个.env.example文件。首次启动前必须复制一份.env并填好变量。t3code 里最核心的几个:
# 数据库连接 DATABASE_URL="postgresql://user:password@localhost:5432/t3code" # NextAuth NEXTAUTH_SECRET="此处生成一个随机字符串" NEXTAUTH_URL="http://localhost:3000" # 如果使用 OAuth 提供商 GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET=""NEXTAUTH_SECRET可以用命令生成:
openssl rand -base64 32这个 secret 要妥善保管,生产环境换掉本地值,如果泄露,攻击者可以伪造会话。我见过不少项目把它写死在代码里或者推到 Git 仓库,这是很低级的错误。
3.3 目录结构:每层代码的责任边界
正常初始化后的 src 目录结构如下(这是 t3code 最核心的组织方式):
src/ ├── components/ # UI 组件 ├── pages/ # 路由页面(初期) ├── server/ │ ├── api/ # tRPC router 根定义 │ ├── auth/ # NextAuth 配置 │ ├── db.ts # Prisma Client 实例 │ └── trpc.ts # tRPC 上下文与中间件 ├── styles/ # 全局样式 └── utils/ # 工具函数如果用 App Router,会进一步调整为app/目录,但server/下的核心逻辑保持稳定。这里的黄金法则是:所有数据库访问代码只放进 server 层,组件里不直接 import Prisma Client。理由很简单,Prisma Client 如果被打进客户端 bundle,会暴露数据库操作能力,这是安全底线。tRPC 的存在让这条约束变得容易遵守,因为所有数据请求都能通过 router 方法发出。
3.4 首次同步数据库:migrate 与 db push
环境变量配置好之后,第一次要做的就是同步数据库结构。Prisma 提供两个命令,我使用场景如下:
# 开发阶段表结构频繁调整时 npx prisma db push # 正式环境、需要版本化迁移记录时 npx prisma migrate devdb push是直接推 schema 到数据库,不生成迁移文件,适合原型期;migrate dev会生成带时间戳的迁移记录,进入团队协作阶段必须用它。我踩过的坑是:在项目早期一直用 db push,后来数据库里有真实数据了才开始 migrate,这时 Prisma 会要求确认是否重置数据,稍不留神就把开发数据清空了。正确做法是第一天就用 migrate 建立习惯。
跑通之后可以执行:
npx prisma studio打开图形化管理界面,直观看到表结构和数据。开发期用它确认 schema 是否正确。
4. 端到端类型安全的完整链路:从数据库表到页面按钮
4.1 先看这条链路怎么串起来的
t3code 的核心体验用一个场景就能说清楚:我在数据库里加一张Project表,然后前端页面立刻就能以类型安全的方式查询、新增、修改。整个过程分四步:
- Prisma schema 定义模型;
- tRPC router 定义查询与变更方法;
- 前端组件通过
useQuery/useMutation调用; - NextAuth 提供当前用户身份,router 中间件做权限控制。
下面用一个简单的“项目列表”功能完整演示。
4.2 Prisma 模型定义
model Project { id String @id @default(cuid()) name String description String? status Status @default(ACTIVE) ownerId String owner User @relation(fields: [ownerId], references: [id]) createdAt DateTime @default(now()) } enum Status { ACTIVE ARCHIVED }这里加了ownerId外键,指向 User 表。有了这个字段,权限中间件就可以保证用户只能操作属于自己的项目,这是后续一切安全逻辑的基础。
4.3 tRPC Router:定义接口的同时定义类型
在server/api/routers/project.ts中定义:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; export const projectRouter = createTRPCRouter({ // 获取当前用户的项目列表 listMine: protectedProcedure.query(async ({ ctx }) => { return ctx.db.project.findMany({ where: { ownerId: ctx.session.user.id }, orderBy: { createdAt: "desc" }, }); }), // 创建项目 create: protectedProcedure .input(z.object({ name: z.string().min(1).max(60), description: z.string().optional(), })) .mutation(async ({ ctx, input }) => { return ctx.db.project.create({ data: { name: input.name, description: input.description, ownerId: ctx.session.user.id, }, }); }), // 归档项目 archive: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const project = await ctx.db.project.findUnique({ where: { id: input.id }, }); if (!project || project.ownerId !== ctx.session.user.id) { throw new Error("无权操作该项目"); } return ctx.db.project.update({ where: { id: input.id }, data: { status: "ARCHIVED" }, }); }), });这段代码有几个关键点。protectedProcedure是 tRPC 中间件的一种,它在执行 handler 之前先检查用户是否登录;zod负责校验输入参数,运行时和 TypeScript 类型一致;ctx.session.user.id是强类型,不会出现undefined的隐性风险。
4.4 前端组件调用:真正的类型安全在调用侧体现
在客户端组件里调用:
"use client"; import { api } from "~/utils/api"; export function ProjectList() { const { data: projects, isLoading } = api.project.listMine.useQuery(); const createProject = api.project.create.useMutation({ onSuccess: () => { // 让列表自动刷新 }, }); if (isLoading) return <div>加载中...</div>; return ( <ul> {projects?.map((project) => ( <li key={project.id}> {project.name} - {project.status} </li> ))} </ul> ); }这里projects的类型是从listMine的返回类型自动推导出来的,TypeScript 能识别出project.name是 string、project.status是Status枚举。你改数据库模型,这里就会跟着报错或自动更新,不需要任何手工同步。
4.5 权限中间件:t3code 的访问控制标准做法
权限控制建议放在 tRPC 中间件层,而不是散落在每个 handler 里。我常用的一个模式是“按角色区分”:
// server/api/trpc.ts import { initTRPC, TRPCError } from "@trpc/server"; import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; const t = initTRPC.context<{ db: PrismaClient; session: Session | null }>().create(); const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); }); export const protectedProcedure = t.procedure.use(isAuthed);这样只要把某个 procedure 定义成protectedProcedure,它天然具备登录校验和 session 注入,不用每个路由重复写。
4.6 服务端组件里的数据获取:靠 server caller
App Router 模式下,首屏数据可以直接在服务端获取,减少客户端请求数量和加载状态闪烁。方式是利用 tRPC 的 server caller:
// app/page.tsx import { api } from "~/server/api/root"; import { createTRPCContext } from "~/server/api/trpc"; import { ProjectList } from "~/components/project-list"; export default async function HomePage() { const ctx = await createTRPCContext({ req: {}, res: {} } as any); const caller = api.createCaller(ctx); const projects = await caller.project.listMine(); return <ProjectList initialData={projects} />; }这样做的好处是首屏不经过客户端请求,页面直接渲染数据,对 SEO 和首屏性能都有帮助。但要注意createTRPCContext参数要能从请求中提取 session,具体实现因 NextAuth 版本略有差异,我在踩坑部分会再讲。
5. 部署与性能:把 t3code 推到生产环境的实战配置
5.1 数据库与连接池:Serverless 环境下最容易翻车的地方
t3code 使用 Next.js 的 Serverless 部署时,最大隐患是数据库连接管理。Prisma Client 每次冷启动都要新建连接池,连接数会被快速打满,尤其 Postgres 默认连接限制是 100 左右。
我在生产环境实测过,不处理连接池的情况下,线上稍一波动就会出现P1001连接超时错误。后来采用了两个措施:
- 使用连接池模式的数据库连接串,例如在
DATABASE_URL后加?pgbouncer=true&connection_limit=1,配合 PgBouncer 做连接中转; - 在数据访问层做 Prisma Client 的全局复用,避免每次热更新都创建新实例。
一个常用的全局实例写法:
// server/db.ts import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }; export const db = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"], }); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;这段代码在 Next.js 开发模式热重载时非常重要,否则每保存一次文件都会 new 一个 PrismaClient,连接数直接爆炸。
5.2 平台选择:Vercel 与常规服务器的取舍
我实际部署了两套环境,对比结果如下:
| 场景 | Vercel | Docker + 常规服务器 |
|---|---|---|
| 部署速度 | 极快,git push 即发 | 需要搭流水线 |
| Serverless 特点 | 按需扩展,冷启动存在 | 常驻进程,连接管理简单 |
| 适合项目 | 中小型、流量波动大 | 有固定流量、要求低延迟 |
| Prisma 连接 | 需要 PgBouncer 配合 | 直接连接即可 |
| 成本 | 免费额度友好 | 服务器费用稳定 |
t3code 最初部署在 Vercel 上,开发体验确实好,git push 到 main 分支就自动构建。后来因为某段时间接口调用量暴涨、冷启动频繁,我迁移到一台 4C8G 的云服务器上用 Docker Compose 跑,延迟降了约 40%。如果你的项目流量预计比较稳定,我更推荐一开始就用容器化部署。
5.3 React Query 缓存与 tRPC 的配合
tRPC 底层使用 React Query 管理客户端服务端状态。默认配置下,useQuery的staleTime是 0,意味着每次组件挂载都会重新请求,这在数据不常变的页面会重复浪费请求。我在 t3code 里统一设置了合理的默认值:
// utils/api.ts import { httpBatchLink } from "@trpc/client"; import { createTRPCNext } from "@trpc/next"; export const api = createTRPCNext({ config() { return { links: [httpBatchLink({ url: "/api/trpc" })], queryClientConfig: { defaultOptions: { queries: { staleTime: 60 * 1000, // 1分钟内认为数据新鲜 refetchOnWindowFocus: false, }, }, }, }; }, ssr: false, });staleTime设为 60 秒之后,列表页来回切换不再频繁请求,而需要实时性的操作可以通过api.something.useMutation().onSuccess里的invalidate刷新特定数据。
const utils = api.useUtils(); const createProject = api.project.create.useMutation({ onSuccess: async () => { await utils.project.listMine.invalidate(); }, });这样既保证了实时刷新,又不会让整个列表重复请求。
5.4 构建优化与包体积
Next.js 的构建产物如果你观察过,会发现大部分体积来自依赖包。t3code 中最占体积的是@trpc/server、prisma(构建期)、next-auth。有几个实际能见效的优化手段:
- 确认
@prisma/client没有被打进客户端 bundle,检查 next.config.js 的serverComponentsExternalPackages; - 使用
next/dynamic拆分非首屏组件; - tRPC router 按模块拆分,避免“调一个接口把整个 router 全打进来”。
// next.config.js const nextConfig = { serverComponentsExternalPackages: ["@prisma/client", "@prisma/engines"], }; export default nextConfig;6. 踩坑实录:我在 t3code 里真实倒过的五道坑
6.1 多环境 Prisma Client 生成不一致
问题表现:本地跑得好好的,push 到预览环境构建失败,报错信息类似PrismaClient is not defined或schema.prisma not found。
排查链路:我先以为缺少依赖,执行npm install重装无效;后来对比本地和云端.env发现DATABASE_URL格式不同;最后定位到是构建流程中prisma generate没有拿到正确环境变量。解决方案是在构建命令里显式加一步:
npx prisma generate && next build并在 package.json 的 postinstall 中保留prisma generate。这样无论是本地、CI 还是云平台,都会在构建前强制执行 Prisma Client 生成。
6.2 tRPC 上下文类型与 NextAuth 会话类型没打通
问题表现:ctx.session.user.id在类型检查时直接报“Property ‘id’ does not exist on type ‘User’”,但运行时数据却有值。
根因:NextAuth 默认的 Session 类型里 user 只有name、email、image,并没有你自定义的id和role字段。必须在项目里声明类型扩展:
// types/next-auth.d.ts import { DefaultSession } from "next-auth"; declare module "next-auth" { interface Session { user: { id: string; role: "USER" | "ADMIN"; } & DefaultSession["user"]; } }这个坑很容易被忽略,因为大多数情况下运行时一切正常,只有当你显式访问未扩展字段并用到类型校验时才会暴露。我建议初始化项目后第一件事就是补齐类型声明,而不是等用到再说。
6.3 Server Component 里调用 tRPC 时 session 拿不到
问题表现:我把caller.project.listMine()写在app/page.tsx,本地开发正常,部署后却一直返回未授权错误。
排查链路:一开始以为环境变量问题,查了NEXTAUTH_URL;后来发现是因为createTRPCContext需要从 Cookies 里读 session token,而 Server Component 中如果没有正确透传req信息,NextAuth 拿不到会话。解决方式是使用headers()或cookies()来构造请求上下文,不同 Next.js 版本实现略有差异,但核心思路是“让服务端能感知到当前请求的 cookie”。
这个坑提醒我:t3code 的第一版代码里把“是否登录”的检查放在了客户端,后来才移动到服务端。安全边界必须放在服务端,客户端判断只影响 UI 展示体验。
6.4 Tailwind 配置改动后样式长时间不生效
问题表现:往tailwind.config.ts里加了自定义颜色变量,页面刷新后很久没变化,甚至需要重启 dev server。
根因:Tailwind 和 Next.js 的缓存机制导致配置变更未被热更新捕获。解决方法比较粗暴但有效:改了tailwind.config.ts或者globals.css后,手动重启 dev server;如果是生产环境,确认构建缓存已经清理。另外检查content配的路径是否覆盖了实际用到的文件目录。
// tailwind.config.ts export default { content: [ "./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], theme: { extend: { colors: { brand: "#5B5BD6", }, }, }, };6.5 打包时 .env 缺失导致构建失败
问题表现:CI 环境里next build总在某个阶段报NEXTAUTH_SECRET未定义。
根因:构建阶段 Next.js 会把环境变量内联进代码,如果配置不存在,引用它的模块就会报错。处理方案是在 CI 平台上添加环境变量,而不是只写在本地.env。另外可以给关键变量设置默认值兜底,但绝不能把真实 secret 默认值写进代码。我当时花时间最多的是排查“为什么本地没问题”,后来才意识到 CI 环境完全是空白环境,该有的变量一个都没有。
7. 演进方向:t3code 下一步还能往哪里走
7.1 从 RPC 到 Server Actions:技术路线的观察
t3code 做的过程中,Next.js 持续在推 Server Actions,它可以让某些交互场景完全不经过 tRPC,直接在 Server Component 里定义函数并绑定到表单。这个方向对简单场景很友好,但我的判断是:tRPC 在复杂应用里依然有优势——它的显式路由结构、输入校验、中间件体系更成熟,且不受框架渲染模式变迁的影响。如果你要新建项目,可以先看 Next.js 官方文档里 Server Actions 的能力边界,再决定是否保留 tRPC 层。t3code 目前的做法是两者并存:表单类交互用 tRPC mutation,简单的展示型数据获取可以考虑 Server Actions 或 RSC 直接读取。
7.2 实时能力的加入
如果业务需要聊天、通知、实时看板,下一步可以在 t3code 里引入@trpc/server的订阅能力,或者直接接 WebSocket 网关。tRPC 的订阅基于ws实现,与现有 router 无缝衔接,可以复用已经定义好的校验和中间件逻辑。需要提醒的是,Serverless 平台对 WebSocket 支持差异很大,如果要加实时功能,建议迁移到长连接友好的部署方式。
7.3 测试与 CI 集成
t3code 的自动化测试是我目前投入还不够的部分。后续计划在 CI 流水线中增加:
- 构建前执行
prisma generate和next lint; - 关键 router 做集成测试,跑在测试数据库上;
- 用 Playwright 做核心用户流程的端到端测试。
测试数据库用 Docker 起一个 Postgres,集成测试直接调用 tRPC caller,同样能获得类型安全的测试代码。这一块整理好之后,t3code 就能从一个个人项目样板,变成团队可以直接上手的基础设施。