news 2026/10/26 17:01:59

T3 Stack实战:从零搭建类型安全的全栈应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
T3 Stack实战:从零搭建类型安全的全栈应用

如果你最近在开发者社区里刷到过t3code这个热词,大概率已经对 T3 Stack 这套技术栈有了点印象。我一开始看到它的时候,第一反应是:这不就是又一个全栈项目的模板吗?但真正自己上手把一整套东西跑通之后,我才意识到,它更像是一次对“类型安全到底能给业务带来什么”的完整验证。这篇文章我不想做那种罗列功能的说明书,而是把 t3code 从零到一搭建、踩坑、调优的真实过程掰开揉碎讲给你听,包括每个技术选型背后的考量、目录结构的组织逻辑、auth 会话接入时容易翻车的地方,以及上线部署时那几个最容易把人卡住的环境变量问题。不管你是刚接触全栈开发的新手,还是已经在用 React/Node 但想引入更严谨开发流程的工程师,这篇文章应该都能给到一些可以直接抄作业的东西。

1. 为什么做 t3code:从一套模板到一次全栈验证

1.1 先搞清楚 t3code 到底解决什么问题

t3code 本质上是一个以 T3 Stack 为核心的全栈项目底座。你把它 clone 下来或者通过脚手架初始化之后,得到的是一套包含用户认证、数据模型、API 路由、数据库迁移、前端页面和部署配置的完整代码骨架。换句话说,如果你有一个新产品想法,不需要再花两周去配 NextAuth、搭 Prisma、写 tRPC router,t3code 已经帮你把这些基础设施全部盘好了,你只需要往里面填业务。

但我不觉得它只是“效率工具”这么简单。我反复使用这套组合的真实感受是:它逼着你在写业务代码之前,先把数据模型、会话策略、类型边界这些“地基”问题想清楚。以前用传统的 REST API + 任意前端框架,前后端之间靠手写接口文档和 Postman 同步,时间一长难免出现字段对不上、参数传错类型的尴尬。t3code 这套东西会把这种“沟通成本”直接消灭在编译期——类型不对,编译都过不了,更不用说跑到线上才报错了。

这个项目适合谁来动手?我自己的判断是两类人。第一类是那些折腾过不少框架、但始终觉得项目后期维护很痛苦的中级开发者,用 t3code 走一遍全流程,你会对“类型安全能带来多少确定性”有非常具体的体感。第二类是打算快速验证产品 MVP 的独立开发者,有了这套底座,你省下来的时间可以用在真正的业务逻辑上,而不是反复配置环境。

1.2 技术选型:T3 Stack 为什么值得认真对待

T3 Stack 并非某个公司推出的企业级框架,而是一套社区里被反复验证过的组合拳,核心成员是 TypeScript、Next.js、Tailwind CSS、tRPC、Prisma,以及 NextAuth.js。我第一次看到这个组合时的想法和大部分人类似:每个单点都是常见选择,但把它们放在一起,化学反应比想象中要大。

先说 TypeScript 和 Next.js。Next.js 负责应用框架、路由和 SSR/SSG,TypeScript 负责让整个项目在一双无形的手里被“捏合”起来。这两个结合起来,直接让前后端共享同一套类型定义成为可能。以前写接口要手写类型定义再靠人工确保前后端一致,现在只要后端 router 的 schema 改了,前端所有调用点会立刻报错提示你修复。这种“一处修改、全链路感知”的体验,用过之后就很难回去了。

再来说 tRPC。如果不熟悉这个概念,你可以把它理解成一个不需要写 REST 路由的远程调用层。后端定义好某个 procedure,前端就能像调用本地函数一样调用它,而且是类型安全的。它解决问题的核心场景是:全栈应用里前后端都是自己人,没必要用 REST 的字段协商成本,直接 RPC 调用反而最快。t3code 整个项目的 API 层几乎都是靠 tRPC 支撑的,调用逻辑极其直白,新手看两遍代码就能上手。

Prisma 和 NextAuth 则负责数据与身份这最后一公里。Prisma 用 schema 文件定义数据库表结构并自动生成类型和迁移脚本,这比手写 SQL 和手工维护 migration 舒服得多。NextAuth 则把 OAuth、邮箱、凭证登录这些常见身份方案封装成插件式配置,配合 Prisma 做 session 持久化,整条认证链路在 t3code 里是闭环的。写到这里我想说,技术上没有绝对“最好”的组合,但 t3code 这套组合最让我喜欢的地方,是它的心智负担足够小——你不需要在不同范式之间来回切换,数据库、API、前端全在 TypeScript 的世界里,节奏非常统一。

2. 核心代码层的细节拆解:类型安全不是口号

2.1 数据模型先行:Prisma Schema 的设计取舍

在 t3code 里动手写业务之前,我习惯先把 Prisma schema 建好。这个顺序非常重要,因为 t3code 的整个类型链路都是从数据库模型出发的——Prisma 根据 schema 生成类型,tRPC 的输入输出基于这些类型,前端页面消费 tRPC 返回的数据,后端模型一旦改动,整条链路都会在编译期感知到。

先看一份我实际在项目里用过的简化 schema:

// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? access_token String? expires_at Int? token_type String? scope String? id_token String? session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

这里有一个非常值得注意的设计细节:User、Account、Session 这几张表几乎是 NextAuth + Prisma 的标准配套。Account 表用于存放第三方 OAuth 账号信息,Session 表用于 session 持久化。你不需要手动去建 session 逻辑,NextAuth 会自动读写这张表。如果你在 t3code 里选择的是 GitHub 登录,那 GitHub 返回的 access_token、id_token 都会落在 Account 表里。

另外,我在设计 Post 表时有意加了published字段,并且默认是 false。这个小设计在后端接口里非常重要——草稿和已发布文章在权限控制上完全不同,前端列表也只展示已发布内容。有时候我们急着写业务,容易把这种状态字段忽略掉,后期再加时就得做数据迁移,麻烦得很。t3code 的示范意义就在这儿:数据模型多想一步,后面能少踩很多坑。

说个实际动手时的建议。schema 文件改动后,一定要执行npx prisma migrate dev,而不是手动去数据库里改表。t3code 的迁移机制会把所有历史修改记录成 SQL 文件,团队协作时其他人只需要执行npx prisma migrate deploy就能把库表结构同步到一致状态。如果你图方便直接改库,项目里的 schema 文件就会和实际数据库脱节,后面再跑 migration 会报一堆不一致的错误。

2.2 tRPC 路由的编写范式:过程、中间件与错误处理

t3code 的 API 层不像传统项目那样放在一个api目录下定义多条路由,而是全部收在 tRPC router 里。刚开始不习惯,但用顺了之后,你会发现它的分层比 REST 更紧凑:一个 router 相当于一个领域的接口集,一个 procedure 相当于一个接口。

我在 t3code 里实际写过一段文章模块的 router,这里展示核心逻辑:

// src/server/api/routers/post.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const postRouter = createTRPCRouter({ list: publicProcedure .input( z.object({ limit: z.number().min(1).max(50).default(10), cursor: z.string().optional(), }) ) .query(async ({ ctx }) => { const posts = await ctx.db.post.findMany({ where: { published: true }, orderBy: { createdAt: "desc" }, take: 11, ...(cursor ? { skip: 1, cursor: { id: cursor } } : {}), select: { id: true, title: true, createdAt: true, author: { select: { name: true } }, }, }); let nextCursor: typeof cursor | undefined; if (posts.length > 10) { const nextItem = posts.pop(); nextCursor = nextItem!.id; } return { items: posts, nextCursor }; }), create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().min(1), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });

这段代码透露了几个关键信息。

第一,zod在输入校验里扮演核心角色。tRPC 和 zod 是深度集成的,你在 procedure 上写好的 input schema,不光运行时校验,类型也会自动推导给前端。前端调用postRouter.list时,编辑器里会明确提示你需要传limit和可选的cursor,传错类型直接报错。这种体验跟 REST API 里查文档调接口完全不是一个量级。

