☰
基于TypeScript的T3 code全栈工程化实践:构建类型安全应用
2026/10/27 15:52:44 网站建设 项目流程

在技术社区里泡久了,会发现一个有意思的现象:很多项目写着写着就散了,不是功能做不出来,而是代码库维护不下去——类型对不上、接口改一处崩三处、新成员入职光看项目结构就要花一周。我自己也经历过这种痛苦,后来搭了一套名为t3code的全栈项目基底,把前后端类型安全、数据建模、构建部署这些容易翻车的环节全部拧成了一股绳。这篇文章就把我搭建 t3code 时的完整思路、技术选型逻辑、落地过程以及踩过的坑记录在这里,希望能给正在纠结全栈项目技术选型、或者想了解 T3 生态组合拳怎么用的朋友一些参考。

t3code 本质上不是一个“框架”,而是一套基于现代 TypeScript 生态的全栈工程化实践集合。它把 TypeScript 的静态类型检查能力从前端一直贯穿到数据库层,让 API 的请求参数、响应体、数据库字段在编译期就被校验,运行期几乎不会出现“前端传错字段、后端取不到值”这种低级问题。如果你厌倦了 API 文档和人肉对齐字段,或者想用一个项目同时覆盖 Web 应用、后台管理系统、甚至服务端脚本,t3code 这套打法可以直接抄作业。

1. t3code 的选型逻辑:为什么是 T3 这套组合

1.1 T3 生态三板斧的核心优势

T3 指的是TypeScript + Tailwind CSS + tRPC这套组合,外加 Next.js 做应用框架、Prisma 做 ORM、NextAuth 做认证。很多人第一次听到这个组合会觉得“工具也太多了”,但实际用下来你会发现每一环都是被逼出来的。

先说 TypeScript。t3code 选择全栈 TypeScript 最大的原因不是“类型安全很酷”,而是业务迭代速度极快时,类型系统能兜住重构的底线。比如你改一个用户数据结构,如果前后端都是 JS,改了接口后常有漏改的地方,运行到那儿才报错;而全栈 TS 会在你改动的一瞬间把关联的所有引用点标红,改完基本就等于测完。t3code 里有个经验数字:因为类型检查的存在,联调阶段的时间大约能省掉 40% 到 50%,这部分时间平时都浪费在“你对文档、我对字段”上了。

再说 Tailwind CSS。我知道很多人对原子化 CSS 有成见,觉得 HTML 里一坨 class 很难看。t3code 选择它的原因特别实际:不需要为类名起名了。做技术分享类项目时,页面更新频繁,今天加个卡片、明天调个间距,如果用传统 CSS 方案,每加一个模块就要想“这个是叫 card-wrapper 还是 card-container”,这个心智负担在项目初期很容易拖慢节奏。Tailwind 的 utility class 直接表达样式意图,配合 Prettier 插件自动排序,写起来很顺手。而且 t3code 用了 Tailwind 的 JIT 模式,最终产出的 CSS 体积非常小,首屏加载不背锅。

最后就是 tRPC。这是 T3 组合里最容易被低估的一环。传统的前后端交互方式是 REST 或者 GraphQL,REST 需要手动维护 API 文档和类型定义,GraphQL 要写 Schema、配 Resolver,复杂度不低。tRPC 的思路完全不同:它让前端直接“调用”后端函数,像调用本地函数一样自然。类型定义在前后端之间自动共享,不需要生成 SDK,不需要手写类型。这意味着删掉一个 API 函数时,前端引用处会直接标红,这种“编译器帮我做 API 文档”的体验,用习惯了就回不去了。

1.2 Monorepo 结构:t3code 的工程化底座

t3code 选择了 Turborepo 做 Monorepo 管理,而不是像很多简单项目那样一个文件夹塞到底。原因在于 t3code 从一开始就包含三个应用:一个面向用户的 Next.js Web 端、一个面向管理员的精简后台、以及一个跑定时任务的 Node 脚本服务。如果不用 Monorepo,这三个应用各自建仓库,会面临三份依赖、三套构建配置、跨仓库共享代码只能发包的困境,迭代起来非常痛苦。

Monorepo 带来的直接好处是共享代码包化。t3code 把类型定义、工具函数、UI 基础组件抽成了@t3code/shared、@t3code/ui、@t3code/config这几个内部包。Turbo 的增量构建会识别包之间的依赖关系,比如改了@t3code/shared里的工具函数,它只会重新构建依赖了这个包的子应用,不会把三个应用全部重新跑一遍。实测下来,整体构建时间比单仓单应用有 30% 以上的缩减。而且因为所有代码在一个仓库里,跨应用的重构(比如把某个逻辑从 Web 端下沉到 shared 包)就是一次 PR 的事,不需要跨仓库联调。

Monorepo 还有一个隐性好处:统一配置文件管理。ESLint 配置、Prettier 配置、TypeScript 的 tsconfig 基础配置都收敛在根目录,三个应用共用一套规范,新人入职后无论看哪个应用,代码风格都是一样的,维护成本大大降低。当然,Monorepo 的代价是 Git 仓库会变大、CI 缓存需要设计好,这个后面在部署章节我会展开讲。

1.3 t3code 的目录结构与职责划分

t3code 的目录结构如下,这个结构经过几轮迭代后基本稳定了:

t3code/ ├── apps/ │ ├── web/ # 面向用户的主应用(Next.js) │ ├── admin/ # 后台管理应用(Next.js,精简版) │ └── worker/ # 定时任务/脚本服务(Node + Prisma) ├── packages/ │ ├── ui/ # 基础 UI 组件库(React + Tailwind) │ ├── shared/ # 共享类型、工具函数、常量 │ └── config/ # 共享配置文件(ESLint、Prettier、tsconfig) ├── package.json └── turbo.json

每个应用内部再按功能域划分,比如apps/web下面有modules/article、modules/user、modules/comment等目录,每个模块自带api.ts、types.ts、components/、hooks/。这样的划分逻辑很直白:以后想单独拆模块出去独立部署,边界已经天然画好了。t3code 在发展初期做过一次大重构,把原本按“页面”组织的代码改为按“功能域”组织,虽然当时痛苦,但后来加新功能时所有相关文件在同一目录下改,效率提升非常明显。

2. 从零搭建 t3code:类型安全的胶水层与数据建模

2.1 初始化项目与依赖安装的注意事项

初始化 t3code 时我用了create-t3-app脚手架,它提供了一个非常稳的起点:Next.js 是 App Router 模式、Tailwind 预装、tRPC 和 NextAuth 的 boilerplate 齐全、Prisma 接好 SQLite 用于早期开发。如果你是从零开始,我强烈建议直接用它初始化,省去自己磨合配置的时间。

这里有个需要注意的点:create-t3-app默认用的是 SQLite,本地开发很顺手,但如果你的项目目标是长期运营,建议一开始就切换成 PostgreSQL,避免后期因为数据库特性差异踩坑。t3code 在早期没注意这点,后来从 SQLite 迁到 PostgreSQL 时遇到了一堆隐性问题——比如 SQLite 对部分查询的宽松处理在 Postgres 里变成了严格报错。如果你的项目还处于起步阶段,可以直接在环境变量里把 DATABASE_URL 指向一个 Postgres 实例,Prisma schema 里的 provider 改成postgresql,然后重新生成 migration,整个过程并不复杂。

依赖安装时还要注意npm 和 pnpm 的区别。t3code 选择了 pnpm,因为 Monorepo 场景下 pnpm 的软链式 node_modules 结构能天然避免“幽灵依赖”问题,而且安装速度快、磁盘占用小。用 npm 跑 Monorepo 不是不行,但你需要手动维护 workspaces,依赖提升规则也更折腾。如果你打算复刻 t3code 这套结构,直接上 pnpm 会省很多心。

2.2 tRPC 的 Router 设计:从少即是多到合理的边界划分

tRPC 的核心是 Router,你可以把它想象成一组后端函数的集合,每个函数通过input校验接收参数,通过output定义返回结构。t3code 里 Router 的划分遵循“领域自治”原则:文章相关操作放articleRouter,用户相关放userRouter,评论相关放commentRouter,每个 Router 在独立文件中定义,再用router({...})合并进根 Router。

举个例子,文章模块的 Router 长这样:

import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; export const articleRouter = createTRPCRouter({ // 获取文章列表,公开接口 list: publicProcedure .input( z.object({ cursor: z.string().optional(), limit: z.number().min(1).max(50).default(10), tag: z.string().optional(), }) ) .query(async ({ ctx, input }) => { const articles = await ctx.prisma.article.findMany({ take: input.limit + 1, // 多查一条用于判断是否还有下一页 where: { tags: input.tag ? { some: { name: input.tag } } : undefined }, orderBy: { createdAt: "desc" }, cursor: input.cursor ? { id: input.cursor } : undefined, include: { author: { select: { name: true, image: true } } }, }); let nextCursor: typeof input.cursor = undefined; if (articles.length > input.limit) { const nextItem = articles.pop(); nextCursor = nextItem!.id; } return { articles, nextCursor }; }), // 创建文章,需要登录 create: protectedProcedure .input( z.object({ title: z.string().min(1).max(200), content: z.string().min(1), tags: z.array(z.string()).optional(), }) ) .mutation(async ({ ctx, input }) => { return ctx.prisma.article.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, tags: input.tags ? { connectOrCreate: input.tags.map((name) => ({ where: { name }, create: { name }, })), } : undefined, }, }); }), });

这个例子可以看出 tRPC 的两个优势。一是zodschema 同时承担入参校验和类型推导,前端调用时输入参数会自动获得类型提示,不用写二次定义。二是 procedure 分publicProcedure和protectedProcedure,受保护的接口在服务端会先检查 session,没有登录直接抛异常。t3code 里所有涉及写操作的接口都用了 protectedProcedure,避免了不少安全隐患。

Router 设计里有一个常见的坑:把 Router 做得太细碎。比如把“获取单篇文章”和“获取文章点赞数”拆成两个 Router 文件,看似职责清晰,实际前端一次渲染要调用两三个接口,网络开销翻倍。t3code 的应对方式是:以页面渲染需求为维度聚合数据。比如文章详情页,getBySlug这一个 query 就把文章内容、作者信息、标签、点赞数全部返回,前端一个请求拿到所有数据,渲染无缝衔接。tRPC 本身没有规定你必须怎么分 Router,这个边界要靠自己在实践中摸索。

2.3 Prisma 数据建模与迁移管理

数据层是 T3 组合里最容易被低估但也是最高价值的部分。t3code 使用 Prisma 作为 ORM,它的核心价值在于schema 即文档、迁移即版本控制。

schema 文件里定义模型,比如文章和标签是多对多关系、用户和文章是一对多关系:

model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? articles Article[] comments Comment[] sessions Session[] } model Article { id String @id @default(cuid()) slug String @unique title String content String published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt author User @relation(fields: [authorId], references: [id]) authorId String tags Tag[] comments Comment[] likes Like[] } model Tag { id String @id @default(cuid()) name String @unique articles Article[] } model Comment { id String @id @default(cuid()) content String createdAt DateTime @default(now()) author User @relation(fields: [authorId], references: [id]) authorId String article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) articleId String } model Like { id String @id @default(cuid()) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) userId String article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) articleId String @@unique([userId, articleId]) }

用 Prisma 后,改表结构走一次prisma migrate dev就能生成迁移文件,并且自动同步到数据库。t3code 里所有历史的迁移文件都保留在prisma/migrations目录,意味着你可以随时从零建库并恢复到任意历史版本。对于团队协作来说,另一个人 pull 代码后执行prisma migrate deploy就能把自己的库状态对齐到最新,不需要手动执行 SQL,非常稳。

