资讯详情

资讯详情

T3 Stack 全栈项目 t3code 完整复盘:从初始化到部署

如果你在开发者社区里刷到过t3code这个词大概率会把它和 T3 Stack 联系起来。没错t3code 就是我维护的一个全栈项目仓库核心是用 T3 Stack 做一套代码片段管理工具。它能干什么一句话把散落在本地和聊天记录里的代码片段集中收藏、打标签、按语言筛选还能一键复制甚至分享给朋友。适合什么人看正在学 Next.js 全栈、想搞清楚 tRPC 和 Prisma 怎么配合的人或者纯粹想找一个能跑起来的 T3 Stack 参考项目的人。这篇文章我会从项目设计、技术选型、初始化、数据模型、前后端实现到部署排错完整复盘一遍。1. 项目整体设计t3code 想做成什么样1.1 从痛点出发的功能清单我最初想做 t3code是因为自己经常在开发中遇到一个尴尬场景一段调试了很久才跑通的配置代码今天调完下周再用就忘了放哪儿了。微信收藏夹里一大堆代码截图本地 notes 里混着各种临时记录真要找的时候只能硬翻。所以 t3code 的第一个版本功能列表非常克制。它只做四件事展示所有已保存的代码片段按创建时间倒序排。新建片段时填写标题、正文内容、编程语言并支持挂多个标签。首页提供语言筛选框可以快速过滤出 JavaScript、Python、Go 等特定语言的片段。每条片段右侧有一个复制按钮点一下就把完整代码复制到剪贴板。这个功能范围看起来简单但已经足够覆盖 T3 Stack 里大部分核心能力Prisma 负责数据写入和查询tRPC 负责前后端接口Next.js 负责页面渲染Tailwind 负责外观。如果一开始就把权限、评论、点赞都塞进来项目复杂度会被迫拉高反而不利于搞清楚技术栈本身的协作方式。另外我在设计时坚持了一个原则所有操作都不需要手动刷新页面。创建完片段后自动回到列表页列表页数据自动更新。这个体验优化在传统开发里需要写一堆状态同步逻辑但在 T3 Stack 里几乎是由 React Query 的缓存失效机制白送的后面会详细说。1.2 技术选型的取舍逻辑先解释一下 T3 Stack 到底是什么。它不是一个单一框架而是一套组合方案常见组成是 TypeScript、Tailwind CSS、Next.js、tRPC、Prisma。也有人把这套东西叫全栈开发全家桶因为它从数据库到样式到前端交互全都有明确分工。我选择这套组合核心原因是“类型安全可以贯穿一整个请求链路”。你在 Prisma 里定义的数据模型生成出的 TypeScript 类型可以直接被 tRPC 的 Router 使用tRPC 的 query 返回类型又可以自动映射到前端 React Hooks 的返回值。也就是说从数据库字段到前端渲染类型是咬死的字段名写错、类型不对在编译期就会报错不用等运行时。如果把项目改成传统前后端分离用 Next.js 只做前端后端单独用 Express 或者 NestJS那至少要多维护一份 OpenAPI 文档或者手写类型声明。对于 t3code 这种轻量工具这属于过度设计。REST 接口虽然通用但每次加字段都要手动同步类型频率一高就容易出现前后端不一致的隐患。tRPC 在这套架构里替代的是 REST 层。它不需要你用 URL 去描述动作直接调用 TypeScript 函数就行。我刚开始用的时候也觉得不够“工程化”后来发现团队协作反而更简单因为接口定义就是函数签名后端改了签名前端立刻报红根本不会等到联调才发现参数对不上。状态管理方面t3code 没有引入 Redux 或者 Zustand。原因很简单服务端数据状态全部由 React Query 管本地表单状态只需要 useState。两个状态域分得清清楚楚没必要再塞一个全局状态库把水搅浑。这个取舍在多人协作时尤其重要代码评审一看就知道哪些是服务端数据、哪些是本地临时态。2. 从零初始化 t3code环境准备与目录解读2.1 开发环境的最低配置在跑 create-t3-app 之前先把环境准备好。我实际用的配置是 Node 20.11包管理器用 pnpm数据库先用 SQLite后面部署到生产环境时再切 PostgreSQL。工具推荐版本备注Node.js18.0 以上推荐 20create-t3-app 要求较新的 Node20 是稳定选择包管理器pnpm 8npm 和 yarn 也能用但 pnpm 安装速度更快数据库SQLite 本地PostgreSQL 生产本地零配置生产避免 SQLite 的 serverless 限制Git2.30初始化仓库后方便回滚每一步实验这里有个经验分享不要在 Node 16 上折腾。T3 Stack 里的 Next.js 14 和 tRPC 10 已经明确要求 Node 18我在初期踩过坑安装依赖能成功但 dev server 经常内存溢出后来换了 Node 20 才稳定。2.2 create-t3-app 初始化命令与交互选项初始化 t3code 我直接用了官方脚手架命令是pnpm create t3-applatest t3code交互过程中会有很多选择项我当时选了这些Next.js选 App Router。TypeScript必选这是 T3 Stack 的根基。Tailwind CSS必选样式方案。tRPC必选后面所有接口都走它。Prisma必选数据模型和数据访问都靠它。NextAuth.js暂时不选t3code 第一版不需要登录系统。如果你像我一样暂时不需要鉴权可以在初始化时去掉 NextAuth。后面要加也不是很难但脚手架会多生成一堆示例代码反而容易干扰理解核心流程。初始化完成后项目里会出现一堆文件和目录。初学者容易被吓到我整理一下最关键的几个其余可以先不用管src/appNext.js App Router 的页面目录所有页面都在这里。src/server/apitRPC 的 Router 和上下文配置后端逻辑都在这层。src/server/db.tsPrisma Client 的单例实例供 tRPC Router 调用。prisma/schema.prisma数据模型定义改数据库表就是改这个文件。src/env.mjs环境变量校验启动时用 Zod 检查缺失变量。2.3 环境变量与数据库连接配置create-t3-app 会生成一个.env.example第一次启动前要把它复制成.env否则框架会因为找不到环境变量直接退出。cp .env.example .env默认的.env里其实只有两个重点变量。t3code 本地用 SQLite所以DATABASE_URL就是本地文件名DATABASE_URLfile:./db.sqlite如果你用的是 Postgres需要改成连接串DATABASE_URLpostgresql://user:passwordlocalhost:5432/t3code这里想多说一句Next.js 项目的.env文件千万不要提交到 Git。你可以在.gitignore里直接写死.env提交到远程仓库的只保留.env.example。这不仅是为了保护密钥更重要的是如果团队其他人拉下来代码能靠.env.example知道需要配置哪些变量而不是对着空仓库发呆。3. 数据模型与后端逻辑搭建3.1 Prisma Schema 设计思路t3code 的第一个版本只有两个核心模型代码片段Snippet和标签Tag。关系很简单一个片段可以有多个标签一个标签可以挂在多个片段上标准的多对多关系。我在prisma/schema.prisma里定义的数据模型是这样的model Snippet { id String id default(cuid()) title String content String db.Text language String default(plaintext) tags Tag[] relation(SnippetTags) createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Tag { id String id default(cuid()) name String unique snippets Snippet[] relation(SnippetTags) }有几个设计细节想说明一下。主键我用了cuid()而不是autoincrement()因为在实际生产中数据库可能从单实例迁移到分布式环境自增 ID 容易冲突而 cuid 可以在任何节点独立生成。本地开发可能看不出区别但养成这个习惯以后少踩坑。content字段加上了db.Text这是告诉 Prisma 在 SQLite 里用 TEXT 类型而不是 VARCHAR。如果不加默认会生成有限长度的字符串类型一段完整的代码随便就超了。这个细节不注意等到保存长一点的脚本时会直接报数据超长错误。language字段没有做成单独的模型。有人可能有疑问为什么不设计成 Language 表因为语言这是一个有限的枚举集合直接存字符串最省事。等真正需要维护语言的元数据时再抽表不迟。3.2 tRPC Router 与业务流程实现数据模型有了接下来就是写后端逻辑。T3 Stack 里后端逻辑都放在 tRPC Router 中。我在src/server/api/routers/snippet.ts里实现了两个接口查出所有片段和新建片段。import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { z } from zod; export const snippetRouter createTRPCRouter({ getAll: publicProcedure.query(({ ctx }) { return ctx.prisma.snippet.findMany({ include: { tags: true }, orderBy: { createdAt: desc }, }); }), create: publicProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().min(1), language: z.string().default(plaintext), tags: z.array(z.string()).optional(), }) ) .mutation(async ({ ctx, input }) { return ctx.prisma.snippet.create({ data: { title: input.title, content: input.content, language: input.language, tags: input.tags?.length ? { connectOrCreate: input.tags.map((name) ({ where: { name }, create: { name }, })), } : undefined, }, }); }), });这里要说清楚 tRPC 的一个核心概念publicProcedure表示公开可访问的过程query对应 GET 类型的读取操作mutation对应写操作。如果以后要加登录只需要把publicProcedure换成protectedProcedure然后在中间件里校验 session不用改 Controller 层的业务代码。getAll里的include: { tags: true }是 Prisma 的关联查询它会一并返回这个片段关联的标签数组。我建议在列表页默认就把标签带出来否则前端还要为了标签单独发请求体验会差很多。create里的connectOrCreate是 Prisma 处理多对多关联的利器。它的逻辑是如果标签已经存在就连接已有记录如果不存在就创建这个标签。每一行代码都很直观。输入参数校验我用了 Zod。因为 tRPC 官方默认就集成了 Zod不用单独引入其他验证库。在前后端类型共享的背景下Zod 的 schema 既能在服务端做校验又能在前端自动推导出入参类型。你不需要写两份同名的 interface这就是类型安全的红利。3.3 挂载 Router 到根路由写好的 Router 要挂载到根路由里才能在客户端被调用。在src/server/api/root.ts中把snippetRouter放进去import { snippetRouter } from ~/server/api/routers/snippet; export const appRouter createTRPCRouter({ snippet: snippetRouter, }); export type AppRouter typeof appRouter;注意最后一行导出的AppRouter类型。这个类型会作为 tRPC 前后端的“契约”前端所有的 Hooks 都基于它推断返回类型和入参类型。如果这里漏了export type客户端就没法获得类型提示。挂载完之后我习惯先执行一次迁移把表结构同步到本地数据库npx prisma db pushdb push适合开发阶段快速同步 schema。如果你用的是 Postgres 并且已经上了生产那最好用prisma migrate dev生成迁移文件而不是直接 push。这是两种场景的管理方式本地怎么快怎么来生产可不行。4. 前端页面与交互实现4.1 App Router 下的页面结构t3code 的页面结构简单直接全部放在src/app目录下src/app/page.tsx首页展示全部代码片段。src/app/new/page.tsx新建片段页面。App Router 目录结构就是 URL 结构。new文件夹对应的就是/new路径不需要额外配置路由表。这种约定优于配置的设计对于小项目非常友好少一个路由配置文件就少一处出错的可能。首页布局用 Tailwind 写起来很快。我这儿给一个最直接的骨架思路div classNamemx-auto max-w-4xl px-4 py-8 header classNamemb-8 flex items-center justify-between h1 classNametext-2xl font-boldt3code 片段库/h1 Link href/new classNamerounded-md bg-blue-600 px-4 py-2 text-white 新建片段 /Link /header div classNamegrid gap-4 {snippets.map((snippet) ( div key{snippet.id} classNamerounded-lg border p-4 shadow-sm {/* 片段内容 */} /div ))} /div /divTailwind 的优势在这个时候体现得很明显你不用切到 CSS 文件去给类名命名所有的间距、圆角、阴影、字号全部在模板里用 class 完成。对于一个人维护的项目这套工作流省下来的时间很可观。4.2 用 tRPC Hooks 连接前后端前端调用 tRPC 接口的方式非常简单。在page.tsx里加上use client然后用api这个自动生成的 Hooks 对象use client; import { api } from ~/trpc/react; export default function HomePage() { const { data: snippets, isLoading } api.snippet.getAll.useQuery(); if (isLoading) { return div classNamep-8加载中.../div; } return ( // 渲染 snippets ); }这里有个关键边界问题tRPC 的 Hooks 依赖 React Query而 React Query 的缓存存在于客户端所以使用useQuery的组件必须标记为 Client Component。如果你放在 Server Component 里会直接报错useQuery is not a function或者类似的水合错误。新建页面的提交逻辑是use client; import { useRouter } from next/navigation; import { api } from ~/trpc/react; export default function NewSnippetPage() { const router useRouter(); const utils api.useUtils(); const createSnippet api.snippet.create.useMutation({ onSuccess: async () { await utils.snippet.getAll.invalidate(); router.push(/); }, }); function handleSubmit(formData: FormData) { createSnippet.mutate({ title: String(formData.get(title)), content: String(formData.get(content)), language: String(formData.get(language)), tags: [String(formData.get(tags))], }); } return ( form action{handleSubmit} classNamemx-auto max-w-2xl space-y-4 p-8 {/* 表单字段 */} /form ); }这段代码里最重要的就是invalidate()方法。它是 React Query 的缓存失效机制我会在创建成功后调用它让之前缓存的片段列表强制重新请求。这一步保证了返回首页后新创建的片段立刻出现在列表顶部不需要手动刷新浏览器。我在没有了解这个机制之前曾经用别的方式绕过缓存结果导致数据不同步改起来比直接使用正确机制更复杂。4.3 细节体验语言筛选与复制按钮首版 t3code 的体验有两个细节我想展开讲讲。第一个是语言筛选框。我在首页放了一个select下拉框选择语言后只展示对应语言的片段。实现方式是在前端根据language字段过滤。虽然数据量大了之后这样做不够高效但第一版完全够用。后续如果片段数量上万再改成把语言参数传给后端 query 也不迟。第二个是复制按钮。每个片段卡片右上角放了一个小按钮点击后调用浏览器的剪贴板 APIasync function handleCopy(content: string) { await navigator.clipboard.writeText(content); alert(已复制到剪贴板); }这个 API 在 HTTPS 环境下才能正常工作本地开发时 localhost 不受影响。如果你部署到 HTTP 的测试环境复制功能可能静默失败这一点建议提前考虑到。代码高亮我暂时没有上重型库。第一版直接用precode加 Tailwind 的灰底样式就够用。如果你想让 t3code 更完整一点可以研究一下 Shiki 或 Prism它们是更常规的选择但不要一开始引入否则后续难度会超出主线学习目标。5. 上线部署与常见问题排查记录5.1 构建与部署流程t3code 整个项目构建起来非常简单因为 T3 Stack 默认就是围绕 Next.js 设计的最省心的部署方式是 Vercel。本地先验证构建是否通过pnpm build如果一切正常项目会输出.next目录。然后把代码推送到 Git 仓库在 Vercel 里导入项目填写环境变量即可。这里有个极其容易踩的坑如果你还沿用本地 SQLite 的DATABASE_URL在 Vercel 的 serverless 环境里会频繁报错因为函数实例的文件系统是临时的连数据库文件都写不进去。所以上生产前我把 t3code 的数据库切到了 PostgreSQL。操作步骤也很简单在 Vercel 的 Storage 模块创建一个 Postgres 数据库。把生成的连接串填到项目环境变量里。本地执行npx prisma migrate deploy让生产库把表结构同步上。重新部署验证读写正常。如果不想用 Vercel 的 Storage也可以用任何云数据库服务的 Postgres 实例只要连接串正确就没问题。5.2 典型问题速查表我在开发 t3code 的过程中遇到过一些非常磨人的问题这里整理成表格方便你直接对照。问题现象主要原因解决方法npx prisma db push失败报数据库连接错误SQLite 文件路径不对或数据库文件被占用检查.env里的DATABASE_URL确认为file:./db.sqlite关闭其他数据库管理工具前端调用api.snippet.create时无类型提示类型没更新dev server 缓存了旧类型重启 dev server或重新执行pnpm dev让 tRPC 重新生成类型Tailwind 样式没生效页面看着像无样式 HTMLTailwind 的内容路径没配置在tailwind.config.ts里确认content包含src/app/**/*.{tsx,ts}创建片段成功后列表不刷新没有调用utils.snippet.getAll.invalidate()在onSuccess回调里执行缓存失效操作部署到 Vercel 后写入数据成功但读取时数据丢失SQLite 无法用于持久化存储切换数据库为 PostgreSQLNext.js 报错SerializableState或者水合不匹配Server Component 和 Client Component 混用渲染结果不一致检查页面是否用了use client确认不会把 Date 对象直接传给组件tRPC 报No create exists in the union of ...类型不匹配Router 路径不对或 ctx.prisma 访问失败核对root.ts挂载名确保与前端调用名一致这些问题中最让我记忆深刻的是第二个。tRPC 强类型虽然爽但它依赖类型生成器在 dev 运行时动态更新。如果你像我一样经常改 Router 签名建议改完之后等 dev server 自动 reload 几秒再去写前端代码不然编辑器会给你错误的红色波浪线容易造成误判。5.3 避开 T3 Stack 的三个隐藏坑第一个坑是 React Server Component 和 Client Component 的边界。tRPC Hooks 只能在 Client Component 里反复强调但仍然有人会下意识在默认的 Server Component 页面里写useQuery。我建议从一开始就把页面根部标记为use client这样才能避免水合错误。第二个坑是 Prisma 在 serverless 平台的连接数问题。Vercel 的 serverless 函数按需创建如果用 Postgres每个函数实例都会尝试建立数据库连接连接池很快被打满。解决方式是在server/db.ts里使用 Prisma 官方推荐的加速连接方案或者给数据库加一个连接池代理。这个问题在个人项目里不明显但流量稍高就会爆发。第三个坑是升级依赖时要谨慎。T3 Stack 是多个框架的叠加任何一方升级大版本都可能牵一发动全身。比如 Next.js 升级大版本后tRPC 适配器可能不兼容Prisma 升级大版本后生成的 Client 类型缓存要重新构建。我后期吃了一次升级的亏之后形成了自己的习惯小版本升级直接上大版本升级先查 Upstream 的兼容性说明绝不盲目执行pnpm update。最后再分享一个小技巧我在实际维护 t3code 的过程中最受用的是把整个项目拆成“可复现的最小步骤”来做先让数据库读写通再让 tRPC 接口通最后才碰页面样式。看起来多花时间但每一步出了问题都能立刻定位。还有一个更小但很实用的细节就是给每个 Router 都加一个getLatest之类的简单 query作为“探针接口”。前端联调时如果什么数据都拿不到先用这个探针确认基础链路是否正常能省掉很多无意义排查。如果你自己也在搞类似的 T3 Stack 项目建议先不要急着堆功能把一个最简单的 CRUD 流程跑通比任何教程都能让你更快理解这套组合的打法。t3code 会持续迭代后续我计划加上全文搜索和用户区分但这都是把根基打牢之后的事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →