☰
基于T3 Stack的自托管代码片段管理工具设计与实现
2026/10/26 23:27:39 网站建设 项目流程

我平时写代码有个习惯,攒代码碎片。一段正则、一个工具函数、一种布局写法,随手往本地笔记里一扔,真到要用的时候翻半天。t3code 就是冲着这个问题来的:一个自托管的代码片段管理工具,把散落的代码统一收进来,打上标签,支持全文搜索,点一下就能复制走。项目技术栈选的是 T3 Stack,也就是 Next.js + TypeScript + tRPC + Tailwind CSS + Prisma 这条标准路线,前后端一个仓库跑完,部署也省心。

这篇文章写给两类人:一类是想给自己搭一个类似的私人代码库,另一类是想看看 T3 全家桶在实际项目里怎么落地的人。我会把从设计到部署的关键环节都过一遍,包括数据模型怎么定、搜索怎么做、代码高亮怎么选方案,以及那些文档里不会写的坑。整体节奏偏实操,能直接照着搬的配置和代码我会尽量给全。

1. 整体设计思路与方案选型

1.1 t3code 到底解决了什么问题

先说说出发点。我观察到一个现象:大多数开发者都有两套笔记,一套记业务知识,一套记代码。前者的工具很成熟,后者却很尴尬——通用笔记软件对代码支持太弱,没有语法高亮是小事,关键是搜索体验极差,想搜一段半记得的代码片段基本靠缘分。

t3code 把这个需求收敛成三个动作:存、找、取。

  • 存:粘贴代码,填主题、语言、标签,最多 10 秒完成。
  • 找:支持标题、内容、标签三个维度的搜索,还可以按语言和标签过滤。
  • 取:详情页一键复制,支持多语言高亮预览,解决了"翻出代码还得重新格式化"的麻烦。

这个定位决定了整个技术架构不会太重,不需要微服务,不需要消息队列,一个全栈单体就能扛住。T3 Stack 恰好就是为这种"中等复杂度、追求开发效率"的项目设计的。它不解决几十万并发的问题,它解决的是"一个人用一个周末把一个想法变成能用的产品"的问题。

1.2 为什么是 T3 Stack 而不是其他组合

选型的时候其实纠结过几种方案。单独写后端用 Express 或 NestJS,前端再用 React 或 Vue 单独搭一套,中间用 REST 对接口,这是最传统的路子,但意味着要维护两套工程,还要写接口文档、处理 CORS。对于单人维护的工具类项目,这个成本明显偏高。

T3 Stack 吸引我的核心是 tRPC。它让前后端共享类型定义,写一个服务端 procedure,前端那边自动就有类型提示,连多余的 API 层代码都省了。类型即契约,这个价值在单人项目里被很多文章低估了——当你三个月后回来看自己写的代码,类型系统能帮你避免大部分"我当初是怎么调这个接口的"的困惑。

各层选型的理由整理成了表格:

技术职责为什么选它
Next.js (App Router)页面路由与服务端渲染生态成熟,脚手架齐全,部署友好
TypeScript全栈类型前后端共享类型,减少沟通成本
tRPCAPI 层端到端类型安全,省掉 REST 样板代码
PrismaORM 与数据库迁移Schema 即文档,迁移管理清晰
Tailwind CSS样式快速迭代 UI,不用来回切 css 文件
NextAuth.js登录鉴权与 Next.js 深度集成,支持多种登录方式

补充一点,有人会觉得 Next.js 的 Route Handlers 也能充当 API 层,为什么还要加个 tRPC?我的回答是:Route Handlers 没有类型约束,参数校验、错误处理都要自己从头写;tRPC 把这些收进了一整套流程里,配合 zod 做输入校验,可以少写很多防御代码。当然,如果你的项目需要给第三方提供公开 REST API,Route Handlers 依然有用武之地,t3code 里没有这个需求,就干脆统一走 tRPC。

1.3 项目目录结构怎么摆