在数据建模层面,t3code 有几个小技巧值得分享:第一个是所有表的主键都用cuid()而不是自增 ID。自增 ID 在分库分表、数据合并、URL 暴露隐私方面都有隐患,cuid 从根本上避免了这些问题。第二个是用@unique约束业务上的唯一字段,比如文章的 slug、用户的 email、Tag 的 name,这样代码里无需先查再建,直接写create就能靠数据库保证不重复。第三个是合理使用onDelete: Cascade,删除文章时级联删除评论和点赞,不会留下孤儿数据。这些看起来不起眼的决定,到了数据量起来之后会省掉大量修复脏数据的时间。

2.4 前端数据获取与状态管理的选型

聊完数据和 API 层,前端怎么拿数据、怎么管理状态也需要定调子。t3code 的选择是:服务端数据全走 tRPC Query + React Query,客户端状态只保留 UI 自身的临时状态。

React Query(t3code 中叫@tanstack/react-query)在 create-t3-app 里默认集成,它解决的痛点是缓存和失效刷新。比如文章发布后,列表页的数据会变陈旧,t3code 的做法是在 mutation 成功后调用utils.article.invalidate(),让所有相关查询自动重新获取。这套机制比手动 setState + 手动刷新接口的旧方案优雅太多。你只需要声明“这个查询依赖哪些数据”,React Query 会帮你处理缓存一致性问题。

对于全局状态,t3code 刻意没有引入 Redux 或 Zustand。因为大部分全局信息(用户会话、主题、配置)要么在服务端 session 里、要么在 React Context 里就能解决。引入全局状态库容易导致状态泛滥,最终变成“任何地方都在 setState,没人知道当前值是哪来的”。如果你在做类似 t3code 的项目,我的建议是先别上状态库,把 React Query 的缓存和 Context 用好了,90% 的状态需求都能覆盖。真到需要跨模块共享复杂状态时,再考虑 Zustand 这种轻量方案也不迟。

3. 核心功能模块的实现与实测

3.1 用户认证与会话管理:NextAuth 的正确接入方式

认证功能是 t3code 最早实现的模块。这里用的方案是 NextAuth(现在叫 Auth.js)的 Credentials Provider + 数据库 session。为什么不直接上 GitHub OAuth 这类第三方登录?因为 t3code 定位于技术分享平台,初期用户群体小,邮箱 + 密码注册足够,后面要加 OAuth 再扩展即可。用 Credentials Provider 需要自己处理密码哈希,t3code 选择了 bcrypt,密码存储层面不会有明文风险。

NextAuth 接入时最需要注意的是配置 session 策略。t3code 使用数据库 session(默认的 session strategy 其实更适合第三方 OAuth,但既然用 Credentials 登录,建议改成 database strategy),把 session 数据存到 Session 表里,这样服务端任何接口都可以通过ctx.session判断当前用户身份。同时把 JWT 时间设短一点,比如 24 小时过期,过期后自动回到登录页,安全性更高。登出流程直接调用 NextAuth 的signOut()即可,服务端会清除 session 记录,无需额外处理。

实际使用中有一个细节坑:首次注册时密码确认流程。如果让用户先填邮箱密码提交,再接一个“确认密码”字段,这个字段必须在前端判断一致后才发给服务端,不能把两个密码都传给后端。t3code 用 zod 在客户端的 schema 里做了refine校验,确保用户在提交前就看到密码不一致的提示,体验比服务端返回错误好得多。

3.2 内容发布与编辑器选择:Markdown 渲染的兼容性陷阱

文章发布是 t3code 的核心功能,编辑器的选型花了不少时间。最终采用了 CodeMirror 的自定义模式 + 前端 Markdown 预览的方案,理由很实在——文章内容需要频繁插入代码块,CodeMirror 对代码编辑体验足够好,而完整富文本编辑器(像 TipTap 这类)在代码块场景下的表现反而别扭。Markdown 对技术类内容极其友好,写代码、贴配置、列文档都很自然,存储上就是文本字段,查询和迁移几乎没有额外负担。

