☰
基于T3 Stack的代码片段管理工具t3code开发复盘
2026/10/26 23:13:43 网站建设 项目流程

我从今年上半年开始,一直在做一个小型的代码片段管理项目,名字就叫t3code。起因很朴素:我平时收集了大量零散的代码片段,散落在本地 Markdown、语雀、微信收藏和公司的 Wiki 里,每次想找一个正则、一个 Docker 启动命令,都要翻半天。更烦人的是,博客里分享的代码在复制和格式化过程中经常乱掉,缩进、转义符号全都变样。后来我决定自己动手,做一个围绕 "T3 Stack + 代码片段管理" 的在线工具,把录入、检索、分享这几件事一口气打通。这个项目的技术栈非常明确:Next.js、TypeScript、tRPC、Prisma、Tailwind,外加 NextAuth 做登录。如果你正好想找一个 T3 全栈项目作为参考,或者是想做一个类似的"开发者工具类网站",这篇复盘应该可以帮你少走很多弯路。

整个项目做完给我的最大感受是:T3 Stack 并不是一套"网红技术栈的组装",它其实是在逼迫你把每一层的数据契约都想清楚。tRPC 的端到端类型安全不是营销概念,它真的能把很多运行时错误提前到编译期。接下来我会按照"需求拆解 → 技术选型 → 数据模型 → 核心功能 → 部署优化 → 踩坑记录"的顺序,完整复盘 t3code 的搭建过程。

1. 为什么叫 t3code:从随手记代码的痛点说起

先聊聊这个项目的起点。我不确定其他人有没有同样的问题,但对我来说,开发者日常工作中"收藏代码片段"这件事的体验极其分裂:

  • 本地文件方式:靠文件夹分类,用 IDE 打开,最大的问题是换电脑以后没有同步,手机上看不了。
  • 笔记软件方式:能记但是不好排版,而且大多数人最后都会把代码渲染成截图,截图里的代码不能复制、不能搜索。
  • 社交收藏方式:刷到一条好代码就"点赞/收藏",但收藏夹本身就是一个垃圾场,既不能打标签也不能预览效果。

t3code 的定位也因此非常清晰:做一个面向开发者自己的代码片段管理站,同时支持把片段生成只读分享链接给其他人。它不是要在线上做编辑器,也不是要做社区,就是自己用、团队内部用的小工具。项目名字很像项目的全部解法:T3 Stack 驱动,代码优先。

需求拆解下来其实只有五个核心模块:

  1. 登录:至少支持 GitHub OAuth。
  2. 录入:代码 + 语言 + 标题 + 简短描述 + 标签。
  3. 列表:按语言、标签、关键字过滤,列表页最好能看到代码开头。
  4. 详情/编辑:支持语法高亮,并编辑历史内容。
  5. 分享:未登录访客可以通过公开短链接查看指定片段,不需要登录。

这五个模块看起来很简单,但真正开始搭建的时候,每一块都会牵出不少问题。比如"列表页要看到代码开头"这件事,就要考虑代码块在首页会被截断成什么样;"语法高亮"要考虑是放浏览器端做还是服务端做,直接影响首屏性能和包体积。这些细节我在后面几个章节会展开聊。

2. 技术选型复盘:与其说是选了 T3,不如说是在避免"伪全栈"

很多人把 T3 Stack 理解成"Next.js + Prisma + tRPC + Tailwind"的固定配方,我在做完 t3code 之后更倾向于把它理解成一组互补的约束。每个组件都解决的是一个具体痛点,而不是为了组合而组合。

2.1 为什么用 App Router 而不是 Pages Router

t3code 一开始其实是用 Pages Router 起步的,后来翻新成了 App Router。主要原因有两个:

  • 代码片段在详情页是只读的,非常适合服务端渲染(SSR 或静态生成),App Router 的 Server Component 天然能"尽量少往浏览器发 JS"。
  • 很多工具类页面,比如"公开分享页",希望搜索引擎收录而不是直接进浏览器渲染。

如果你也在纠结 App Router 和 Pages Router,我的建议是先想清楚你的页面是以"用户交互"为主还是"内容展示"为主。像 t3code 这种 70% 是只读内容的站点,App Router 的优势非常明显。如果是个管理后台,页面几乎全是表单交互,App Router 依然能用,但那些"客户端状态管理"的复杂度要提前考虑清楚。

