资讯动态

用T3 Stack从零实现代码片段管理工具:t3code全栈开发复盘

发布时间:2026/10/26 17:06:33 来源:尧图企业网站定制
作为一个写了快十年业务代码的人我最受不了的其实不是复杂需求而是每次想找一段以前写过的代码都得翻遍本地文件、GitHub Gist、还有聊天记录的收藏夹。t3code就是我为了解决这个毛病用T3 Stack从零搭出来的一个代码片段管理工具。T3 Stack指的是Next.js TypeScript Tailwind CSS tRPC Prisma NextAuth.js这一套全家桶而t3code的核心功能很纯粹快速记录一段代码、按语言筛选、全文搜索、以及给每个片段加一条权限边界。如果你正在研究T3 Stack的全链路写法或者正准备找一个能落地的全栈项目练手这篇复盘应该能帮你省不少时间。1. 项目从哪来一个代码收藏强迫症的自救方案1.1 散落的代码到底有多难找写业务代码久了每个人都会积累大量片段工具函数、正则表达式、特定组件写法、SQL查询模板甚至还有一堆改了三版才定稿的配置项。这些片段通常散落在好几个地方——IDE自带的snippet、公司Wiki、个人博客、聊天记录有的还躺在旧电脑里没同步过。我统计过自己一个月内找代码的频次平均每天至少七次时间成本从几十秒到十几分钟不等。真正让人崩溃的是你明知道这段代码写过但就是想不起到底存哪了。这种状态持续到某次项目重构我需要找一个两年前写过的金额格式化函数。先翻了VS Code的snippet没有再搜微信收藏一片混乱最后在GitHub某个私有仓库里找到了——光是定位这一步就花了半个小时。当时我就决定必须给自己做一个集中管理代码片段的小工具。t3code这个项目名其实就是取T3 Stack的T3加上code代码片段的组合目标是让收藏代码像发微博一样简单让找代码像用搜索引擎一样快。1.2 需求拆解我对t3code的四个硬性要求因为这毕竟是个人的效率工具我一开始就不打算做一个功能庞杂的产品而是把需求收敛成四个硬性要求记录成本要低从打开页面到存下一个片段最多不能超过三次点击。如果保存要填一堆表单我用两次就会放弃。检索要快可以先按语言过滤再在标题和代码内容里做模糊搜索。能不能全文检索是核心体验分水岭。语法高亮要顺眼作为一个每天跟代码打交道的人看纯黑色代码块是一种折磨。高亮风格最好接近VS Code。数据要私有可控至少能做到只有登录用户本人能看、能改、能删不能像某些公开paste服务一样所有人都能扫到你的私有片段。这四个需求听起来简单但放到技术选型里就会牵扯出一堆问题。比如只有登录用户本人可见意味着必须做认证系统全文搜索意味着数据库查询不能只做等值匹配。我评估了几个方案最后发现T3 Stack几乎是为这种个人全栈项目量身定做的。1.3 为什么偏偏选T3 Stack做选型时我认真对比过三套方案。第一套是传统的React Express REST API。这套最熟但痛点也很明显前端定义一个接口类型后端又要重复定义一遍改字段时经常两边不同步等运行时报错才发现。对于个人项目来说维护这种隐形的契约关系很消耗精力。第二套是GraphQL。类型安全解决了但引入了schema定义、resolver、codegen等一系列额外概念对一个小工具来说太重了。杀鸡用牛刀维护成本甚至比REST还高。第三套就是T3 Stack。核心思路是端到端类型安全用tRPC把前端调用和后端实现直接连起来函数的入参和返回值类型会自动从后端推导到前端。改一个后端字段前端编译立刻报错根本不用等联调。对比项React Express RESTGraphQLT3 Stack类型安全需手写重复类型需要schema和codegen自动推导全链路覆盖学习成本低高中等维护成本前后端契约靠自觉额外概念多编译器盯着你适合场景团队传统项目多客户端复杂查询个人全栈/快速原型一句话总结tRPC把前后端的数据契约变成了编译期的检查这对一个没有专职接口文档的私人项目来说太重要了。再加上create-t3-app这个官方脚手架几分钟就能把项目骨架拉起来我几乎没有犹豫就选了这条路。2. 脚手架实战create-t3-app的安装与项目结构2.1 初始化命令与交互选项我用的是pnpm初始化命令是pnpm create t3-applatest命令跑起来之后CLI会问你项目名我输入t3code。接下来是一串交互式选项包括要不要启用Next.js、TypeScript、Tailwind CSS、tRPC、Prisma、NextAuth.js、ESLint、Prettier这些模块。我当时的选择是全部启用。为什么全选我的想法很直接既然这个项目的目标就是体验T3 Stack全链路那先让每个模块都跑起来看看它们怎么咬合比一开始就精简配置更有参考价值。实际用下来每个模块都有它存在的意义没有一个让人觉得纯属凑数。比如ESLint和Prettier虽然平时觉得烦但在一个人写代码的时候确实是保证质量下限的底线。有一点需要提醒初始化结束后CLI会自动运行prisma db push并尝试连接数据库。如果你还没准备好PostgreSQL这一步会卡住。我当时的处理是先随便填一个数据库URL让流程继续之后再去单独配置。2.2 Prisma数据模型从Snippet实体开始作为整个项目的数据核心Snippet模型我第一版设计成这样model Snippet { id String id default(cuid()) title String content String db.Text language String default(plaintext) tags String[] default([]) createdAt DateTime default(now()) updatedAt DateTime updatedAt author User relation(Snippets, fields: [authorId], references: [id], onDelete: Cascade) authorId String }字段有几个地方值得细说。language字段为什么用String而不是enum因为编程语言实在太多了而且新的语言还在不断冒出来用enum意味着每次遇到新语言都要改数据库再迁移一次很烦。用字符串虽然少了点数据库层面的约束但配合前端一个select下拉框已经能覆盖绝大多数场景还能随时加自定义值。tags字段为什么用String[]而不是单独的标签表初版需求里我只需要给片段打标签不需要维护标签的颜色、描述、关联文章这些额外属性。PostgreSQL原生支持数组类型Prisma映射得也干净。如果以后要做按标签统计数量或者标签管理页面再来拆表也不迟。在项目初期把不必要的关系表砍掉能让CRUD简单一大截。2.3 目录结构tRPC的代码到底放在哪create-t3-app生成的项目结构里和tRPC相关的核心文件都在src/server目录下src/server/api/root.ts组装所有子路由的总入口src/server/api/routers/具体业务路由比如snippet.tssrc/server/api/trpc.tstRPC实例、上下文、procedure定义src/server/db.tsPrisma客户端实例我第一次看这个结构时有个疑问为什么tRPC的接口层不叫controllers或者api后来理解了tRPC的哲学里没有独立接口层每个Router就是一个后端功能模块它直接导出函数给前端调用。这种后端函数即API的思路在个人全栈项目里特别舒服——不用为每个接口单独写路由路径、HTTP方法、请求参数解析一切都交给tRPC框架处理。前端调用侧也有讲究。App Router下tRPC的客户端被分成两份一份给Server Component用路径是src/trpc/server.ts一份给Client Component用路径是src/trpc/react.tsx。两者都是基于同一个AppRouter类型生成的所以不管在哪一层调用类型检查都完整存在。3. 核心功能实现从表单到查询的完整链路3.1 用tRPC写第一个snippet接口业务代码从snippetRouter开始我的第一版实现了五个方法创建、列表、详情、更新、删除。核心代码长这样import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ../trpc; export const snippetRouter createTRPCRouter({ create: protectedProcedure .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()).default([]), })) .mutation(async ({ ctx, input }) { return ctx.db.snippet.create({ data: { ...input, authorId: ctx.session.user.id, }, }); }), list: protectedProcedure .input(z.object({ language: z.string().optional(), query: z.string().optional(), })) .query(async ({ ctx, input }) { return ctx.db.snippet.findMany({ where: { authorId: ctx.session.user.id, ...(input.language ? { language: input.language } : {}), ...(input.query ? { OR: [ { title: { contains: input.query, mode: insensitive } }, { content: { contains: input.query, mode: insensitive } }, ], } : {}), }, orderBy: { createdAt: desc }, take: 50, }); }), });这里有两个关键点。第一protectedProcedure和publicProcedure的区别。protectedProcedure会自动检查当前请求有没有登录态的session没有就直接抛401不用我在每个方法里写if判断。这是T3 Stack官方模板在trpc.ts里封装好的能力非常实用。第二zod校验器的摆放位置。tRPC的input校验是运行在服务端的但它提供了完整的TypeScript类型推导。前端表单传过来一个多余字段或错误类型还没进数据库就被拦下了这在项目维护期能省下大量排查接口参数不一致的时间。3.2 前端调用react-query与useMutation的体感t3code的前端表单页直接用tRPC提供的React Hooks。比如新增片段的提交函数长这样const utils trpc.useUtils(); const createSnippet trpc.snippet.create.useMutation({ onSuccess: () { utils.snippet.list.invalidate(); router.push(/); }, }); function handleSubmit(data: SnippetFormValues) { createSnippet.mutate(data); }用起来的体感是create.mutate(data)调用后如果后端报错前端这里会第一时间拿到结构化的错误信息如果成功手动invalidate一下列表查询页面数据就自动刷新了。整个过程没有任何手写的fetch、axios、错误码映射也没有联调接口的文档沟通成本。useQuery侧更简单列表页直接这样const { data, isLoading } trpc.snippet.list.useQuery({ language, query });数据加载状态、缓存、重新获取全都由react-query管理。对于个人项目少写几十行状态管理代码是实实在在的好处。3.3 语法高亮我最终选了Shiki语法高亮组件我不止试了一个简单列一下踩过的情况。react-syntax-highlighter最容易上手但它是基于highlight.js的语言包体积不小而且高亮样式偏网页感和VS Code的观感有明显差距。Monaco Editor功能最强但它是给IDE用的光JS体积就快1MB加载一个只读的代码展示页过于奢侈。CodeMirror 6可定制性强但要配置一堆扩展包初版学习成本不低。最后我选了Shiki。它的核心优势有三个基于TextMate语法文件高亮结果和VS Code是同一套配色逻辑视觉上零违和感。可以只在服务端调用codeToHtml直接生成带高亮class的静态HTML字符串浏览器端不再加载几十万行的语法解析器。按需加载语言包我常用的JavaScript、TypeScript、Python、SQL、JSON这些加起来体积很小。t3code的详情页阅读量最大我用的是服务端渲染方案。做法很简单直接在服务端调用codeToHtml然后把生成的HTML用dangerouslySetInnerHTML插入页面。这样高亮工作完全发生在构建期或服务端客户端拿到的就是纯HTML首屏性能很理想。import { codeToHtml } from shiki; async function SnippetViewer({ code, language }: { code: string; language: string }) { const html await codeToHtml(code, { lang: language, theme: github-dark-default, }); return div dangerouslySetInnerHTML{{ __html: html }} /; }需要注意的是language的值必须能被Shiki识别。如果你的片段是纯文本或者Shiki没有对应的语言包要加一个fallback否则会直接渲染失败。3.4 搜索和筛选Prisma查询怎么写才够用搜索功能最关键的是where条件。我的实现思路是语言筛选用精确匹配关键字搜索则同时匹配标题和内容。Prisma的contains默认是区分大小写的所以加上mode: insensitive让搜索更宽容一些用户输入tRPC和trpc都能命中。这个方案在数据量不大的时候完全够用。我预判个人片段的量级在几千条以内contains走数据库扫描没任何压力。但如果以后数据量到十万条级别或者需要支持中文分词、按相关度排序那就要认真考虑Postgres的tsvector全文搜索或者上Meilisearch这类外部搜索引擎了。我的原则是第一版不做过度设计等真实数据把这个方案压垮了再升级——而且到时候迁移路径也清晰Prisma的raw查询可以平滑过渡到tsvector。列表页的交互上我加了一个小细节语言筛选和后端查询联动但搜索关键字用防抖。用户输入时等300毫秒再发请求既不会每敲一个字母就请求一次又不会觉得搜索卡顿。前端体验上这个300毫秒的取舍很重要。4. 认证和权限让t3code记住是谁在登录4.1 GitHub OAuth的接入细节NextAuth.js是T3 Stack认证层的默认选项。我选择GitHub作为唯一登录方式因为个人项目大头就是自己用GitHub账号也不需要额外注册新东西。接入流程分三步。第一步去GitHub Developer Settings里创建一个OAuth App。填Homepage URL和Authorization callback URL时回调地址必须是https://你的域名/api/auth/callback/github这是最容易填错的地方。我见过不少人把回调地址写成了首页根路径结果登录后NextAuth一直在回调里打转。第二步把生成的Client ID和Client Secret放进.envGITHUB_ID你的_Client_ID GITHUB_SECRET你的_Client_Secret NEXTAUTH_SECRET一串随机字符串 NEXTAUTH_URLhttp://localhost:3000NEXTAUTH_SECRET必须设置这个字段是给session cookie做签名的。如果你不设置NextAuth会抛警告生产环境会直接拒绝登录。生成随机串可以用openssl rand -base64 32。第三步在src/server/auth.ts里配置providers: [GitHubProvider]。模板默认已经写好了只需要把环境变量接上。比较贴心的是T3 Stack模板里的session策略默认是jwt也就是说用户信息是编码在cookie里的不需要额外建Session表对个人项目来说省了一次数据库查询。4.2 tRPC上下文把session塞到每个请求里认证和业务API之间能不能无缝衔接取决于tRPC的上下文。在src/server/api/trpc.ts里模板已经写好了一个createTRPCContext函数它会从请求头里拿到NextAuth的session对象并挂到上下文中。让我把官方模板的逻辑简化说明一下每次tRPC请求进来都会先执行getServerSession拿到当前登录用户。然后业务代码里就可以通过ctx.session.user访问当前用户信息。protectedProcedure正是依赖这个数值来判断是否放行——如果session为空直接抛401错误。这里的闭环是GitHub登录成功 → NextAuth生成JWT cookie → 浏览器后续请求自动携带cookie → tRPC上下文解析出session → 业务代码知道当前是谁。整条链路不需要手写token分发和刷新逻辑对个人项目来说省了很大一块工作量。4.3 删除接口的权限校验别只在前端隐藏按钮在t3code里最让我警惕的安全点是资源归属校验。很多人做个人工具时会想前端已经把删除按钮隐藏了坏人应该点不到吧这个想法在真实互联网环境下非常危险。因为API接口是公开的别人完全可以直接向你的接口发请求绕过前端界面。我的做法是在删除和更新操作里where条件必须同时包含id和authorIddelete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const result await ctx.db.snippet.deleteMany({ where: { id: input.id, authorId: ctx.session.user.id, }, }); if (result.count 0) { throw new Error(片段不存在或无权操作); } return { success: true }; }),用deleteMany而不是delete有个好处当传入的id不属于当前用户时删除影响行数为0我们把它当成不存在处理即可不会抛数据库层的外键或记录不存在错误。这种设计即使用户猜到了别人的片段ID也无法越权操作。这个习惯从t3code开始现在已经变成我写所有全栈项目的基本要求任何资源操作无论查询、更新还是删除只要资源有归属概念都要在数据库层把归属条件一起带上而不是相信前端传来的userId参数。5. 部署与实测Vercel Neon PostgreSQL5.1 数据库与环境变量准备本地开发阶段我用Docker跑了一个PostgreSQL实例。部署到线上时数据库我选了Neon原因是它提供免费的serverless PostgreSQL并且支持分支数据库和自动休眠个人项目的量级完全够用也不用自己维护数据库服务器。把线上数据库连接串准备好之后有几件必须做的事在本地先执行prisma migrate dev生成迁移文件再执行prisma migrate deploy在线上库跑一遍迁移。设置环境变量时数据库连接串要加上?sslmoderequire确保是加密连接。Vercel的Serverless函数和Neon之间走的是公网不加密等于裸奔。NEXTAUTH_URL要改成线上域名。我一开始忘了改导致线上OAuth回调一直跳回localhost登录永远失败。环境变量清单大致如下变量作用必填DATABASE_URLPrisma连接生产PostgreSQL是NEXTAUTH_URLNextAuth回调的基准地址是NEXTAUTH_SECRETsession cookie签名密钥是GITHUB_ID/GITHUB_SECRETGitHub OAuth应用凭证是5.2 构建与性能数据部署完成后我做了两轮实测。第一轮是next build的产物分析。因为详情页的语法高亮是走服务端Shiki的客户端JavaScript被控制在很小的范围主要包来自React和tRPC客户端。整个首页的JavaScript资源压缩后大约90KB对于这种交互型个人工具来说已经很轻了。第二轮是用Lighthouse跑性能结果分数在手机上依然很稳Performance 98、Accessibility 100、Best Practices 100、SEO 100。T3 Stack默认带的Tailwind CSS在样式体积控制上立功不小全站没有引入任何UI组件库写了一部分自定义组件整体样式文件不到20KB。接口延迟方面在冷启动状态下tRPC列表接口大约耗时120ms其中大部分花在Serverless函数的冷启动上。热启动后接口响应基本稳定在30ms以内。对个人工具来说完全够用我甚至没做任何缓存优化。如果哪天觉得慢了优先考虑在react-query里加staleTime让列表数据一分钟内不重复请求。5.3 部署中踩过的坑最后集中说说部署时踩过的坑希望能帮你绕开。第一个坑是NEXTAUTH_URL忘记改上面提过。症状是点击登录后跳转到GitHubGitHub授权成功后又跳回localhost:3000自然打不开。解决方式就是把它改成线上域名并重启Vercel实例让环境变量生效。第二个坑是Prisma Client版本不一致。本地用的Prisma版本和Vercel构建时安装的版本不一致会导致线上运行时出现PrismaClientInitializationError报错信息指向generated client版本冲突。解决方法是锁死版本在package.json里用精确版本号并让Vercel构建命令里包含prisma generate。用pnpm的话还要注意如果锁文件里有多个Prisma版本很容易出现这个情况建议删除node_modules和锁文件后重新pnpm install。第三个坑是Neon的连接池问题。Serverless环境下如果每个请求都新建数据库连接很快会打满Neon的连接数上限。Prisma官方推荐在Serverless环境启用连接池模式我最终在DATABASE_URL里开启了PgBouncer兼容的连接串同时在Prisma schema里设置了connection_limit 10。调整之后连续刷新几十次页面也没有再报连接数超限。这些坑单看都不复杂但每个都能让上线时间往后推半天。提前知道至少能少折腾一会儿。t3code这个项目给我的最大收获不是做出了一个多好用的工具而是让我重新理解了一遍T3 Stack每一块组件在真实项目里是怎么咬合的。如果你也想做一个类似的自用工具我强烈建议第一版别贪多先把记录-检索-展示-权限这条主链路跑通。等它真正用起来了再去想AI自动补标签、浏览器插件快捷保存、标签管理这些锦上添花的功能也不迟。工具是给自己用的好用比好看重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