T3 Stack 脚手架 create-t3-app 生成的目录规划已经很合理,我在其基础上做了少量调整。先看结论,完整的目录结构如下:

t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ ├── snippet.ts │ │ │ │ ├── search.ts │ │ │ │ └── tag.ts │ │ │ └── root.ts │ │ ├── db.ts │ │ └── auth.ts │ ├── app/ │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── snippets/ │ │ ├── [id]/page.tsx │ │ └── new/page.tsx │ └── components/ │ ├── editor/ │ ├── snippets/ │ └── ui/

有几个调整点值得说明。第一,我把所有 tRPC 路由集中在src/server/api/routers,业务逻辑尽量收进 service 层,而不是全堆在 procedure 里。第二,组件按功能域拆分,editor管代码编辑器,snippets管列表与详情,ui放通用小组件。这个分法不复杂,但维护起来很明确——改搜索就进snippets,改编辑器就进editor,不需要在几十个文件里找。目录结构这东西,越早定好越省心,项目到后期再重构目录是很痛苦的事。

2. 数据模型与后端核心实现

2.1 数据库表结构设计

t3code 的核心数据就是代码片段,围绕它我设计了四张表:User、Snippet、Tag、Favorite。直接看 Schema 更直观:

model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? snippets Snippet[] favorites Favorite[] } model Snippet { id String @id @default(cuid()) title String description String? code String language String @default("plaintext") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) tags Tag[] favorites Favorite[] @@index([authorId, language]) } model Tag { id String @id @default(cuid()) name String snippets Snippet[] @@unique([name, ownerId]) ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) } model Favorite { id String @id @default(cuid()) userId String snippetId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) snippet Snippet @relation(fields: [snippetId], references: [id], onDelete: Cascade) @@unique([userId, snippetId]) }

这里有三个设计决策想展开讲讲。

第一个是 snippet 和 tag 的多对多关系。一个片段可以有多个标签,一个标签也可以挂在多个片段上,用 Prisma 的隐式多对多就够。但要注意,隐式多对多没法在中间表存额外字段,比如"某个标签在某名下被引用了几次"。t3code 暂时不需要这个统计,所以用隐式关系,省一张表。如果以后要做标签热度排行,再拆成显式的中间表不迟。

第二个是索引策略。@@index([authorId, language])这个复合索引针对的是列表页最常见的场景:先按语言过滤,再按作者限定。千万别给每个字段都加索引,Prisma 生成的开发库还好,生产环境里 Postgres 的写入性能会被多余索引拖累。我在开发中后期删掉了一个多余的@@index([createdAt]),就是因为实际查询走了复合索引后它根本没用上。

第三个是 Favorite 表的唯一约束。@@unique([userId, snippetId])保证同一个人不能对同一个片段收藏两次,这个约束写在数据层,比在应用层用代码判断更可靠。真实经验是:先把约束建好,再写收藏切换逻辑,代码可以写得很放心,不用每次先查后写。

主键这里我用了cuid()而不是自增整数。自增 id 在单机场景足够用,但有个隐患:id 会暴露记录数量,别人能猜到你的片段总量。cuid 没有这个问题,且在分布式或迁移场景下更从容,代价只是存储空间略大一点,完全可以接受。

2.2 tRPC 路由与服务层怎么分

tRPC 的 procedure 写得不好会变成灾难——所有逻辑堆在回调里,调试起来非常痛苦。我在 t3code 里遵循一个简单原则:procedure 只做参数校验和调用 service,真正的业务逻辑放 service 函数。

先看路由划分:

// src/server/api/routers/snippet.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import * as snippetService from "../services/snippetService"; export const snippetRouter = createTRPCRouter({ list: publicProcedure .input(z.object({ language: z.string().optional(), tag: z.string().optional(), page: z.number().int().positive().default(1), pageSize: z.number().int().positive().max(50).default(20), })) .query(async ({ ctx, input }) => { return snippetService.list(ctx.db, input); }), byId: publicProcedure .input(z.object({ id: z.string().cuid() })) .query(async ({ ctx, input }) => { return snippetService.getById(ctx.db, input.id); }), create: protectedProcedure .input(z.object({ title: z.string().min(1, "标题不能为空").max(100), description: z.string().max(500).optional(), code: z.string().min(1, "代码不能为空"), language: z.string().default("plaintext"), tags: z.array(z.string().min(1).max(20)).max(5).default([]), })) .mutation(async ({ ctx, input }) => { return snippetService.create(ctx.db, ctx.session.user.id, input); }), update: protectedProcedure .input(z.object({ id: z.string().cuid(), title: z.string().min(1).max(100), description: z.string().max(500).optional(), code: z.string().min(1), language: z.string(), tags: z.array(z.string().min(1).max(20)).max(5), })) .mutation(async ({ ctx, input }) => { return snippetService.update(ctx.db, ctx.session.user.id, input); }), delete: protectedProcedure .input(z.object({ id: z.string().cuid() })) .mutation(async ({ ctx, input }) => { return snippetService.delete(ctx.db, ctx.session.user.id, input.id); }), });

zod 在这里扮演了双保险的角色。前端表单提交前先safeParse一次,给用户即时反馈;后端 procedure 的 input 再解析一次,防止绕过前端直接打接口。z.string().cuid()这个校验很有用,它能在查询数据库之前就拦截掉格式非法的 id,省掉一次无效 DB 查询。

service 层举一个 update 的例子,重点看所有权校验:

// src/server/api/services/snippetService.ts export async function update( db: PrismaClient, userId: string, input: UpdateSnippetInput ) { const existing = await db.snippet.findUnique({ where: { id: input.id }, select: { authorId: true }, }); if (!existing) { throw new TRPCError({ code: "NOT_FOUND", message: "片段不存在" }); } if (existing.authorId !== userId) { throw new TRPCError({ code: "FORBIDDEN", message: "无权修改此片段" }); } return db.snippet.update({ where: { id: input.id }, data: { title: input.title, description: input.description, code: input.code, language: input.language, tags: { set: [], connectOrCreate: input.tags.map((name) => ({ where: { name_ownerId: { name, ownerId: userId } }, create: { name, ownerId: userId }, })), }, }, }); }

这里踩过一个坑,值得重点说。更新标签时,我第一次用的写法是直接把tags字段设成connectOrCreate数组,但 Prisma 的隐式多对多关系不会自动清掉旧的关联。结果是:改一次标签,旧的标签还挂在片段上,列表页越看越怪。解决办法就是在 data 里先set: []清空,再把新标签连接进来。这个"先清后连"的模式,是所有用 Prisma 做多对多更新的项目都能用得上的经验。

所有权校验放到 service 层,保证每个 mutation 都做一次"这数据是不是你的"检查。这个检查不能省,即使你的应用只有你自己注册使用,养成习惯后写出来的代码才敢给别人部署。

2.3 搜索功能的三种方案对比

搜索是 t3code 的核心功能,我对比了三个方案。

第一个是数据库原生查询。Prisma 的contains和mode: "insensitive"能搞定简单的模糊匹配,数据量小的时候完全够用。但问题是:搜"数组去重"这种词,如果内容里没有连续的完整句子,结果就不理想;索引效率也一般,表一变大查询会明显变慢。

第二个是 Postgres 自带全文搜索。用tsvector和tsquery做匹配,性能比LIKE好很多,还支持简单的相关性排序。t3code 部署在 Postgres 上,不需要额外引入服务,但需要写原生 SQL,Prisma 的$queryRaw可以支持,要注意 SQL 注入,参数得用模板字符串的占位符。

第三个是引入独立的搜索服务,比如 Meilisearch 或 Typesense。这类搜索引擎体验最好,中文分词也支持,但代价是多一个服务要部署、要维护,对于自托管的个人项目来说有点重。

