ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

T3Code:T3 Stack全栈项目脚手架的设计与实现

T3Code:T3 Stack全栈项目脚手架的设计与实现

这两年做全栈项目,我最大的感受是:开新项目的成本,大头不在写业务代码,而在搭骨架。装依赖、配 TS 路径、搭 tRPC 路由、初始化 Prisma、写好 Tailwind 的主题变量……这一套下来,快的也要半小时,慢的能折腾一上午。尤其团队里每个成员的习惯还不一样,A 喜欢把 API 写一个文件里,B 习惯按 feature 分组,最后合并代码时全靠人工对齐,光 review 就要浪费大量时间。

T3Code 就是为解决这个问题做的。它是一个命令行工具,用来快速生成标准化的 T3 Stack 项目骨架——直接帮你把 Next.js、Tailwind、tRPC、Prisma、TypeScript 这些技术栈全部初始化好,再按统一规范生成功能模块的 CRUD 代码。简单说,你只要告诉它“我要做一个带用户和通知中心的项目”,它就能把项目目录、数据库模型、API 路由、前端页面骨架都给你铺好。这篇文章就聊聊 T3Code 的设计思路、核心实现和实际使用中的注意事项,给准备自己写脚手架或者想在团队内统一工程规范的朋友做个参考。

1. 整体设计与思路拆解

动手写 T3Code 之前,我最先想清楚的问题不是“怎么写代码”,而是“这个工具到底要解决谁的什么问题”。方向没定好,后面全是返工。

1.1 痛点:每次开新项目,重复劳动太多

我统计过自己过去一年的项目启动时间:新建一个全栈应用,从create-next-app开始,到能跑通第一个带数据库读写的前后端联调页面,平均花 45 分钟。这 45 分钟里真正写业务的时间大概只有 10 分钟,其余全在配置——比如 Next.js 的 server 端配置、tRPC 的 router 结构、Prisma 的 datasource 和 model、TS 的路径别名、ESLint 规则等等。

更要命的是团队场景。我自己手写没问题,但三个人同时开同样的模块,写出来的目录结构、命名风格、错误处理方式一定五花八门。有人喜欢userRouter,有人写成user.routes.ts;有人把所有模型塞进一个 schema.prisma,有人拆成十几个文件。代码 review 的时候,大量精力花在“这个文件该放哪”而不是“这个逻辑对不对”上。

所以 T3Code 的定位很明确:不做业务,只做规范和骨架。它把技术栈的初始化和项目结构的规范化固化成模板,让每个新项目从第一行代码起就长成一个样子。

1.2 定义与核心能力:T3Code 到底做什么

T3Code 是一个 Node.js 环境下运行的命令行工具,核心流程分三步:

  1. 读取用户输入的项目名、模块列表和功能选项。
  2. 根据输入从内置模板引擎生成对应的目录结构和文件内容。
  3. 自动安装依赖、初始化数据库 schema、输出后续操作指引。

它最核心的卖点不是“一套模板”,而是“可组合的模块化生成”。你不需要每次生成整个项目,也可以单独在已有项目里跑t3code add post,只新增一个 post 功能模块——包括 Prisma model、tRPC router、页面组件、API 类型导出,一次生成,全部接好。

1.3 适合谁用,解决什么问题

我的目标用户有两类。第一类是个人开发者,尤其是刚接触 T3 技术栈的人。T3Code 生成的代码是规范且可直接运行的,照着读代码就能理解 Next.js 和 tRPC 怎么配合,比看文档一个一个粘配置要快得多。第二类是团队,用一个强制统一的脚手架来消除风格分叉,新成员进组后不需要再问“我们项目结构是啥”,跑一遍t3code init就知道了。

也正因为想同时服务这两类人,我在设计时坚持了一个原则:生成的代码必须“完全可读可改”,不能像很多代码生成器那样生成一堆让人看不懂的封装层。T3Code 生成的代码应当接近手写水平,甚至比手写更整齐。否则就是先生成垃圾再手动清理,完全没有意义。