第二,protectedProcedure和publicProcedure的区别。前者会先走一次 session 校验,当前端调用create这个 mutation 时,后端可以放心地从ctx.session.user.id里拿到当前登录用户,不需要再去数据库里查。这个中间件机制在 t3code 里是被反复使用的,比如只有登录用户才能创建文章、只有作者本人才能更新或删除。我甚至写过一个小技巧:在protectedProcedure上包一层isAuthor中间件,用来校验文章作者身份,代码复用度非常高。

第三,cursor 分页的实现。为什么要取 11 条而不是 10 条?因为需要判断“还有没有下一页”。如果返回了 11 条,说明下一页还有数据,就 pop 掉多余的那条,再把它的 id 作为 nextCursor 返回。前端拿到 nextCursor 后,传给下一次请求的 cursor 参数。这套分页方案在 t3code 这种 tRPC 模式下非常自然,比传统的 page 分页更适合不断追加数据的动态列表。

再说错误处理。我已经遇到过太多接口在数据库写入失败时给前端返回一个 500 的闹心事。在 t3code 里我会用 tRPC 提供的TRPCError做更细粒度的错误分类:

import { TRPCError } from "@trpc/server"; if (!post || post.authorId !== ctx.session.user.id) { throw new TRPCError({ code: "FORBIDDEN", message: "您没有权限操作这篇文章", }); }

前端拿到 FORBIDDEN 之后,可以弹出友好的提示,而不是傻乎乎地显示“服务器错误”。这种错误流转方式,比我在传统 REST 项目里自己约定错误码规范要省心得多,因为 t3code 把整个约定内置了。

2.3 NextAuth 会话接入:打通后端与前端

在 t3code 里,认证是个绕不开的模块。我在项目里选择了 GitHub OAuth 登录加邮箱凭证登录的组合。NextAuth 的配置集中在src/server/auth.ts里,核心配置大概是下面这样的:

// src/server/auth.ts import NextAuth from "next-auth"; import GitHub from "next-auth/providers/github"; import CredentialsProvider from "next-auth/providers/credentials"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { db } from "./db"; export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(db), providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), CredentialsProvider({ name: "credentials", credentials: { email: { label: "邮箱", type: "email" }, password: { label: "密码", type: "password" }, }, async authorize(credentials) { // 从数据库校验用户名和密码 // 返回用户对象或 null }, }), ], session: { strategy: "database", maxAge: 30 * 24 * 60 * 60, // 30 天 }, callbacks: { session({ session, user }) { if (session.user) { session.user.id = user.id; } return session; }, }, });

这里有两个非常容易踩的坑。

第一个坑是 session 策略。如果使用了 PrismaAdapter,默认建议把strategy设为database。如果保持 jwt 策略,虽然也可以跑,但部分 adapter 相关的能力会受限,而且登录态存数据库更符合多实例部署时的需求。就在最近一次升级 NextAuth 版本时,我发现某些旧配置的写法被废弃了,比如JWT相关类型重命名等,升级后要手动修一遍才算完。

第二个坑是 session callback 里补充user.id。在新版本里,默认的 session.user 对象并不一定包含数据库里的 userId,需要你手动把user.id赋给session.user.id,前端才能通过session.user.id拿到当前登录用户的 ID。我最初没有配置这段,导致创建文章时 authorId 一直写不进去,排查了半天才发现 session 里根本没有用户 ID。这个问题如果你也遇到了,优先看看有没有加这一段 callback。

前端消费 session 用的是next-auth/react的useSession,t3code 在根布局里包了一层<SessionProvider>。然后页面里就可以直接这样写:

const { data: session, status } = useSession(); if (status === "loading") { return <div>正在加载登录状态...</div>; } if (!session?.user) { return <div>请先登录</div>; }

有经验的读者应该感受到了:这不就是“用户态”的管理吗?没错。在 t3code 里,前后端对“当前用户是谁”这件事有完全一致的认知,后端的protectedProcedure依靠这个 session 做鉴权,前端的 UI 也依靠同一个 session 做展示控制,安全感很强。

