搭建t3code的起因其实很朴素就是我自己存代码片段的方式太乱了。本地备忘录、微信收藏、仓库里一个叫notes的文件夹到处都是东一句西一段的代码。真要找某个函数的写法翻半天还不一定找得到。后来我干脆做了个专门的项目把它取名叫t3code核心思路就一句话用TypeScript全链路统一管理代码片段让写过的代码随时能搜回来、看得懂、用得顺手。t3code并不是一个商业项目最初只是我个人的效率工具。但我把它的架构、数据模型、还有踩过的坑都梳理了一遍之后发现这套东西非常适合分享给两类人一类是想学习T3技术栈Next.js tRPC Tailwind Prisma全流程开发的同学另一类是跟我一样受够了零散代码片段、想自己动手做个工具的开发者。本文不会讲太多花哨的概念我尽量还原当时的真实决策过程包括为什么选tRPC、为什么用Monaco编辑器、部署时碰到Prisma连接池相关问题怎么处理等等。1. 整体设计思路先想清楚要解决什么问题1.1 重新审视“存代码”这件事你可能觉得存代码片段不就是复制粘贴吗有什么好设计的。但真的随手存过半年以上代码的人大概率遇到过这三类问题第一内容太碎片。今天存一段React hooks明天存一个Linux命令后天又放了一段Python爬虫脚本。它们混在一起的时候你根本不知道该去哪里翻。第二上下文信息缺失。光秃秃一段代码放那儿一周之后再看完全不记得这段代码当时是用在什么场景里的。没有描述、没有标签、没有关联项目代码就失去了复用的价值。第三搜索能力几乎为零。系统备忘录或者普通文本文件自带的搜索只能做关键词匹配搜个“date format”可能把一堆不相关的东西都翻出来但搜“React date format”就什么都找不到了。所以t3code的设计目标非常明确一个结构化的、支持多维筛选、能够保留上下文的代码片段管理工具。它不仅是存储更是知识库。1.2 为什么选择T3 Stack而不是自己拼一套我一开始也犹豫过是不是直接用Express MongoDB Vue组一套就行。后来认真列了一下需求发现这个项目需要的是类型安全、开发效率高、前后端共享逻辑、部署简单。这套需求几乎就是T3 Stack的完美匹配对象。T3 Stack由Next.js、tRPC、Tailwind、Prisma组成。最吸引我的点是tRPC。它跟REST和GraphQL都不一样不需要写路由规范、不需要手动定义API文档、不需要前后端各维护一套类型定义。只要在服务端定义一个procedure前端直接调用类型是自动推断的。这带来的体验太爽了写代码的时候前端哪里写错了TypeScript编译器当场报错根本不需要跑一遍接口才知道返回结构不对。有人会问为什么不用GraphQLGraphQL确实灵活但你需要维护schema、resolver、fragment等一系列概念对于t3code这种体量的工具引入GraphQL等于给一个工具箱配了一台起重机。tRPC在keep类型安全的同时心智负担要小很多。1.3 设计原则类型即契约筛选即体验整个项目我给自己定了三条原则后面所有功能模块都是围绕着它们展开的。第一条一切类型共享。数据模型在服务端用Prisma定义然后通过tRPC导出给前端使用。这样任何字段变更编译期就能发现所有受影响的地方。第二条筛选比搜索更重要。大多数代码片段工具把重心放在搜索框上但实际使用中我更多是带着“某种语言”、“某个标签”、“某个项目”这种意图来的所以t3code把标签筛选和语言筛选做成了主导航的一部分全文搜索反而只是辅助。第三条代码编辑体验优先。既然存的是代码那么查看代码的时候至少要保证有合适的语法高亮。后来我不仅让片段以代码块展示还引入了一个嵌入式的代码编辑器方便快速修改和复制这些细节下面会细说。2. 核心功能解析存储、检索与编辑怎么写才顺手2.1 数据模型Fragment才是基本单位设计数据模型的时候我没有直接用Snippet这个单词而是起了个Fragment。因为Snippet在开发者认知里通常是”一小段零碎的代码“但我需要表达的是”一段有上下文、有归属、可引用的内容“。Fragment表的核心字段包括id、title、description、content、language、visibility、authorId、tags关联、project关联、createdAt、updatedAt。language字段不能是普通字符串我使用了枚举类型这样前端筛选时才能拿到正确的选项列表而不是靠用户自由输入导致”javascript“和”JavaScript“并存的情况。标签和片段的关系是多对多。因为一段代码可能既属于”React“又属于”Hooks“还可能属于”性能优化“。如果强行做成一对多每次存代码都要纠结主分类是什么时间成本太高。而Project是可选的关联对象主要用来表示”这段代码是t3code项目里的“方便按项目维度聚合。我把description字段设计的比一般工具更大允许写比较长的背景说明。很多片段工具只给一行简介实际使用中根本不够。我们在三个月后回看一段代码时最需要的不是代码本身而是那段”为什么当时要这么写“的上下文。2.2 认证体系NextAuth的取舍t3code需要一个用户隔离机制因为有些代码片段涉及公司业务或者隐私逻辑不能直接公开浏览。我最终选择了NextAuth.js原因很简单它跟Next.js集成最顺支持GitHub和Email两种登录方式。GitHub登录对于开发者场景非常自然而Email登录则是给偶尔不上GitHub的用户准备的。配置上有几个需要特别注意的地方。首先是Secret环境变量生产环境下如果没有正确设置NEXTAUTH_SECRET用户登录状态会随机失效表现形式就是”上午还能登录下午就不认识了“排查起来很隐蔽。其次是回调地址如果你在Vercel部署必须把站点的正式域名加到NextAuth的authorized redirect URIs里否则GitHub登录会报redirect_uri错误。认证信息挂载到tRPC上下文后所有需要登录的procedure都能通过ctx.session.user.id来识别当前用户。查询时强制加上authorId过滤条件保证用户只能看到自己的私有片段。这个过滤条件必须是服务端强制校验不能依赖前端传参否则改个请求参数就能看到别人数据了。2.3 编辑器选型Monaco还是CodeMirror如果你只是展示代码一个语法高亮库就够了但t3code还允许直接编辑已有片段所以编辑器引擎的选择就摆上了桌面。我对比了Monaco Editor和CodeMirror 6。Monaco是VS Code的编辑器内核特点是功能极其丰富智能提示、断点调试支持、Command Palette、多光标编辑都有。CodeMirror 6则轻量得多包体积更小定制能力强移动端体验好得多。最终我选的是Monaco。理由有两个第一t3code的核心用户群体是开发者他们早就习惯了VS Code风格的交互Monaco能提供一种”不用学就会用“的熟悉感。第二代码片段的编辑场景虽然轻但需求并不少比如格式化、缩放、搜索替换Monaco全都原生支持。缺点是首包体积大全套加载完可能有几MB但我用动态加载的方式解决只有在用户打开编辑页面时才加载Monaco列表页和详情页都不受影响。2.4 多维筛选标签、语言、项目三轴联动t3code的筛选界面没有走大而全的搜索框路线而是搞了三轴联动顶部的筛选栏显示全部语言列表侧边栏显示标签云每个片段卡片下方显示所属项目。三个筛选条件的关系是叠加过滤。比如我可以选LanguageTypeScript同时点标签Hooks再限定Projectt3code剩下的就是当前项目里所有带Hooks标签的TypeScript片段。这种组合式筛选远比单一搜索框精确而且每次点击都有即时反馈。后端实现上我使用了Prisma的where组合查询。language是枚举等值匹配tags是包含匹配project是外键等值匹配。前端通过tRPC传入一个filter对象类型是Prisma自动生成的WhereInput这样既限制可传入字段又保证所有条件都是类型安全的。3. 实操过程从初始化到部署的全流程记录3.1 初始化项目create-t3-app的参数选择创建项目用的是标准命令但有几个选项需要根据自己的需求选不是无脑下一步。我在初始化t3code时选择了Next.js App Router、TypeScript、Tailwind、Prisma、NextAuth、tRPC。后面那个ESLint和dotenv选项也顺手选上。以下是实际执行的命令和关键回填内容npm create t3-applatest t3code交互中需要选择的几个点是否启用TypeScript这个项目必须选Yes因为tRPC的前提就是TypeScript。是否使用Tailwind选Yes。样式层面我确实从零手写过不少自定义CSS但布局、颜色、间距等基础部分用了Tailwind省了很多事。是否启用Prisma选Yes。是否启用NextAuth选Yes。是否启用tRPC选Yes。这里有一个容易踩的坑如果你不需要App Router或者刚好需要的是Pages Router创建时要注意版本提示。T3 Stack近两年的新项目默认都是App RouterPages Router的老教程已经不适用了照着旧教程嵌代码会出现布局命名不一致的问题。项目生成完后先跑一次npm run dev确认基础页面能开然后立刻git初始化并提交一个初始commit。这样做是为了后续改动时能随时回到一个干净基线。3.2 数据库设计与Prisma迁移数据库用的PlanetScale一开始我试过但马上发现它对于外键约束支持不全。后来就换了普通的MySQL托管实例本地开发用的是SQLite。实际部署用的还是PostgreSQL。这里说明一下Prisma对不同数据库的支持程度有细微差异开发阶段用SQLite提速生产用PostgreSQL只要schema里没有用数据库专属类型切换成本基本为零。初始迁移脚本我分成了四步走。第一步定义基础枚举enum Language { TYPESCRIPT JAVASCRIPT PYTHON GO RUST SHELL SQL HTML CSS JSON OTHER }第二步定义Fragment模型model Fragment { id String id default(cuid()) title String description String default() content String db.Text language Language default(OTHER) visibility Visibility default(PRIVATE) createdAt DateTime default(now()) updatedAt DateTime updatedAt author User relation(fields: [authorId], references: [id], onDelete: Cascade) authorId String tags Tag[] project Project? relation(fields: [projectId], references: [id], onDelete: SetNull) projectId String? }第三步定义Tag和Project模型Tag和Fragment之间用隐式多对多关系。第四步执行npx prisma migrate dev。设好Schema后会有个常见问题浮出来cuid还是uuidt3code里我选了cuid。原因很简单片段ID会被用在URL里cuid比uuid短而且不需要像自增ID那样暴露数据规模。还有一个实际原因cuid是生成友好的不需要等数据库返回适合需要离线生成ID的场景。3.3 tRPC路由设计像写函数一样写接口tRPC路由是t3code里最让人舒服的部分。不需要单独定义DTO不需要维护openapi schema每个procedure就是一段函数。我按领域划分子路由fragmentRouter、tagRouter、projectRouter、authRouter。以fragmentRouter为例核心方法包括export const fragmentRouter createTRPCRouter({ list: protectedProcedure .input( z.object({ language: z.nativeEnum(Language).optional(), tagId: z.string().optional(), projectId: z.string().optional(), keyword: z.string().optional(), }) ) .query(async ({ ctx, input }) { return ctx.db.fragment.findMany({ where: { authorId: ctx.session.user.id, language: input.language, tags: input.tagId ? { some: { id: input.tagId } } : undefined, projectId: input.projectId, OR: input.keyword ? [ { title: { contains: input.keyword } }, { content: { contains: input.keyword } }, ] : undefined, }, include: { tags: true, project: true }, orderBy: { updatedAt: desc }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), description: z.string().optional(), content: z.string().min(1), language: z.nativeEnum(Language), tagIds: z.array(z.string()).optional(), projectId: z.string().optional(), }) ) .mutation(async ({ ctx, input }) { return ctx.db.fragment.create({ data: { authorId: ctx.session.user.id, title: input.title, description: input.description ?? , content: input.content, language: input.language, tags: input.tagIds ? { connect: input.tagIds.map((id) ({ id })) } : undefined, projectId: input.projectId, }, }); }), });值得注意的是list方法里每个字段都做了undefined处理。因为Prisma的WhereInput有个特性传undefined的字段会自动忽略所以用这种形式可以写出既安全又简洁的组合条件。前端的调用方式更加直接const listQuery api.fragment.list.useQuery({ language: selectedLanguage, tagId: selectedTagId ?? undefined, });这就是tRPC的类型魔力后端接受什么参数、返回什么结构前端全都知道。连tagId拼错都会被TypeScript揪出来。3.4 前端页面组织列表页、详情页与编辑页t3code前端主要由四个页面组成。首页是片段列表左侧标签栏顶部语言筛选中间内容是片段卡片流。每张卡片上展示标题、语言标识、描述摘要、标签列表和更新时间。再往下是详情页点进卡片之后能看到完整内容Monaco编辑器直接以可编辑形态载入右上角是保存按钮。然后是新建页跟编辑页基本共用同一个表单组件只是初始值不同。最后是项目页按项目维度列出所有片段施工的时候用来查某个技术方案落地情况非常高效。页面交互上有一个关键体验点筛选条件变了之后列表要无缝刷新而不是跳转页面。tRPC的useQuery本身支持根据输入参数自动重新请求所以我只需要把筛选状态提升到页面顶层传给query参数页面就会自动更新数据。React的useState存筛选条件选中后setState触发重取。这里需要额外注意的是不要每次都把筛选状态重置否则用户切换页面时会丢掉筛选上下文。用useState初始化后配合useRouter的query参数做到URL同步这样用户刷新页面后筛选条件也不会丢。3.5 部署实践Vercel PostgreSQL的组合前端部分部署在Vercel数据库用托管PostgreSQL。这个组合是目前最省心的搭配。部署前要处理好几个环境变量DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL以及GitHub OAuth的两个key。部署时最典型的问题是Prisma客户端没有正确生成。有些部署平台在构建阶段跑的prisma generate与生产环境的二进制文件不匹配会报PrismaClientInitializationError。解决方式是在package.json的build脚本里显式加上build: prisma generate next build另外一个关于数据库连接的坑如果你的数据库提供方限制了连接数而Vercel又默认开启了vercel-postgres的connection pool可能出现连接数爆掉的情况。跑定时任务迁移或多次频繁构建时尤其常见。最终我把连接字符串单独走了directUrl让Prisma的migrate使用直连而应用运行时走池化连接。这个配置字段写在prisma/schema.prisma里的datasource块中datasource db { provider postgresql url env(DATABASE_URL) directUrl env(DIRECT_URL) }3.6 性能优化Monaco动态加载与列表分页Monaco重这是事实。解决办法是只在实际需要编辑时加载用Next.js的dynamic函数const MonacoEditor dynamic( () import(monaco-editor/react), { ssr: false, loading: () p编辑器加载中.../p } );srr:false很重要因为Monaco依赖window对象服务端渲染时会直接报错。列表页不加载Monaco这样首页白屏时间不会受编辑器体积拖累。分页方面t3code每页显示20条片段超过20条显示“加载更多”按钮。实现方式是基于cursor的分页不是传统页码分页。因为cursor分页更稳定在数据集动态变化时页码会漂移cursor不会。接口改造时只要list方法增加一个cursor参数配合Prisma的cursor与skip即可const items await ctx.db.fragment.findMany({ take: 20, skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, ... });4. 常见问题与排查技巧实录整个开发和上线过程中踩过的坑确实不少。挑几个最有代表性的记下来都是真实发生的问题排查过程和解决方式都比较典型。4.1 tRPC类型不更新的问题症状是后端新增了一个procedure但前端的api对象里没有出现。最常见原因是Next.js的dev server没有重新编译tRPC类型包。解决方式简单粗暴重启dev server或者检查tsconfig里包含t3code中/server/api/root的路径是否正确。还有一次我改了输入schema里的一个字段名前端调用时编辑器没报错但运行时一直提示数据校验失败。后来发现是前端用了旧对象属性TypeScript只检查了类型没检查是否该属性真的在运行时存在。这个问题的根源在于zod的input类型和tRPC的推断类型是两层不能混着用。经验是改输入schema后一定清一下Next的缓存编译产物再重启避免顽固的.tsbuildinfo导致类型没有更新。4.2 Prisma Client在Vercel部署后连接被拒症状是本地跑得好好的部署上去后所有数据库查询都报Cant reach database server。排查步骤先看环境变量有没有配置上去再看数据库白名单有没有放行Vercel的出口IP。这里有个很容易被忽略的点如果你的数据库托管商默认开启了TLS加密连接那你要确认Prisma的connection string里设置了正确的sslmoderequire参数否则握手阶段就会被拒。后来我做了个调整把健康检查接口直接暴露出来页面调一下就能看到数据库连接状态不用每次都进日志折腾。这个健康检查是通过一个特殊的tRPC procedure实现的返回{ status: ok, db: true }前端在页面底部显示一个小绿点很有安全感。4.3 NextAuth登录回调地址错误症状是用GitHub登录后浏览器地址栏开始跳转然后GitHub页面显示redirect_uri is not allowed。原因几乎都是用的环境变量没有设置AUTH_URL或NEXTAUTH_URL或者这个地址跟GitHub OAuth apps里填写的回调地址不一致。要注意GitHub OAuth App里需要填的Authorization callback URL并不是你的主页地址而是/api/auth/callback/github这个完整路径。在App Router版本的NextAuth配置里这里的字段名称有变化。旧教程里写的是NEXTAUTH_URL新版的推荐配置是AUTH_URL和AUTH_SECRET。如果你用的是NextAuth v5 beta那么需要按v5的env命名规范来。这个版本差异问题在README里只写了半句话但是排查起来却花了很久。4.4 Monaco在编辑时按CtrlS会刷新页面这是个比较弱智但确实会遇到的坑。浏览器默认的CtrlS是保存网页并不是保存你的代码。在Monaco编辑器上用户按下CtrlS想存片段时竟然把整个页面给刷新了草稿全丢。修复方式是在组件里加键盘事件监听useEffect(() { const handler (e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) e.key s) { e.preventDefault(); handleSave(); } }; window.addEventListener(keydown, handler); return () window.removeEventListener(keydown, handler); }, [handleSave]);其实还可以封装得更好一点比如只有Monaco有focus的时候才绑定快捷键不过考虑到t3code的页面结构直接全局绑定也问题不大。4.5 搜索中文时不准确MySQL和PostgreSQL对中文搜索的处理都不算理想。之前用了简单的contains这在小数据量下够用但数据一旦上千条中文分词的问题就暴露了。后来我的处理方案是标题和描述字段做关键词匹配content的全文搜索暂时不做太重的处理而是靠标签筛选兜底。这不是一个完美的方案但确实把一个复杂问题简化成了够用的状态。如果后续t3code的片段数量继续膨胀我会考虑把搜索改成SQLite的FTS5或者PostgreSQL的tsvector。但那属于另一个阶段的优化目标现在这个阶段不建议过度设计。5. 一些我后来才想明白的实践经验5.1 代码片段工具的核心不是存而是找如果只做一个能存代码的工具那和贴一堆文本到数据库里有什么区别一个片段管理工具体验好坏的分水岭完全在于能不能快速找到人想找的东西。t3code的标签设计、语言筛选、项目关联全部服务于“让用户在五秒内精确定位到需要的代码”这个目标。存的时候多一点结构化信息找的时候就能少很多翻来翻去的动作。5.2 设计标签体系的深度建议使用一段时间后我整理出了一套自己比较满意的标签规范按技术方向React、Vue、Node、Python、DevOps按用途utils、hooks、components、config、script按业务模块auth、payment、email、search按经验类型pitfall、trick、pattern这套规范的目的是保证每个维度都有标签但标签总量控制在可控范围内。当标签超过30个时我会做一次合并把语义太接近的标签合并掉。t3code的侧边栏里显示标签列表时会同步显示每个标签下的片段数量这样哪些标签已经成了垃圾标签一目了然也方便做清理。5.3 代码片段里的敏感信息处理这是最需要提醒的一点我差一点就栽在这里。早期我在t3code里存了一个包含数据库连接字符串的代码片段还把可见性设成了公开。后来发现已经有外部搜索索引收录了吓得赶紧改密码加删除记录。从那以后我给t3code加了一个客户端检查规则凡是内容里出现sk-、api_key、password、secret等关键特征的字符串保存时强制提醒让用户确认是否真的要存。同时所有包含疑似敏感信息的片段默认强制标记为私有不允许设置为公开。这个检查不是百分之百可靠但它多少能起到一层防护网的作用。任何工具只要跟代码相关敏感信息管理就永远不能偷懒。5.4 为什么叫t3code有人问过我这个名字的来历。T3本身在社区里有特定的技术含义也就是Theo发起的T3 Stack。t3code既是对这个技术栈的致敬也刚好体现了项目的核心一种围绕代码的、以TypeScript为第一公民的组织方式。有趣的是t3code这个命名天然带有一种“第三空间”的意味或者说它是工作区和记忆区之外的另一个与代码有关的私有空间。6. 写在最后的个人经验做完t3code之后我的一个独立体会是很多效率工具不需要去商店里买自己能做一个完全匹配自己工作流的版本即便丑一点也会比大而全的通用工具更好用。你不需要迎合别人的使用习惯只需要让自己的代码查找路径最短。如果你也想动手做类似的东西我的建议是从最小的可用版本开始不要一上来就做标签、项目、搜索、编辑器、团队协作全上。先做一个能存、能读、能删、能列出清单的基础版用上两周记录自己在哪里觉得别扭再去迭代。很多功能想的时候觉得重要真用起来觉得鸡肋反过来你以为不会用到的功能可能才是救命功能。t3code目前的扩展方向我想把Monaco的智能提示做得更深入一些比如支持根据语言加载对应语言的类型定义让片段编辑时也能获得接近IDE的补全体验。另外还在考虑做一个导入功能直接从代码仓库的注释标记里批量提取片段比如把snip注释块转为t3code的数据那样长期维护的成本会更低。最后分享一个在开发后期才意识到的小技巧给列表页加一个复制代码按钮这个按钮看起来一点都不起眼但却是整个项目被使用频率最高的功能。你花了大量精力做的编辑体验、标签筛选可能还不如一个普普通通的复制按钮在日活上的贡献大。这让我切实体会到用户真正要的往往不是花哨的功能而是流畅顺手的基本操作。