T3 Stack实战:从零构建代码片段收藏与管理工具

t3code 这个名字,一看就是冲着 T3 Stack 去的。T3 不是一个单独的框架,而是全栈开发里越来越流行的一套组合:Next.js、TypeScript、Tailwind CSS、tRPC,再搭配 Prisma 这类数据层工具。t3code 是我用这套技术栈从零做完的一个代码片段收藏与管理工具,核心就是把散落在各种聊天记录、本地笔记、Gist 里的代码统一收进一个地方,支持标签、搜索、语法高亮和快速复制。这篇文章不打算泛泛聊概念,我会从需求拆解、数据模型、tRPC 路由、代码高亮选型到部署上线,把整个项目的思考过程和实操细节完整记录下来。无论你是第一次接触 T3 Stack,还是已经写过敏捷全栈项目,想找一个能落地的练手参考,这篇都能给你一些直接能用的东西。

1. 项目拆解:t3code 想解决的痛点与设计方案

1.1 为什么需要自己做一个代码片段工具

做 t3code 之前,我一直被一个很常见的问题折磨:写的代码太多,但真正要复用的时候永远找不到。本地笔记软件里夹着几十段随手粘贴的代码,GitHub Gist 上也存了一部分,微信文件传输助手里还有一堆突然想到的片段。等到项目里要用某个算法、某段正则或者某个工具的配置,只能一个个地方翻,效率极低。

市面上的代码片段工具其实不少,比如很多开发者会直接用 VSCode 的 Snippets,或者用 Notion 建表格。但我的核心需求很具体:第一,必须支持标签和多维筛选,比如想找"React + 性能优化"相关的片段,能按复合条件筛出来;第二,代码要高亮展示,最好复制时能保留原始缩进;第三,要支持私有部署,代码放在自己的服务器上比较安心;第四,输入要足够快,最好能快捷键唤起。市面上的工具要么太笨重,要么数据不在自己手里,要么不支持精细的标签体系。综合评估下来,自己做一个小而美的工具是性价比最高的方案。

这个需求听起来简单,但真正做起来涉及的东西相当多:用户体系要不要做、权限怎么控制、搜索做到什么深度、代码编辑器集成到什么程度。一开始我就明确了两条原则:不追求大而全,只做自己日常真正会用到的东西;不过度设计,先跑通一个最小闭环,后续再迭代。t3code 最初的 MVP 只包含片段增删改查、标签管理和简单的关键词搜索,但正是这个清晰的范围让整个项目能在两三个周末内完成,并且一直稳定运行到现在。

1.2 T3 Stack 为什么适合这个场景

选 T3 Stack 不是追热点,而是这个组合恰好覆盖了 t3code 的全部技术需求。先看 Next.js:t3code 需要服务端渲染来保证首屏速度,也需要 API 的能力来支撑前后端交互,Next.js 的 App Router 把这两件事放在同一个框架里,不需要再单独搭一个后端服务。再看 TypeScript:代码片段管理工具本身就是给开发者用的,整个项目的数据结构、类型系统如果不严谨,后面维护起来会非常痛苦。TypeScript 让前端、后端、数据库查询的模型保持一致,改一个字段会立刻在编译期暴露所有受影响的代码。

t3code 里最有意思的部分是 tRPC。传统前后端接口方案需要写 REST 路由、定义请求响应类型、维护 API 文档,稍微复杂一点就会前后端类型不同步。tRPC 的思路是直接把服务端函数暴露给前端调用,类型定义从服务端一路传到客户端,不需要生成独立的类型包,也不需要手写接口文档。当我修改了 snippet 查询函数的返回结构,前端代码立刻会在编译阶段提示哪里用错了字段。这种端到端类型安全的体验,用过一次就很难回去。

Tailwind CSS 可能看起来只是一个样式工具,但对 t3code 这种工具型应用来说价值很大:它不限制你用组件库,可以快速实现代码编辑器面板、标签气泡、列表视图这些自定义 UI。Prisma 则负责数据层,用 schema 定义 Snippet 和 Tag 的关系,迁移、查询都写得非常顺手。整套技术栈有一个共同特征——足够现代、类型安全、开发体验好,正好匹配"一个人快速做出一个高质量工具"的目标。

1.3 功能边界与整体架构