3. 从零搭建一次 t3code:完整实操过程

3.1 初始化项目:create-t3-app 的参数选择

如果你完全没有装过 t3code 这类项目,最简单的方式是用官方脚手架初始化。我具体执行的命令是:

npx create-t3-app@latest t3code

这一步会进入交互式提问,选择要包含的模块。我实际勾选的是:

  • Next.js
  • Tailwind CSS
  • tRPC
  • Prisma
  • NextAuth.js

TypeScript 是默认语言,没有选择不装的理由。其实 create-t3-app 还提供了多个选项,比如 ESLint、Prettier 这些 lint 相关配置我建议默认全开。至于要不要使用 npm 之外的包管理器,我用的是 pnpm,因为它的依赖管理更严格、安装更快,特别是像 t3code 这种页面和 API 代码混在一个进程里的全栈项目,pnpm 的隔离机制能避免不少依赖幽灵问题。

初始化完成之后,项目目录结构大致是:

t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ ├── pages/ │ ├── server/ │ │ ├── api/ │ │ └── auth.ts │ ├── trpc/ │ └── styles/ ├── public/ ├── env.js ├── next.config.js ├── package.json └── tsconfig.json

看到这个结构,你可能会有个疑问:为什么既有app目录又有pages目录?其实在新版 Next.js 中,app是最推荐的目录结构,pages则更多是兼容旧项目的遗留选择。t3code 初始化时默认使用的是app目录模式,它在布局、服务端组件、路由约定上更现代化。我建议新项目直接用 app 目录,不要为了兼容老代码去选 pages。

3.2 环境变量与数据库接入

项目初始化完成后,第一步先处理env环境变量。t3code 内置了环境变量校验,它依赖env.js这个模块去读取process.env,类型上用的是 zod 的校验。打开项目根目录下的.env.example文件,你会看到需要填这些常见值:

DATABASE_URL="file:./db.sqlite" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="your-secret-here" GITHUB_CLIENT_ID="xxx" GITHUB_CLIENT_SECRET="xxx"

开发阶段我直接用 SQLite,省去安装数据库服务的麻烦,DATABASE_URL写成 sqlite 文件地址即可。正式部署时再切换到 PostgreSQL,只需要改环境变量和重新跑迁移就行。这个切换过程非常顺滑,因为 Prisma 的抽象层把它包装得很好,不会因为换了数据库而翻新业务代码。

改完环境变量后,我会执行一次数据库迁移,让 Prisma 把建表 SQL 落到本地数据库:

npx prisma migrate dev --name init

这一步会调用 Prisma Client 的生成逻辑,同时生成node_modules/.prisma/client下的类型代码。如果中途报错,十有八九是.env配置有问题或者 schema 里还有语法错误,只要根据提示修完再执行一遍即可。

然后启动开发服务器:

npm run dev

访问http://localhost:3000的时候,你会看到一个带登录按钮的页面。点一下登录,走完 GitHub OAuth 流程,就能看到当前用户信息出现在页面右上角了。到这一步,整个 t3code 的基座已经跑通。

3.3 完成第一个业务闭环:文章模块

环境通顺之后,我开始实现 t3code 里的第一个完整业务模块:文章发布与列表。

第一步,在 Prisma schema 里加入 Post 模型,就像前文展示的那样。第二步,在post.tsrouter 里实现list和create两个 procedure。第三步,创建页面组件去消费这些接口。

t3code 里有个约定:后端的 tRPC 访问入口封装在src/trpc/server.ts和src/trpc/react.tsx中。服务端组件调用 tRPC 时,需要用到react-server版本的 adapter。具体页面代码可以这样写:

// src/app/posts/page.tsx import { api } from "~/trpc/server"; export default async function PostsPage() { const posts = await api.post.list({ limit: 10 }); return ( <main> <h1>文章列表</h1> <ul> {posts.items.map((post) => ( <li key={post.id}> <h2>{post.title}</h2> <p>{post.author?.name}</p> </li> ))} </ul> </main> ); }

