☰
基于T3栈的全栈脚手架t3code:从设计到落地全记录
2026/10/27 6:51:16 网站建设 项目流程

t3code这个代号第一次出现在我记事本上,是去年冬天。当时团队刚接了一条新业务线,按惯例又要从零搭一个全栈项目。我原本以为半天能搞定环境,结果一周过去,还在折腾依赖版本冲突。那段时间我反复琢磨一个问题:为什么每次新项目都要把这些破事重新走一遍?于是t3code这个内部脚手架项目就立项了。它不是开源明星项目,也不是什么高大上的产品,就是一个专门解决“团队启动项目太慢、技术栈太散、新人上手成本太高”的初始化代码生成器。如果你也经常被“搭环境”折磨,或者正在纠结全栈技术选型,这篇笔记应该能给你一些实在的参考。

1. 项目由来与核心背景

1.1 从一次痛苦的“搭环境”说起

先说说我为什么会做这件事。去年冬天接的新业务线,前后端加起来六个人,技术背景五花八门:有人习惯Vue那套,有人React更熟,还有人后端出身,前端只写过jQuery。当时定技术栈就吵了两轮,最后勉强定了Next.js加TypeScript。等真正开工才发现,真正的折磨才刚刚开始。

每个人本地Node版本不一样,包管理器用的也不一样,有人npm有人yarn,还有人坚持用pnpm。配置文件各自为政,ESLint规则有三套版本,Prettier的配置互相冲突。更离谱的是,某个同事拉完代码后跑不起来,查了半天发现是.env文件里的变量名和代码对不上。那周光是帮大家统一环境,我就花了至少两个整天。

我相信很多团队都经历过这种“搭环境地狱”。问题不在于某个具体工具难用,而在于一切全靠口头约定和个人自觉。没有一份所有人都遵循的起点模板,项目初始化就变成了一场碰运气的冒险。t3code最初的想法特别朴素:把团队认可的技术选型、目录规范、基础配置全部固化成一份模板,任何人拿到手之后,一条命令就能跑起一个结构一致、配置完整、质量底线有保障的新项目。

1.2 为什么是T3栈,而不是其他组合

确定了要做脚手架之后,第一个问题就是技术栈怎么选。当时摆在我面前的有三条路:一是传统前后端分离,React加NestJS写两套服务;二是Next.js加Prisma加REST接口;三是T3栈,也就是Next.js、TypeScript、Tailwind CSS、tRPC这一套组合。

我认真列了个对比表,把三种方案从类型安全、开发效率、部署复杂度、团队上手成本几个维度过了一遍。

方案类型安全开发效率部署复杂度上手成本
React + NestJS前后端各有一套类型,需手动维护映射中,联调成本高高,需同时维护两个服务较高
Next.js + REST接口类型靠手写或生成器维护中上,仍需要写接口文档低,单应用中等
T3栈(tRPC)前后端类型天然共享,零映射成本高,无需文档即可联调低,单应用中等偏上,需理解tRPC心智模型

我最终选了T3栈,最重要的原因就是tRPC带来的类型安全。以前写前后端分离项目,后端改了字段名,前端可能隔两天才在运行时发现报错,运气好的话能当场看出来,运气不好就悄悄上线了。用了tRPC之后,后端路由的类型会直接暴露给前端,改个字段名,前端编译期就能看到错误,根本走不到联调环节。

另外,T3栈里Next.js同时承担了页面渲染和API层,Tailwind解决了样式规范化的痛点,整个链路都在TypeScript的覆盖范围内,一个类型系统贯穿到底,这对脚手架来说是最关键的资产。做脚手架不是选“最好”的技术,而是选“最能保证一致性和不犯错”的技术。

2. 核心设计思路:t3code到底在管哪些事

2.1 目录结构按业务域划分,而不是按技术层划分

很多脚手架的目录结构是典型的“按层分包”:components下面放所有组件,pages下面放所有页面,lib下面放所有工具函数。看起来规整,但业务一复杂就全是问题。你想改订单相关的功能,要在组件目录里找订单卡片,在API目录里找订单接口,在工具目录里找订单格式化函数,来回跳转的时间比写代码还多。

t3code的模板采用了按业务域划分的方式。每个业务模块是独立的文件夹,模块内部自己管理组件、API、工具函数和类型定义。