最后我选了方案二。理由很实际:t3code 的片段量在个人使用场景下很难超过几千条,Postgres 全文搜索在这个量级下性能绰绰有余,而且少一个外部依赖,部署的时候就少一个需要盯着的组件。搜索的 SQL 大概是这样的:

SELECT id, title, ts_headline(code, query, 'StartSel=<mark>, StopSel=</mark>') AS highlighted_code FROM snippet, plainto_tsquery('simple', ${searchTerm}) AS query WHERE tsv @@ query ORDER BY ts_rank(tsv, query) DESC

这段 SQL 里有几个细节:plainto_tsquery会自动把用户输入分词并转成查询条件,空字符串搜出来会没结果,所以前端要做好输入校验;ts_headline用于生成带高亮标记的摘要,前端拿到<mark>标签后配合样式就能展示搜索命中位置。

从整个 t3code 的经验来看,搜索功能最忌讳一上来就上重型方案。先用最简单的方案跑通流程,等数据增长到明确感觉到慢了,再升级不迟。这个原则我会在后面的复盘部分再讲一遍,因为它是这个项目里我最想强调的一点。

3. 前端交互与编辑器体验

3.1 代码高亮方案的选型与实现

代码高亮看似小事,其实是 t3code 里最容易翻车的部分。高亮库加载太慢,页面明显卡顿;高亮不准,语法名不副实,工具会立刻失去可信度。我从三个方案里做对比。

highlight.js 最轻量,打包后体积小,支持语言多,但语法解析深度一般,遇到复杂写法会高亮错。Prism.js 居中,词法高亮比 highlight.js 细,体积也可控,但语言包要手动按需加载。Shiki 走的是 TextMate 语法路径,拿的是 VS Code 同一套语法文件,高亮准确度最高,缺点是要么预生成 HTML,要么在浏览器里跑 WASM。

我的选择是 Shiki 的预渲染方案。核心思路是:在服务端把代码片段渲染成带高亮 class 的 HTML,客户端只负责展示,不加载任何高亮 JS。这样首屏性能非常好,同时享受 VS Code 级别的高亮质量。

具体做法是用shiki提供的codeToHtml,配合 Next.js 的 Server Component:

// src/components/snippets/CodeViewer.tsx (Server Component) import { codeToHtml } from "shiki"; export async function CodeViewer({ code, language, }: { code: string; language: string; }) { const html = await codeToHtml(code, { lang: language, theme: "github-dark", }); return <div dangerouslySetInnerHTML={{ __html: html }} />; }

需要留意的是dangerouslySetInnerHTML只会插入高亮后的 HTML,Shiki 输出的 HTML 默认做了转义,不会执行脚本,安全性没有问题。真正要注意的反而是语言参数——用户提交的语言名可能不在 Shiki 支持列表里,比如写了个"text"或自定义值,codeToHtml会直接报错。稳妥做法是在写库之前做一次语言名白名单映射,把常见别名(比如"js"映射到"javascript")统一掉。

3.2 新建与编辑页的编辑器怎么选

新建页需要让用户粘贴并编辑代码,这里我试过简版和重版两手方案。

最简的做法就是用<textarea>套一层 CSS,让字体变成等宽字体,开个自动换行。优点是零依赖,缺点是没有任何语法高亮,长代码很容易看花眼。我一开始先用 textarea 把整个流程跑通,等核心功能稳定了再往上叠体验。

后来换成了 CodeMirror 6。它是模块化的,不像老版本那样一个包全搞定,按需引入语法高亮插件。基础用法大概是这样:

"use client"; import { EditorView, basicSetup } from "codemirror"; import { javascript } from "@codemirror/lang-javascript"; useEffect(() => { const view = new EditorView({ doc: initialCode, extensions: [ basicSetup, javascript(), EditorView.lineWrapping, EditorView.updateListener.of((update) => { if (update.docChanged) { onChange(update.state.doc.toString()); } }), ], parent: containerRef.current!, }); return () => view.destroy(); }, []);