这里的api.post.list是一个类型安全的异步调用。可能有人会奇怪,为什么服务端可以直接 await 一个 tRPC 调用?因为在服务端环境下,tRPC 会直连后端函数,不需要走 HTTP 网络请求。这套能力是 t3code 比较精华的部分,它让服务端渲染和 API 层天然共享类型和上下文,少了很多 RPC over HTTP 的序列化开销。

客户端组件里的写法也类似,区别是用api.post.list.useQuery(),然后配合useUtils做缓存失效。我在创建文章成功后会执行await utils.post.list.invalidate(),让列表自动刷新。这个体验比自己在事件回调里重新 fetch 清爽多了。

3.4 页面消费与样式落地

T3 Stack 的第四个点是 Tailwind CSS。t3code 的 UI 层全部采用 Tailwind 编程风格,几乎没有写独立 CSS 文件。我喜欢它的地方在于,原子类直接落在 className 上,整个页面组件一眼就能看出布局结构,不用来回切样式表。

登录后写文章的表单组件,我用的是客户端组件加 React Hook Form 的方式:

"use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { api } from "~/trpc/react"; const formSchema = z.object({ title: z.string().min(1, "标题不能为空"), content: z.string().min(1, "内容不能为空"), }); type FormValues = z.infer<typeof formSchema>; export function CreatePostForm() { const utils = api.useUtils(); const createPost = api.post.create.useMutation({ onSuccess: () => { void utils.post.list.invalidate(); }, }); const { register, handleSubmit, reset } = useForm<FormValues>({ resolver: zodResolver(formSchema), }); async function onSubmit(values: FormValues) { await createPost.mutateAsync(values); reset(); } return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-3"> <input {...register("title")} placeholder="标题" /> <textarea {...register("content")} placeholder="内容" /> <button type="submit">发布</button> </form> ); }

注意,这个表单的校验 schema 和后面 tRPC 里的 input schema 其实是重复定义的。理想的项目里,我倾向于把公共 zod schema 抽到一个共享模块里,前后端引用同一份,从根上避免校验规则分叉。t3code 也支持这样组织,只是默认模板没有帮我建,需要自己动手抽一层。

3.5 生产构建与部署要点

本地开发跑通之后,下一步是生产构建。执行:

npm run build

如果项目里有类型错误或者未使用的变量,Next.js 会直接报错。这一步在 t3code 里相当于把类型检查、lint 和编译一起跑完,非常严格,不适合“差不多得了”的开发态度。

部署方面,我选择的是 Vercel。由于 Next.js 本身就是 Vercel 一手推动的项目,部署配置几乎为零。唯一要注意的是,正式环境里数据库不能再用 SQLite 了,我会换成 PostgreSQL,并把DATABASE_URL指向线上数据库地址。同时,在首次部署后要手动执行一次数据库迁移,让生产库的表结构建立起来:

npx prisma migrate deploy

Vercel 构建时不会默认帮你执行 migrate,所以这个步骤必须在部署流水线里单独做。我记得第一次部署时就是少了这一步,线上直接 500,排查了十分钟才想起来数据库表还没建。

另一个部署时容易忽略的问题是NEXTAUTH_SECRET。如果这个值没有填写,下次构建时会生成一个临时的随机值,导致已经登录的用户 session 失效,严重的还会出现登录态反复跳变。你应该在部署平台的环境变量管理面板里填一个稳定且足够长的随机字符串,比如用openssl rand -base64 32生成。

4. 常见问题与排查实录:踩过的坑都在这里

4.1 类型报错与客户端/服务端边界

跑 t3code 过程中,我遇到最多的报错是类型错误,特别是 “Type 'X' is not assignable to type 'Y'”。新手可能会一看到类型错误就发怵,但我摸清楚套路后发现,这类问题基本都是三种原因。