但在 Markdown 渲染环节要特别留意,t3code 实测踩过不少坑。一个是XSS 问题接着 markdown-it 等渲染引擎时的高危 HTML:如果允许用户输入任意 HTML,攻击者可以在文章里嵌入<script>脚本。t3code 对用户生成的 HTML 做了严格白名单过滤,只允许少数安全标签和 class 放行,代码块统一转义处理。另一个是代码块的语法高亮对语言的识别:有些 Markdown 编辑器高亮效果好,但渲染端语言列表不齐全,导致高亮失效。t3code 最终选择了 rehype-highlight,按需加载语言包,并且为不支持的代码语言提供最简单的文本底色,保证界面统一。

此外,还有一个小众但很实用的细节:文章的 slug 生成。如果直接让用户自由输入 URL 别名,可能出现中文未编码、空格未替换等问题。t3code 写了一个工具函数,在保存时自动把标题转换为 URL 友好的 slug,遇到重名加后缀,确保文章链接永远干净可分享。这个函数被放在了@t3code/shared包里,前后的验证、列表页跳转、SEO 配置都能复用,省去了重复实现。

3.3 评论与互动逻辑:并发场景下的数据一致性

评论模块看起来简单,但 t3code 在处理“点赞”这个互动逻辑时遇到了并发问题。最早版本的实现是“前端点击后先update,把 likeCount 字段递增 1”,结果在高并发下会出现丢失更新。比如 5 个人同时点赞,数据库层可能只加了 1 次而不是 5 次。

t3code 的修复方案有两层。第一层是数据库约束,Like 表上有@@unique([userId, articleId]),同一用户对同一篇文章只能有一条点赞记录,从数据源上杜绝了重复点赞;第二层是计数查询而非计数存储——前端不再维护 likeCount 字段,而是通过ctx.prisma.like.count({ where: { articleId } })实时查询点赞数量。这个方案看起来多了一次 count 查询,但 MySQL/Postgres 对带索引的 count 查询很快,而像 t3code 这种内容平台的数据量,性能完全吃得消。后来为了减少一次性查询开销,列表页的热门排序才引入了一个可延迟更新的likeCount冗余字段,但这个字段只做排序展示,详情页权威数据仍以查询结果为准。

评论内容的层级结构上,t3code 目前只做了一层,不做楼中楼,因为楼中楼会显著增加查询和渲染复杂度。如果你的项目从一开始就需要多级嵌套评论,建议用 closure table 或路径枚举方案,而不是在表里存 parentId 后递归查询,否则数据量大后性能会非常难看。

3.4 搜索功能的取舍:直接数据库模糊查询还是引入搜索引擎

搜索功能是 t3code 上线后用户提需求最多的一项。第一版我直接用 Prisma 的contains模糊查询:

await prisma.article.findMany({ where: { OR: [ { title: { contains: keyword, mode: "insensitive" } }, { content: { contains: keyword, mode: "insensitive" } }, ], }, take: 20, });

这个方案在几百篇文章时表现良好,但到几千篇以上就明显力不从心了。LIKE %keyword%无法利用索引,全表扫描会拖慢整个数据库。t3code 的阶段性选择是:先上了 PostgreSQL 的原生全文搜索 tsvector,用一个生成列存储文章标题和内容的词向量,查询时直接用to_tsquery匹配。这一步改动不大,但搜索速度提升了一个数量级。如果你用的不是 Postgres,可以考虑 Meilisearch 这类轻量搜索引擎,部署简单、中文分词也好用。

不过说实话,对内容型平台来说,“搜索”的价值不只是找到相关文章,搜索结果的排序质量更影响体验。数据库全文搜索的排序通常是简单的 TF 权重,很难做到“标题命中 > 正文命中 > 新文章 > 经典文章”这种智能排序。如果你打算做一个长期内容站,我建议早早调研专门搜索服务,把搜索排重、同义词、近义词这些机制留给专业工具,不要自己在数据库层硬拼。

4. 部署与运维:t3code 上线过程中的踩坑记录

4.1 数据库连接池打满:Prisma 在 Serverless 环境下的配置问题

t3code 上线前后遇到的第一个大坑,就是数据库连接数被打满。起因是 Prisma 在 Serverless 环境下默认连接数是基于进程数的,而 Serverless 实例弹性扩容后可能瞬间创建几十个进程,每个进程都维持一个连接池,直接把数据库的连接上限耗尽。