用 CodeMirror 6 遇到的最大的坑是依赖版本升级。它的核心包拆分得很细,basicSetup里包含折叠、括号匹配、自动补全这些功能,但从某个版本开始,basicSetup的导入路径变了,直接导致构建报错。我的建议是装包时锁定精确版本号,别用^浮动版本,免得哪天重新安装依赖时莫名其妙崩。

另外有个交互细节:编辑器回传内容的时机。如果每次输入都触发useState更新,再传到 tRPC 的 mutation 参数里,输入过快时会频繁渲染。体验上倒是没大问题,但在编辑大代码块时能感觉到输入延迟。优化方法是加一层useDeferredValue,让搜索和保存操作走延迟后的值,编辑内容本身保持即时反馈。

3.3 列表页与搜索交互的细节打磨

列表页是用户用得最多的页面,也是细节最容易被低估的地方。t3code 的列表页有几个交互设计拿出来单独说说。

首先是标签点击过滤。列表页左侧有一列标签,点一个就过滤出对应片段。这个功能实现不难,但要注意 URL 状态同步——用户点了标签,URL 里的 query 得跟着变,这样刷新页面后状态不丢,别人分享链接过来也能看到同一个过滤结果。我用的是 Next.js 的useSearchParams配合useRouter().replace,而不是单纯用组件内部 state。

其次是搜索的防抖与请求取消。搜索框输入时,每敲一个字就发一次请求是灾难性的。我用 300ms 的防抖,配合 React Query 的自动去重机制,相同关键词不会重复请求。这里 React Query 比手写useEffect加setTimeout优雅的地方在于:它会自动取消过期的请求,旧结果的渲染不会覆盖新结果,这类并发竞态问题框架都处理好了。

第三是代码块在列表里的展示。列表页不展示完整代码,而是截取前几行做个预览。这里要小心把高亮 HTML 直接渲染到列表里,数据量一大 DOM 节点会很多。我的做法是只取代码前 80 个字符做纯文本预览,不渲染高亮,列表页的渲染压力一下就降下来了。

表格对比一下几个交互细节的处理方式:

交互初步实现最终方案原因
标签过滤组件内 stateURL query 同步刷新不丢状态,可分享
搜索请求每次输入立即发300ms 防抖 + React Query减少无效请求,天然防竞态
列表代码预览高亮 HTML 截断纯文本前 80 字符降低列表 DOM 复杂度

3.4 一键复制与 Toast 反馈

一键复制是工具类网站的基本功,做不好很劝退。我用的是navigator.clipboard.writeText,这是现代浏览器标准能力。但有几个坑值得记录:clipboard API 在非 HTTPS 环境下不可用,本地开发时 localhost 是例外可以用,但如果部署在 HTTP 的内网服务器上,用户会碰到复制静默失败。t3code 部署在 Vercel 上天然走 HTTPS,没这个问题,但本地测试一定要留意。

复制成功后的反馈,我用了一个 2 秒自动消失的 Toast 组件。实现很简单,用setTimeout后把状态清空。真正要注意的是连续点复制按钮时 Toast 要重置计时器,不然用户连点两次,第一次的 Toast 会提前消失。这个竞态我用useRef存 timeout id,每次点之前先clearTimeout,再把新的计时器存进去。

还有一个小细节——复制时是否要格式化。有的代码存进去时排版可能乱了,我加了一个"复制时尝试格式化"的选项,用 Prettier 的formatAPI 在客户端格式化后再写入剪贴板。当然,用户如果不需要格式化,可以在设置里关掉,避免复制出来的代码和自己写的不一致造成困惑。

4. 实操过程中的坑与排查办法

4.1 我踩过的典型问题汇总

做 t3code 的过程中,我整理了一张问题排查表,记录了几个最耽误时间的问题。一个小项目都会遇到这些问题,更不用说生产环境,拿出做个速查表很有价值。