第一种,Prisma 模型的 nullable 字段没考虑周全。比如User.name是String?,那么前端展示用户昵称时就必须处理name !== null的判断,否则 TypeScript 会提醒你“对象可能为 null”。我在写列表时经常遇到,后来习惯直接用post.author?.name ?? "匿名用户"兜底。

第二种,tRPC 输入输出类型和前端使用点的类型不匹配。比如后端list返回的是{ items: Post[]; nextCursor?: string },但前端试图把返回结果直接赋值给Post[]类型的变量,自然报错。遇到这种问题,我会先点开编辑器里的类型提示看一眼前端拿到的结构,而不是急着改代码。

第三种,客户端组件和服务端组件混用。Next.js 的 app 目录下,服务端组件里不能直接访问window等浏览器全局变量;反过来,客户端组件里也不能直接用服务端组件才能拿到的 tRPC 上下文。t3code 对此是有明确约定的:凡是需要交互的 UI,统一加"use client",然后在客户端组件里使用api.post.list.useQuery();服务端组件里才用api.post.list()。如果你在客户端组件里硬要调用服务端版 API,报错信息会明确指向这一层。

这里有个小技巧可以显著减少类型问题:改完 Prisma schema 或者 tRPC router 之后,先重新跑一次npm run dev让类型生成重新触发,再回编辑器看报错。否则 VSCode 里显示的还是旧类型,容易误导排查。

4.2 会话与授权相关的经典问题

NextAuth 在使用过程中换代很快,而且 API 经常有破坏性变化。t3code 遇到过的典型问题不外乎以下几类。

登录按钮点下去没有跳转授权页。先检查GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET是否正确,再确认回调地址和 GitHub OAuth App 里配置的Authorization callback URL完全一致。开发环境默认是http://localhost:3000/api/auth/callback/github,线上则是对应域名加同样的路径。如果回调地址不匹配,GitHub 会直接拒绝授权。

已经登录但 session 始终为空。这种情况,我第一个排查的就是SessionProvider有没有包住整棵应用树。其次检查src/server/auth.ts里的session.user.id是否赋值成功。还有一个隐藏问题:新版 NextAuth 的类型声明中,session.user上的自定义字段需要扩展类型定义,否则session.user.id会在 TypeScript 里直接报类型不存在。

登录态一会儿有、一会儿没有,像“抽风”一样。这个大概率是NEXTAUTH_SECRET不稳定导致的。本地开发时,如果你没有在.env里设置固定值,NextAuth 可能会自动生成临时密钥,进程重启后就变了,登录态自然失效。解决办法:在.env里写入固定的NEXTAUTH_SECRET,并用openssl rand -base64 32生成一个强度足够的随机值。

4.3 部署阶段的环境变量与数据库迁移

部署阶段最常见的问题,我梳理成下面这样一个排查速查表,方便你对照检查:

症状常见原因排查与解法
构建失败,提示缺少DATABASE_URL部署平台上的环境变量没配置在 Vercel/Railway 里添加DATABASE_URL指向线上数据库,并确保值以postgresql://开头
构建成功但页面访问 500数据库表结构还没创建执行npx prisma migrate deploy,确认迁移已生效
登录后 session 瞬间失效NEXTAUTH_SECRET为空或临时生成设置固定的、长度足够的NEXTAUTH_SECRET
OAuth 授权回调 404回调地址不匹配核对线上地址和 GitHub OAuth App 里的 callback URL
所有 API 请求都超时数据库连接地址不对或网络隔离确认数据库连接串是否允许外部访问,防火墙规则是否放行

还有一个容易被忽略的坑:Vercel 构建期间是没有正式网络访问的,如果构建命令里包含了prisma generate,那没问题,因为本地可以生成客户端。但你千万不要在构建阶段尝试连接 PostgreSQL 执行prisma migrate deploy,这不是构建的工作。正确的做法是把迁移放在部署完成后的独立步骤里,或者用 GitHub Actions 在构建后手动触发迁移。

4.4 性能与体验的细节优化

t3code 跑通之后,我花了一些时间做性能和体验层面的优化,这里分享几个比较实用的点。