解决方式是在 Prisma 初始化时,通过环境变量控制连接池大小,并开启连接超时策略:

// prisma.ts import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], datasourceUrl: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma; }

同时在连接串里追加连接池参数:

DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=10

这就把单个实例的连接数限制在 5 个左右,避免实例一多就打满库。如果你在 Serverless 或容器平台上部署 Prisma 应用,一定要把 connection_limit 和连接复用机制想清楚,这是最容易被忽略但对稳定性影响巨大的参数。

4.2 环境变量的传播链条:一个拼写错误引发的排查

环境变量问题是我在 t3code 运维中遇到最诡异的一次。现象是线上文章图片加载 404,但这个图片地址在本地开发完全正常。排查了图片存储服务、CDN 配置都没问题,最后发现是环境变量里上传服务的 endpoint 少了末尾的/,导致拼接 URL 时出现了//,CDN 把它解析成了新路径。这类问题用一句话总结就是:环境变量不是“配完就不管”的东西,它们的拼写、格式、解析逻辑都要写进代码检查里。

t3code 之后在初始化逻辑里加了一层启动时校验:用 zod 定义环境变量 schema,启动时解析一次,任何缺失或格式错误都会在应用启动阶段直接抛错,而不是等到运行时爆出奇怪现象。类似的操作你在 t3code 里也会看到很多——凡是配置类的东西,全部集中解析,集中校验,避免“配置错误 + 运行时自洽 = 表象诡异”这种组合。

4.3 容器化部署中 Next.js 最容易忽略的细节

t3code 最终采用 Docker 部署,Next.js 应用镜像、Prisma 迁移容器、Nginx 反向代理三层结构。在容器化过程中有几个细节必须注意。

第一个是Next.js 的输出模式。t3code 使用的是 Node.js server 模式(而非纯静态导出),因为服务端需要处理动态请求和生成 meta 信息。这种模式下,容器里的 Node 进程必须长驻,且要设置好HOSTNAME=0.0.0.0和端口环境变量,否则容器内默认监听 localhost 会把外部请求全部拒掉。

第二个是migration 的执行时机。t3code 没有把prisma migrate deploy塞进应用启动命令里,因为容器漂移时多个副本同时跑迁移可能产生锁冲突。正确做法是拆成一个独立的 initContainer(在 Kubernetes 场景)或部署编排中的一次性 job,确保数据库 schema 升级只在发布流程中执行一次。上了这个流程后,schema 变更再没出过迁移类问题。

第三个是日志与监控。t3code 在应用里配置了 pino 日志库,JSON 格式输出,配合日志采集工具做结构化检索。同时用 Sentry 采集前端错误。容器部署不是“能跑就行”,可观测性一定要在一开始就接好,不然后期线上问题排查就像大海捞针。

4.4 CI/CD 流水线的构建策略

Monorepo 的 CI/CD 相比单仓要复杂得多,t3code 的流水线刚开始走了弯路——每次 push 都会把三个应用全部构建一遍,一次 CI 五六分钟,非常耽误节奏。后来用了 Turborepo 的turbo run build配合 remote caching,实现了只构建变更过的部分。具体做法是在 CI 配置里设置--filter=[changed],然后如下命令只会构建受影响的包和它们依赖的下游:

pnpm turbo run build --filter=[web,admin,worker]

这里的[web,admin,worker]不是固定值,而是 Turborepo 根据 git diff 动态解析的。实际体验下来,大多数 PR 的 CI 时间都压缩到了 1 分钟以内,只有全量依赖更新才会触发全量构建。CI 流水线里的另一个细节是Prisma Client 的生成步骤。因为 t3code 用了共享包,多个应用依赖同一个 Prisma schema,生成 Client 的动作最好只在根级别的任务里执行一次,然后让子任务引用生成产物,避免每个应用重复跑prisma generate。

5. 性能优化与未来的迭代方向

5.1 首屏加载的优化手段:字体、图片、代码分包