t3code 的整体架构其实不复杂:Next.js 作为应用入口,服务端组件负责页面渲染,客户端组件负责编辑器和搜索交互;tRPC 承担所有数据查询和变更;Prisma 连接 PostgreSQL 数据库;认证部分用 NextAuth 接入 GitHub 登录。数据流向是单向的,用户操作先到 Next.js 页面,页面通过 tRPC 调用服务端函数,服务端函数再通过 Prisma 操作数据库,响应原路返回。

从功能上看,我把它分成三层。基础层是片段管理:新增、编辑、删除、复制,片段内容包括代码正文、编程语言标识、标题和描述,这个不用多说。管理层是标签和搜索:每个片段可以有多个标签,标签本身独立维护;搜索支持按标题、描述、代码内容三个维度进行,也可以组合标签过滤。体验层是代码展示:支持常见语言的高亮、主题切换、一键复制、行号显示,这些细节决定了工具日常用起来是否顺手。

我刻意没有做的功能也值得说一说:没有做权限细分,只分登录用户和匿名用户;没有做片段分享链接,因为自己用的场景用不上;没有做 WebDAV 同步,数据直接存在数据库里。这样做的原因是,任何功能都有维护成本,与其做一堆"看起来有用"的功能,不如把核心链路打磨好用。如果后续真的需要分享功能,再单独加也来得及。

2. 核心功能与关键实现

2.1 数据模型设计:Snippet、Tag 与用户的关系

数据模型是整个 t3code 的地基,我花了不少时间在设计 Prisma schema 这一步。代码片段最基本的结构是 Snippet 模型,包含 id、标题、代码内容、编程语言、创建时间、更新时间。标题和代码内容用字符串存储,编程语言用默认值,这样从编辑器粘贴代码时不用每次都手动选一次语言。

标签模型看起来简单,但关系处理容易踩坑。一个片段可以有多个标签,一个标签下可以挂多个片段,这是典型的多对多关系。Prisma 里我用了隐式多对多表,没有在产品代码里直接操作关系表,而是通过snippet.tags和tag.snippets访问关联数据。用隐式关系的好处是查询代码非常简洁,生成迁移时自动帮你建好中间表,避免了手写联表查询的繁琐。

用户模型和片段之间是一对多关系:一个用户拥有多个片段。这里我做了个实用性的选择——默认不提供匿名创建功能,所有片段都必须关联到登录用户。虽然会增加登录环节的摩擦,但换来的是数据隔离的清晰:每个用户只能看到自己的片段,不涉及复杂的权限判断,查询 where 条件直接写userId: session.user.id就完事。

schema 里面还有一个容易被忽视但很重要的细节:数据库索引。刚开始我只给 Snippet 建了主键索引,结果片段数量到几千条之后,按标题搜索和按标签过滤明显变慢。后来给language、createdAt和Tag.name都加了索引,大部分查询都能走索引,体验差距非常大。Prisma schema 里定义索引的方式也简单:

model Snippet { id String @id @default(cuid()) title String content String @db.Text language String @default("plaintext") isPublic Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) tags Tag[] @relation("SnippetTags") @@index([userId, createdAt]) @@index([language]) } model Tag { id String @id @default(cuid()) name String snippets Snippet[] @relation("SnippetTags") @@unique([name]) }

这里我特别说明一个设计取舍:isPublic字段虽然是随手加上的,但它是后续做分享链接功能的关键,默认 false 也符合私有工具的定位。onDelete: Cascade表示用户注销时,其所有片段会自动删除,避免留下孤儿数据。这些看似无关紧要的细节,在数据量变大后都会变成维护成本的一部分。

2.2 tRPC 路由与类型安全实践

t3code 的 API 层全部由 tRPC 承担,如果你第一次接触 tRPC,可以把它理解成"像调用本地函数一样调用远程函数"。不用写 REST URL,不用手工维护接口文档,函数定义在服务端,客户端拿到的是带完整类型推断的调用对象。

服务端的入口文件一般叫trpc.ts,负责初始化 tRPC 并定义基础上下文。我的实现里,上下文会从 Next.js 请求中解析 session,并把当前登录用户塞进 context。这样一个程序可以区分登录状态,并在每个 procedure 里读取用户信息。