数据库查询尽量做字段裁剪。比如列表页只需要文章标题和作者名,就不要select文章全文内容。Prisma 的select参数能显著减少数据库返回的数据量,也让前端类型更“窄”。如果你不做裁剪,Post 模型里的content大字段会整包传到前端,白白浪费带宽和内存。

tRPC 查询尽量用无限查询的useInfiniteQuery配合后端 cursor 分页。列表页用无限滚动替代传统的上一页/下一页,交互更顺畅,也符合 t3code 这种现代全栈应用的预期。

图像资源建议用 Next.js 的next/image组件。它会自动做响应式尺寸和格式优化。我在展示用户头像时直接使用了<Image>,宽度、高度、alt 都给出明确值,避免 layout shift。这个小优化虽然不是技术难点,但能明显提升页面的 LCP 表现。

React 服务端组件下,能用服务端组件的地方尽量不要“use client”。t3code 默认的首页很多区块都不需要客户端事件,全都写在服务端组件里,渲染更快。只有表单、点赞、评论这类需要交互的部分,才声明为客户端组件。

最后再分享一个小技巧。如果你在 t3code 里经常修改 Prisma schema,可以在package.json的 scripts 里加一条"postinstall": "prisma generate",这样团队成员 clone 项目后执行一次npm install就会自动生成最新的 Prisma Client,不用每次手动跑prisma generate。这个细节我以前不在意,直到有一次同事拉完代码后因为没跑 generate 导致类型缺失才意识到它的必要性。t3code 这类高度依赖类型生成的项目,把生成步骤自动化是体验优先级很高的一件事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 17:00:41

YOLO26与SPPF_attention:目标检测的多尺度注意力创新

1. YOLO26与SPPF_attention的创新设计解析在目标检测领域&#xff0c;YOLO系列算法一直以其高效的检测速度和良好的精度平衡著称。最近我们团队在YOLO26架构中原创研发了SPPF_attention模块&#xff0c;这个改进的核心在于重新设计了传统的SPPF&#xff08;Spatial Pyramid Poo…

作者头像 李华
网站建设 2026/10/26 17:00:18

空时自适应信号处理STAP与ACP工程实践:从原理到雷达杂波抑制落地

简介&#xff1a;这份资源面向雷达信号处理方向的学生、科研人员与工程实践者&#xff0c;聚焦空时自适应信号处理&#xff08;STAP&#xff09;这一提升目标检测与抗干扰能力的核心技术。内容围绕合成孔径雷达场景&#xff0c;讲解如何通过空域与时间域联合滤波区分弱目标与强…

作者头像 李华
网站建设 2026/10/26 16:59:39

C语言中前置++与后置++的底层原理与最佳实践

1. 前置与后置的本质区别 在C语言中&#xff0c;运算符的前置&#xff08;i&#xff09;和后置&#xff08;i&#xff09;形式虽然都能实现变量自增1的效果&#xff0c;但它们的底层机制和编译行为存在本质差异。理解这个差异是正确使用它们的关键。 1.1 运算符的返回值差异 …

作者头像 李华
网站建设 2026/10/26 16:58:43

南瑞101规约C代码解析:从RTU101.rar到报文调试实战

简介&#xff1a;这份资源围绕IEC 101规约与南瑞101实现展开&#xff0c;面向电力自动化方向的开发者、RTU调试人员及电力系统通信协议学习者&#xff0c;帮助理解远动通信中RTU与主站之间的数据交互机制。压缩包为rar格式&#xff0c;共2个文件&#xff0c;包含1个cpp源码与1个…

作者头像 李华
网站建设 2026/10/26 16:56:59

人物音色档案加音频参考:构建统一且有辨识度的AI角色声音

一、为什么要看这份榜单 AI角色声音制作通常包含几个连续环节&#xff1a;确定人物设定、建立声音方向、准备音频参考、生成台词、结合画面和剧情进行调整。如果这些环节彼此割裂&#xff0c;常见问题包括&#xff1a; 同一角色在不同片段中音色变化明显&#xff1b;角色的年龄…

作者头像 李华