首页 / 资讯中心 / 文章详情

T3 Stack实战:构建代码分享平台的技术选型与调优全记录

T3 Stack实战:构建代码分享平台的技术选型与调优全记录 ★ FEATURED ARTICLE
最近我终于把我折腾了一个多月的 t3code 项目正式开源了。先说清楚它是个什么这是一个基于 T3 Stack 技术栈开发的代码片段分享平台核心功能就是让大家像发推特一样随手粘贴一段代码、配上标题和标签然后生成一个短链接分享给朋友。你也许会觉得“GitHub Gist 不就能干这事吗”但我的出发点很明确——趁手、轻量、有社区感。t3code 没有用传统的 REST API 那一套而是把整个应用建立在 Next.js、tRPC、Prisma、Tailwind 这四大件上也就是大家常说的 T3 Stack整个项目从前端组件到数据库表结构都是端到端类型安全的这一点在实际开发中帮了我大忙。如果你最近正在纠结要不要入坑 T3 Stack或者你正好想做一个带分享功能的代码工具型网站这篇复盘应该能给你不少参考。我会把当时做技术选型的思考、数据库结构怎么设计、tRPC 的 service 层如何组织、部署上遇到的坑、以及几个实测过的调优细节全部倒出来。这不是一篇“教你怎么复制粘贴”的教程更像是一个老开发做完项目之后的现场总结你从里面能直接拿走的都是我踩出来的经验。1. 项目起源与技术选型为什么是 T3 Stack1.1 t3code 到底要解决什么问题先说项目动机。我平时写代码的时候经常遇到一个场景在群里帮别人排查一个报错我把关键代码贴出来结果被聊天软件的格式化功能蹂躏得完全没法看。又或者我想把一个有意思的算法技巧分享给同事但在微信里传文件太蠢直接发代码块又会被压缩。GitHub Gist 虽然成熟但总觉得它的交互偏重我没法很快地给片段加标签、按语言筛选更没有办法像论坛帖子那样让其他人评论几句。t3code 想要解决的就是这个“轻量分享”的缺口输入一个标题粘贴代码选一下语言和标签一分钟内得到一条可分享的链接。整站风格走极简路线你不用注册登录也能浏览只有你自己发帖的时候才需要验证邮箱。后来我还加了一个非常实用的功能支持短链自定义可以让分享链接变成t3code.dev/c/onboarding-guide这种形式。这个需求听起来简单但从产品逻辑到技术实现拆开来看涉及的东西一点都不少。1.2 选型依据没有选 XX而是选了 Next.js tRPC Prisma Tailwind其实一开始我脑子里蹦出的方案是“Next.js Express PostgreSQL”前端用 React后端单独开一个 server再配上 Swagger 文档。这种方案当然没有问题但很快我意识到对于一个以“代码分享”为核心的小型项目来说维护两套服务器和一套 REST 规范本身就超重了。T3 Stack 的思路是不要让类型定义在前后端之间来回拷贝而是让编译器直接帮你校验。tRPC 的出现把这个过程做成了“写一个函数然后在组件里直接调用”函数参数和返回值的 TypeScript 类型是全程传递的我改了一个数据库字段的类型前端调用的地方立刻标红报错。这一点在一个人开发的全栈项目里极具价值因为它变相替我守护了 API 契约。换一个角度说如果你是做中小型项目、团队不大、又想快速把想法落地成产品Next.js 的 App Router 配合 tRPC 的效率远高于前后端分离方案。tRPC 不需要像传统 REST 那样定义 path、method、query param它的每个procedure就是一个函数端点天然支持输入校验zod和输出序列化。Prisma 则把数据库访问变成了类型安全的 ORM我个人不太喜欢写 SQL 和 migration 混在一起Prisma Schema 那种声明式的模型定义让我可以在 5 分钟内理清所有表关系。Tailwind 更不用多说了写页面的时候不用来回切 CSS 文件这个对于做分享类 UI 非常友好因为代码高亮、布局响应式这些都需要大量微调。1.3 技术栈背后的“组合拳”逻辑单纯说选型很容易变成罗列名词真正好用的地方在于这几个工具之间的默契。T3 Stack 的默认组合就是 Next.js tRPC Prisma Tailwind这个组合里 Next.js 负责页面路由、SSR 和部署tRPC 管理内部数据流Prisma 直接连接数据库Tailwind 处理样式。因为都是 TypeScript 生态所以从数据库里的Snippet模型到页面上渲染出来数据类型没有断过层。服务器端拿到一条记录通过 tRPC 返回给前端前端直接就有自动补全提示。再配合zod做输入校验我可以在同一个文件里定义参数 schema 和函数实现不用像以前那样跑去查 OpenAPI 文档。这个“组合拳”可以说就是为“快速开发又不想放弃类型安全感”的人准备的。对我这个具体项目来说选用这个组合让我节省了至少三天的联调时间因为几乎所有类型错误都在编译期暴露了上线后几乎没出现过“字段名拼错”的线上 bug。2. 核心功能拆解与数据结构设计2.1 核心功能清单梳理动手写代码之前我给 t3code 规划了一圈 MVP 功能。首要的当然是代码片段的发布标题、正文代码、语言标签、说明性副文本。第二个功能是浏览和搜索首页展示最新片段支持按语言筛选还做了一版简单的关键词搜索用的是 PostgreSQL 的全文检索。第三个功能是详情页展示代码高亮、便捷复制按钮、以及基于标签的相似内容推荐。第四个功能是用户系统我一开始不想做复杂登录就用 NextAuth 接 GitHub OAuth配合邮箱校验。后面又加了用户主页展示这个用户发布过的所有片段。评论功能我放到了第二期因为第一版核心是“分享”而不是“讨论”。另外还有管理后台的一些计数功能浏览数、点赞数、复制次数这些数据在列表页不需要但详情页可以用来优化推荐算法。功能清单听起来很平常但每一条落实到数据结构上都需要仔细考虑。2.2 数据库模型设计Prisma Schema 要点Prisma Schema 是整个 t3code 的根基。我先把最核心的Snippet模型放出来看一下model Snippet { id String id default(cuid()) title String code String db.Text plainText String? db.Text description String? db.Text language String default(text) tags Tag[] relation(SnippetTags) author User? relation(fields: [authorId], references: [id]) authorId String? slug String unique viewCount Int default(0) likeCount Int default(0) forkCount Int default(0) createdAt DateTime default(now()) updatedAt DateTime updatedAt index([language]) index([createdAt]) }这里面有几个关键考量。首先是slug字段必须唯一我生成 slug 的逻辑是如果标题是纯英文就转小写加连字符如果是中文就加一段随机 hash 后缀避免重复。然后是plainText字段这个字段是原代码的纯文本形态用来做全文检索和高亮降级。为什么要专门存一份纯文本而不直接用code因为我在前端使用代码高亮库时会预先做 HTML 转义存储时如果用富文本格式反而会增加复杂度。language字段直接用字符串而非外键是因为 Prisma 里的枚举对大量语言支持很不友好我宁愿在应用层做白名单校验。tag 表我用了多对多关系每个 snippet 最多打五个标签。其实一开始我考虑过用数组字段但为了后面做标签云和标签筛选还是开了关联表。2.3 关键业务逻辑代码片段的保存与分享保存逻辑中最重要的不是 insert 语句本身而是“防注入、防破坏、防垃圾内容”。前端提交上来的代码先经过 zod schema 校验限制正文最长 50000 个字符语言必须在支持列表里。真正保存的时候我不会直接存用户传入的高亮 HTML而是把原始代码存进code字段。这个动作在当初写的时候费了不少劲因为很多项目为了图方便会把前端渲染好的 HTML 直接入库其实这是大坑一旦前端渲染逻辑更新历史数据全部需要重新转换而且存入带样式的内容极容易产生 XSS 漏洞。我要分享的链接是/s/[slug]详情页读取时根据language动态选择 PrismJS 语言包进行服务端高亮同时保留一份纯文本用于复制按钮和代码对比。这样既保证了展示效果也保证“复制代码”不会复制一堆 span 标签。用户分享后看到的是一个站内短链这里我额外处理了短链冲突检测。因为 t3code 允许自定义 slug所以我在createSnippet这个 tRPC procedure 里做了捕获唯一性冲突的逻辑如果用户自定义的 slug 被占用了就自动在后面加上一个短 id。这里我踩过一个很隐蔽的坑tRPC 里的错误如果直接抛Prisma.PrismaClientKnownRequestError前端会拿到很抽象的 message所以我封装了一个TRPCError映射层把 Prisma 的错误码转成中文用户提示。3. 实操细节从零搭建 t3code 的关键步骤3.1 初始化项目与依赖配置T3 Stack 推荐用create-t3-app脚手架来初始化我实际操作的时候用的命令是pnpm create t3-applatest t3code初始化的时候建议直接勾选NextAuth、Prisma、Tailwind、tRPC四个选项因为后面加进来比较麻烦。初始化完成后第一件事是修改环境变量文件配置数据库连接串、NextAuth 的 GitHub 回调地址。这里有个经验如果本地和线上用一个数据库连接串里的 schema 名字一定要指定清楚否则 Prisma 的 migrate 可能会作用到错误的目标库。我的.env长这样DATABASE_URLpostgresql://postgres:postgreslocalhost:5432/t3code?schemapublic NEXTAUTH_SECRET... NEXTAUTH_URLhttp://localhost:3000 GITHUB_ID... GITHUB_SECRET...然后执行pnpm db:push pnpm dev如果是全新项目db:push会直接创建表结构非常快。但后续改动 schema 必须用prisma migrate dev我习惯了给每个 migration 写提交信息方便回滚。3.2 tRPC Router 编写与类型安全实践tRPC 的核心在于 router 的拆分和类型归纳。我在server/api/root.ts里把子路由snippetRouter、userRouter、tagRouter合并到了总路由组件里调用 API 是完全的类型穿透这是 T3 Stack 最爽的体验。减少重复操作用户可以一次请求获取“片段详情加作者信息加相似标签”的聚合数据这时候直接新建一个 procedure 来整合数据。有一个重要性极高的点tRPC 的input schema和output schema要独立构建比如 list snippets 的时候前端并不需要plainText字段我就在输出 schema 里用.omit()剔除能轻量一点传输。上传片段时的流程是先验证用户登录protectedProcedure再验证语言是否在列表里然后生成 slug。代码大致如下export const createSnippet protectedProcedure .input(createSnippetSchema) .mutation(async ({ ctx, input }) { const userId ctx.session.user.id; const slug await generateUniqueSlug(input.slug || input.title); const snippet await ctx.db.snippet.create({ data: { title: input.title, code: input.code, plainText: input.plainText, language: input.language, slug, authorId: userId, }, }); return snippet; });这里有一个我刚用 tRPC 时容易犯的错把所有校验都写在input的 zod schema 里但没考虑数据库的唯一约束。比如 slug 在数据库层已经是 unique但我只做了前端格式校验结果并发请求时会有两个一样的 slug 同时走进创建逻辑然后其中一个报错。后来我兜底捕获了唯一约束冲突再重新生成一个带随机数的 slug 重试一次才算平稳落地。3.3 前端页面与 Tailwind 样式的实现t3code 的 UI 走的是黑底白字的极简风原因是代码展示需要较高对比度。Tailwind 在这里最大的帮助是我能够直接在组件里通过apply抽公共样式比如代码编辑器容器、标签组件等。具体到实现上首页和列表页我都是放在 RSCReact Server Component里调 tRPC 的吗这里必须澄清一下tRPC 的 server caller 在 Server Component 里使用需要极其谨慎因为它们依赖请求上下文如果直接调用没有包裹请求作用域的 caller很多 session 信息是拿不到的。所以我选择了更稳的方案在客户端组件里使用useQuery然后配合 Next.js 的getServerSideProps做首屏数据注入的替代品——其实更准确的说法是我用的是app router里的 Server Component 配合 server caller 传入createContext的方式。这块细节比较复杂我个人建议不要一上来就在 RSC 里调 tRPC最好是先在页面里初始化一个trpcServercaller然后只把返回结果序列化传给客户端。页面布局方面列表页就是一个响应式网格卡片展示标题、语言标签、时间、第一段代码摘要。代码高亮我用的是prism-react-renderer配合一个自定义主题。这个库在 SSR 时直接渲染不会产生闪烁而且支持自定义语言包我只加载了 30 个常见语言包打包体积控制在 250KB 左右。3.4 部署与运维Vercel 数据库连接池t3code 部署我选择了 Vercel因为 Next.js 和 Vercel 的适配性确实没得说。但部署后第一周就遇到了一个经典的坑Vercel 的无服务器函数在使用 Prisma 时如果连接数据库的配置不对冷启动会非常慢甚至直接超时。解决思路是把 Prisma 客户端实例挂到全局对象上避免每个函数都新建一次 database connection。我在lib/db.ts里这样写import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma?: PrismaClient }; export const prisma globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV development ? [query, error] : [error], }); if (process.env.NODE_ENV ! production) globalForPrisma.prisma prisma;数据库方面如果直接用 Vercel Postgres 的 serverless driver和 Prisma 的适配需要我们显式配置driverAdapters不然只能走普通的 TCP 连接。为了少折腾我最后把数据库放在 Supabase 上连的是连接池端口而不是直连端口。这里有个重要细节地址是aws-0-us-east-1.pooler.supabase.com如果你不小心配成了直连地址在 Vercel 函数里会因为连接数限制而频繁报 504 错误。另外我在部署环境里专门加了几个只读副本的查询入口列表页用副本详情页写操作走主库。这个优化也是被逼的——上线第三天列表页的查询因为多个热门片段同时被访问导致主库 CPU 直接飙到 90%。分离读写之后主库压力下降了 70%。4. 实测中踩过的坑与性能调优记录4.1 代码高亮与防 XSS 的取舍代码高亮是分享平台最核心的体验但也是最容易出现安全漏洞的环节。最开始我图省事直接把 Markdown 转成 HTML 后在前端用dangerouslySetInnerHTML渲染高亮结果结果被内部测试发现可以在代码片段里注入img onerror造成反射型 XSS。后来我痛定思痛做了一套严格的净化逻辑服务端接受代码后用isomorphic-dompurify做消毒再交给prism-react-renderer的 token 机制逐个渲染。逐个 token 渲染虽然相对慢一点但它天然避免了所有 HTML 注入面。为了保持性能我给 Prism 语言包加了缓存所选语言的 token 结果第一次计算后存储在 Redis 上有效期 24 小时。这个方案实测下来详情页的 TTFB 时间从 1.2 秒降到了 320 毫秒收益非常明显。另一个容易被忽略的坑是代码片段里包含了回车符和 tab 键很多高亮库在转换时会把某些字符切换成nbsp;或br/如果复制按钮直接复制 innerText原来的缩进会全部丢失。我最后做复制功能时统一使用navigator.clipboard.writeText并传入plainText字段而不是页面上的高亮文本这样彻底解决了复制变形的问题。4.2 tRPC 在 SSR 和客户端调用的缓存陷阱tRPC 和 React Query 配合虽然舒服但也意味着你要理解 React Query 的缓存机制否则会产生非常奇怪的问题。我在开发列表页时发现当用户从首页进入详情页再返回首页时首页数据虽然被缓存了但分页参数一旦变化它会自动重新请求新数据旧数据也不会被清理。默认配置下React Query 的gcTime是 5 分钟如果你在短时间内在列表和详情之间多次切换内存里会积攒很多数据还会触发较多的重复请求。为了缓解这个问题我自定义了 tRPC 的defaultOptionsconst trpcClient trpc.createClient({ links: [ httpBatchLink({ url: /api/trpc }), ], });这里的httpBatchLink是很关键的一招它能把同一时间窗口内多个调用合并成一个 HTTP 请求。比如详情页同时请求 snippet、user、tags 三个 query浏览器只会发一个请求显著降低了网络往返。缓存时间方面我把列表数据设为staleTime: 30_000详情页设为staleTime: 120_000不改动的话用户看到的数据平均延迟最多两分钟体验完全可以接受。当然也有极端场景用户 A 在自己主页删除了一个片段但详情页的缓存还是会残留。我个人的解决方案是删除成功后调用trpc.useUtils().snippet.invalidate()强制让相关查询失效。只要你在 mutation 之后记得 invalidate 对应的 query key就不会出现这种逻辑不一致。4.3 Prisma 连接数与冷启动问题处理冷启动的另一个角度的坑是 Prisma 在 Vercel 上默认会做一次 schema 校验这个校验会增加几百毫秒的启动时间。如果你用的是服务端渲染页面会比较明显。我后来直接把previewFeatures和校验开关关了generator client { provider prisma-client-js binaryTargets [native] }还有一个经验是数据库连接池的数量。Supabase 免费版的连接池默认只能支持 10 个并发连接如果你的 Vercel 函数同时收到一批查询尤其是像 tRPC 批量请求那种就很容易把连接池打满。我做了两层优化第一层在 Prisma 客户端的datasource url里配上pgbouncertrue和connection_limit3让每个函数实例最多占一个连接第二层列表页和详情页全部加上 Redis 缓存自动降级当查询失败或者超时的时候直接走预渲染的静态快照。上线之后我没有再收到过量连接导致的错误报告。如果你也要做类似的部署我建议一定要在本地先模拟一下几十个请求同时打进来的场景用wrk或者k6跑一下别等到线上炸了才去翻日志。5. 我的最终体会与几个超有用的扩展方向整个 t3code 做下来我最大的体会是端到端类型安全带来的安全感远超想象。一个人维护全栈项目时最怕的不是写不出功能而是数据流在某个地方发生悄然失真。tRPC 把前后端契约固化在 TypeScript 类型上我重构字段时几乎不需要去前端到处找哪里访问到了被改名的属性。第二个体会是不要为了追求所谓的主流架构而过度设计。最开始我也曾想上一套微服务用 Docker 把 Redis、Postgres、Nginx 全部编排起来但后来我把最核心的部分一直控制在 Vercel Supabase Upstash 这个极简组合上结果证明稳定性非常高。如果你也想搭一个类似的代码分享工具我还想给你三个扩展建议。第一把代码片段模块封装成一个可嵌套的组件比如你的片段内部还可以引用另一个片段这样就能做出“代码拼接”的玩法。第二可以做基于标签的隐式关系推荐因为我已经给每个 snippet 挂上了标签之后只需要用余弦相似度计算标签集合之间的距离就能实现轻量的关联推荐。第三给用户主页增加一个“代码足迹”时间线记录用户发布的代码和点赞过的代码这个功能虽然不复杂但社区归属感一下就上来了。最后分享一个小技巧在提交 tRPC mutation 的时候我习惯统一把错误信息通过errorFormatter转换成语义化文案。前端拿到formattedMessage直接展示就不会让用户看到“Request failed with status code 500”这种既吓人又没用的话。这个技巧看起来很小但对于一个面向开发者群体的工具来说每一个细节都在影响口碑。t3code 目前还在快速迭代中我最近正在琢磨怎样把评论功能做成推流式实时更新让分享者们真正交流起来。如果你也在做一个轻量工具型产品希望这篇记录能帮你少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站