2.2 tRPC 为什么能减少一层"心里维护成本"

以前用 REST 接口的时候,写一个前端页面通常要经历这几步:去后端看路由定义、看请求参数、看返回结构、复制类型定义、写获取数据的代码、处理错误。前后端类型不同步是常态,经常出现"明明上线前都测过,结果一重构就崩"的情况。

tRPC 的核心价值在于:路由定义本身就是类型定义。在 t3code 里,我定义一个snippet.listprocedure,前端调用trpc.snippet.list.useQuery(),返回的类型和 Zod 校验后的类型是同一个,改一个字段名,编译直接报错,不需要等到接口跑一遍才能发现问题。

我准备了一个很小的示例,展示 tRPC 路由和前端调用的关系:

// server/api/routers/snippet.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const snippetRouter = createTRPCRouter({ list: protectedProcedure .input( z.object({ language: z.string().optional(), tag: z.string().optional(), keyword: z.string().optional(), }) ) .query(async ({ ctx }) => { return ctx.db.snippet.findMany({ where: { userId: ctx.session.user.id, }, orderBy: { createdAt: "desc" }, take: 50, }); }), });

对应前端页面:

const { data, isLoading } = api.snippet.list.useQuery({ language: "typescript", tag: "hooks", });

这里有个细节值得特别注意:tRPC 返回的数据类型是通过 TypeScript 的类型推断得到的,客户端根本不需要自己再写一遍 interface。这就避免了一个项目里出现"后端定义一套类型、前端定义一套同名类型"的经典问题。

2.3 Prisma 选型不是"数据库好看",而是迁移和类型体验

数据层我选了 Prisma + PostgreSQL。选择 PostgreSQL 的原因很简单:片段数据天然带有搜索和标签过滤需求,PostgreSQL 的全文搜索、JSONB 字段、数组字段都很顺手。Prisma 则主要是看中它的三件事:

  • Schema 文件就是数据模型文档,团队协作时不用翻数据库。
  • Migration 流程可以串到 CI/CD 里。
  • 生成的 TypeScript 类型和 tRPC 配合非常自然。

Prisma 的 schema 后面会给出完整代码,这里先说一个容易忽略的原则:不要把标签直接设计成字符串数组就完事。字符串数组做筛选没问题,但一旦你要做"点击标签查看关联片段"的计数统计,字符串数组就远远不如关系表舒服。

2.4 Tailwind 的收益和"约束"

Tailwind 在这个项目里不是主角,但它解决了两个实际问题:

  • 代码高亮块之外的各种 UI 组件,不需要单独写 CSS 文件,组件代码里直接看到样式。
  • 深浅色模式切换,用dark:变体做起来非常快。

不过我也要提醒一下:Tailwind 不是魔法,它最适合的仍然是"快速搭出工具类页面"。如果你要做重设计感的官网,古典 CSS 或 CSS-in-JS 依然有存在价值。t3code 里我用 Tailwind 写了很多列表、表单和面板,整体体验是舒服的。

3. 数据模型设计:从 Prisma Schema 到列表页查询

数据模型直接决定了一个工具类项目的骨架。t3code 的模型最初比较简单,只有一个Snippet表,后来加了Tag和SnippetTag关系表。完整结构如下:

generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? snippets Snippet[] tags Tag[] accounts Account[] sessions Session[] } model Snippet { id String @id @default(cuid()) slug String @unique title String description String @default("") language String @default("text") code String @db.Text isPublic Boolean @default(true) views Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt author User @relation(fields: [authorId], references: [id]) authorId String @index tags SnippetTag[] } model Tag { id String @id @default(cuid()) name String userId String @index user User @relation(fields: [userId], references: [id]) snippets SnippetTag[] @@unique([userId, name]) } model SnippetTag { snippetId String tagId String snippet Snippet @relation(fields: [snippetId], references: [id]) tag Tag @relation(fields: [tagId], references: [id]) @@id([snippetId, tagId]) }

3.1 slug 字段:公开分享页的核心

slug字段一开始我觉得只是一个短 ID,后来发现它直接决定了"公开分享链接"的形制。我生成 slug 的方式是通过nanoid:

import { nanoid } from "nanoid"; const slug = nanoid(10);