import { initTRPC, TRPCError } from "@trpc/server"; import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; import { getServerSession } from "next-auth"; import { authOptions } from "@/pages/api/auth/[...nextauth]"; export const createContext = async (opts: CreateNextContextOptions) => { const session = await getServerSession(opts.req, opts.res, authOptions); return { user: session?.user ?? null }; }; const t = initTRPC.context<typeof createContext>().create(); export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use((opts) => { if (!opts.ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return opts; });

真正写业务接口时,我会把所有和片段相关的逻辑拆成一个snippetRouter,里面包含 list、create、update、delete、getById 五个 procedure。以创建片段为例,输入用 zod 做运行时校验,因为 tRPC 只保证类型静态一致,数据到运行时还是要防一手。zod 的 schema 既能做校验,又能推断出输入类型,和 TypeScript 配合起来非常自然。

列表查询是 t3code 最核心的接口,因为它直接决定了搜索体验。我实现的listSnippets接受keyword、tagIds、language、cursor四个参数,其中cursor用于分页。这里我没有用传统页数,而是用基于游标的分页,主要是为了应对多条搜索结果时的不稳定问题。具体到 Prisma 查询,关键词会拆开匹配标题、描述和代码内容,标签条件通过some: { id: { in: tagIds } }实现,语言条件直接精确匹配。代码实现如下:

listPublic: protectedProcedure .input( z.object({ keyword: z.string().optional(), tagIds: z.array(z.string()).optional(), language: z.string().optional(), cursor: z.string().optional(), limit: z.number().min(1).max(50).default(10) }) ) .query(async ({ ctx, input }) => { const where = { userId: ctx.user.id, ...(input.language ? { language: input.language } : {}), ...(input.keyword ? { OR: [ { title: { contains: input.keyword, mode: "insensitive" } }, { content: { contains: input.keyword, mode: "insensitive" } } ] } : {}), ...(input.tagIds && input.tagIds.length > 0 ? { tags: { some: { id: { in: input.tagIds } } } } : {}) }; const snippets = await ctx.prisma.snippet.findMany({ where, take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: "desc" }, include: { tags: true } }); let nextCursor: string | null = null; if (snippets.length > input.limit) { nextCursor = snippets.pop()?.id ?? null; } return { snippets, nextCursor }; });

这一段代码基本代表了 t3code 后端的主要模式:zod 定义输入边界,protectedProcedure 保证只有登录用户能访问,Prisma where 动态拼接,最后返回数据。每次改动输入输出结构,前端的所有调用点都会被 TypeScript 自动检查,这种安全感是 REST API 很难给的。

2.3 代码高亮与编辑器选型

代码片段工具的灵魂是代码展示,高亮库选型直接影响用户体验。市面上的方案主要有三种:Monaco Editor、CodeMirror、hljs(highlight.js)或 Shiki 这类的纯高亮库。

Monaco 是 VSCode 的编辑器内核,功能非常强大,支持自动补全、多光标、错误提示,但代价是体积巨大,即使按需加载也需要几百 KB。对 t3code 来说,用户主要场景是"看代码、复制代码",不是写代码,所以 Monaco 属于严重杀鸡用牛刀,直接排除。CodeMirror 6 模块化做得很好,也支持编辑和语法高亮,但配置复杂度略高,如果只做展示还得包一层外壳。最终我选择了 Shiki,它基于 TextMate 语法做高亮,能够生成带精确类名的 HTML,样式可以直接复用常见 VS Code 主题,视觉效果和代码语义都很好。

实际使用中我发现一个关键问题:Shiki 如果同步调用,会让服务端渲染变得很慢。比如一个包含几百行代码的片段,每次访问页面都要重新做一次语法解析,首屏时间完全不能看。解决办法是把高亮过程缓存起来,利用 Next.js 的服务端渲染能力,在请求完片段数据后直接把高亮后的 HTML 返回给客户端,而不是在浏览器里再跑一遍高亮逻辑。再加上 Next.js 的静态缓存,同一个片段的页面重复访问几乎是秒开。

代码展示组件里还有一个很容易被忽略的细节:复制按钮的状态反馈。t3code 里我用了navigator.clipboard.writeText配合一个copied状态,点击后按钮文案短暂变为"已复制",然后再变回来。这个交互看起来简单,但需要在useEffect里处理定时器清理,否则快速连点会造成状态错乱。这些细节对工具型应用的实际体验影响很大。

2.4 全文搜索与筛选的真实做法

搜索是最容易被低估的功能。t3code 早期版本就是简单地在内存里遍历片段,关键词用includes做模糊匹配。片段少的时候看不出问题,几百条之后,标签筛选一旦和关键词组合使用,响应延迟就变得很难受。

