☰
t3code:一条命令搞定TypeScript全栈项目基建与代码规范
2026/10/26 8:31:46 网站建设 项目流程

先说一个我这两年反复踩的坑:接手新项目,光是搭 TypeScript、Next.js、Prisma、tRPC 这套 T3 技术栈,再把 lint、prettier、测试框架的配置统一对齐,轻轻松松就耗掉半天。更让人崩溃的是,团队里每个人搭出来的目录结构都不一样,这个用src/models,那个用src/types,改别人的代码就像猜谜。后来我把自己沉淀下来的一套编码实践固化成了一个小工具,命名为t3code,专门用来解决“项目基建重复劳动”和“代码规范没法统一”这两件事。

简单说,t3code是一个命令行工具,它做的事情是:一条命令初始化全栈项目、按规范生成业务模块、聚合执行类型检查和测试门禁,顺带把 CI 配置也生成好。你不需要手工去mkdir src/modules、不需要纠结 zod schema 放哪个目录、也不需要记得覆盖率阈值该设多少。它适合谁?适合中小团队的技术负责人、独立开发者,以及每一个受够了“项目从零搭到能跑要一整天”的人。

这个工具的核心思路其实不复杂:把“最佳实践”变成“默认行为”。下面我就把整个设计思路、实现细节、实操过程,以及我踩过的坑一次说清楚。

1. 为什么我要做 t3code:开发流程里的三个真实痛点

1.1 环境搭建靠复制粘贴,每个人搭出来都不一样

以前团队里来了新人,第一件事是找老员工要“上一个项目的压缩包”,然后删删改改变成新项目。这种方式有很明显的问题:老项目里那些过期依赖、特殊配置、甚至只是某个人本地的临时修改,都被当成“标准”复制了过去。每个人还会按自己的习惯改目录结构,最终代码仓库里充斥着风格完全不同的模块文件。

我想要的不是一套“完美的架构”,而是一套“不依赖个人记忆的默认约定”。t3code把这些约定内置到模板和配置里,让每个新项目生成出来长一个样。这跟建筑行业用预制构件是一个道理:不是每个工地都从烧砖开始,标准化构件能大幅减少偏差。目录结构、文件命名、路由组织方式全部由工具统一输出,新人上手成本直接从“读完整个项目的 README”降到“看一遍生成的代码就能猜个大概”。

1.2 类型安全只停留在“用了 TypeScript”

很多项目虽然用了 TypeScript,但代码里any满天飞。前端调用后端接口时,接口返回什么类型全靠后端同事口头描述,或者干脆靠 Postman 里的响应猜。一旦后端改了字段名,前端只有在运行时才发现报错,这种问题在生产环境出一次就是一次事故。

真正解决这个问题的思路是做“端到端类型安全”,也就是让前后端共享同一份类型定义。在 t3code 生成的模块里,我用 tRPC 作为接口层,用 Zod 做参数校验,前端调用后端路由时,输入输出类型会从服务端自动推导出来。后端return user,前端拿到的一定是User类型,不用写任何手动的 interface,也不用维护一份可能过期的 API 文档。这个机制的收益在项目变大后尤其明显,改一个字段类型,编译期就能把前端所有用到它的地方全部暴露出来。

1.3 测试要么没有,要么摆样子

测试是另一个敏感话题。很多项目虽然有测试目录,但里面全是空的,或者只有几个“为了跑覆盖率”的冒烟用例。我一直觉得,测试写不写、写得好不好,跟开发者的自律程度关系不大,更重要的是脚手架能不能把写测试的成本降到足够低。

t3code的做法是:生成任何业务模块时,同时生成对应的测试骨架。你拿到手的不是一个光秃秃的业务函数,而是一个“业务代码 + 测试用例 + 类型校验”三者绑定的完整闭环。测试不再是额外工作,而是生成产物的一部分。配合覆盖率门禁,低于阈值就直接让 CI 失败,从流程上杜绝“没测试也能合并”的情况。

2. t3code 的核心设计与实现拆解

2.1 总体架构:一个入口、三个内核、两类产出