内容型 Web 应用最怕首屏又慢又重。t3code 的第一版加载资源有 2.3MB,主要消耗在 JS bundle、图片和字体上。后来逐项排查做了三个针对性优化。

字体是第一个大头。原本引入了两种中文字体且没有做 subset,几十 MB 的字库全部加载了。后来改用系统字体栈优先,自定义字体只在标题场景使用,并且用next/font的自动 subset 能力,体积缩到原来的 10% 以内。

图片是第二个大头。文章封面图如果不压缩,一张动辄两三 MB。t3code 用了 Next.js 的next/image组件,配合sharp做自动 WebP 转换和尺寸裁剪,列表页的小图走缩略图,详情页才加载原图。这一改整站流量开销骤降一半以上。

代码分包是第三个突破口。Next.js 默认按路由分包,但 t3code 的编辑器页面包含 CodeMirror 的全部语言包,体积不小。通过动态 import +ssr: false,把编辑器变成首屏不加载的异步组件,只用到达编辑器页面时才按需加载。经过这三项调整,首屏资源从 2.3MB 降到 450KB 左右,Lighthouse 的 Performance 分数从 58 升到了 90 以上。

5.2 缓存策略:从 React Query 到 Redis 的递进

数据缓存是降低数据库压力的关键。t3code 在缓存层面的策略分两层。

第一层是React Query 客户端缓存。列表页、详情页的数据设置staleTime为 5 到 10 分钟,在这段时间内滚动浏览不会重复请求接口。但这里有一个权衡:用户发布新文章后,其他用户看到的列表不会立刻更新,除非 mutation 后执行 invalidate。t3code 的做法是:发布类操作后立即 invalidate 列表,评论点赞这类轻操作则容忍 5 分钟的延迟。

第二层是服务端级别的缓存。当内容 API 的访问量上升后,短时间大量相同 query 会打到数据库,React Query 只能缓存浏览器端,服务端压力还是大。t3code 在热点接口上引入了 Redis 缓存,比如文章详情页的 GET 请求,先查 Redis,命中则直接返回,未命中再查库并写入缓存。缓存 key 的设计是article:detail:${slug},过期时间 5 分钟。这个改动让数据库读负载掉了近 40%。如果你预计某个数据会被高频读取且更新频率不高,服务端缓存几乎是必选项。

5.3 t3code 的下一步:多端扩展与开放 API

t3code 这套基底目前已经稳定运行,后续的迭代方向有两个比较明确。

一个是移动端扩展。Web 端效果虽然不错,但很多用户习惯在手机上刷内容,直接做 App 的成本和心力都不小。t3code 的 API 层因为已经通过 tRPC 做了类型安全封装,理论上可以对外提供一套 HTTP 接口让移动端调用,或者直接用 React Native + expo 复用@t3code/shared里的类型定义和工具函数。Monorepo 的结构天然适合这种演进——新的移动应用作为apps/mobile加入仓库,共享 UI 组件和 API 层代码即可。

另一个是开放 API 给第三方。等用户量进一步上来后,可以做 API keys 管理、限流、用量统计,对外提供只读接口让其他开发者还能基于 t3code 的内容做二次开发。这块的复杂度主要在鉴权和计量,底层数据结构暂时不需要大改动。

写在最后的实操心得

如果你要照 t3code 的路线搭自己的项目,我最想强调的一件事:先把数据模型想清楚,再动手写业务代码。因为 T3 生态里数据模型直接牵动 Prisma schema、API 的 zod 校验、前端类型定义,数据模型稳定了,后面所有层级的效率都会翻倍。另外,Monorepo 虽好,但如果你只是一个人做一个简单原型,没有必要为了 Monorepo 而上 Monorepo,单仓应用起步更轻快。t3code 选择 Monorepo 是因为从第一天起就有三个应用的需求,这个背景决定了它的架构。最后再分享一个小技巧:T3 项目里所有跨前后端共享的类型,都尽量放在@t3code/shared包的types.ts里,前端和后端都从那里引用,永远不要各自复制一份类型定义。坚持这个习惯,几年后你会感谢现在的自己。

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

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

立即咨询