问题现象根因解决办法
页面偶尔报 hydration 错误服务端渲染的 HTML 与客户端不一致,常见于时间格式化、随机数统一用Intl.DateTimeFormat,并把格式化函数放进服务端组件输出
tRPC 查询返回 500Prisma 模型改了但没跑迁移每次改 schema 后跑npx prisma migrate dev和npx prisma generate
修改标签后旧标签残留隐式多对多不会自动清空更新时先set: []再connectOrCreate
搜索中文没结果Postgres 的默认分词不支持中文用simple配置做基础分词,或使用 icu 分词插件
CodeMirror 升级后构建报错核心包拆分导致导入路径变化锁定精确版本,升级时看官方迁移指南

这里面有两个问题想单独展开说。

hydration 错误是 Next.js 项目里最容易出现的隐性坑。原因是服务端已经渲染出 HTML,客户端渲染时如果结果不一致,React 会报错甚至放弃交互。常见触发点是new Date().toLocaleString()这类依赖时区的调用。t3code 里我写过一段展示"3 分钟前添加"的相对时间,结论是要么用服务端组件直接输出,要么用suppressHydrationWarning配合同一个时区的格式化函数,别在客户端再算一遍。

中文搜索的问题是 Postgres 全文搜索的经典盲区。默认的分词配置english会把中文按空格切,中文没有空格,一整段搜索词会被当成一个 token,结果自然为空。t3code 最终选择在写入 snippet 时同时生成一个用simple配置的tsvector字段,再把搜索词也按simple转成查询条件,虽然不是完美的中文分词,但对个人工具来说已够用。更彻底的做法是装pg_trgm扩展做 trigram 匹配,可以按需替换。

4.2 性能优化的几个实际动作

t3code 是个人工具,并发量不高,但页面响应速度直接影响使用意愿,所以我做了几件性价比很高的优化。

第一件是列表页的 React Query 缓存。snippetRouter.list的查询结果默认会被缓存,配置staleTime: 60_000以后,一分钟内的重复访问直接走缓存,列表滚动和筛选的响应速度会有质的提升。代价是刚编辑完的数据可能要等缓存过期才刷新,所以创建、更新、删除成功后我统一调用了queryClient.invalidateQueries({ queryKey: ["snippet.list"] })把列表缓存清掉,保证数据一致。

第二件是 Prisma 的查询瘦身。列表页之前把code字段全量查出来,只为了渲染一个 80 字符的预览,纯属浪费。改成用select只拿必要字段后,列表接口的响应体小了 60% 以上,数据库查询也轻快。这个优化很多人会忽略,因为开发环境数据量小感受不到差异,一上生产就原形毕露。

第三件是静态资源的处理。t3code 没有图片,但有个功能是预览代码封面,生成效果图的时候用过一条/og动态路由。这个路由每次访问都会跑一次服务端渲染,我给它加了revalidate = 3600,让它一小时重新生成一次,避免被频繁访问打爆。这类优化对个人小项目尤其划算——几乎不花精力,但能挡住不少无效渲染。

性能优化这块,我想强调一个判断标准:先量后优。用curl或者浏览器 DevTools 的 Network 面板看看接口耗时,确认瓶颈在数据库、渲染还是网络传输,再动手。不要看到一个能优化的点就优化,很多所谓优化只是自我感动。

4.3 部署与日常维护经验

部署环节,t3code 选择了 Vercel + Vercel Postgres 的组合。Vercel 对 Next.js 的支持是原生级别的,部署就是git push的事,很多配置都不用管。Postgres 用云数据库而不是本地 Docker,省掉了自己维护数据卷、备份的麻烦。对个人项目来说,时间成本比订阅费更值钱。

部署阶段有几个细节值得列一下。一是环境变量:DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL必须在 Vercel 的项目设置里配好,尤其NEXTAUTH_SECRET如果不设置,NextAuth 在浏览器环境会随机生成,导致登录态不稳定。二是数据库迁移:生产环境不能用migrate dev,要用prisma migrate deploy,这两者一个用于开发自动重置,一个用于生产应用变更,千万别搞混。