升级方案是用 PostgreSQL 的全文本搜索。Next.js 应用 + Prisma + PostgreSQL 的组合下,不需要额外引入 Elasticsearch 这种重量级组件。我通过 Prisma 的$queryRaw写了一个带tsvector全文索引的查询,把标题、描述、代码内容拼接成一个可搜索的文本向量,再用websearch_to_tsquery解析用户输入。这个方案在几万条片段范围内响应时间都在几十毫秒,完全够用。

CREATE INDEX snippet_search_idx ON snippet USING GIN ( to_tsvector('english', title || ' ' || content) );

但这里要注意:PostgreSQL 全文检索对中文支持不友好,默认的 english 分词会直接把中文句子切成单个字,导致中文关键词搜索效果很差。t3code 的目标用户是开发者,代码注释通常夹杂中英文,所以我在前端做了一层处理——如果搜索关键词包含中文,就退回到contains模糊查询,英文关键词才走全文索引。这是一个务实的妥协方案,既不引入额外的分词组件,也能让常见场景保持流畅。

标签筛选的实现相对简单,但有个细节值得一提:标签联动。当用户选择了某个标签后,标签下拉里的其他选项应该根据当前片段集合动态调整。这个效果用 Prisma 查询做会有点绕,我直接用 tRPC 暴露一个getTagsWithCount接口,统计每个标签关联的片段数量,前端通过checkbox展示数量,筛选时只显示数量不为零的标签。用户看到的就是"选了这个标签组合后还剩多少片段",体验比静态标签列表好很多。

3. 实操过程:从初始化到跑通全流程

3.1 初始化 Next.js 项目与 Tailwind 配置

开始动手的第一步是用官方脚手架创建项目。我在空目录执行了:

npx create-next-app@latest t3code --typescript --tailwind --eslint --app

这里我强烈建议加上--app,因为 Next.js 的 App Router 新项目结构更适合服务和客户端组件混合使用。模板生成完,我会先清理默认页面,只保留根布局、核心排版和响应式配置。Tailwind 在模板里已经就绪,但需要根据自己的设计调色板改一下tailwind.config.ts,比如代码片段卡片需要等宽字体、背景色和边框色的命名规范。