src/ ├── features/ │ ├── auth/ │ │ ├── components/ │ │ ├── api/ │ │ ├── hooks/ │ │ └── types.ts │ ├── order/ │ │ ├── components/ │ │ ├── api/ │ │ ├── hooks/ │ │ └── types.ts │ └── user/ │ ├── components/ │ ├── api/ │ ├── hooks/ │ └── types.ts ├── shared/ │ ├── ui/ │ ├── lib/ │ └── config/ └── app/

这样做的直接好处是,新人接手一个模块时,只需要打开一个目录,所有相关代码都在眼前。共享的部分放进shared目录,只放那些真正被多个业务域复用的东西。它逼着你把业务内聚做好,而不是随手把函数丢进一个global utils文件。

2.2 数据链路:tRPC让前后端类型共用成为默认选项

t3code的数据链路设计,是整个模板最核心的部分。我们在模板里预置了完整的tRPC路由结构,包括初始化后端router、挂载Next.js API handler、前端封装好带类型推断的react-query hooks。任何新业务模块,只需要在router目录里添加一个子路由,前后端类型立刻自动对齐。

简单解释一下tRPC的原理。它不像REST那样通过网络路径来定义接口,而是把后端函数直接暴露成可调用过程。前端引入一个类型安全的代理对象,调用后端函数就像调用本地异步函数一样,参数和返回值类型完全一致。这样做之后,接口文档的存在感会被压缩到最低,因为代码本身就是文档。

模板里的基础router长这样:

// src/server/api/root.ts import { createTRPCRouter } from "@/server/api/trpc"; import { authRouter } from "@/features/auth/api/auth.router"; import { orderRouter } from "@/features/order/api/order.router"; export const appRouter = createTRPCRouter({ auth: authRouter, order: orderRouter, }); export type AppRouter = typeof appRouter;

前端调用时,TypeScript会自动推断出orderRouter里所有procedure的输入输出类型。如果你在写代码的时候发现某个接口的返回字段报错了,那一定不是前端猜错了,而是后端真的改过字段,这种“错误早暴露”的体验,用惯了真的回不去。

2.3 样式与组件规范:Tailwind加设计令牌的落地姿势

样式这块,模板选择了Tailwind CSS,但同时做了两层约束,避免它变成一场自由发挥的灾难。