t3code本身的架构不复杂,我把它分成三层:

  • CLI 入口层:基于 Commander.js 实现,负责解析new、generate、check等子命令。
  • 配置与模板层:用 cosmiconfig 读取t3code.config.ts,用 Handlebars 作为模板渲染引擎。
  • 执行与校验层:负责调用 TypeScript 编译器、ESLint、Vitest、Prisma 等底层工具,并汇总执行结果。

两类产出分别对应“项目级”和“模块级”:t3code new产出的是一个完整可运行的项目骨架,t3code generate产出的是项目内部的业务模块代码。两类产出的核心设计原则是一致的:内容由配置驱动,文件由模板渲染,最终结果必须能通过内置校验。

选择 Handlebars 而不是直接用字符串拼接,是因为模板需要支持条件渲染、循环遍历和局部复用。比如生成字段类型时,z.string()、z.number()、z.enum()的逻辑完全不同,如果靠手工拼字符串,每加一种类型就要改一次代码;用 Handlebars 的 helper 函数处理字段类型映射,扩展成本低很多,而且模板本身也更容易被团队自定义。

2.2 模板引擎与项目生成机制

t3code new my-app的实际执行流程是这样的:

  1. 读取内置模板目录,找到project模板。
  2. 根据配置参数(包管理器、是否启用 Prisma、是否需要 Docker 等)渲染模板文件。
  3. 统一处理文件命名:组件文件用 PascalCase,配置文件用 kebab-case,测试文件统一加.test前缀。
  4. 写入文件后执行一次依赖安装,并调用t3code check做初始校验。

模板目录结构大致是这样:

templates/ ├── project/ │ ├── package.json.hbs │ ├── tsconfig.json.hbs │ ├── src/ │ │ ├── pages/ │ │ ├── server/ │ │ │ ├── api/ │ │ │ └── db/ │ │ └── styles/ │ └── .github/ │ └── workflows/ │ └── ci.yml.hbs └── module/ ├── schema.ts.hbs ├── router.ts.hbs ├── service.ts.hbs ├── repository.ts.hbs ├── test.ts.hbs └── index.ts.hbs

这个结构看着简单,但每个文件的渲染逻辑都经过精心设计。拿schema.ts.hbs举例,它要根据命令行传入的--fields参数动态生成 Zod 校验器,处理string、number、boolean、enum、optional等多种类型组合。这块是整个工具里最容易写乱的部分,我最初实现时直接嵌套了七八层 if,后来全部改成字段类型驱动的数据映射,清晰多了。

2.3 类型安全检查的实现思路

t3code check是工具里我使用频率最高的子命令,它的本质是“一个入口聚合三道闸门”:tsc --noEmit做类型检查、eslint .做代码规范检查、vitest run做测试执行。三道检查全部通过才返回退出码 0,任何一项失败都以非零码退出。

我这样设计的原因很简单:开发者其实不需要知道底层调用了多少个工具,他们只需要知道“代码是不是可以提交了”。把三个检查命令封装成一个原子操作,能避免很多“我以为我检查过了”的侥幸心理。

执行顺序也有讲究。我最初是三个命令并行跑的,后来发现tsc和eslint同时输出时,报错信息混在一起特别难读,而且 ESLint 在某些类型错误下会给出误导性的建议。改成串行之后,体验好了很多:先跑类型检查,类型不过就不用浪费时间去跑测试。用命令行的话来说,这叫“短路求值”——最可能失败的检查优先执行,节省整体时间。

2.4 测试骨架与覆盖率门禁的联动

生成测试骨架是一回事,保证测试真的被执行是另一回事。很多脚手架也生成测试文件,但开发者不跑,等于没有。所以我给t3code check加了覆盖率门禁逻辑:

export default defineConfig({ coverage: { enabled: true, thresholds: { lines: 80, functions: 80, branches: 70, statements: 80 } } })

这几个阈值的含义分别是:行覆盖率不低于 80%,函数覆盖率不低于 80%,分支覆盖率不低于 70%,语句覆盖率不低于 80%。设置门禁之后,跑测试的命令变成vitest run --coverage,如果覆盖率低于阈值,Vitest 会返回非零退出码,CI 也随之失败。