2. 架构设计与技术选型

定完目标,下一步是选技术。这个阶段我踩了不少坑,选型结果和最初设想差别挺大,逐个说明。

2.1 底层的 T3 Stack 为什么值得用

T3Code 生成的底层项目默认基于 T3 Stack 的标准组合:Next.js(App Router)+ TypeScript + Tailwind CSS + tRPC + Prisma。这套组合不是随便凑的,它的核心逻辑是“端到端类型安全”。

举个例子,在传统前后端分离项目里,前端调接口拿到的数据是一个“约定”的类型,后端改了字段,前端编译时毫无察觉,直到运行时报错或者页面白屏。而 tRPC 的做法是在服务端定义 query 和 mutation,前端直接 import 这个 router 的类型来调用,一旦服务端返回结构变了,前端代码立刻编译不过,错误直接暴露在开发期的编辑器里。

T3Code 生成的项目把这个优势贯彻到每个模块:数据库 model 由 Prisma 定义,tRPC router 依赖 Prisma 的 client 类型,前端页面又从 tRPC 的类型里拿到返回数据。一条链路下来,类型是通的,改一处,全链路的类型检查都会提醒你。这对团队协作来说是巨大的收益——接口文档都省了一大半,因为类型本身就是最好的文档。

2.2 CLI 工具的技术选型:模板引擎与交互库

T3Code 自己是用 TypeScript 写的,运行在 Node.js 环境。几个关键依赖:

  • commander:解析命令行参数,处理init、add、list这些子命令。
  • prompts:做交互式提问,比如问“项目放在哪个目录”“要生成哪些模块”。
  • handlebars:模板引擎,用{{variable}}语法把用户输入注入到代码模板里。
  • prettier:生成完代码后做一次统一格式化,避免模板里缩进不一致。

最开始我考虑过直接复制静态模板目录,但很快就发现不够灵活。比如同一个 Prisma model,用户选“包含软删除”和“不包含软删除”,生成的字段就差一个deletedAt。如果用静态模板,就得准备两套目录;要是选项多了,模板数量爆炸。换成 handlebars 之后,模板里可以写条件判断和循环,一个模板文件同时覆盖多种情况。

其实也考虑过用ejs或者mustache,它们的语法更简单,但对复杂逻辑支持不够。handlebars 的 helper 机制很合适——我可以自定义{{camelCase name}}、{{pascalCase name}}这样的 helper,在模板里直接做命名转换,不需要在代码里到处处理字符串大小写。

2.3 命名规范与目录结构的取舍

这一块是团队协作的核心,也是 T3Code 和普通脚手架最大的区别。我定了一套严格的命名规范,并在生成器层面强制实施:

  • 文件名统一用kebab-case,比如user-list.tsx、create-user.ts。
  • 函数和变量用camelCase,组件和 Prisma model 用PascalCase。
  • tRPC router 文件统一叫<feature>.router.ts,且每个 feature 一个目录。
  • 目录按 feature 聚合,不再按文件类型聚合。

很多人会问:为什么不按components/、pages/、api/这种按类型分层?早期项目确实这么组织,但在功能不断膨胀后,每次改一个功能要同时打开四五个目录,跳来跳去很累。按 feature 聚合后,一个通知中心相关的所有代码——数据库模型、API 接口、页面组件、工具函数——都在features/notification/下,改起来顺心很多。

这个改动是 T3Code 被团队接受的关键。没有一个现成脚手架这么组织,但这是我认为大型项目后期维护最舒服的结构。T3Code 把这种结构固定下来,免去了每次新项目都跟团队成员解释一遍“为什么这么放”的烦恼。

3. 核心实现与实操要点

聊完设计思想,来说具体实现。这一章是干货密集区,有大量可以直接参考的代码思路。

3.1 命令设计:init 和 add 的交互流程

T3Code 对外暴露两个主要命令:

t3code init my-project t3code add notification