用nanoid(10)的好处是:长度合适,URL 短,不需要查重,碰撞概率在低量级数据下可以忽略。用户创建一个片段后,无论是否公开,都会生成一个 stable 的 slug。这样分享链接始终是https://你的域名/s/{slug},而不暴露数据库自增 ID。

3.2 标签为什么不用"字符串数组"

开发初期我想省事,直接在Snippet里加一个tags String[]。后来加"标签统计"功能时发现,要统计某个标签下面有多少公开片段,就得把所有片段拉出来在应用层过滤。于是改成了Tag+SnippetTag的多对多关系。

当你有一个"用户维度"的数据模型时,Tag表一定要带上userId,否则所有人的标签都混在一起,就成了一种"全局标签"。工具网站可以这么做,但个人/团队工具这么设计,长期会有数据污染问题。

3.3 列表查询的一处常见性能陷阱

列表页要显示片段代码开头。如果直接:

const snippets = await db.snippet.findMany({ ... });

前端要渲染snippet.code时,整个片段全文都会被传过去。如果用户存了一个 5000 行的脚本,列表页一次拉 50 个,Payload 就很可观了。

t3code 的做法是在查询里创建一个"预览片段"字段,用数据库函数截断,而不是前端截断:

const snippets = await db.snippet.findMany({ where: { authorId: session.user.id }, select: { id: true, title: true, language: true, slug: true, createdAt: true, codePreview: true, }, });

codePreview是视图里的一个生成列,或者我通过 Prismaraw查询拼接。最差的情况也可以用 Prisma 的take+ 内存截断,但只要片段数量上去,这个字段就不能省。

4. 核心功能实现:录入、检索、语法高亮和分享页

模型定好了,功能实现才是真正花时间的地方。这一章我会把 t3code 里四个核心功能的技术方案完整拆开。

4.1 创建片段的完整链路

创建片段的体验我希望尽量快:标题默认用代码的第一行注释,语言自动根据代码内容猜,用户只需要粘贴代码,点保存。为了做到这一点,创建表单的提交函数是这样的:

const createSnippet = api.snippet.create.useMutation(); const handleSubmit = (values: SnippetFormValues) => { createSnippet.mutate({ title: values.title || guessTitle(values.code), code: values.code, language: values.language, tags: values.tags, isPublic: values.isPublic, }); };

这里guessTitle只是一个辅助函数:

function guessTitle(code: string): string { const firstLine = code.split("\n")[0]?.trim() ?? ""; if (firstLine.startsWith("//") || firstLine.startsWith("#")) { return firstLine.replace(/^[#\/\s]+/, "").slice(0, 60); } return "未命名片段 " + new Date().toLocaleDateString(); }

这里的重点不是猜标题多聪明,而是表单交互要减少用户的摩擦。代码片段工具,核心竞争力不应该是"多一个漂亮的编辑器",而是"粘贴后只要能选中语言,保存动作越少越好"。

服务端路由也很直白:

create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), code: z.string().min(1), language: z.string().default("text"), tags: z.array(z.string()).default([]), isPublic: z.boolean().default(true), }) ) .mutation(async ({ ctx, input }) => { const authorId = ctx.session.user.id; const snippet = await ctx.db.snippet.create({ data: { slug: nanoid(10), title: input.title, code: input.code, language: input.language, isPublic: input.isPublic, authorId, }, }); if (input.tags.length > 0) { await Promise.all( input.tags.map((name) => ctx.db.tag.upsert({ where: { userId_name: { userId: authorId, name } }, create: { userId: authorId, name }, update: {}, }) ) ); await ctx.db.snippetTag.createMany({ data: input.tags.map((name) => ({ snippetId: snippet.id, tagId: await ctx.db.tag .findUnique({ where: { userId_name: { userId: authorId, name } }, }) .then((t) => t!.id), })), }); } return snippet; });

createMany里嵌套 await 的方式其实效率不好,更好的做法是先查出 tagId 集合,再一次性创建关联。实际项目里我用的是后者。这里保留前一种写法,是想提醒大家:链式 await 在循环里看起来能跑,数据量上来后早晚会后悔。

4.2 检索功能:先做"够用",再考虑全文搜索

t3code 的检索并不复杂,按语言、标签、关键词过滤即可。关键词匹配我用了最简单的方案,先查标题和描述:

keyword: z.string().optional(), // 在 query 里组装 where where: { userId: ctx.session.user.id, ...(input.keyword ? { OR: [ { title: { contains: input.keyword, mode: "insensitive" } }, { description: { contains: input.keyword, mode: "insensitive" } }, ], } : {}), ...(input.language ? { language: input.language } : {}), }

对于个人工具级别(几百到几千个片段),这个查询完全够。如果未来片段数量很大,建议用 PostgreSQL 的tsvector做全文索引。其实加一个searchVector字段和 GIN 索引也很简单:

ALTER TABLE "Snippet" ADD COLUMN searchVector tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(description, '')), 'B') || setweight(to_tsvector('english', coalesce(code, '')), 'C') ) STORED;

然后通过 Prisma$queryRaw查询。不过我不建议一上来就上全文搜索,因为大多数场景下,把标题和描述过滤做到位,已经解决了 80% 的问题。

4.3 语法高亮:服务端渲染而不是浏览器端再高亮

这是 t3code 整个项目里最值得讲的一个技术决策。

一开始我直接用react-syntax-highlighter,因为配置简单。但后来发现列表页在手机端打开时,JS 加载很大,因为高亮组件打包时会把一堆语言定义塞进主 bundle。于是我改成了用 Shiki 高亮文本,在 Server Component 里提前生成 HTML 字符串,浏览器端完全不加载高亮相关 JS。

// app/s/[slug]/page.tsx import { codeToHtml } from "shiki"; export default async function SnippetSharePage({ params, }: { params: { slug: string }; }) { const snippet = await findPublicSnippetBySlug(params.slug); const highlightedCode = await codeToHtml(snippet.code, { lang: snippet.language, theme: "github-dark", }); return ( <div> <h1>{snippet.title}</h1> <div dangerouslySetInnerHTML={{ __html: highlightedCode }} /> </div> ); }

dangerouslySetInnerHTML这个词看着吓人,但 Shiki 会把代码做安全转义,同时把高亮标记用<span>包裹。只要代码内容本身来自数据库且是在服务端渲染,XSS 风险是可控的。你在使用任何 code-to-html 库时,都要确认它内部做了 HTML 转义。这里有个前提:渲染分享页时,不能直接把用户代码塞进 dangerouslySetInnerHTML 不做处理,一定要用 Shiki 这类库,它内部输出的是转义后的 HTML。

这个方案带来的性能提升非常明显:分享页的 HTML 静态可缓存、SEO 可爬、浏览器 JS 几乎为零。缺点也很明显:Shiki 的语言包在服务端解析时有一定 CPU 开销,所以我把公开片段页面做了缓存,或者设置export const dynamic = "force-static"给公开链接开启静态生成。

4.4 公开分享页的权限控制

分享页的权限模型我做了三种:

状态访问规则
isPublic = true任何人可访问,实时计数
isPublic = false且作者登录作者可访问
isPublic = false且访客未登录404,不提示存在

在 tRPC 里实现"未登录访问公开数据",要单独定义一个 publicProcedure 而不是 protectedProcedure。这个细节很多人会忽略:如果你把分享页的数据加载也放在protectedProcedure里,那么未登录访客就永远看不到公开片段。

公开分享页获取数据的函数长这样:

export async function findPublicSnippetBySlug(slug: string) { return db.snippet.findFirst({ where: { slug, isPublic: true, }, include: { tags: { include: { tag: true } } }, }); }

注意我不能直接查where: { slug, OR: [{ isPublic: true }, { authorId: ... }] },因为数据库查询时根本不知道当前请求来自哪个用户。我的做法是在 Server Component 里先通过getServerSession()拿到用户,再决定用哪种查询。这正好体现了 Next.js 混合渲染的一个优势:服务端可以先做身份判断,再决定数据源。

5. 依赖注入、路由组织与真正可维护的 API 结构

做完核心功能后,我开始回头审视代码组织。T3 Stack 的目录结构看似固定,但项目一大,路由文件会变得很长。t3code 最后用了一种很顺手的组织方式。

5.1 把 tRPC Router 拆细

我不赞成把几十个 procedure 塞进一个 router。t3code 的最终目录长这样:

server/ api/ root.ts trpc.ts routers/ snippet.ts tag.ts user.ts search.ts

root.ts里把子路由合并:

export const appRouter = createTRPCRouter({ snippet: snippetRouter, tag: tagRouter, user: userRouter, search: searchRouter, });