阈值怎么定是我纠结过的问题。定太高,比如 90%,会导致开发者为了凑覆盖率写一堆无意义的用例;定太低,比如 50%,又起不到约束作用。结合我自己维护项目的经验,核心业务模块 80% 是一个比较合理的基线,UI 组件和配置文件可以单独设置豁免。我的建议是:阈值要跟着模块的重要程度走,不要一个数字管全部。

3. 实操:用 t3code 从零跑通一个模块开发

3.1 安装与初始化项目

先从初始化开始。如果你是在一个干净目录里新建项目,执行:

npm create t3code@latest my-app cd my-app npm install

顺利的话,项目结构会自动生成,依赖也会装好。你可能会注意到它会优先尝试用你本机已安装的 pnpm 或 yarn,这里我建议团队项目统一指定包管理器,避免同一份package-lock.json和pnpm-lock.yaml换来换去。t3code 提供了一个--pm参数,可以强制指定:

npm create t3code@latest my-app -- --pm pnpm

初始化完成后,第一件事是看一眼src/server/api目录,确认 tRPC 的入口文件是否正常生成。我碰到过好几次依赖更新后模板里 import 路径失效的情况,所以这里的检查建议不要跳过。

3.2 配置 t3code.config.ts

项目根目录下会自动生成t3code.config.ts,这是整个工具的灵魂。一份典型的配置长这样:

import { defineConfig } from 't3code' export default defineConfig({ packageManager: 'pnpm', modules: { location: 'src/modules', }, database: { provider: 'prisma', }, coverage: { enabled: true, thresholds: { lines: 80, functions: 80, branches: 70, statements: 80 } }, templates: 'config/templates' })

重点看几个字段。modules.location决定业务模块的存放位置,我把它统一为src/modules,这样所有业务代码都收敛在同一层目录下,新成员搜索代码时不用猜。database.provider决定是否在生成模块时联动生成 Prisma model。templates允许你把自定义模板放到仓库里,实现“团队模板跟着代码走”,而不是依赖工具内置的那份。

3.3 生成业务模块并观察完整产物

初始化完成后,生成一个用户模块试试:

t3code generate module user \ --fields name:string email:string role:enum?[admin,member]

执行完,src/modules/user/下会生成六个文件:schema、router、service、repository、测试文件和统一的入口 index。这里我摘录两份最关键的文件给大家看。

schema 文件长这样:

import { z } from 'zod' export const userSchema = z.object({ id: z.string().uuid().optional(), name: z.string().min(2, '名字至少两个字符'), email: z.string().email('邮箱格式不正确'), role: z.enum(['admin', 'member']).default('member') }) export type UserInput = z.infer<typeof userSchema>

router 文件长这样:

import { createTRPCRouter, publicProcedure } from '~/server/api/trpc' import { userSchema } from './user.schema' import { createUser, listUsers } from './user.service' export const userRouter = createTRPCRouter({ list: publicProcedure.query(() => listUsers()), create: publicProcedure.input(userSchema).mutation(({ input }) => createUser(input)) })

拿到这份代码,前端直接调用userRouter.create,输入的校验规则和后端一致,输出的类型也是后端实际返回的类型。整个过程没有生成任何重复的 interface,也没有手工维护的 api.d.ts。这就是 t3code 想达到的效果:业务代码看着朴素,但类型安全是刻在骨子里的。

生成模块的同时还会生成测试骨架:

import { describe, it, expect } from 'vitest' import { userSchema } from './user.schema' describe('user schema', () => { it('should parse valid input', () => { const data = userSchema.parse({ name: '张三', email: 'zhang@example.com' }) expect(data.role).toBe('member') }) it('should throw on invalid email', () => { expect(() => userSchema.parse({ name: '张三', email: 'bad' })).toThrow() }) })

3.4 把 check 接到 CI 流水线

单机跑t3code check只是第一步,真正要发挥作用的是把它接进 CI,让每一次 PR 都过一遍同样的三道闸门。t3code 生成的.github/workflows/ci.yml长这样:

name: CI on: pull_request: branches: [main] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install - run: npx prisma generate - run: npx t3code check

这套 CI 配置是我个人比较满意的形式:简单、直白、没有任何多余环节。之所以把prisma generate单独拎出来放在t3code check前面,是因为 Prisma 的 client 是代码生成产物,如果直接跑类型检查,很可能出现prisma.user找不到类型定义的情况。顺序错了,CI 会给你上一课。