三是日志与错误追踪。Vercel 有内置的 function 日志,但排查 tRPC 内部错误还是不够直观。我接了一个轻量的错误上报服务,把 procedure 里的未捕获异常记录到远程,方便偶尔翻看。这一步不是必须,但自托管服务一旦出问题,没有日志等于盲修。

日常维护上,我的习惯是每两周看一眼数据库余量和备份状态。Vercel Postgres 的备份可以配置自动备份策略,我设了每周一次。个人项目的数据丢失是难以接受的——那里面存的是一段段验证过的代码逻辑,重新找回来成本极高。

5. 复盘与后续扩展方向

5.1 这个项目最值得复用的三条经验

做完 t3code,如果把收获压缩成三条,我会选这几条。

第一,全栈类型安全带来的长期价值被低估了。前后端共享类型意味着改一个字段,所有用到的地方都会在编译期报错,而不是等到运行时才炸。单人项目里没有队友帮忙看代码,这个保障特别值。

第二,数据库 schema 是地基,设计时要为扩展留余地,但别替未来过度设计。t3code 的 Favorite 表一开始是没有的,后来加收藏功能时才补上。因为 Prisma 的迁移工具足够顺手,加表、加字段成本很低,真正的负责人应该在设计阶段理清核心关系,比如 snippet 和 tag 的多对多,而收藏这类外围功能可以等需求明确后再演进。

第三,工具类项目的核心是"低摩擦"。任何一个操作如果超过三步,用户就会流失。t3code 的设计里,"存"只需要"粘贴 + 标题 + 标签"三件事,"取"只需要点一下复制。整个产品的价值不来自功能多,而来自每个功能都被打磨到顺手。

5.2 后续可以继续做的方向

t3code 目前已经满足我的日常需求,但要继续演进的话,有几个方向是明确有价值的。

第一个方向是导入导出。很多开发者手里已经有一批从笔记软件里导出的代码片段,如果能提供 Markdown 或 JSON 格式的批量导入,迁移成本会大幅降低。同时提供 SQLite 备份导出,数据不绑定在服务器上,这对自托管用户是刚需。

第二个方向是代码片段版本历史。每次编辑都留一份快照,可以随时回退。这个功能对"改坏了想找回原来的写法"的场景非常实用。Postgres 的 JSONB 字段可以存快照数组,几千行的数据量完全撑得住。

第三个方向是内容分析和智能建议。比如统计每个语言的片段数量、找出引用最多的公共代码块,辅助用户发现自己经常重复写的代码,继而抽象成更通用的函数。这个方向会用到比较多数据分析的活儿,但对开发者群体来说非常有吸引力。

这三个方向我都不打算立刻开工。个人维护的开源项目最忌讳大包大揽,把核心场景做到极致,剩下的交给真实使用反馈来决定。

5.3 做这个项目最深的几个体会

这个项目从头到尾的节奏跟我想象的不太一样。最初以为最大的工作量会在代码编辑器上,结果发现最花时间的反而是搜索结果怎么展示、复制按钮的交互反馈、列表页的缓存策略这类"不起眼"的环节。工具类产品的差别,恰恰就藏在这些细节里。

另一个体会是,T3 Stack 这种全家桶思维确实适合个人项目的快速落地。它替你做了大量技术选型决策,让你把精力集中在业务逻辑本身。但全家桶不等于万能钥匙,如果你的项目需要深度定制的前后端架构,或者需要给外部提供标准 REST API,那还是回到传统分层更合适。

最后想对想复刻这类项目的人说一句:别怕从简陋开始。t3code 第一版就是一个没有任何高亮、只能存和搜的页面,后面才一步步长出现在的样子。先把核心流程跑通,再谈体验,这个顺序比技术选型还重要。

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

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

立即咨询