☰
从零搭建T3全栈代码片段管理工具t3code实战
2026/10/27 7:28:07 网站建设 项目流程

t3code 是我今年花了大半个周末从零搭起来的一个全栈项目:基于 T3 Stack 的轻量级代码片段管理与分享工具。简单说,它就是用 Next.js、TypeScript、Tailwind、tRPC、Prisma、NextAuth 这一整套 T3 技术栈,把“收藏一段代码、打上标签、随时搜索、一键分享”这件事做扎实。项目本身既是给我自己日常开发用的 snippet 仓库,也是我验证 T3 全栈套路是否真的“生产力拉满”的试验田。如果你正准备学全栈、想找一个类型安全从头贯穿到数据库的完整范例,或者只是好奇 tRPC 到底比 REST 好在哪,这篇内容应该能让你少走不少弯路,里面的踩坑记录都是真实调过的。

1. 项目定位与整体设计思路

1.1 为什么叫 t3code,它想解决什么问题

“t3code”这名字拆开看就是 T3 + code。T3 指的是社区里流行的 T3 Stack 技术栈:TypeScript、Tailwind CSS、tRPC,通常还会带上 Next.js、Prisma、NextAuth。而 code 则是这个项目真正的业务场景——代码片段管理。

我做这个项目之前,本地一直用的是一个轻量笔记软件收藏代码,但它有几个让我很烦躁的点:第一,代码没有语法高亮,复制出来还要手动排版;第二,标签体系太弱,想找一段几周前存过的 React hook 要翻半天;第三,跟团队协作完全不沾边,一个同事问我要某段配置,我得截图发过去。t3code 就是冲着这三个痛点去的:支持 Markdown 和高亮,支持灵活标签和全文搜索,支持通过链接公开分享代码片段。

选定这个场景还有一个原因——它复杂度适中,非常适合验证全栈技术栈的完整链路。一个代码片段要经过“用户认证 → 表单提交 → API 校验 → 数据库存储 → 列表查询 → 详情展示 → 公开分享”,几乎是所有 Web 应用的标准骨架,又不至于复杂到让人失去耐心。做起来能覆盖到认证、数据库、API 层、前端状态管理、服务端渲染缓存这些核心问题,非常适合拿来当一个全栈样板项目。

1.2 技术选型背后的权衡:为什么是 T3 这一套

当时摆在我面前的不止 T3 一个选择,我认真对比过几条路线,这里直接说结论。

第一,Supabase + Next.js 的组合也很流行,数据库、认证、实时订阅都省了,但问题在于业务逻辑和类型安全很难端到端闭环。Supabase 的客户端 SDK 虽然可以生成类型,但写 RPC 函数或者复杂查询时,类型还是容易断裂,而且很多逻辑要写在服务端 SQL 里,调试成本比本地 Prisma 高。我想要的是所有数据访问都从 TypeScript 类型里“长出来”,而不是靠运行时碰运气。

第二,NestJS 加上 Prisma 做后端很成熟,但前后端要维护两套类型定义,接口文档也得多写一份。一个 snippet 管理工具如果还要先定义 OpenAPI schema 再生成客户端代码,开发体验就太重了。tRPC 最吸引我的地方是 procedure 直接引用服务端函数,类型天然穿透,改一个输入 schema,前端调用处立刻报错,根本不需要 mock。

第三,数据库方面我没有一开始就让步生产级基础设施,开发环境直接用 SQLite,部署到线上再平滑换 PostgreSQL。Prisma 最大的好处就是 schema 文件是唯一事实来源,换成 PG 只需要改 provider 和连接串,业务代码几乎零改动。这个决策让我前期省掉了数据库服务的运维成本,本地跑一个命令就能把全部表结构同步出来。

还有一个小选择值得说:前端样式为什么选 Tailwind 而不是 CSS Modules 或 styled-components。代码片段管理工具里,代码块的高亮主题、标签的彩色圆角块、布局的间距响应式,全部是高频定制样式。Tailwind 的原子类配合 darkMode: class 策略,让我在实现亮暗主题时基本没写过新的 CSS 文件,效率差别非常明显。