4. 高频问题排查与避坑指南

4.1 生成的项目报 “Cannot find module '~/server/api/trpc'”

这个问题我遇到不下十次,多半是 tsconfig 的路径别名没生效。t3code 初始化时会生成baseUrl和paths配置,但如果你把 t3code 用在一个 monorepo 的子包中,根目录的 tsconfig 可能会覆盖子包配置。排查步骤很简单:先看子包自己的 tsconfig 里有没有paths,再看根目录 tsconfig 的include是否覆盖了子包的src目录。两条都排除了,基本就是 restart TS Server 的问题。

4.2 Prisma Client 类型缺失

报错信息通常是Property 'user' does not exist on type 'PrismaClient'。这不是代码写错了,而是 Prisma Client 还没生成。在 CI 里漏掉npx prisma generate,或者本机改了schema.prisma之后没有重新生成,都会触发这个问题。解决办法很机械:

npx prisma generate

注意,只要改了schema.prisma,本地开发也要重新执行一次这条命令,否则类型提示和运行时不一致,容易出现“编辑器不报错但编译报错”这种诡异体验。

4.3 tRPC 路由自动生成的边界情况

t3code 生成 router 时默认使用publicProcedure,也就是未认证过程。如果你生产的业务模块需要登录态,手动改成protectedProcedure即可。这里有个小坑:如果团队里多个模块都做同样修改,建议把“是否默认保护”加进配置项,我在新版里加了router.authGuard: true选项,生成时自动带protectedProcedure,省得每次改完再全局替换。

4.4 Windows 环境下的脚本兼容问题

t3code 早期版本里用到了rm -rf这类 Unix 命令,Windows 环境下会直接报错。后来我把所有文件删除操作都换成了 Node.js 内置的fs.rmSync,跨平台行为才稳定下来。如果你在 Windows 上使用 t3code 遇到奇怪的脚本报错,优先检查是不是命令里混入了 shell 专属语法。我自己现在写代码偏好一个原则:所有文件操作都走 Node API,不依赖 shell 命令,这样开发者和 CI 跑出来的行为才能一致。

4.5 覆盖率误报,新增页面没写测试导致 CI 失败

覆盖率门禁带来的问题是:你加了一个纯展示的静态页面,手写几个页面 UI 组件,然后 CI 因为 pages 目录下没有测试而失败。这个现象会让人觉得门禁“管得太宽”。我的建议是在配置里区分核心模块和 UI 模块的覆盖率范围,核心模块必须过 80%,UI 模块只做冒烟级检查。具体做法是通过 t3code 的coverage.include和coverage.exclude来控制统计范围,而不是一刀切禁止门禁。

5. 我建议的落地路径和后续玩法

如果你准备在团队里引入 t3code,我的建议是从“模板固化”开始,不要一上来就折腾覆盖率门禁。第一步先用t3code new生成统一的项目骨架,让所有人基于同一套结构开发;第二步启用generate module规范模块产出,确保新代码的风格一致;第三步才开coverage.enabled,给老项目和新项目分别设置过渡阈值。这三级推进节奏能减少团队抵触情绪,避免“工具上来就把 CI 拦死”造成的逆反心理。

我个人还留了两个后续扩展点。一个是把 AI 代码生成能力接进来——t3code 已经掌握了项目结构和字段定义,完全可以在生成模块时自动补充业务注释、生成更完整的测试用例,甚至调用大模型给路由添加合理的错误处理逻辑。另一个是支持组件级生成,不止生成业务模块,还能生成符合设计规范的 React 组件骨架,带上配套的 Storybook 模板。这两个方向都能进一步压缩“从需求到代码”的时间。

实际使用这段时间,我最深的体会是:这类工具的价值不在于写代码的能力,而在于把隐性知识显性化。以前团队里的各种约定都写在文档里,而文档没人看。现在约定直接嵌入到代码生成流程里,拿到的就是符合规范的代码,根本不需要记忆规则。如果你也被同样的问题困扰,我建议你从小范围开始尝试,先跑通一个模块的生成和检查流程,再逐步扩展。等习惯了这个节奏,你会很难再回去手工搭结构。

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

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

立即咨询