同时我会把tsconfig.json的路径别名配置好,默认已有@/*,后续所有导入都从@/开始,避免过长相对路径。这一步看着基础,但对后面 t3code 这种多模块项目非常重要。路径混乱会让重构时改一个文件名就全校报错。

接下来是安装核心依赖。我通常一次性装好全部会用到的包:

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod prisma @prisma/client next-auth @auth/prisma-adapter @tanstack/react-query

注意 tRPC v11 官方推荐配 @tanstack/react-query,负责客户端状态管理。NextAuth 用于 GitHub 登录,Prisma 适配器让它直接复用 Prisma 的用户表。依赖多,但装完就一劳永逸。

3.2 配置 Prisma 与数据库迁移

Prisma 的配置重点有两个:数据库地址和 schema。我开发环境用了本地 PostgreSQL,生产环境用的是线上数据库服务,两者只差一个DATABASE_URL环境变量。环境变量文件.env一定是被.gitignore忽略的,这个细节我会在部署环节再强调一次。

写 schema 时我会有意识地按业务模块分组。t3code 的 schema 不算复杂,只有三个主要模型:User、Snippet、Tag。User 是 NextAuth 适配器自动关联的标准模型,Snippet 和 Tag 之间是多对多关系。为了减少查询次数,关系字段都配置了include级别的外键索引。

定义好 schema 后执行:

npx prisma migrate dev --name init

这个命令会做两件事:生成真正的 SQL 迁移文件,并且同步生成最新的 Prisma Client。开发模式下它会自动执行迁移,数据库结构马上就位。如果中途调整了字段类型,比如把content从String改成@db.Text,也要再跑一次迁移,确保开发数据库和生产数据库结构完全一致。

3.3 实现第一个 tRPC 接口

tRPC 的路由注册路径和文件命名有一套推荐约定。我在项目里建了一个server/api目录,里面分trpc.ts、root.ts、routers/。trpc.ts负责初始化实例和定义上下文,routers/下每个业务模块一个文件,root.ts把所有子路由聚合到一起。

创建接口我采用了标准的 mutation 写法。输入用 zod 定义一个对象,包含标题、代码、语言和标签 ID 数组。因为标签可能要在创建时一并处理,我会先在connectOrCreate里把新标签批量建好,再建立关联关系。

用connectOrCreate是我非常喜欢的一个 Prisma 功能。它的意思是:如果标签名已存在,就通过唯一索引连接;否则先创建再连接。这样前端不用先手动创建标签,直接传标签名进来就好,简化了交互流程。对应的代码大概是:

create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().min(1), language: z.string().default("plaintext"), tagNames: z.array(z.string().min(1)).optional().default([]) }) ) .mutation(async ({ ctx, input }) => { return ctx.prisma.snippet.create({ data: { title: input.title, content: input.content, language: input.language, userId: ctx.user.id, tags: { connectOrCreate: input.tagNames.map((name) => ({ where: { name }, create: { name } })) } }, include: { tags: true } }); });

前端调用也非常直觉。组件里直接生成一个 mutation,提交时把表单数据传过去,成功后刷新列表:

const utils = trpc.useUtils(); const createSnippet = trpc.snippet.create.useMutation({ onSuccess: () => { utils.snippet.list.invalidate(); } });

第一次完整跑通这个链路的时候,最让我震撼的是前端代码能直接补全后端 mutation 返回的字段,不需要在客户端重新定义任何接口类型。这种全栈类型连接就是 t3code 选择 T3 Stack 最直接的价值。

3.4 前端页面与交互设计

后端通了之后,前端就是把"片段管理"这四个字变成具体页面。t3code 的布局极其简单:左侧是筛选栏,右侧是片段列表和编辑器。筛选栏包含搜索框、语言下拉框、标签多选列表。列表页用服务端组件直接读取 tRPC 查询结果,编辑器则是一个客户端组件。

编辑页的核心是代码输入区域。我这里故意没有用笨重的编辑器组件,而是直接用一个textarea,配合font-mono和合理的 tab 键行为。为什么这样选?因为 t3code 的核心场景是"保存已有代码",而不是"手写大量代码",一个 textarea 足够满足需求。不过我做了一个增强体验的小功能:Tab键插入两个空格或者缩进,减少编辑体验的割裂感。这个实现只需在onKeyDown里操作插入文本,代码量很少但感知很强。

列表展示时,每个片段卡片包含标题、语言徽章、标签列表、更新时间和复制按钮。我设置了点击卡片就进入详情页,同时复制按钮的点击事件用stopPropagation阻止触发父级跳转。这个细节如果没有处理,用户想复制时会先跳到详情页,体验非常糟糕。详情页则是把整段代码高亮渲染出来,加一个顶部信息栏。

交互部分还要考虑空状态和加载状态。t3code 搜索无结果时,我会显示"没有找到匹配的片段"和清除筛选按钮,引导用户返回全量列表。第一次加载数据时显示骨架屏,而不是一个转圈动画;因为骨架屏可以保持页面布局稳定,交互感知更快。

3.5 部署上线与环境变量管理

t3code 的部署走的是 Vercel + 托管数据库的路线,因为 Next.js 应用部署到 Vercel 几乎不用额外配置。导入仓库后,Vercel 会自动识别框架,把npm run build作为构建命令。我遇到的最大麻烦是环境变量管理:本地.env和生产环境的环境变量必须严格区分,尤其要注意DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL、GitHub OAuth 的GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET。

Vercel 的项目设置里有一个 Environment Variables 面板,我建议把所有变量一次性配置好再触发部署,否则首次构建很容易因为缺少DATABASE_URL或NEXTAUTH_SECRET而失败。NEXTAUTH_SECRET可以用openssl rand -base64 32生成,不要随意编一个短字符串。GitHub OAuth 应用的回调地址要填写生产域名下的/api/auth/callback/github,这个地址如果写错,登录会一直转圈。

部署之后还需要做一次数据库迁移。Vercel 的构建流程会在生产环境跑 Prisma Client 生成,但不会自动执行迁移。我采用的方式是在本地执行npx prisma migrate deploy指向生产数据库,或者在线的数据库管理面板里执行迁移文件。这一步顺序错了会直接导致线上应用能启动但所有查询报错,最好在部署文档里明确写出步骤。

4. 常见问题与排查技巧实录

4.1 tRPC 类型报错:为什么客户端看不到服务端方法

开发 t3code 的过程中,我浪费时间最多的就是 tRPC 的类型报错。典型场景是新增了一个 procedure 后,前端trpc.snippet.xxx一直提示不存在,或者返回类型没有更新。排查下来,大部分原因是根路由root.ts没有把新子路由合并进去。tRPC 是静态类型系统,服务端路由没有注册,客户端类型自然感知不到。

另一个隐蔽原因是 TypeScript 增量缓存。tsconfig.json里开了incremental或者.next/cache未清理时,新文件可能不会立刻被类型系统识别。我的标准排查顺序是:先确认routers/文件导出正常,再确认root.ts的appRouter包含该 router,最后重启next dev或执行tsc --noEmit检查整个项目类型。如果还不行,删除.next和node_modules/.cache再试,大概率问题解决。

4.2 Prisma 字段变更导致数据丢失或迁移失败

t3code 迭代过程中,我把 Snippet 模型加过isPublic字段,也调整过 Tag 的唯一约束,期间遇到过一次迁移失败。原因是在开发环境里,表中已有数据不满足新加的唯一约束。比如 Tag 表里已经有两条同名数据,现在要给 name 加唯一索引,迁移必然失败。

准备做这种破坏性变更时,我现在的习惯是先备份,或者直接在本地开发库跑一次试迁移。生产环境中要谨慎使用prisma migrate reset,因为它会清空所有数据。如果是改字段类型,比如从String改成@db.Text,最好在迁移 SQL 里加上USING子句做隐式转换,否则复杂场景下会不可预测地报错。这些经验和 t3code 本身无关,但对任何使用 Prisma 的项目都有参考价值。

4.3 代码高亮卡顿:从按需加载到缓存

早期版本我用 Shiki 做高亮时,把所有常用语言都导入了,结果首次加载一个 Java 代码片段要等两秒多。排查后发现 Shiki 每个语言都需要注册对应语法,注册全部语言会显著增加初始化时间。这个问题的标准解是只注册常用语言集合,比如typescript、javascript、bash、python、json、tsx。冷门语言可以按需动态注册,但 t3code 的使用频率决定了"少而精"更好。

另一个优化是让 Shiki 只做一次高亮,然后把结果交给 React 组件渲染。虽然 t3code 是服务端渲染,但如果把高亮放在客户端组件里,每次重新挂载都会重复计算。我把高亮计算放到了服务端数据层,返回给前端的数据就是一个已经带高亮 HTML 的字符串,前端只用dangerouslySetInnerHTML渲染。副作用是需要小心 XSS 风险,但 t3code 渲染的是用户自己保存的代码,这个风险可控。

4.4 数据存储容量和性能监控

t3code 运行一段时间后,数据库体积和查询性能是需要关注的。PostgreSQL 在数据量增至几万行时仍表现良好,但如果不维护索引、不清理无效数据,查询延迟会逐步上升。我给自己加了一个简单的定时任务,定期清理超过 180 天且从未被访问的临时片段,同时用EXPLAIN ANALYZE检查慢查询。

这里我发现一个有意思的点:很多人喜欢在代码里到处加日志,但在 t3code 这种小工具里,更有价值的是给每个 tRPC 请求加耗时统计。我会在protectedProcedure上通过 middleware 统一记录请求路径和耗时,这样哪个接口变慢了,第一时间就知道。写日志本身很简单:

t.procedure.use(async ({ path, type, next }) => { const start = Date.now(); const result = await next(); const duration = Date.now() - start; console.log(`${type} ${path} took ${duration}ms`); return result; });

这个中间件对性能影响极小,但让我在后期的优化中省了大量排查时间。

4.5 自己运维的最终心得

最后分享一个实际体会。t3code 做完之后,我最大的收获不是技术栈本身,而是理解了"工具的边界"有多重要。每次想加新功能的时候,我都会问自己:这个功能是为了让日常使用更顺畅,还是只想证明我能实现它?很多功能后来都没加,但现有功能的打磨却帮我把使用频率稳定地保持了下来。

具体到技术上,如果你也想照着这个思路做一个类似的工具,我的核心建议是:先把最小的闭环跑通,也就是"输入代码、保存标签、搜索代码"这一条链路;然后在这个链路上追求极致体验,包括高亮、复制、筛选速度;最后再考虑加分享、同步这类加分项。T3 Stack 的优势恰恰在于,它能让你用很小的代价把这个闭环做得非常完整,而且全程类型安全,改起来不慌。

现在 t3code 已经变成我日常开发离不开的工具,截图、代码片段、临时命令都可以快速存进去,搜索响应基本都是毫秒级。如果你也被自己散落各处的代码折磨,我建议你也试着用这套技术栈花两三个周末做一个自己的版本,过程中学到的全栈知识,可能比工具本身更有价值。