资讯详情

T3 Stack全栈实战:用tRPC打造类型安全的代码片段管理工具

📅 2026/10/9 20:10:55 | 华诺云谱 👁 阅读
T3 Stack全栈实战:用tRPC打造类型安全的代码片段管理工具
去年年底我在整理自己的代码片段时发现一个尴尬的事实同一段分页逻辑、同一个 hook 的用法、同一个坑的解决方案分别躺在 GitHub gist、公司内网文档、本地 Typora 和微信收藏里真正想找的时候哪个都不可靠。当时正处于重新学习全栈的空窗期就把“搭一个顺手的代码片段管理工具”当作练手项目来做这个项目就是 t3code——一个基于 T3 StackNext.js、TypeScript、Tailwind CSS、tRPC、Prisma的轻量级片段收藏与分享工具。这套技术组合一度是社区讨论度最高的全栈方案之一但网上大部分文章都在讲“怎么跑通一个 demo”很少讲清楚工程落地时每个决定背后的取舍。这篇文章会把 t3code 从技术选型、数据建模、核心实现、权限处理到部署优化的完整过程聊一遍重点不是贴一遍官方文档而是把我自己踩过的坑和验证过的做法讲透。如果你也在纠结要不要用 tRPC 做全栈、或者拿一个真实项目练手这里面的经验可以直接抄。1. 为什么把 t3code 押注在 T3 Stack 上我的选型逻辑1.1 需求先摆出来t3code 到底要做什么t3code 的定位一开始就不是“再做一个语法高亮的在线贴代码网站”而是解决我自己的实际问题把散落在各处的代码片段收进一个统一入口能快速搜、能分类、能一键复制最好还能分享给同事。基于这个目标我列出了第一版必须有的功能通过 GitHub OAuth 登录片段与用户绑定创建/编辑/删除片段支持 Markdown 正文与代码块高亮用标签对片段分类支持标题和正文的关键词搜索个人主页能看到自己所有的公开片段片段可见性控制公开可分享、私密仅自己可见这个范围不大不小恰好覆盖了一个全栈项目该有的基本模块认证、CRUD、关系型数据模型、搜索、权限判断。相比当时流行的“先搭个 Blog 练手”t3code 更偏向工具型应用逻辑上足够有趣又不会大到做不完。1.2 对比了三条路线之后我为什么选了 tRPCT3 Stack 的核心不是 Next.js 也不是 Tailwind而是整个体系里最容易被误解的 tRPC。在决定用 tRPC 之前我把另外两条路线也认真对比过。先看传统 REST 方案用 Next.js 的 Route Handlers 或者 Pages API 写/api/snippets、/api/snippets/[id]前端再用 fetch 手动封装。这套方案最大的痛点是类型割裂——后端定义一个Snippet类型前端要根据接口文档再手写一个相似的 interface字段一多、接口一改两边同步全靠自觉。用 OpenAPI 生成器可以缓解但小项目引入一整套代码生成流程维护成本反而更高。再看 GraphQL 方案用 Apollo 或者 urql 搭一套 client/serverSchema 要写、Resolver 要写、Fragment 要写语义强、缓存设计灵活但是对 t3code 这种 CRUD 为主的应用来说收益撑不起复杂度。第七个字段、加一个查询条件都要动 schema 定义开发速度明显被拖累。tRPC 的逻辑很简单函数即 API类型从服务端自动推到客户端。我不用定义路由字符串也不用维护 interface直接在后端写一个snippetRouter.create前端 import 过来就能拿到完全类型安全的调用入口。它的学习成本几乎为零因为本质上就是在写 TypeScript 函数。这是我当时做的快速对比供参考维度RESTGraphQLtRPC类型安全手动维护或代码生成Schema 与类型需对齐端到端自动推导学习成本低高极低缓存控制手动内建配合 React Query 手动适合项目体量中大型、多端复杂聚合查询前后端同构的全栈应用tRPC 适合 t3code 的关键前提是前后端由我一个人维护且都跑在 TypeScript 下。如果未来要开放公共 API 给第三方tRPC 还要额外包装一层 REST 出口那是后话现阶段没有必要为了“万一有一天”牺牲开发效率。至于为什么没有从零手写一套架构而直接用create-t3-app脚手架理由很朴素脚手架把 NextAuth、Prisma、tRPC、Tailwind 的集成细节处理好了把精力留给业务逻辑。T3 Stack 的哲学是“在需要复杂之前保持简单”我不需要一开始就背上微服务、消息队列、Redis 这些多余的装备。1.3 Next.js 版本选型App Router 与 CRUD 应用的适配t3code 使用的是 Next.js 14 的 App Router。选择 App Router 而不是 Pages Router除了官方方向的考虑最主要的原因是 App Router 的 Server Components 和 tRPC 配合得当页面级的数据预取可以在服务端完成交互层面的更新走 client component分工清晰。当然 App Router 也有需要花时间理解的部分比如缓存语义。我在开发中就因为在 Server Component 里缓存了 tRPC 数据导致编辑片段后页面不刷新这个坑在后面的排查章节会专门展开。先记住一个结论对于 t3code 这类强交互、频繁变化的工具型应用页面数据要么不缓存要么每5秒做个短缓存绝对不能依赖默认的静态缓存行为。2. t3code 的骨架目录结构、数据模型与第一版边界2.1 create-t3-app 之后我把目录改成了这样create-t3-app生成的默认结构已经很清晰我在它的基础上做了少量调整。最终核心结构如下t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ │ ├── (auth)/ # 登录相关页面 │ │ ├── (main)/ # 主应用布局 │ │ │ ├── snippets/ │ │ │ │ ├── [id]/ # 片段详情页 │ │ │ │ └── new/ # 新建片段页 │ │ │ ├── u/[username]/ # 用户主页 │ │ │ └── page.tsx # 首页推荐流 │ │ └── api/trpc/[trpc]/route.ts │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts # tRPC 路由聚合 │ │ │ ├── routers/ │ │ │ │ ├── snippet.ts │ │ │ │ ├── user.ts │ │ │ │ └── auth.ts │ │ │ └── trpc.ts # tRPC 初始化与上下文 │ │ ├── auth.ts # NextAuth 配置 │ │ └── db.ts # Prisma Client 实例 │ └── styles/globals.css这里有一个值得说明的点我把业务页面按(auth)和(main)做了 Route Group 分离而不是为了省事把所有页面平铺在app/下。Route Group 不会影响 URL 路径但可以在不同页面组之间用不同的布局——比如主应用的顶部导航和登录页的极简布局就分开了。这种组织方式让新增一个页面时第一反应是“它属于哪个区域”而不是“它该放在哪一层”对长期维护非常重要。2.2 Prisma 模型User、Snippet、Tag 是怎么设计的数据模型是 t3code 最核心的设计决策它直接决定了功能边界。第一版我定了三个模型User、Snippet、Tag外加一个关系表SnippetTag。model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? snippets Snippet[] accounts Account[] sessions Session[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Snippet { id String id default(cuid()) title String description String? body String db.Text language String default(plaintext) visibility Visibility default(PRIVATE) authorId String author User relation(fields: [authorId], references: [id], onDelete: Cascade) tags SnippetTag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt index([authorId, visibility, createdAt(sort: Desc)]) index([language]) } model Tag { id String id default(cuid()) name String unique snippets SnippetTag[] } model SnippetTag { snippetId String tagId String snippet Snippet relation(fields: [snippetId], references: [id], onDelete: Cascade) tag Tag relation(fields: [tagId], references: [id], onDelete: Cascade) id([snippetId, tagId]) } enum Visibility { PUBLIC PRIVATE }几个关键决策背后都有原因为什么 Tag 不用字符串数组而单独拆一张表数组在查询“所有包含某标签的片段”时只能用字符串匹配性能和准确性都不理想。拆成多对多关系后findMany({ where: { tags: { some: { tag: { name: react } } } } })就是标准的关系查询以后做标签热度统计也只是 groupBy 的事。而且Tag.name加了唯一约束重复标签会被自然合并。为什么SnippetTag用复合主键而不是自增 id一个片段和一个标签的组合只会出现一次复合主键在关系层面就杜绝了重复关联省掉了一次手动去重查询。为什么index([authorId, visibility, createdAt(sort: Desc)])要建这个复合索引这是用户主页和首页流最核心的查询模式找到某个人或所有人的公开片段按时间倒序展示。如果不在数据库层加索引数据量上来之后这个查询会走全表扫描。这个索引的设计我在上线前才补上当时已经能感觉到查询变慢后面迁移到 Postgres 时一起加上了。2.3 数据库选型SQLite 起步最后换 Postgrescreate-t3-app默认会给 Prisma 配 SQLite我初期也一直用 SQLite 开发。这个选择让我非常受益本地零配置、数据文件直接入库、测试快完全不用关心数据库服务。但我也明确知道这是“开发期便利”不是生产方案。SQLite 在并发写和网络访问上天生不适合线上服务部署阶段我换成了 Postgres用的 Neon 云数据库。迁移过程比预想中顺利主要工作是三步把schema.prisma里的provider从sqlite改成postgresql、把db.Text这类方言标记换成 Postgres 兼容写法、重新生成 migration。数据类型上String在两边语义一致enum也都是原生支持所以没有伤筋动骨。如果一开始就上 Postgres本地开发会多出不少环境维护成本。先用轻量工具跑通业务逻辑上线前再换重型数据库这个路径对个人项目非常友好。3. 核心链路实现tRPC 的类型安全如何从数据库一路推到按钮3.1 路由组织与 tRPC 初始化tRPC 的核心单位是router和procedure。router是一组相关接口的集合procedure是每个具体的查询或写操作。t3code 里我分成了三个 routersnippetRouter片段增删改查、userRouter用户信息、authRouter登录态相关。初始化部分遵循社区规范做法先把受保护的 procedure 定义好// src/server/api/trpc.ts import { initTRPC, TRPCError } from trpc/server; import superjson from superjson; import { ZodError } from zod; import type { Context } from ./context; const t initTRPC.contextContext().create({ transformer: superjson, errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); export const createTRPCRouter t.router; export const publicProcedure t.procedure; const enforceUserIsAuthed t.middleware(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); }); export const protectedProcedure t.procedure.use(enforceUserIsAuthed);protectedProcedure是理解 tRPC 权限体系的关键入口。它通过中间件拦截所有需要登录的请求一旦 session 不存在就直接抛UNAUTHORIZED后续业务代码不需要再重复判断登录态。有一个细节值得单独说superjson。它作为 transformer 能让 tRPC 在客户端和服务端之间序列化Date、Map这类特殊类型。我在 Snippet 模型里用了createdAt: DateTime如果不用 superjson返回给前端的时间会变成字符串再想用Intl.DateTimeFormat格式化还要自己 parse。这个选择虽然只是一个配置项但省掉了很多隐性类型问题。3.2 用 Zod 把输入端守住tRPC 生态里输入校验基本绑定 Zod。我不信任任何“只靠前端表单验证”的写法因为 API 是可以被直接调用的服务端不做校验等于把数据库裸奔在公网上。创建片段的 procedure 长这样// src/server/api/routers/snippet.ts import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ../trpc; const createSnippetSchema z.object({ title: z.string().min(1, 标题不能为空).max(120), description: z.string().max(500).optional(), body: z.string().min(1), language: z.string().max(50).default(plaintext), tags: z.array(z.string().max(30)).max(10).default([]), visibility: z.enum([PUBLIC, PRIVATE]), }); export const snippetRouter createTRPCRouter({ create: protectedProcedure .input(createSnippetSchema) .mutation(async ({ ctx, input }) { const userId ctx.session.user.id; const snippet await ctx.db.snippet.create({ data: { title: input.title, description: input.description, body: input.body, language: input.language, visibility: input.visibility, authorId: userId, tags: { create: input.tags.map((name) ({ tag: { connectOrCreate: { where: { name }, create: { name }, }, }, })), }, }, include: { tags: { include: { tag: true } } }, }); return snippet; }), });这里有两个容易忽略的设计一是 tag 的入库方式用了connectOrCreate。用户在表单里填了三个标签其中两个已经存在于库里、一个是新的这个写法会只创建新的那个两个旧的自动关联。不需要先查库判断存在性Prisma 内部帮我处理了竞态。二是protectedProcedure保证了ctx.session.user一定存在因此可以直接用ctx.session.user.id作为authorId。user id 的类型是stringPrisma 的authorId也是string这个链路从 NextAuth 到数据库全程对齐。3.3 页面消费 APIuseQuery、useMutation 与“失效缓存”服务端定义完成后客户端的使用体验是 tRPC 最让人舒服的地方。// src/app/(main)/snippets/[id]/page.tsx 中的客户端组件 use client; import { api } from ~/trpc/react; export function SnippetActions({ snippetId }: { snippetId: string }) { const utils api.useUtils(); const updateSnippet api.snippet.update.useMutation({ onSuccess: async () { await utils.snippet.getById.invalidate({ id: snippetId }); await utils.snippet.getAll.invalidate(); }, }); const copyToClipboard async () { const snippet await api.snippet.getById.fetch({ id: snippetId }); await navigator.clipboard.writeText(snippet.body); }; return ( div classNameflex gap-2 button onClick{copyToClipboard}复制代码/button button onClick{() updateSnippet.mutate({ id: snippetId, visibility: PUBLIC })} 设为公开 /button /div ); }我特别想讲一下useMutation的onSuccess里为什么要invalidate而不是手动修改本地缓存。tRPC 客户端内置了 React Queryinvalidate的含义是“让这条 key 的数据失效”下次组件重新渲染时会自动重新请求。这样做的优势是你不需要关心当前页面上有多少个地方用了这份数据一次失效全部同步永远不会出现“A 页面改了、B 页面还显示旧值”的不一致问题。刚开始我也尝试过用 optimistic update乐观更新来提升手感把新值先提交到缓存、失败再回滚。但 t3code 的操作频率不高用户写一个片段再点保存等几百毫秒的请求完全是可接受的体验。乐观更新适合高频交互点赞、拖拽排序对表单提交类操作用不好反而会引入回滚 bug。这是我实测之后得出的结论。4. 用户态与权限校验NextAuth 在 t3code 里不能只“配好了事”4.1 NextAuth 与 Prisma 的第一次握手t3code 的登录方式选择 GitHub OAuth。使用 NextAuth 的 GitHub Provider 是标准流程但真正要把认证和业务数据打通还需要让 NextAuth 的session.user.id和 Prisma 里的User.id对应起来。create-t3-app模板里已经处理了这个映射NextAuth 的authConfig中注册了PrismaAdapter适配器会在用户首次 OAuth 登录时自动创建数据库里的User记录。我在自己的api/auth.ts里额外加了一个检查步骤// src/server/auth.ts 中关键片段 callbacks: { session({ session, user }) { if (session.user) { session.user.id user.id; } return session; }, },这个回调做的事很简单把数据库里的User.id注入到 session。不做的后果是前端session.user.id是 undefined后续所有关联查询都会失败。很多教程在配置 NextAuth 后没有加这个回调导致登录成功后一脸懵。4.2 所有者校验更新和删除时必须确认“这是我的片段”有了登录态还不够t3code 还需要保证一个用户只能改自己的片段。tRPC 提供了protectedProcedure但它只解决“有人登录”不解决“这个人是谁”。我在更新和删除的 procedure 里做了两层检查update: protectedProcedure .input(z.object({ id: z.string(), title: z.string().min(1).optional() })) .mutation(async ({ ctx, input }) { const existing await ctx.db.snippet.findUnique({ where: { id: input.id }, select: { authorId: true }, }); if (!existing) { throw new TRPCError({ code: NOT_FOUND }); } if (existing.authorId ! ctx.session.user.id) { throw new TRPCError({ code: FORBIDDEN }); } // 通过校验后再执行更新 return ctx.db.snippet.update({ where: { id: input.id }, data: { title: input.title }, }); }),这里的关键是先findUnique拿到authorId再和 session 里的userId比较。看似多了一次查询但这是避免“权限字段被拼进 where 条件时误伤他人数据”的最稳妥做法。有人会想用update({ where: { id: input.id, authorId: currentUser } })一步完成但 Prisma 的where里如果你传了authorId当记录不存在时它返回的是“更新了 0 行”错误信息对用户不友好而且语义上不够明确。先查后改配合FORBIDDEN错误码前端才能做精准提示。4.3 可见性控制公开片段与私密片段snippet 的visibility字段承担了内容权限分层。公开片段任何人可以看私密片段只有作者自己能看。查询端我在publicProcedure.getById里做了判断getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) { const snippet await ctx.db.snippet.findUnique({ where: { id: input.id }, include: { author: true, tags: { include: { tag: true } } }, }); if (!snippet) { throw new TRPCError({ code: NOT_FOUND }); } if (snippet.visibility PRIVATE snippet.authorId ! ctx.session?.user?.id) { throw new TRPCError({ code: FORBIDDEN }); } return snippet; }),这个模式我建议所有做内容型应用的开发者都记下来访问控制不是在“渲染层”判断而是在“数据获取层”判断。只要数据是通过服务端接口拿的未授权内容根本不会离开数据库前端的任何绕过都无效。5. 开发期最折磨人的三个 Bug完整排查过程复盘这一节是全文最有价值的章节。下面三个问题每一个都让我花掉至少半天时间把排查过程完整复盘比直接给结论更能帮大家建立自己的调试思路。5.1 OAuth 登录后 Prisma 直接报错NextAuth 与 Prisma 的适配版本冲突现象点击 GitHub 登录后回调页面先是空白随后浏览器地址栏出现errorConfiguration控制台能看到 Prisma 抛出的TypeError: e.includes is not a function类似错误。排查过程我首先确认 GitHub Provider 的clientId和clientSecret没有拼错排除了配置问题。接着看 Prisma 的错误栈发现是next-auth/prisma-adapter在执行createUser时挂了。那段时间 NextAuth 正好处在 v4 向 v5后来改名为 Auth.js的过渡期create-t3-app生成的依赖版本组合比较激进我装的 Prisma adapter 和 NextAuth 版本之间 schema 字段名不匹配导致回调函数里读取不到 email 字段。解决方案把 NextAuth 固定到大版本 v4 的稳定版同时调整schema.prisma里 Account、Session、VerificationToken 的模型字段使其和 adapter 版本要求的字段严格对齐。之后重新生成 Prisma Client、重启 dev server登录恢复正常。经验总结遇到“登录失败 Prisma 报错”时第一反应不要怀疑自己的代码优先检查 next-auth 和 prisma adapter 的版本是否官方验证过。脚手架生成的依赖可能有激进更新项目开始前把关键依赖锁在稳定版能省很多事。5.2 修改片段后页面不刷新Next.js App Router 的缓存把 tRPC 数据“冻结”了现象我在详情页编辑片段标题保存成功后返回列表页标题还是旧值。刷新浏览器才恢复正常但每次都要手动刷新交互体验非常差。排查过程一开始怀疑 tRPC 的invalidate没有生效。但我在onSuccess里加了console.log确认 invalidate 已经被调用。然后怀疑 React Query 缓存问题把gcTime改成 0 也没用。直到我打开 Network 面板才发现问题列表页根本没有重新发请求。问题其实出在 Next.js App Router列表页是一个 Server Component里面用了 tRPC 的服务端调用方式createCaller直接查询。App Router 默认会缓存服务端渲染结果所以无论客户端 invalidate 多少次只要 Server Component 的缓存没失效页面就不会重新执行查询。解决方案有两层解法。一是把列表页标记为动态渲染在页面顶部加export const dynamic force-dynamic强制每次请求都重新渲染二是客户端交互完调用router.refresh()主动触发 Server Components 重渲染。我在 t3code 里实际用的是force-dynamic因为列表页数据变化频繁缓存收益本来就低。经验总结App Router 的缓存是“全局默认开启”的而 tRPC 的 invalidate 只管客户端。两者叠加时服务端缓存会让数据更新看起来像是失效了。遇到“改了数据但页面不刷新”先分清这一层页面是 Server Component 还是 Client Component再去决定用invalidate、router.refresh()还是force-dynamic比闭着眼睛改配置高效得多。5.3 部署到 Vercel 后 API 冷启动慢、构建产物超限现象本地开发一切正常部署到 Vercel 后却出现两类问题一是函数包超过 250 MB 上限构建直接失败二是勉强通过构建后首次请求 API 要等好几秒。排查过程首先是构建上限问题原因是 Prisma 默认只带了本机平台的 query engine 二进制文件但 Vercel 是linux平台需要通过binaryTargets显式声明。其次冷启动慢是因为我把整个prisma/client打进了 edge runtime 的函数里Prisma Engine 在无服务器环境里第一次初始化要加载原生二进制开销很大。解决方案在 Prisma schema 中添加多平台二进制目标generator client { provider prisma-client-js binaryTargets [native, linux-musl-openssl-3.0.x] }同时把涉及 Prisma 的 tRPC 路由强制运行在 Node.js runtime而不是 Edge runtime// src/app/api/trpc/[trpc]/route.ts export const runtime nodejs;这两步改完后构建产物体积显著下降冷启动从“秒级”降到“几百毫秒”整体可用性才达到可以给别人演示的水平。经验总结“本地能跑”和“线上能跑”之间隔着一个部署平台的运行时差异。用 Prisma 时务必要先了解目标部署平台的系统架构在 schema 里提前配好 binaryTargets用 NextAuth Prisma 时更要小心 Edge Runtime 的限制数据库查询相关的逻辑尽量留在 Node.js runtime。6. 上线后的性能优化、成本控制与下一版本规划6.1 从 SQLite 迁移到 Postgres以及我加的三个索引前面说过上线前我把数据库切到了 Postgres。迁移本身并不痛苦真正需要思考的是索引设计。我在使用过程中总结出三个高频查询模式逐个加索引查询场景数据特征加的索引用户主页按时间倒序列出公开片段同一用户片段多、需要过滤可见性index([authorId, visibility, createdAt(sort: Desc)])按语言筛选片段语言字段重复度高index([language])按标签关联查询片段多对多关联频繁SnippetTag表主键已经是(snippetId, tagId)再加反向索引这里想说的是不要为了“未来可能用到”去提前建一堆索引。索引加快查询的同时会拖慢写入而且占用额外空间。我是在实际功能开发完、发现某个查询明显慢或者 EXPLAIN 显示走全表扫描之后才针对性加的。个人项目没有量级压力提前优化通常只是自我感动。6.2 前端体积与体验优化从加载时长说起t3code 前端的核心页面是片段列表和片段详情这里有一个很容易被忽略的问题react-syntax-highlighter之类的语法高亮库体积非常大如果把它全量打进主 bundle首屏加载会变得很慢。我做的优化很简单把语法高亮组件用next/dynamic动态加载只在渲染代码块时才引入对应语言的高亮模块const CodeBlock dynamic(() import(~/components/CodeBlock), { loading: () pre classNameanimate-pulse加载中.../pre, });按语言拆分成独立的 prism language 模块后常见十几二十种语言的语法定义总大小从 800 KB 降到 150 KB 左右。首页列表不再需要高亮组件因此首屏完整 JS 体积明显下降TBTTotal Blocking Time在本地性能测试里从 1.8s 降到了 0.4s 左右。如果你也在做代码展示类应用这个优化思路几乎必做。先量一量页面加载的主 bundle 里有没有高亮库再决定拆不拆。6.3 t3code 下一步能做什么t3code 的功能基本达到我最初设定的目标后我把它部署上线给几个朋友试用收集到了一些真实反馈。下一版本比较确定要加的功能有这几个方向分享链接的独立路由目前详情页 URL 结构是/snippets/[id]不够直观计划改成/s/{slug}风格方便同事之间口头交流代码片段引用在描述里支持snippet-id的语法A 片段可以直接引用 B 片段应对“我上次给你那段代码”这类对话场景简单全文搜索先不引入 ElasticSearch 这类重型方案直接在 Postgres 里用全文检索配合现有标签过滤应该能覆盖当前数据量下的搜索需求公开 API如果未来真有人要用 t3code 的数据做集成把它对应的执行计划写在 tRPC 中方便之后的迁移。在持续迭代过程中我也会调整这样一个问题的边界它到底要不要成为一个多人协作的应用目前我的答案是暂时不。协作意味着评论、通知、权限组复杂度是指数级上升的。t3code 的核心价值是“个人知识库 轻量分享”守住这个边界功能才不会跑偏。后续如果某天真的需要协作再拆独立的 service 或者引入数据库级权限也不迟。最后说一个我认为比任何技术方案都重要的经验所有优化的前提是你真的在持续使用自己的项目。t3code 能走到上线这一步是因为我每天都在往里面存新的代码片段而不是做完一版就丢在仓库里吃灰。当你成为自己产品的第一个重度用户会发现很多“想当然的设计”自然会被淘汰留下来的都是真实需求。我个人在写这套东西时最庆幸的一点就是没有在第一个月就加入花里胡哨的功能而是先把“收藏、搜索、分享”这条主线打磨顺畅——建议你也沿着这个思路来全栈能力是在不断完善自己工具的过程中提升的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