init命令的完整处理流程是这样的:

  1. 检查目标目录是否存在,为空还是已有文件。
  2. 通过 prompts 问几个关键问题:包管理器用 npm 还是 pnpm、是否需要用户认证模块、数据库用 SQLite 还是 PostgreSQL。
  3. 调用模板引擎,生成基础项目和用户选中的额外模块。
  4. 在所有.env、配置文件里替换成用户输入的内容。
  5. 运行npm install,完成后打印下一步操作指引。

有一个细节值得说:生成器的交互问题不能太多。我第一版设计了 12 个问题,用户还没看到生成结果就先被问烦了。后来砍到最多 5 个,其余依赖默认值,用户可以在生成后手动改。这个经验也分享给你们——CLI 工具的交互设计,克制比丰富重要。

add命令则简单很多,只有一个必要参数:模块名。它的流程是:

  1. 读取当前项目的结构,判断是 T3Code 生成的标准项目。
  2. 解析模块名,自动生成 PascalCase、camelCase、kebab-case 三种命名。
  3. 生成 Prisma model、tRPC router、页面组件、导航菜单更新。
  4. 提醒用户运行npx prisma migrate dev来同步数据库。

3.2 代码生成模板的编写思路

模板是 T3Code 的心脏。我把模板文件放在templates/目录下,每个功能模块一套,核心文件有这几个:

templates/module/ model.prisma.hbs # Prisma model 片段 router.ts.hbs # tRPC router schema.ts.hbs # zod 输入校验 component.tsx.hbs # 前端页面组件 menu.ts.hbs # 导航菜单片段

以 tRPC router 的模板为例,简化后的代码长这样:

import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; import { {{pascalCase name}}Schema } from "./schema"; export const {{camelCase name}}Router = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.{{camelCase name}}.findMany({ orderBy: { createdAt: "desc" }, }); }), create: publicProcedure .input({{pascalCase name}}Schema) .mutation(async ({ ctx, input }) => { return ctx.db.{{camelCase name}}.create({ data: input, }); }), getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { return ctx.db.{{camelCase name}}.findUnique({ where: { id: input.id }, }); }), });

你注意{{pascalCase name}}和{{camelCase name}}这两个占位符。用户输入notification时,handlebars helper 会自动生成Notification和notification,模板里不用手动写大小写转换逻辑。这一步看起来小,但实际体验差别巨大——没有 helper 的话,你得在代码里写一堆toPascalCase()的字符串处理,容易漏掉边界情况。

Prisma model 的模板会更有意思,它需要在用户已有的 schema 里自动插入新 model。我的做法是生成一个片段文件,然后由 Node 脚本读取主 schema 文件,找到model关键字的结尾位置,插入片段。为了避免重复插入,我在 model 顶部加了一段注释标记// T3CODE_MODEL:notification,下次生成时先检查这个标记是否存在。

3.3 配置注入与依赖管理

生成完代码文件,还有一个关键步骤是配置注入。Next.js 项目里有几个文件必须同步更新:

  • package.json:新增模块如果带了额外依赖,需要自动加进去。
  • src/server/api/root.ts:把生成的 router 挂载到根 router 上。
  • src/server/db/schema.prisma:追加 model 定义。
  • src/config/nav.ts:在侧边导航里加入新模块入口。