第一层是设计令牌。我们把颜色、字体、圆角、间距全部定义为Tailwind的theme扩展配置,开发者只能从预置的令牌里取值,不允许直接写类似text-[#fa2be2]这种任意值语法。这样保证了视觉上的一致性,也方便后续切换品牌主题。

第二层是组件封装。模板内置了一套极简的UI组件基础集,包括按钮、输入框、卡片、弹窗。它们不是某个重型组件库的完整复刻,而是只封装了项目里真正用得到的样式逻辑。业务页面里写样式,优先使用这些封装好的组件,而不是每次从零开始排Tailwind类名。

这里有个执行细节值得说一下。我们给Tailwind配置加了ESLint规则,禁止在jsx里直接使用过于冗长的类名组合。刚开始有人觉得管得太宽,但跑了一个迭代之后,大家发现代码review时真正要看的逻辑变多了,样式层面的无效讨论少了很多。规范的意义不在于限制自由,而在于把精力留给更重要的事情。

3. 从零实现t3code:关键环节实操记录

3.1 第一步:确定基础依赖与版本锁定策略

首先定义基础的依赖集合。t3code基于Next.js 14 App Router路由模式,搭配React 18、TypeScript 5.x、tRPC 10、Prisma 5、Tailwind CSS 3.x,以及zod做运行时校验。

依赖锁定这件事,我觉得必须作为起步动作来做。模板仓库里强制使用pnpm,并且提交pnpm-lock.yaml文件。包管理器统一之后,新人拉下代码执行pnpm install,装出来的依赖版本和CI里完全一致。以前那种“我本地跑得好好的”的甩锅场景,在固定lockfile之后基本绝迹了。

初始化项目的核心脚本也一并放在模板里:

pnpm create next-app --typescript --tailwind --eslint my-app cd my-app pnpm add @trpc/server @trpc/client @trpc/react-query react-query zod pnpm add -D prisma @types/node pnpm dlx prisma init --datasource-provider sqlite

版本锁定之后,所有依赖升级都必须走专门的MR,由一个人统一升级并验证,不允许各自在分支上顺手升级依赖。这个策略一开始执行的时候有人嫌麻烦,但实践下来确实省掉了大量“我升级了个小版本结果全挂”的隐性成本。

3.2 第二步:打通tRPC路由与类型安全链路

技术栈里最容易卡住新人的就是tRPC的上下文初始化。模板里把这部分提前做好,而不是让使用者自己去拼。我们需要创建tRPC的初始化实例,包括上下文、中间件和公共procedure。

// src/server/api/trpc.ts import { initTRPC, TRPCError } from "@trpc/server"; import { ZodError } from "zod"; import { prisma } from "@/server/db"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; export const createTRPCContext = async (opts: CreateNextContextOptions) => { const session = await getServerSession(opts.req, opts.res); return { prisma, session }; }; const t = initTRPC.context<typeof createTRPCContext>().create({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { ...ctx, user: ctx.session.user } }); });

这段代码的作用是:把Prisma客户端和会话信息挂到上下文里,同时定义了一个受保护过程,要求必须登录才能调用。有了这个基础之后,业务路由只需要专注于定义输入输出和业务逻辑,不用再关心身份验证和数据库连接怎么初始化。

实际使用中,我强烈建议把zod校验直接放在procedure的input定义里。这样前端传入的参数在网关层就会被校验,格式错误不会污染业务逻辑,而且错误信息会结构化返回,前端可以直接据此渲染表单错误提示。

3.3 第三步:接入认证与权限骨架

新项目十有八九需要登录,所以t3code直接把认证骨架也内置了。我们用的是NextAuth,搭配Credentials和OAuth两种方式,Prisma作为用户数据存储。

认证模块单独放在auth业务域里,包括登录页、注册逻辑、会话管理组件以及受保护路由的配套hooks。业务方接到一个新项目,不需要从零理解登录回调怎么跳转,只需要改一下表单字段和第三方登录的clientId即可。

权限这块,模板内置了两层机制。第一层是前面提到的protectedProcedure,所有需要登录的API直接用它定义。第二层是一个简单的角色判断中间件,可以配置哪些路由需要特定角色才能访问。

export const requireRole = (roles: Role[]) => protectedProcedure.use(({ ctx, next }) => { if (!roles.includes(ctx.user.role)) { throw new TRPCError({ code: "FORBIDDEN" }); } return next(); });

我见过不少团队在做权限时,把判断逻辑散落在各个页面的useEffect里,结果漏掉了某个接口,变成了“前端藏着掖着,后端门户大开”。把权限判断下沉到tRPC中间件这一层之后,接口天然受保护,前端隐藏路由只是锦上添花。

3.4 第四步:CI与代码规范的集成

脚手架光有模板还不够,必须把质量门槛也固化到流水线里。t3code模板里预置了ESLint、Prettier、TypeScript strict模式、Husky git hooks和一套简单的Github Actions工作流。

CI里跑的检查按顺序排列:先是ESLint检查,再是tsc类型检查,然后是单元测试,最后是构建验证。任何一个环节失败,MR都不允许合并。这个顺序是有讲究的,Lint最快先跑,快速反馈格式问题;类型检查能拦住大部分低级错误;测试只跑核心工具函数和业务关键路径,避免CI时间过长导致大家不愿意等。

Husky这边,我们只在pre-commit阶段做了两件事:对暂存文件跑Prettier格式化,以及禁止把调试用的console.log提交上去。别小看这两条,它们把代码提交之后立刻引发的“格式争执”和“调试代码泄漏”从源头卡掉了。

4. 踩坑清单:我在t3code上翻过的车

4.1 tRPC批处理与React Query缓存不一致

第一个让我印象深刻的坑,来自tRPC的批处理机制。tRPC默认会把多个同时发起的请求合并成一个HTTP请求,这本来是件好事,但它和react-query的缓存key放一起时,偶尔会出现一个奇怪的现象:同一个查询在不同页面上展示的数据不一致。

排查到最后发现,问题出在react-query的查询key定义上。如果两个查询使用了相同的key,但传给tRPC的输入参数不同,react-query可能会错误地复用缓存。解决办法是确保查询key里包含完整的入参序列化值,而不是只写一个静态字符串。

const { data } = trpc.order.getOrderList.useQuery( { page, pageSize, status }, { queryKey: ["order.list", { page, pageSize, status }] } );

这个坑提醒我,使用tRPC时不能忽略它底层的react-query行为。模板后续版本里,我们直接在自定义hooks层把queryKey的生成逻辑封装好了,业务代码不需要手动管理。

4.2 环境变量的运行时校验

第二个坑是环境变量缺失导致运行时才爆雷。Next.js内置了构建时环境变量校验,但业务中经常有人需要在运行时读取某个变量,比如第三方服务的密钥,一旦本地没配好,服务启动时没报错,请求一到才挂掉。

后来我在模板里引入了zod来统一校验环境变量。在项目入口处写一个环境变量schema,启动时同步校验一次,缺失或格式不对直接给出明确的错误提示。

const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]), DATABASE_URL: z.string().min(1, "DATABASE_URL is required"), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.string().url(), }); export const env = envSchema.parse(process.env);