这样前端调用依然是api.snippet.create、api.snippet.list、api.tag.list,语义清晰,文件也不用无脑膨胀。

5.2 鉴权中间件的写法

T3 Stack 默认用 NextAuth,所以我直接在 tRPC 上下文里注入 session:

// server/api/trpc.ts export const createTRPCContext = (opts: { headers: Headers }) => { return { session: getServerSession(), db, headers: opts.headers, }; }; const t = initTRPC.context<typeof createTRPCContext>().create(); export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED", message: "请先登录", }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); });

这里有个容易被忽略的点:tRPC 的next({ ctx })返回的 ctx 会覆盖原 ctx,所以后续 procedure 里的ctx.session.user是类型安全且非空的。如果直接在 router 里写ctx.session?.user.id,你还要各种判断,很容易遗漏。

5.3 服务端组件调用 tRPC 要谨慎

我在项目里发现了一个很关键的分界:tRPC 更适合客户端组件,而 Server Component 直接调用 Prisma 更香。原因很简单:

  • Server Component 里没有必要走 HTTP/RPC 层,直接调用 Prisma 查询就是一次函数调用,链路更短。
  • tRPC 路由更多是给交互型页面用的(表单提交、数据变化后的重新获取)。
  • 如果你在 Server Component 里调用 tRPC,又会引入createCaller,还需要处理请求上下文,收益并不明显。

所以 t3code 实际是"两套并行"的架构:公开分享页、详情页、首页列表这种偏向展示的页面,直接靠 Server Component + Prisma 查询;编辑器、设置、标签管理这种交互密集的页面,靠 tRPC。这样分工以后,心智负担小很多。

6. 部署与性能实测:Vercel、Prisma 连接池和高亮包的体积博弈

t3code 的部署链路上没有用太花哨的方案:代码放在 GitHub 仓库,推送到 Vercel 自动构建。数据库放在 Neon 的 PostgreSQL 上,因为免费层对冷启动比较友好。缓存我用了 Vercel 自带的 CDN 缓存。

6.1 部署时的环境变量

T3 Stack 的env.mjs严格校验环境变量,我列一下最终需要的核心变量:

变量用途
DATABASE_URLPrisma 连接字符串
NEXTAUTH_SECRETNextAuth 加密密钥
NEXTAUTH_URL站点的正式 URL
GITHUB_ID/GITHUB_SECRETGitHub OAuth 应用凭证

记住NEXTAUTH_SECRET不能和代码一起提交到 Git 仓库。本地开发可以用openssl rand -base64 32生成一个,然后放到.env。Vercel 的 Environment Variables 面板里也要单独配一份。

6.2 冷启动和 Prisma 连接池

Serverless 环境最大的坑就是数据库连接被频繁创建和销毁。PostgreSQL 的连接是在DATABASE_URL上配置的,如果你想在 Vercel 上使用 Neon,连接串通常长这样:

postgresql://user:password@host/db?sslmode=require&pooled=true

加上pooled=true才走连接池。我第一次部署时忘了带这个参数,北京凌晨的冷启动直接把我数据库连接数打满,整个站 504 了快十分钟。这个教训挺深刻的:连接串上的参数不能被忽略。

6.3 高亮包体积的博弈

上面我提到了用 Shiki 做服务端高亮,它带来的一个隐性问题就是构建时的语言包体积。如果我直接写:

import { codeToHtml } from "shiki";

它默认会一次性加载几十上百种语言定义,构建产物会变大,冷启动也会变慢。解决方案是按需加载:

import { createHighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; import js from "shiki/langs/javascript.mjs"; import ts from "shiki/langs/typescript.mjs"; import css from "shiki/langs/css.mjs"; import html from "shiki/langs/html.mjs"; import bash from "shiki/langs/bash.mjs"; const highlighter = await createHighlighterCore({ langs: [js, ts, css, html, bash], engines: { javascript: createOnigurumaEngine(import("shiki/wasm")) }, });

这样构建时只打包我常用的 5 种语言。用户若传到一种没注册的语言,我再给一个text兜底。我做这个优化后,构建体积大概下降了 40%。如果你的工具只面向自己或团队,按需加载是最划算的优化。

6.4 实测结果

我用自己的个人片段库测试过,大概有 600 条片段,分布在 10 种语言里。实际体验是:

  • 列表页 TTFB(首字节)约 200ms,本地网络环境下的 Vercel 边缘节点。
  • 分享页因为做了静态缓存,TTFB 可以到 60ms 以内。
  • 创建/编辑操作的响应时间约 150ms,瓶颈主要在 Prisma 写入和 GitHub OAuth 回调。

这个数据对于一个个人工具来说是够用的,不需要上额外的 CDN/反代,也不需要在代码里做太激进的内存缓存。

7. 印象最深的六个坑:每个都是代码层面的细节教训

最后记录几个这次项目里真正让我花过时间的坑。这些东西翻官方文档不一定能直接看到,但一旦遇到,排查成本都很高。

7.1 NextAuth 的NEXTAUTH_URL会吃掉一半排错时间

本地开发时,NextAuth 会尝试读取NEXTAUTH_URL。如果你没有设置它,有时却能正常工作,因为 Next.js 会推断 localhost;但部署到 Vercel 后,如果NEXTAUTH_URL没有正确设置为生产域名,GitHub OAuth 回调就会神秘失败。第一反应是 GitHub 应用回调地址写错了,实际是环境变量没传全。建议项目初始化时就把所有环境变量写进部署平台,不要等到出现奇怪问题再排查。

7.2 tRPC 错误在 Server Component 里不会自动中断

如果你在 Server Component 里用了createCaller调一个 tRPC procedure,它抛出的TRPCError不会自动变成 4xx 页面,而是会直接冒泡成 React 渲染错误。最后我统一约定:面向 Server Component 的查询,不用 tRPC,直接用服务端函数返回 null 或自定义错误对象。这样页面流程更可控。

7.3 Prisma 嵌套写入太多会导致 N+1

创建片段时,如果每传一个标签都去findUnique一次,再用createMany,标签数量一多就会变成 N 次查询。优化方式是一次性取回用户现有标签 ID:

const existingTags = await ctx.db.tag.findMany({ where: { userId: authorId, name: { in: input.tags }, }, select: { id: true, name: true }, });

然后用existingTags做映射,只对真正不存在的标签做 upsert。数据量不大时感觉不出来,但代码风格上会更健康。

7.4 公开页面的 NOINDEX 问题

访客公开分享页,我默认设了两个 meta 标签:

export const metadata = { title: "t3code - 代码片段分享", robots: { index: true, follow: true, }, };

不过后来我仔细想了一下,对于包含敏感内部代码(哪怕只是路径和服务器名)的页面,其实应该默认noindex,或者提供"禁止搜索引擎收录"的选项。我的处理是:个人默认index,团队分享默认noindex。这只是个小细节,但很影响内容安全。

7.5 代码内容里的 CRLF 会破坏高亮行数

我在 Windows 上整理的不少旧片段是 CRLF 换行。Shiki 基于行号渲染时,CRLF 的\r在 HTML 中可能会导致行内多余空白。处理方式很简单,入库前统一做一次 normalize:

const normalizedCode = code.replace(/\r\n/g, "\n");

这个操作看似多余,但当你共享给别人,别人复制过去的代码里全是\r残留时,会很影响体验。

7.6 session 更新不及时导致权限判断异常

NextAuth 默认的 session 有效期比较长,用户改了角色或者账号状态后,session 可能不会立刻更新。t3code 里我每次在protectedProcedure中只判断"是否登录",没有判断"角色是否变了"。但如果你要做团队版权限,记得在 NextAuth 的sessioncallback 里刷新用户信息,而不是只读取token.sub。

下一步我想加的功能

现在的 t3code 已经能满足我个人的日常使用,但做完之后我也想到几个接下来很自然的功能扩展方向:

  • 支持按代码片段生成源码归档下载,方便直接放到项目里。
  • 增加"复制代码"按钮,并统一处理 HTML 转义,让复制后的代码完全保持缩进。
  • 给公开分享页增加"过期时间",适合临时把一个片段发给同事,却不希望链接永久有效。
  • 用全文搜索替换现在的contains查询,毕竟代码库里搜代码,最后一定要落到语义搜索。

我在实际迭代中最深的体会是:工具类项目不要去追求大而全,而是要把"保存代码 → 找到代码 → 分享代码"这个闭环做到足够顺滑。t3code 这个名字以后应该也会继续演进去,但核心思路不会变:代码片段不是一个需要复杂管理的资产,它是一个需要被快速拿出来用的生活用品。

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

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

立即咨询