这四件事单独做都不难,难的是不能出错。比如root.ts里的挂载代码,我最初是用字符串替换,找到export const appRouter = createTRPCRouter({这一行,在后面插入新 router。但后来发现这段代码在不同版本模板里格式不同,字符串匹配容易漏。最后改用抽象语法树解析——用 TypeScript 编译器 API 读文件、改 AST、再输出代码。这样无论代码格式怎么变,AST 层面总是能精确定位插入位置。

这也是我想重点说的经验:代码自动生成的底线是“生成结果必须能编译通过”。如果生成出来的代码因为漏了一个 import 或者格式不对导致编译报错,用户的信任感会瞬间崩掉。宁可多花时间在 AST 处理和 prettier 格式化上,也不能输出半成品。

4. 完整实操:从零生成一个通知中心项目

理论说了一堆,实际操作来看一遍。我用 T3Code 从零生成一个带用户认证和通知中心的项目,记录完整的操作过程。

4.1 安装与初始化

T3Code 已经发布到 npm registry,全局安装:

npm install -g @t3code/cli

安装完成后,运行初始化命令:

t3code init my-app

这时终端会出现交互提示,选择包管理器(我选 pnpm)、数据库类型(选 SQLite,方便本地开发)、是否包含用户认证(选包含)。选择完成后,终端输出生成进度:

✔ 项目已创建: my-app ✔ 已生成用户认证模块 ✔ 已安装依赖 (耗时 42s) 下一步: cd my-app npx prisma migrate dev --name init npm run dev

整个过程 1 分钟左右。生成后的目录结构是这样的:

my-app/ prisma/ schema.prisma src/ app/ (auth)/ login/page.tsx register/page.tsx (dashboard)/ layout.tsx page.tsx features/ auth/ router.ts schema.ts components/login-form.tsx components/register-form.tsx notification/ router.ts schema.ts components/notification-list.tsx server/ api/ root.ts trpc.ts db.ts trpc/ server.ts client.ts

4.2 生成第一个功能模块

基础项目就绪后,我来加一个“公告公告”模块。公告的核心字段是标题、正文、发布时间、状态,不需要关联到用户,逻辑很简单。

在项目目录下运行:

t3code add announcement

T3Code 会解析当前项目结构,再生成 4 个文件:features/announcement/model.prisma、features/announcement/router.ts、features/announcement/schema.ts、features/announcement/components/announcement-list.tsx,同时更新了root.ts、导航菜单和schema.prisma。

打开router.ts看到的是带有list、create、update、delete四个基础操作的完整 router。我用编辑器打开model.prisma,看到模板自动生成的 model:

model Announcement { id String @id @default(cuid()) title String content String status String @default("draft") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

基本符合预期。这里的status字段是模板默认加上去的,它会在前端组件里映射成一个下拉选择框:草稿、已发布、已下线。如果你不需要这字段,删掉再加迁移即可。

4.3 运行与验证

按提示执行数据库迁移:

npx prisma migrate dev --name add_announcement

看到迁移成功信息后,启动开发服务器:

npm run dev

浏览器打开http://localhost:3000,左侧导航栏已经出现“公告管理”,点进去能看到公告列表的占位页面,右上角有“新建公告”按钮。打开浏览器控制台切到 Network,访问页面时发出的 tRPC 请求路径是/api/trpc/announcement.list,说明 router 已经成功挂载。

这一整套流程跑下来,从安装到看到一个能增删改查的模块页面,耗时不超过 3 分钟。这个速度是 T3Code 存在的最大价值——把内耗降到最低,把时间留给真正需要动脑的业务逻辑。

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

任何工具都有自己的坑,T3Code 也一样。这里把我在开发和使用过程中遇到的高频问题整理出来,按排查顺序排列。

5.1 几个容易踩的坑

问题 1:生成后数据库迁移报外键错误

这通常是模块之间存在关联时出的问题。比如我先生成user模块,再生成notification,而 notification 里有一条userId String外键指向User。如果 Prisma schema 里Usermodel 不存在(因为用户初始化时没选 auth 模块),迁移自然会失败。

解决办法是模板生成时做依赖检查:当生成一个包含外键字段的模块时,先检查主表 model 是否已在 schema 中,不存在则自动追加基础 model,或直接终止并提示用户先添加依赖模块。现在 T3Code 里已经内置了这两种处理,但第一次写的时候没考虑周全,被这个坑绊了一下午。

问题 2:AST 插入后代码丢了 import

用 TypeScript AST 往root.ts里插入 router 引用时,如果没有同步处理 import 语句,生成结果就会在未引用的状态下使用变量。编译器打开就报红。

解决办法是在 AST 操作时先收集所有 import 声明,新增 router 时把import { announcementRouter } from "@/features/announcement/router"自动追加到 import 块末尾。这一步能用抽象语法树一次搞定,但要注意配置里moduleResolution必须匹配项目的 tsconfig。

问题 3:pnpm 安装依赖特别慢

pnpm 在首次安装时会做硬链接,项目多了以后全局 store 会占用不少磁盘。T3Code 在init时用pnpm会明显比npm慢,如果你只是快速体验工具,可以先用 npm。

不过考虑到 pnpm 后续增量安装优势很大,我建议在正式项目里坚持用 pnpm。磁盘上多占几 GB 完全值得。

5.2 排查顺序与调试命令

遇到 T3Code 相关问题,我建议按这个顺序排查:

  1. 先看终端错误信息——是 CLI 本身报错,还是生成后项目编译报错。这两个问题的处理方式完全不同。
  2. 如果是生成后的项目编译报错,先跑npx prisma generate,确认 Prisma Client 是最新的。很多 tRPC 类型报错归根到底都是 Prisma Client 没重新生成。
  3. 如果 tRPC 路由找不到,检查root.ts里 router 的 key 是否和前端调用一致。T3Code 统一用<feature>.list这种命名,手动改过 router 名称就容易对不上。
  4. 如果页面样式丢失,检查 Tailwind 的 content 配置是否包含src/features/**/*.tsx。新生成的组件如果没被 Tailwind 扫描到,类名就不会生效。

还有一个通用调试技巧:生成代码后,立刻在项目根目录跑一次npm run typecheck。T3Code 生成的代码是标准的,理论上 typecheck 一定能过;如果没过,优先看提示信息里的文件和行号,那通常比看运行时错误更直观。

5.3 团队协作中的版本管理技巧

最后聊点实战中最重要的东西:T3Code 在团队里怎么维护。

由于每位成员的本地环境不同,我们约定代码模板的改动走代码评审,而且必须有 fixture 验证。T3Code 项目里有几个固定的测试 fixture,每个模板文件的改动都会在 CI 里触发一次“生成项目 → 跑 typecheck → 跑测试”的完整流程。只有这套流程过了,改动才能合并。

对于使用方,我建议在项目里记录生成器版本。做法是生成项目时在根目录生成一个.t3code.json配置文件,记录 CLI 版本和生成时选项。以后升级 T3Code 要重新生成代码时,工具会读取这个文件,自动了解项目结构,避免把已有的二次开发覆盖掉。

6. 从 T3Code 这个工具里我学到的三件事

T3Code 写到今天,代码量不大,但它教会我的远不止代码生成器本身。三个体会分享给大家。

第一个体会是“模板比脚本重要,约定比配置重要”。最初我想把 T3Code 做成一个“全能工厂”,接了各种参数,结果用户反而迷茫。后来不断做减法,把几个高频场景做成固定模板,才真正有人愿意天天用。好的脚手架应该像一张好的表单——只问你真正需要的信息,剩下的都帮你填好默认值。

第二个体会是“生成的代码必须能被人读懂”。很多代码生成器喜欢生成一堆封装层、抽象工厂,看着高级,但实际上手修改时痛苦得要命。我坚持让 T3Code 生成的代码都是平铺直叙的写法,一个功能一个 router、一个页面对应一个组件文件,谁打开都能看懂。代码生成器的终极目标不是减少代码量,而是减少理解成本。

第三个体会是“工具是文化的载体”。T3Code 最初只是我一个人写的小脚本,后来团队里所有人都在用,再后来我加的命名规范、目录结构也潜移默化地影响了每个人写代码的习惯。统一脚手架不只是为了效率,更是为了让大家在协作时少一点“他怎么这样写”的吐槽,多一点“这个放这我懂”的默契。如果你也在带团队,不妨从统一脚手架开始整顿工程秩序,收益会比你想象的大。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进