首页 / 资讯中心 / 文章详情

T3 Stack实战:构建在线代码运行分享工具t3code完整指南

T3 Stack实战:构建在线代码运行分享工具t3code完整指南 ★ FEATURED ARTICLE
直接聊 t3code 这个名字吧。看到这个标题懂的人应该马上能反应过来——这是 T3 Stack 和 code 的结合一个典型的“用整套全栈 TypeScript 技术栈做一个在线代码运行/分享工具”的项目。我在本地跑通类似的链路不止一次今天就把整个项目从定位、选型到核心实现的完整思路拆开讲读完之后你至少能少踩我踩过的那一堆坑尤其是 tRPC 和 Monaco 编辑器集成、服务端沙箱执行这类细节直接照着落地方案改就行。这个项目适合谁两类人。一类是想系统学习 T3 StackNext.js TypeScript tRPC Tailwind 那一套的开发者与其刷文档不如动手做一个带完整业务闭环的全栈项目另一类是团队内部需要“代码片段管理 在线验证”场景的人比如你经常要发一段类型推断的验证代码给同事看或者写技术方案时想让对方直接跑一下示例t3code 这种轻量工具就比本地开 IDE 轻得多也比贴公共代码平台更可控。1. 项目定位与整体设计思路1.1 t3code 到底是什么先说清楚t3code 不是一个“新的编程语言”或者“新的框架”它是一个借助 T3 Stack 全家桶搭建的在线代码编辑与分享平台。核心功能很简单你打开网页左边写 TypeScript 代码右边看到运行结果写完可以保存成一条 snippet生成一个短链接发给别人别人打开链接就能看到代码和运行输出。如果做深一点还可以支持用户登录、收藏、评论区、历史版本回溯。“T3”这个词来自社区里 Theo 提出的一套全栈 TypeScript 方案常见的组合就是 Next.js TypeScript Tailwind CSS tRPC再搭上 Prisma 作为数据库 ORM。t3code 这个名字等于把技术栈缩写直接刻进了项目名一眼就能看出这是个讲究类型安全的 TypeScript 工程。我刻意没有加“playground”这类后缀因为它的定位不只是“自己玩代码”更多是“把代码片段变成一个可以分享、可以复用的资产”。所以它更像一个极简版的 codebin只不过是完全跑在 T3 Stack 上端到端类型安全的那种。1.2 为什么值得自己做一个而不直接抄现成的你可能想问CodeSandbox、CodePen、StackBlitz 已经很成熟了为什么还要自己折腾我当时的真实出发点有三个都很实际。第一公共在线编辑器的强项是“前端项目级预览”你给它一个 npm 依赖列表它会帮你装包、跑构建但它并不擅长“快速验证一个纯 TypeScript 逻辑”。比如你写了一个类型体操或者一段数据转换函数在 CodeSandbox 里还得建文件、配入口太繁琐。t3code 想要的是“打开即写、写完即跑、跑完即分享”的轻体验。第二公共平台的数据不在你自己手里。团队内部的技术方案交流、面试题验证、代码评审前的复现很多时候不希望扔到外部平台上。自己部署一个内部版本放在内网或者私有服务器上才符合实际业务场景。很多大厂内部都有类似的“代码便签”工具就是这个需求。第三也是最重要的这是一个几乎覆盖 T3 Stack 全部核心特性的教学型项目。它有数据库模型设计、tRPC 路由、认证、文件流、部署配置业务复杂度适中不至于让人望而却步也不会简单到学不到东西。做一遍 t3code你对 T3 Stack 的掌握程度至少提升两个档位。1.3 整体架构设计先画一个逻辑分层不是网络拓扑别想复杂了前端层Next.js App Router 页面 Monaco Editor 编辑器组件 React Query 数据请求。API 层tRPC Router统一暴露snippetRouter、authRouter、tagRouter。数据层Prisma SQLite开发/ PostgreSQL生产存储用户、代码片段、标签关系。执行层服务端一个轻量沙箱模块通过child_process或worker_threads隔离运行用户代码限制超时时间和资源占用。这五层都是有明确边界的。我做项目的时候最容易翻车的就是把执行层塞到 API 层里同步跑看起来简单但一旦代码死循环整个 Next.js 服务都卡住。所以 t3code 里执行层必须独立。2. 核心功能拆解与选型分析2.1 编辑器选型Monaco vs CodeMirror在线编辑器这块我对比过两个方案这里直接给结论优先 Monaco Editor。Monaco 是 VS Code 同款编辑器内核对 TypeScript 有着完善的语法高亮、智能提示、错误标注这对一个主打 TypeScript 的代码工具来说非常重要。CodeMirror 6 更轻量、扩展性也好但它的 TypeScript 语言支持插件要靠自己组装提示细节和 Monaco 差一截。既然项目本身就叫 t3code体验上就必须把“写 TS 代码”这件事做到极致Monaco 是更稳的选择。但 Monaco 有一个经典痛点体积大、动态加载麻烦。如果不做分割直接静态 importmonaco-editor/react首屏 JS 会多出将近 4MB。我的处理方式是用next/dynamic做动态导入只在编辑器真正渲染时加载 Monaco同时把loader配置指向本地或 CDN 的monaco-editor/min/vs资源。关键代码大概是这样的import dynamic from next/dynamic; const MonacoEditor dynamic( () import(monaco-editor/react).then((mod) mod.default), { ssr: false, loading: () div classNameh-full bg-muted animate-pulse /, } );这样写之后编辑器只会在客户端出现ssr: false服务器不会浪费资源去渲染一个纯客户端组件。2.2 数据存储设计代码片段相关的数据模型是 t3code 的基石我用 Prisma 定义了几张核心表。这里重点说两件事碎片和版本。先看基础的Snippet模型model Snippet { id String id default(cuid()) title String language String default(typescript) content String db.Text createdAt DateTime default(now()) updatedAt DateTime updatedAt authorId String? author User? relation(fields: [authorId], references: [id], onDelete: Cascade) tags Tag[] relation(SnippetTags) }为什么authorId是可选因为早期版本我打算允许“游客模式”不登录也能创建一个 snippet这个体验对在线代码工具很重要用户通常不希望为了分享一段代码专门注册账号。等后面用户体系完整了再引导游客注册把已有 snippet 挂到用户名下。第二点是代码版本。实践经验告诉我一个代码分享工具如果不记录历史版本很快就会被骂。有一次我在团队里分享一段脚本后来改了逻辑同事看到的是新版本可他们之前复制的旧版本已经扩散出去了对不上问题。所以在Snippet之外我加了SnippetRevisionmodel SnippetRevision { id String id default(cuid()) snippetId String snippet Snippet relation(fields: [snippetId], references: [id], onDelete: Cascade) content String db.Text note String? createdAt DateTime default(now()) }每次用户点“保存”时如果内容和上一次不一致就插入一条 revision然后更新 snippet 的updatedAt。前端展示时提供一个版本下拉列表用户可以查看任意历史版本。这个设计增大了开发复杂度但从产品的角度看非常值。2.3 沙箱执行的设计思路这是整个 t3code 里风险最高、也最容易想当然的部分。很多人第一反应是“前端 eval 跑一下不就行了吗”——大错特错。先说为什么不能在前端跑用户代码。第一安全没法保证代码可以fetch任意地址、读取localStorage、甚至通过嵌套作用域做奇怪的事你根本没有限制手段。第二运行环境不真实浏览器里的 TS 和 Node 环境行为有很大差异比如process、Buffer、fs都没有。第三可控性差一个死循环就能卡死当前页面。所以我把代码执行放到了服务端用独立的子进程来做。大体方案是前端把代码通过 tRPC mutation 发给后端execRouter.runCode。后端把代码写入一个临时文件比如.tmp/exec/{id}.ts。用child_process.fork或者execFile跑一个独立的执行脚本这个脚本内部调用tsx来运行 TypeScripttsx对 TS 和 ESM 支持都很好。给子进程设置timeout比如 5 秒和maxBuffer比如 1MB 输出限制避免一段代码吃满资源。注意这只是“脆弱的演示级沙箱”不能用来跑不受信任的任意代码。真正的生产级沙箱需要容器级隔离Docker/Firecracker这个项目里不做但是实现时一定要在界面提示用户“代码在受控环境中执行请不要执行敏感命令”。这里体现的是工程权衡为了让项目能跑起来同时不把安全漏洞捅得太大用超时 独立进程 资源限制来降低风险并在文档里明确界限。3. 实操过程tRPC 路由与前端数据流3.1 tRPC Router 的完整设计T3 Stack 的灵魂就是 tRPC。它让你不用手写 API 客户端与服务器的对接代码前后端类型自动同步。在 t3code 里我把路由分成三个authRouter、snippetRouter、execRouter。以 snippet 的创建和查询为例核心路由大概长这样export const snippetRouter createTRPCRouter({ create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), language: z.enum([typescript, javascript]), content: z.string().min(1).max(50000), tags: z.array(z.string().max(20)).default([]), }) ) .mutation(async ({ ctx, input }) { const snippet await ctx.db.snippet.create({ data: { title: input.title, language: input.language, content: input.content, authorId: ctx.session?.user?.id ?? null, tags: { connectOrCreate: input.tags.map((name) ({ where: { name }, create: { name }, })), }, }, }); await ctx.db.snippetRevision.create({ data: { snippetId: snippet.id, content: input.content, note: initial version, }, }); return snippet; }), getById: publicProcedure .input(z.object({ id: z.string().cuid() })) .query(async ({ ctx, input }) { return ctx.db.snippet.findUnique({ where: { id: input.id }, include: { tags: true, author: { select: { name: true, image: true } } }, }); }), });这里有个小细节我把“创建 snippet 创建初始 revision”包在一个 mutation 里了因为二者是同一个业务事务。t3code 里真的需要事务的地方不多但这一段如果拆成两步大概率会出现“snippet 有了revision 却是空的”这种脏数据。你可不想排查这种问题。3.2 React Query 在 t3code 里的使用方式tRPC 和 React Query 是天作之合。前端调用一个 mutation 后需要立刻让列表页的数据刷新我用的是utils.snippet.list.invalidate()的方式const utils api.useUtils(); const createSnippet api.snippet.create.useMutation({ onSuccess: async () { await utils.snippet.list.invalidate(); router.push(/explore); }, onError: (error) { setFormError(error.message); }, });对比手动写 fetch 手动管理 loading/error 状态这种写法至少省 70% 的模板代码。更重要的是如果你把create的返回类型改了前端会立刻报类型错误不会等到运行时才发现“哦后端字段名换了”。这就是 t3code 把“类型安全”四个字落到实处的体现。3.3 Monaco 与 tRPC 的联动编辑器里的内容怎么发给后端我的做法是编辑器内容先存到一个 React 状态里并在编辑器onChange时实时更新但不会每次敲键盘都调 tRPC而是由用户手动点“保存”或者按Ctrl/Cmd S才触发 mutation。这样既避免了大量无意义请求又让“保存”这个动作有明确的用户心智。我还加了一个“运行”按钮流程是这样的把当前编辑器内容塞给execRouter.runCode等到返回结果后把 stdout 展示在编辑器下方的面板里。这里要注意 Monaco 的 onChange 默认触发非常频繁如果实现不当可能每秒发起几十次请求所以我用了一个useRef debounce在用户停止输入 800ms 后刷新代码的错误标注信息利用 Monaco 的 TypeScript language Service但运行还是手动触发。4. 常见问题与排查技巧实录以下问题都是我在做 t3code 的过程中实际遇到过的按出现频率从高到低排列每个都给定位思路和解决办法。4.1 Monaco 加载 404 / 样式错乱这是高频问题。症状是编辑器区域白屏控制台里一堆vs/loader.js或monaco-editor/min/vs/editor/editor.main.js的 404。原因通常是monaco-editor/react默认从 jsDelivr 加载资源而你的网络环境或本地代理拦截了 CDN。解决方法是把 Monaco 的 loader 指向你自己维护的资源import { loader } from monaco-editor/react; loader.config({ paths: { vs: /monaco/vs, }, });然后把monaco-editor/min/vs这个目录复制到public/monaco/vs。如果你用 Next.js可以直接把它放在public目录下部署时也会一起打包上传。但要注意这样整个 Monaco 资源会进入你的静态资源目录体积增加不少。我当时选择的是构建时从 npm 包里拷贝postinstall: cp -r node_modules/monaco-editor/min/vs public/monaco/vs这个方案实测下来比较稳定。另外如果同时使用next/dynamic和 loader.config注意loader.config要放在模块顶层并且只调用一次。4.2 tRPC mutation 在客户端报 401症状已经登录了但打开“保存 snippet”功能控制台报UNAUTHORIZED。排查思路其实很简单。tRPC 的protectedProcedure依赖 session 信息而 session 是从 cookie 里解析的。如果你把 API 的请求通过 Next.js 的 Server Component 转发或者在前端直接调用时没有携带 cookiesession 就会丢失。处理办法确认前端调用 tRPC 时走的是httpBatchLink并且return了ctx.res.headers的 set-cookie在 Next.js App Router 里如果使用 server-side tRPC caller需要手动把 cookies 传给createContext。一个常见的坑是你在本地开发时浏览器地址是localhost后端也在localhostcookie 没有问题但部署到线上后前端域名和后端 API 域名如果不同就需要配置credentials: include并把 CORS 白名单放开。4.3 子进程执行代码时内存占用暴涨我遇到过用户贴了一段疯狂递归的代码子进程内存占用冲到 600MB 才被我设置的timeout杀掉。虽然不会把整个服务拖垮但体验很糟糕。解决思路是双保险除了超时还要限制输出缓冲区和子进程的内存。import { fork } from node:child_process; const child fork(workerPath, [codePath], { execArgv: [--max-old-space-size128], stdio: [ignore, pipe, pipe, ipc], }); const timer setTimeout(() { child.kill(SIGKILL); reject(new Error(Timeout: code executed too long)); }, 5000); child.stdout.on(data, (chunk) { output chunk.toString(); if (output.length 1024 * 1024) { child.kill(SIGKILL); reject(new Error(Output limit exceeded)); } });--max-old-space-size128把 V8 堆限制到 128MB超了就 OOM进程会自动退出。配合SIGKILL兜底基本不会出现失控场景。当然这在多租户生产环境下不够严但作为内部工具或教学项目已经够用了。4.4 部署到 Vercel 后 Prisma 找不到数据表本地开发用 SQLite 很方便部署到 Vercel 就必须换成 PostgreSQL否则每次 Serverless 实例冷启动SQLite 文件都不在同一个磁盘上数据直接消失。而且 Prisma 的migrate deploy需要在构建阶段跑。我在 Vercel 上的操作顺序是创建 Postgres 数据库Vercel Postgres / Neon / Supabase 都行。在.env里配置DATABASE_URL和DIRECT_URL后者用于执行 migration 和种子脚本。把postinstall或构建命令设为prisma generate prisma migrate deploy。确认prisma schema里 datasource 的 provider 是postgresql不能用开发环境的sqlite。还有一个隐蔽的坑如果在本地用 SQLite 跑过 migrationPrisma 会在migrations目录生成带_sqlite的迁移记录直接切换到 Postgres 后migrate deploy可能失败。我建议在切换数据库 provider 时把migrations目录删了重新生成一套干净的迁移文件。4.5 问题速查表症状可能原因解决方案编辑器白屏Monaco CDN 资源加载失败本地化monaco-editor/min/vs资源配置 loader.pathstRPC 调用报 401session cookie 未传 / 未配置 credentials检查 httpBatchLink 配置确保 fetch 带 cookie子进程执行超时仍卡住没有调child.kill(SIGKILL)设置定时器 SIGKILL兜底部署后数据消失SQLite 文件不持久化切换 Postgres执行prisma migrate deployTypeScript 类型报错但本地正常tRPC 前后端类型同步没有自动生成执行prisma generate tsc --noEmit重启 dev server保存 snippet 后列表不刷新React Query 缓存未失效调用utils.snippet.list.invalidate()5. 后续可以扩展的方向t3code 如果只做到这里其实已经是一个完整的工具了。但我自己做完之后最感慨的是T3 Stack 的每一层都有继续加料的空间接下来如果我有精力会优先做下面三件事。5.1 接入 AI 代码补全现在 AI 辅助编码已经是标配了。t3code 可以在 Monaco 里集成一个“基于代码上下文的补全”按钮选中代码片段点击后把上下文发给一个模型接口返回的代码直接插入编辑器。技术上不复杂但产品体验会提升很多。唯一要注意的是不能把这段逻辑塞进 tRPC 的 mutation 里长时间阻塞应该用流式接口。Next.js App Router 的streaming能力在这里能派上大用场。5.2 支持多人实时协作实时协作是另一个方向。代码分享工具最自然的延伸是“两个人在同一个 snippet 上一起改”类似于 CodeSandbox 的 Live 模式。要做这个就得把 Monaco 的onChange事件用 WebSocket 广播给房间内其他成员并且处理并发冲突。推荐的方式是引入y-monaco和yjs的协同数据结构用 WebSocket 服务做 state broadcast。这一块工程量不小但做完之后 t3code 就能从“单人工具”变成“轻量协作工具”在团队里的使用价值会明显提升。5.3 模板化的运行环境目前 t3code 只支持纯 TypeScript 逻辑运行没有 UI 渲染能力。如果能扩展成一个“模板容器”用户可以选一个模板比如 React Tailwind、Node CLI 工具、甚至 Python通过服务端容器那它就接近一个简化版 CodeSandbox 了。但要注意这会让执行层的复杂度指数上升。我的建议是先保持“纯 TypeScript 执行”的定位把模板化做成下一步的方向而不是一上来就铺开。6. 我对 t3code 最终的体会最后聊点实在的。这个项目做完之后我自己最常使用它的场景是写技术文章的时候把一段类型体操的推导过程扔进去生成一个短链接放进文章里或者评审别人代码之前先把可疑函数跑一遍看输出是否符合预期。它不重但很顺手。如果你也想做一个类似的 t3code我给你的最终建议是不要一开始就追求把功能做全先把“创建 snippet 服务端运行 生成链接”这条最核心的链路跑通让它像一张便利贴一样好用然后再慢慢加分。T3 Stack 的学习曲线其实很平缓真正阻碍你的不是框架本身而是“意图不明确”——你想不清楚自己要做什么才会在使用 tRPC、Prisma 时不知道该选哪些 API、怎么设计 model。T3 Stack 这套东西最迷人的地方是你写的代码前端和服务器之间像同一个进程内部调用一样无缝类型从数据库一路贯穿到按钮。t3code 就像这个名字本身一样是整个技术栈的一次完整练习。以后再有人问我“怎么学 T3 Stack”我会直接让他照着 t3code 的流程走一遍比看十篇教程都管用。
阅读完成 · 觉得有帮助?
咨询建站