别看这只是多了一层校验,实际价值很大。新人配置环境时,不再对着文档猜半天,而是直接看到哪一项没配好;上线前CI也会自动拦截环境变量缺失的情况,省掉了好几个凌晨排查事故的现场。

4.3 Tailwind与组件库的主题覆盖冲突

模板里本来想引入一个现成的头部UI组件库来加速开发,结果实践下来发现,Tailwind的preflight基础样式和组件库自带的样式经常打架。最典型的是按钮组件的背景色,我们明明在组件库里设置了主色,渲染出来却是Tailwind默认的蓝色。

这个问题的根源在于样式加载顺序。Tailwind生成的层叠样式和组件库的全局样式互相覆盖,谁后加载谁说了算。我们花了一些时间调整导入顺序和控制Tailwind的layer优先级,但始终不太优雅。

最后干脆做了减法,不再引入重型组件库,只保留了一个极简的内部组件集。所有组件用Tailwind重写,样式完全受控。这件事让我得出一个结论:脚手架里的样式体系必须是自己能完全掌控的,第三方组件库带来的便利,往往会被样式调优和时间成本抵消。

4.4 新人对模板“魔法”的困惑

最后一个坑不是技术问题,而是团队协作问题。模板做好之后,新来的同事看着目录结构和各种中间件,经常一脸茫然,不知道一个请求从前端到后端依次经过了哪些环节。代码能跑,但改起来心里没底。

后来我们专门补了一份简短的设计文档,不是那种几百页的说明书,就是一张流程图加几段文字,讲清楚一个完整请求的路径、目录职责划分、以及需要在哪个文件里新增业务代码。同时把模板里的注释写得尽量贴近“为什么要这样写”,而不是“这段代码做了什么”。

文档这块的经验是:脚手架类的项目,真正的交付物不只是代码,还包括让团队理解和信任这套代码的路径。没有这份说明,模板会被当成黑盒,大家遇到问题还是会用“绕过它”的方式解决,那就失去了脚手架原本的意义。

5. 从模板到CLI:t3code的后续进化

5.1 交互式初始化的取舍

用了一段时间之后,我发现固定的模板有一个问题:不是所有项目都需要认证模块,也不是所有项目都需要Prisma。模板开始变得臃肿,新项目总要删掉一堆用不到的东西。

所以下一步,我给t3code设计了一个小的CLI工具,支持交互式选择需要的模块。执行pnpm create t3code@latest之后,会问你项目名称、要不要认证、要不要数据库、UI组件风格偏好等几个问题,然后生成对应裁剪后的代码。

CLI实现上,没有引入复杂的框架,就是Node.js加几个交互问答库,配合模板引擎做文件级别的条件生成。这里我踩过的一个小坑是模板文件里的条件渲染写法,需要确保ejs或handlebars的语法不会和代码文件里的花括号冲突。我的处理方式是尽量用注释标记区块,生成时按标记删除或保留,避免模板引擎污染业务代码的原有格式。

5.2 团队推广的节奏与反馈闭环

工具做好了,推广也是个技术活。我没有选择一步到位强制全团队切换,而是先拿一个低风险的新项目试点,跑完一整个迭代之后,把效果数据和遇到问题拿出来复盘。大家看到确实省了初始化时间、联调返工变少,后续的推广阻力就小了很多。

同时我给每个从模板创建的项目留了一个改进反馈通道,任何人在使用过程中觉得模板某个地方别扭,都可以直接提给负责维护的同事。模板本身保持每两周更新一次的节奏,不积压改进需求。到后来,反而是业务同事主动来提想要的基础功能,模板从“我推给大家用”变成了“大家催着我加功能”。

我个人在实际操作中的体会是,t3code这个项目的真正价值,不在于某个技术多先进,而在于它把团队里那些“明明都知道却没人落地”的共识,用代码固定了下来。技术债的积累往往是从项目第一天就开始了,而一个好的脚手架,至少能保证起点是干净、一致、可维护的。如果你也在做类似的团队基础设施,希望这篇记录能给你一些参考。

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

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

立即咨询