1.3 适合谁用,什么基础能上手

这个项目最适合两类人。第一类是刚学完 React 和 Node.js 基础、想上一个完整全栈项目的同学。你不会被工程化细节劝退,因为 create-t3-app 已经把脚手架全部搞定,你需要关注的只有业务代码本身。第二类是日常工作写 API 写烦了、想感受一下“端到端类型安全”的开发者,t3code 这个体量刚好能让你在几天内完整体会到 tRPC 的爽感。

基础要求方面,我认为至少要在 React 里写过组件、理解 useEffect 和数据请求的基本模式,懂得 TypeScript 的 interface、泛型和类型推断。数据库 SQL 知识不要求精通,Prisma 会帮你抹平大部分差异,但如果你能看懂一对多、多对多关系,调整数据结构会轻松很多。项目中我会用到 NextAuth 的 credentials 和 GitHub provider 两套方案,需要你理解 session 和 cookie 的基本概念。

2. 核心功能与数据模型设计

2.1 数据模型:用户、片段、标签怎么组织关系

t3code 的数据模型是我动手写 UI 之前花了最多时间设计的地方。核心的就四张表:User、Snippet、Tag、SnippetTag。

User 直接复用 NextAuth 默认模型,只有额外扩展一个 username 字段,用于公开主页的 URL 标识。Snippet 保存代码正文、标题、语言类型、可见性(private/public)和作者外键。Tag 是一个非常轻的纯标签表,只存名字和创建时间。Snippet 和 Tag 之间是多对多关系,我额外建了一个 SnippetTag 关联表,这样以后想在关联表上加“在某标签下置顶”这类字段时不至于改成一对多。Prisma schema 里这段关系我写得比较直接:

model User { id String @id @default(cuid()) name String? email String? @unique username String? @unique snippets Snippet[] } model Snippet { id String @id @default(cuid()) title String content String language String @default("text") visibility Visibility @default(PRIVATE) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) tags SnippetTag[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Tag { id String @id @default(cuid()) name String @unique snippets SnippetTag[] } model SnippetTag { snippetId String tagId String snippet Snippet @relation(fields: [snippetId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([snippetId, tagId]) }

多个标签场景里,我一开始考虑过直接用 String[] 数组存标签名,省一张表。后来放弃是因为标签以后要做去重大屏统计,数组字段没法高效做聚合查询。关系表在多对多场景下查询效率虽然多一次 join,但对于这个量级的工具完全不是瓶颈,而且语义清楚。

2.2 端到端的类型安全核心:tRPC router 与 Zod 校验

t3code 的 API 层没有写一个传统意义上的 REST 接口,全部请求都走 tRPC router。每个路由对应一个 procedure,输入和输出都由 Zod schema 定义。这里最大的感受是:校验和类型是同一个东西,前端调用时获得的不只是运行时保护,更重要的是编译期就能发现参数错误。

举个例子,创建代码片段的输入必须要做三件事:标题不能为空、正文不能超过 50000 字符、language 必须在一个支持列表里。我直接在 Zod schema 里写下这些规则:

import { z } from "zod"; export const snippetInput = z.object({ title: z.string().min(1, "标题不能为空").max(100), content: z.string().min(1).max(50000), language: z.enum(["text", "typescript", "javascript", "python", "bash", "sql", "json"]), visibility: z.enum(["PRIVATE", "PUBLIC"]).default("PRIVATE"), tagNames: z.array(z.string().max(30)).max(5), }); export type SnippetInput = z.infer<typeof snippetInput>;

然后 router 里用 protectedProcedure 把这个输入 schema 包进去。只要是登录用户就能调用,服务端自动完成解析、校验、注入 session 用户信息,我再写业务代码时完全不用管鉴权前置逻辑:

create: protectedProcedure .input(snippetInput) .mutation(async ({ ctx, input }) => { const ownerId = ctx.session.user.id; // 批量 upsert 标签再关联 return await ctx.prisma.snippet.create({ data: { title: input.title, content: input.content, language: input.language, visibility: input.visibility, authorId: ownerId, tags: { create: input.tagNames.map((name) => ({ tag: { connectOrCreate: { where: { name }, create: { name } } }, })), }, }, }); }),

前端 React Query 的调用也极其顺手。useMutation 之后只需要写 onSuccess 更新缓存,类型是自动跟过来的:

const utils = api.useUtils(); const createSnippet = api.snippet.create.useMutation({ onSuccess: () => { utils.snippet.invalidate(); router.push("/dashboard"); }, });

整个链路里,我甚至没有写过一次手动的 fetch、没有定义过 API 返回值的 interface。这个开发体验一旦适应了就回不去,因为你少掉的那部分工作,正是过去最让人头疼的联调和 debug 工作。

2.3 页面与交互:App Router 下 RSC 和客户端组件怎么配合

T3 默认生成的 Next.js 使用了 App Router,这个架构下最高频的问题就是“哪些组件可以服务端渲染、哪些必须是客户端组件”。t3code 的处理原则我总结成一句话:静态展示走 RSC,交互状态走 Client Component,两者之间用 Server Actions 或者 tRPC query 连接。

例如公开页面的代码片段详情,我直接让它在服务端通过 tRPC server caller 查询数据,页面组件本身是异步的,代码高亮在服务端完成,用户拿到的 HTML 里直接就有完整内容,不需要先渲染骨架再加载数据。这个对爬虫和分享链接的打开速度都非常友好。

而编辑页面里的标签选择器、代码编辑器、可见性切换开关,这些都是强交互组件,必须标记 “use client”。为了不让整个页面变客户端组件,我把交互组件拆得很小,页面主体仍然是服务端组件,只有局部交互区域用客户端组件拼接。这样也保证编辑器切换代码语言时不会影响整页重新请求数据。

代码高亮这里我前后换过两个方案。第一个是 react-syntax-highlighter,生态老、主题多,但切换数据时会有短暂闪烁。后来换成 Shiki,它基于 TextMate 语法,高亮质量和 VS Code 完全一致,而且支持给代码生成静态 HTML,正好配合服务端渲染。Shiki 初始化需要加载 JSON 语法文件,我在服务端组件里用 dynamic import 按需加载,避免把语法包打进客户端 bundle。

3. 实操过程:从零到一跑起 t3code

3.1 初始化项目与关键配置

动手第一步是用官方脚手架创建项目,我推荐你用最新的 create-t3-app 命令:

pnpm create t3-app@latest t3code

交互式选项里我选择的组合是:Next.js(App Router)、TypeScript、Tailwind CSS、tRPC、Prisma、NextAuth。没选的包括 Drizzle(Prisma 对我来说还是更稳)、ESLint 默认勾上、Prettier 默认勾上。

项目生成后第一件事不是写业务,而是把环境变量配好。开发环境最简单的方式是 SQLite + GitHub OAuth。.env文件里关键几项:

DATABASE_URL="file:./db.sqlite" NEXTAUTH_SECRET="随便生成一段足够长的随机串" NEXTAUTH_URL="http://localhost:3000" GITHUB_CLIENT_ID="你的id" GITHUB_CLIENT_SECRET="你的secret"

GitHub OAuth 的申请步骤不复杂:GitHub 账号 -> Settings -> Developer settings -> OAuth Apps -> New OAuth App。Homepage URL 填http://localhost:3000,Authorization callback URL 填http://localhost:3000/api/auth/callback/github。这里有个细节我踩过一次,回调路径千万不要写错成auth/callback/github,少了api段,NextAuth 会直接报错并且日志里没有任何有效线索。

配置好之后直接跑迁移和开发服务器:

pnpm db:push pnpm dev

db:push会根据 Prisma schema 直接创建数据库表,不用生成迁移文件,开发阶段我最常用它。等到要上生产环境前再跑prisma migrate dev生成正式迁移。

3.2 核心模块代码实现:从 schema 到页面的闭环

这个环节我们完整走一遍“创建片段”的核心链路,你会看到类型安全是怎么层层穿透的。

第一步,Prisma schema 已经定义好模型后,执行生成客户端代码:

pnpm prisma generate

第二步,在 tRPC router 根文件里注册业务子路由。项目生成的文件通常叫src/server/api/root.ts,把 snippet router 挂进去:

export const appRouter = createTRPCRouter({ snippet: snippetRouter, user: userRouter, });

第三步,写 API 的列表查询。列表页需要按当前用户筛选,并统计标签名称。这里 tRPC 的 protectedProcedure 能直接拿到 session,因此过滤条件非常自然:

listMine: protectedProcedure .input(z.object({ search: z.string().optional() })) .query(async ({ ctx, input }) => { const userId = ctx.session.user.id; return ctx.prisma.snippet.findMany({ where: { authorId: userId, ...(input.search ? { OR: [ { title: { contains: input.search } }, { content: { contains: input.search } }, ], } : {}), }, orderBy: { updatedAt: "desc" }, include: { tags: { include: { tag: true } }, }, }); }),

第四步,前端页面直接调用这个 query,React Query 的 key、缓存、重试全部自动管理。列表页面代码大致长这样:

const { data: snippets, isLoading } = api.snippet.listMine.useQuery({ search: debouncedKeyword, });

在输入框输入关键词时,我用了一个 300ms 的防抖 hook,避免每个字符都打一次数据库查询。这个小细节对体验提升很明显。

3.3 部署实践:从本地到 Vercel 的迁移

本地开发跑通之后,部署上线我建议直接用 Vercel,和 Next.js 同源集成的体验最顺。但有两个改动不能漏。

第一,数据库要换成 PostgreSQL。SQLite 在 Vercel 的无服务器环境不能持久化,所以先把 Prisma schema 的 provider 改成 postgresql,然后创建一个免费的线上 PG 实例,把 Vercel 环境变量覆盖为新的连接串。我的迁移命令是:

pnpm prisma migrate deploy

注意这里要用migrate deploy,不是db:push,因为生产环境需要按迁移历史执行,保证表结构一致。

第二,NextAuth 的 secret 和 OAuth 回调地址必须改成线上域名。Vercel 的环境变量分 Production、Preview、Development 三套,我把生产环境单独设置,避免预览分支自动评论时把生产 OAuth 回调弄混。

部署完成后我实测了一下公开片段页的 Lighthouse 分数,桌面端 Performance 在 95 以上。服务端渲染 + 静态化改造后的结果确实很干净,首屏没有多余的 JS 请求。

4. 常见问题与排查实录

4.1 tRPC 类型更新不及时:重启 dev server 才生效

这是用 tRPC 开发时最常遇到的现象:改完 router 里的输出类型,前端组件里的类型提示还是旧的。查了一圈后发现,create-t3-app 生成的类型在开发模式下是有缓存的,尤其是编辑器(VS Code)开了 TS server 长时间不重启时。

我的解决办法分两层。第一层,修改 router 后手动重启 dev server,强制重新生成类型;第二层,如果重启还不行,直接pnpm tsc --noEmit看真正的编译错误,一般这时错误就能暴露出来,说明是编辑器缓存滞后而非代码问题。后来我还顺手在 VS Code 设置里关了 TypeScript 自动引入缓存,错误提示会及时很多。

4.2 Prisma 连接池耗尽与 SQLite 并发写

开发阶段我用 SQLite 单位一切正常,但有一次测试并发提交 snippet 时,界面直接抛了SQLITE_BUSY。原因是 SQLite 只允许一个写锁,两个请求同时写入就会冲突。因为是单人开发工具,这个频率本不该出现,排查后发现是我在列表页写了一个“每次提交成功后同时 invalidate 三个 query”的操作,导致多个写事务在短时间内集中触发。

处理方式:开发环境把 Prisma 的连接池开启并串行写事务,生产环境因为换成了 PostgreSQL,并发写问题就没再出现。我的经验是:开发期用 SQLite 爽快,但涉及并发测试一定要尽早切到 PostgreSQL,别在最后才暴露。

4.3 NextAuth 回调地址不匹配

部署完成后,用户点击 GitHub 登录,跳转回来时偶尔会有redirect_uri错误。大多数情况下是因为我在 GitHub OAuth App 里只配置了生产域名,而每次本地开发又是另一个地址,两个回调地址没有同时加入白名单。

解决方法是去 GitHub OAuth App 设置里,把生产域名和http://localhost:3000都加进 Authorization callback URL。NextAuth 自身也能控制允许的回调域名,在authOptions.ts里加一下就不会乱跳了:

callbacks: { async redirect({ url, baseUrl }) { if (url.startsWith("/")) return `${baseUrl}${url}`; if (new URL(url).origin === baseUrl) return url; return baseUrl; }, },

4.4 markdown 正文里的换行和空格没保住

做代码片段工具,对空白字符的敏感度比普通文章编辑器高得多。初期我用 textarea 保存正文,展示时直接渲染成 HTML,结果代码块里的每一行前面的缩进都没了。后来改成两步:存储时保留原文不裁剪,展示时先统一换行符,再交给 Shiki 做高亮,高亮展示用<pre>包裹,CSS 里设置white-space: pre和overflow-x: auto。这三个动作缺一不可,尤其是white-space: pre,是保住缩进的核心。

4.5 典型问题速查表

现象可能原因处理办法
前端类型不更新tRPC 类型缓存滞后重启 dev server,或运行 tsc 验证
SQLITE_BUSYSQLite 并发写冲突开发期串行化写请求,生产换 PG
OAuth 登录回调报错回调地址白名单不全GitHub OAuth 设置中加入所有环境域名
代码缩进丢失渲染时未保留空白字符使用 pre + white-space: pre
重新部署后数据丢失SQLite 文件未持久化生产环境切换到 PostgreSQL

5. 经验心得与后续扩展想法

5.1 我做完 t3code 后最真实的三个体会

第一个体会是,T3 这套全栈方案在中小型工具类项目里的效率确实高得离谱。t3code 从初始化到核心功能跑通,满打满算不到两天,大部分时间不是浪费在接口联调,而是花在数据模型设计和我自己想加的细节交互上。类型安全节省的隐形时间,远比刚开始想象得多。

第二个体会是,tRPC 和 Server Component 搭配时,需要想清楚数据获取的边界。tRPC 方便的同时也容易用顺手了,导致所有页面数据都走客户端 query,错过 SSR 的机会。我建议把“需要登录的私有数据”和“公开的展示数据”分开处理,前者用客户端 query 享受实时更新,后者用服务端调用走渲染缓存,这个平衡需要在项目早期就定好规则。

第三个体会是关于代码高亮和编辑器体验的。Snippet 工具的核心不是复杂逻辑,而是使用者复制代码、搜索代码、编辑代码时的每一丝手感。Shiki 高亮、防抖搜索、快捷键保存,这些看起来小的点,全都直接影响着一个工具是否值得日常使用。我后面给编辑器加了 Cmd/Ctrl+S 保存的快捷键,就这一点就让 t3code 的使用频率翻了不止一倍。

5.2 后续可以扩展的方向

t3code 当前版本做到这里,往后还有几个我认为很值得深入的方向。

第一,给代码片段增加评论或收藏功能。现在公开分享只是单向展示,如果别人看到一段好代码想点赞收藏,就没有入口。这需要扩展 Snippet 的关联关系,加上 Favorite 表和对应的 tRPC procedure,改动不大但对社区互动帮助明显。

第二,增加 CLI 工具,让用户可以直接从命令行把本地文件推送到 t3code。这个属于典型的“体验放大器”,做一个 Node CLI,通过 token 认证,读取文件后调用同一个 tRPC procedure。因为类型定义已经完整存在,CLI 端复用服务端输入 schema 非常顺畅。

第三,用全文检索引擎替换当前的 LIKE 查询。数据库内contains对中文和长代码内容支持一般,未来可以考虑接入 Meilisearch 或 Postgres 全文索引,让搜索真正变成毫秒级。开发量不大,但属于越到后期越刚需的能力。

如果你也想搭一个自己的工具类项目,又不想陷入 API 设计、联调、类型维护这些重复劳动,t3code 这个方向值得动手试一次。把 T3 这一套链路完整跑一遍,学到的东西绝对不只是一个代码片段管理器那么简单。

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

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

立即咨询