news 2026/10/14 17:21:41

T3 Stack全栈开发实战:TypeScript、Next.js与tRPC的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
T3 Stack全栈开发实战:TypeScript、Next.js与tRPC的完整链路

1. 从项目代号说起:t3code 到底是个什么项目

第一次看到"t3code"这个名字,是在一个开发者社群的讨论帖里。当时大家正在聊个人项目的命名习惯,有人晒出了自己的仓库列表,其中一个文件夹就叫 t3code,旁边备注是"全栈练手项目"。点进去看了下结构,发现这不是一个简单的 Demo,而是一个把 TypeScript 全栈技术栈完整跑通的个人作品集项目。

t3code 这个代号其实挺有意思。t3 在技术圈通常指向那个著名的全栈技术栈组合——TypeScript、Tailwind CSS、tRPC 这三件套,也有人习惯再加个 Next.js 凑成 T3 Stack。但这个项目实际做的是:用 Next.js 14 的 App Router 搭建前端页面,配合 tRPC 做端到端类型安全的 API 层,再用 Prisma 操作 PostgreSQL 数据库,最后部署到 Vercel 上。说白了,这就是一个把当下主流全栈开发范式从头到尾走了一遍的实践项目,目标用户是那些想学习现代 Web 开发、又不想只看碎片化教程的开发者。

我之所以觉得这个项目值得拆开来讲,是因为它踩中了一个很实际的痛点:网上的教程要么只讲前端、要么只讲接口,很少有一个项目能把类型安全、数据库设计、部署上线这些环节串成一条完整的链路。t3code 的价值不在于它的功能有多复杂,而在于它把"从零到上线"的完整路径展示了出来。

这篇文章我会从项目规划、技术选型、目录设计、核心功能实现、部署运维这几个维度来拆解,重点讲清楚每一步为什么这么做,以及我在复现过程中踩过的坑。无论你是刚接触全栈开发的新手,还是想找个完整项目练手的中级开发者,这篇文章应该都能给你一些可以落地的参考。

2. 项目整体设计与技术选型思路

2.1 为什么选 T3 Stack 这套组合

先说结论:T3 Stack 不是性能最优解,也不是功能最全解,但它是对个人开发者最友好的一套组合。我自己的体会是,它解决了两件最烦人的事——类型不匹配和配置地狱。

传统的前后端分离开发中,前端调后端接口时,经常出现字段对不上、类型报错的问题。明明后端返回了userName,前端代码里写的是username,运行到一半才发现。T3 Stack 的核心理念是让类型从前端到后端全程贯通,你改了数据库模型,TypeScript 类型、API 路由、前端调用全部跟着变,编译期就能发现问题,不用等到运行时报错。

再说配置问题。Next.js 本身就是一个"约定大于配置"的框架,App Router 推出后更是把路由、布局、加载状态这些逻辑都收纳到了文件系统里。配合 tRPC,你不用手写 REST 接口文档,也不用单独维护 GraphQL 的 schema,方法和类型都在同一个地方定义,前后端直接"共享"这一份代码。

这套技术栈适合什么场景?团队不大、产品迭代快、个人开发者想独立搞定前后端的时候最合适。如果你是在大厂做那种十几个微服务的大型系统,tRPC 可能反而不合适,但作为个人项目或者中小型应用的底座,它能把开发效率拉高一截。

2.2 核心依赖与版本选型

我这里把 t3code 项目的核心依赖清单列一下,方便你对照参考。这些版本是我实测能跑通的组合,直接用不会有兼容性问题。

依赖包版本用途说明注意事项
next^14.2.xReact 全栈框架App Router 模式下服务端组件和客户端组件的边界要理清
react^18.3.xUI 库Next 14 对应 React 18,别轻易升 19
@tanstack/react-query^5.x服务端状态管理tRPC 底层依赖它做数据缓存和请求生命周期
@trpc/server / client / next^10.45.x端到端类型安全 RPC版本要和 Next 14 兼容,11.x 有 API 变化
prisma / @prisma/client^5.22.xORM 与数据库迁移Schema 文件里 multi-file 特性需要 5.4+
zod^3.x输入校验和 tRPC 配合使用做参数校验
tailwindcss^3.4.x样式方案Next 14 默认支持,配置语法比较稳定

这里重点说一下 zod。tRPC 的 mutation 或者 query 入参直接拿 zod schema 来校验,前端和后端用的是同一套校验规则。你在前端表单里填的数据,提交到后端时会被同样严格地校验一遍,不用写两套验证逻辑。这个在传统开发里是非常常见的重复工作,在 T3 Stack 里被自动消解了。

另外一个值得关注的是 Prisma。它不只是个 ORM,更像是一个"数据库结构的版本管理器"。你通过prisma/schema.prisma定义模型,然后跑prisma migrate dev生成 SQL 迁移文件,数据库结构就和代码一起进入了版本控制体系。改模型、加字段、回滚,全程有记录,不会出现本地数据库和线上不一致的情况。

2.3 项目结构规划:代码该放哪一目了然

t3code 的目录结构沿用了 T3 Stack 的官方脚手架布局,但做了一些调整。我直接贴出来:

t3code/ ├── prisma/ │ ├── schema.prisma # 数据库模型定义 │ └── migrations/ # 自动生成的迁移文件 ├── src/ │ ├── app/ # Next.js App Router 页面目录 │ │ ├── api/trpc/ # tRPC 的 HTTP 入口 │ │ ├── (auth)/ # 登录注册相关页面 │ │ └── app/ # 主应用页面 │ ├── components/ # 通用组件(按用途分目录) │ │ ├── ui/ # 基础 UI 组件 │ │ └── layout/ # 布局组件 │ ├── env.js # 环境变量校验 │ ├── lib/ # db 客户端、工具函数 │ ├── server/ # tRPC router、context 定义 │ │ └── db.ts # Prisma Client 单例 │ ├── trpc/ # 前后端共享的 tRPC 类型与 hooks └──

这套结构的设计思路是:app目录管页面,server目录管接口逻辑,trpc目录管前后端之间的"桥梁",prisma目录管数据库。各个焦点互相隔离,改页面不会碰接口,改模型不会碰 UI。

有个细节很多人刚上手时会搞混:src/app和src/server的区别。App Router 的app目录不只是放页面,你放在app下的任何文件都会参与路由解析,所以像db.ts这种工具文件一定要放lib或server目录,否则 Next.js 可能会把它当成路由处理,编译报错。我第一次迁移项目时就是没注意这个细节,结果报了一堆"invalid page export"的错误。

3. 核心细节解析与实操要点

3.1 类型安全链路:从数据库到前端页面

t3code 最核心的技术亮点是端到端类型安全。理解这条链路,基本就理解了 T3 这套架构的精髓。

链路从 Prisma 的模型定义开始。你在schema.prisma里写了一个User模型,Prisma Client 会自动生成对应的 TypeScript 类型。然后是 tRPC 的 router,你在server/api/root.ts里定义一个user.getById的 procedure,它的入参是z.string()(用户 ID 字符串),返回类型直接是 Prisma 生成的User类型。这一步做完,类型信息就被"锁"在了这套代码里。

前端调用时,tRPC 会把 router 的类型推给客户端 hook——trpc.user.getById.useQuery(params)。你的 IDE 里写这段代码时能看到完整的类型提示,返回的data对象上有什么字段,编辑器直接列出来,不用去翻文档。

这个链条的价值在项目迭代时体现得淋漓尽致。比如我给User模型加了一个avatarUrl字段,Prisma Client 重新生成后,前端代码里访问不到data.avatarUrl就会立刻看到黄色警告线。整个改动链路是"牵一发动全身"的,但也正因为如此,改动不会带出隐藏的运行时错误。

实用建议:给 tRPC 的 query 加上input的 zod schema 后,记得在output validator里也做一层校验。别嫌啰嗦,这在数据源不稳定的时候能帮你排查掉很多诡异问题。

3.2 鉴权与会话管理

登录鉴权是 t3code 这类全栈项目绕不开的一环。目前主流方案有两种:NextAuth(现在叫 Auth.js)和 Clerk。t3code 用的是 NextAuth 的 credentials 方式,也就是邮箱密码登录。我个人的看法是,小项目完全可以用 Clerk 这种托管方案省时间,但如果你想理解"会话管理"这件事的本质,自己搭一遍 NextAuth 还是有价值的。

NextAuth 在 T3 Stack 中的接入并不复杂,但有一个关键点是:callback 里的 session 对象默认只有user.id、name、email这些基础字段。如果你想在 session 里带上角色的权限信息、自定义字段,需要在callbacks.jwt和callbacks.session两个回调里手动"搬运"数据。

t3code 项目里有个单独的文件叫src/server/auth.ts,里面导出了一个getServerAuthSession函数。这个函数在服务端组件里使用,可以直接拿到当前登录用户的信息。它的底层逻辑是:从 cookie 里读出 session token,然后去数据库查对应的 session 记录,再关联到 user 表。整个过程对前端是透明的,你只需要调用useSessionhook 就能感知登录状态。

需要注意,服务端组件里使用getServerAuthSession时并不能同步拿到前端的 session 更新,它依赖的是请求时携带的 cookie。如果你在客户端组件里通过signOut或signIn改变了会话状态,页面重新渲染时服务端组件的数据可能还是旧状态。解决方法是:在路由变更或页面刷新时依赖 Next.js 的新请求,确保服务端组件能够读取到最新的 cookie。

3.3 UI 与样式系统:Tailwind 的合理使用

Tailwind 在 t3code 里负责全部样式工作。这个选择看似简单,但实际操作上有一个很大的学习曲线:纯用工具类写页面,你的 JSX 会非常长。我见过不少新手写完一个卡片组件,className 占了三行,阅读体验很糟糕。

所以我建议采用"组件化 + 工具类"的组合思路。基础的元素直接用 Tailwind 类,重复出现的模式(比如按钮、输入框、卡片)封装成小组件。t3code 项目里有一个components/ui/button.tsx,它就是用了cva(class variance authority)这个库来管理按钮的样式变体。还是那句话——不重复自己,工具的意义在于解放精力到更重要的逻辑问题上。

另外,如果你用 Tailwind 处理深浅主题,建议直接上next-themes。这个库和 Next.js 的 App Router 配合得很好,可以避免 SSR 阶段闪烁问题。t3code 项目用的策略是:默认跟随系统主题,允许用户在页面上手动切换,用 localStorage 持久化偏好。这也是一种技巧——在html标签加上suppressHydrationWarning,避免主题脚本在渲染时和 React 冲突告警。

3.4 数据校验与错误处理的最佳实践

数据校验在 t3code 里分两层:第一层是 zod 的输入校验,在 tRPC procedure 定义时通过input: z.object({...})声明;第二层是数据库唯一约束、非空约束这些"硬约束"。两层互相补充,前者提供用户友好的错误提示,后者保证数据的最终正确性。

zod 校验有个细节:它的报错信息默认是英文的。如果你的项目是中文界面,需要在zod/i18n里设置自定义错误消息。t3code 项目的做法是封装了一个lib/validators/auth.ts,用中文重写了邮箱格式、密码强度的错误文案。这个设置在项目初期容易忽略,等用户真正用起来才发现提示语很突兀。

错误处理方面,tRPC 的做法和 REST 不同。REST 会用 HTTP 状态码区分错误类型(400 参数错误、401 未登录、403 没权限),而 tRPC 默认所有错误都返回 200 状态码,但错误对象会带一个code字段(如UNAUTHORIZED、BAD_REQUEST、NOT_FOUND)。前端通过trpc的error.data?.code来区分错误类型。刚开始用这个模式的人会不习惯,但一旦用了,就回不去了——你不再需要解析一大堆不同结构的 error response,错误码的枚举是共享的,前后端永远一致。

4. 实操过程与核心环节实现

4.1 从零初始化:一条命令搭好基础

第一步是初始化项目。T3 Stack 有官方脚手架工具,一条命令就能建好项目骨架:

npx create-t3-app@latest t3code

执行后会有交互式选项,我一般这样选择:

  • TypeScript: 选 Yes(这是核心,不用犹豫)
  • Tailwind CSS: 选 Yes
  • tRPC: 选 Yes
  • NextAuth: 选 Yes(做登录功能就选)
  • Prisma: 选 Yes
  • ESLint / Prettier: 全选,代码风格统一很重要,后面省去大把格式化的口舌

初始化完成后,项目里会有一个.env文件模板,让你填写数据库连接串。本地开发建议直接用 Docker 起一个 PostgreSQL 实例,方便、干净、不污染系统环境:

docker run --name t3code-db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=t3code -p 5432:5432 -d postgres:16-alpine

数据库跑起来后,编辑.env里的DATABASE_URL:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/t3code?schema=public"

然后执行初始迁移:

npx prisma migrate dev --name init

这会生成prisma/migrations/0000_init/migration.sql,同时创建 Prisma Client。

4.2 数据库模型设计:用户、帖子和评论

t3code 的核心功能是一个简单的"内容分享"应用——用户可以发帖、评论、给帖子点赞。虽然功能不复杂,但三个实体之间的关系足够演示 Prisma 的关联查询能力。

prisma/schema.prisma的关键部分:

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? posts Post[] comments Comment[] likes Like[] accounts Account[] sessions Session[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(cuid()) title String content String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId String comments Comment[] likes Like[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([authorId]) } model Comment { id String @id @default(cuid()) content String post Post @relation(fields: [postId], references: [id], onDelete: Cascade) postId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId String createdAt DateTime @default(now()) @@index([postId]) } model Like { id String @id @default(cuid()) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) postId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String createdAt DateTime @default(now()) @@unique([postId, userId]) @@index([postId]) }

有几个设计决策我需要特别说明一下:

第一个是主键类型。我用的是cuid()而不是自增 int。好处是:分布式环境下不用依赖数据库自增,不容易暴露数据量;坏处是查询效率略低于 bigint,但对 t3code 这种量级的项目完全可以忽略。

第二个是外键的级联删除策略。帖子的评论和点赞都设置了onDelete: Cascade,意味着删除帖子时,相关的评论、点赞记录也一并清理。这个避免了孤儿数据的问题。要特别注意,生产环境如果数据量很大,级联删全部数据可能造成锁表,需要谨慎评估。

第三个是点赞表加了@@unique([postId, userId])复合唯一约束。这个约束比在应用层去"先查再插"要靠谱得多。如果你忘加这个约束,用代码去判断"是否已点过赞"很容易出现并发漏洞——两次请求同时进来,都判断"没点过",就会插入两条记录。

4.3 tRPC Router 编写:用代码走通业务流程

接下来是业务逻辑层。t3code 的 tRPC 代码都在src/server/api/里,root router 聚合了各个独立的功能路由。

先看帖子相关的 router(简化版):

import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "@/server/api/trpc"; import { TRPCError } from "@trpc/server"; export const postRouter = createTRPCRouter({ // 获取帖子列表,带作者信息和点赞数 list: publicProcedure .input(z.object({ cursor: z.string().optional(), limit: z.number().min(1).max(50).default(10) })) .query(async ({ ctx, input }) => { const posts = await ctx.db.post.findMany({ take: input.limit + 1, skip: input.cursor ? 1 : 0, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: "desc" }, include: { author: { select: { name: true, image: true } }, _count: { select: { likes: true, comments: true } }, }, }); let nextCursor: typeof input.cursor | undefined; if (posts.length > input.limit) { const nextItem = posts.pop(); nextCursor = nextItem!.id; } return { posts, nextCursor }; }), // 创建帖子(必须登录) create: protectedProcedure .input(z.object({ title: z.string().min(3).max(100), content: z.string().min(10).max(5000) })) .mutation(async ({ ctx, input }) => { const post = await ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); return post; }), // 删除帖子(只有作者本人能删) delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const post = await ctx.db.post.findUnique({ where: { id: input.id } }); if (!post) throw new TRPCError({ code: "NOT_FOUND", message: "帖子不存在" }); if (post.authorId !== ctx.session.user.id) { throw new TRPCError({ code: "FORBIDDEN", message: "无权删除" }); } await ctx.db.post.delete({ where: { id: input.id } }); return { success: true }; }), });

这段代码有几个地方需要讲清楚。

publicProcedure和protectedProcedure的区别是 tRPC 在 middleware 层做的区分。protectedProcedure会先检查ctx.session是否存在,如果未登录,直接抛出UNAUTHORIZED错误。所以你不用在业务代码里重复写"如果没登录就返回 401"这种逻辑。

分页查询这里,我用的是基于 cursor 的无限滚动方案,而不是传统的 page/pageSize 跳页。cursor 方案的好处是:大数据量下跳页越深越慢的问题可以避免,因为它是基于唯一索引定位,而不是OFFSET扫描。前端配合useInfiniteQuery使用,滚动到底部自动加载下一页,体感上比翻页按钮流畅得多。

授权逻辑要放在业务代码里。delete这个 mutation 首先要查一次数据库,确认帖子存在,然后判断authorId是否等于当前登录用户 ID。记住:任何删除、修改操作都不能只凭前端传来的参数决定,必须用ctx.session.user.id作为授权依据。这是后端安全的基本原则。

4.4 前端页面与数据交互:React Query 的魔力

t3code 的前端页面用服务端组件(Server Components)做布局框架,客户端组件(Client Components)处理交互。App Router 下,默认组件是服务端渲染的,但如果你想用useState、useEffect、事件处理这些特性,需要在文件顶部加"use client"指令。

页面和数据交互的基础模式是这样的:

"use client"; import { api } from "@/trpc/react"; export default function PostsList() { const { data, isLoading, fetchNextPage, hasNextPage } = api.post.list.useInfiniteQuery( { limit: 10 }, { getNextPageParam: (lastPage) => lastPage.nextCursor } ); if (isLoading) return <LoadingSpinner />; return ( <div className="space-y-4"> {data?.pages.map((page) => page.posts.map((post) => ( <PostCard key={post.id} post={post} /> )) )} {hasNextPage && ( <button onClick={() => fetchNextPage()} className="w-full py-3 text-center text-sm text-gray-500"> 加载更多 </button> )} </div> ); }

api.post.list.useInfiniteQuery这个 hook 是 tRPC 自动生成的——它根据你定义的 router 结构,在src/trpc/react.ts里把post.list对应的 React Query hooks 生成了。用起来像是在调用本地函数,实际网络请求的逻辑(发给/api/trpc/post.list)都被框架处理好了。

但使用useInfiniteQuery时,有个容易踩的坑是输入参数的合并。你在input里写了cursor,但getNextPageParam返回的nextCursor会被 React Query 自动作为下一次查询的pageParam,而 tRPC 会把pageParam映射到input.cursor。这个过程略绕,但不复杂,下方画线记忆:pageParam就是每页的游标,传下去就是拉下一页的命令。

4.5 部署上线:Vercel 与数据库迁移

最后的部署环节,我用的方案是 Vercel 托管应用 + Neon 托管数据库。Vercel 和 Next.js 是同一家公司的产品,部署体验非常丝滑——你的 GitHub 仓库绑定到 Vercel 后,每次 push 代码都会自动触发构建,预览环境也是自动生成的。

数据库选 Neon 的原因是它有免费的 PostgreSQL 实例,而且是 serverless 的,按需启动,不需要你维护一个常驻的数据库服务。这对个人项目来说,成本几乎为零。

部署前需要做的事:

  1. 在.env.production里配置生产环境的DATABASE_URL(用 Neon 提供的连接串,注意选择 pool 连接模式还是 direct 模式)。
  2. 执行数据库迁移:npx prisma migrate deploy(生产环境的命令是deploy,不是dev,dev会在迁移时重置本地数据库,绝对不能在线上用)。
  3. Vercel 的环境变量设置里,把DATABASE_URL、NEXTAUTH_SECRET都配上。
  4. 确保next.config.js里没有硬编码的绝对路径。

有个常见的部署坑:NextAuth 在 Vercel 上部署时,NEXTAUTH_SECRET必须显式配置,不能靠自动生成。否则每次冷启动生成不同的 secret,用户登录状态会莫名其妙丢失。生成方式很简单——在命令行里输入openssl rand -base64 32,复制输出填入 Vercel 的环境变量即可。

5. 常见问题与排查技巧实录

5.1 类型对不上:前端拿到 undefined 的兜底方案

开发过程中我最常遇到的报错,就是Cannot read properties of undefined (reading 'xxx')。原因一般是:UI 上展示的数据来自一个嵌套字段,但查询时没有用include或select关联出来。

解决思路:先确认 tRPC router 里对应的 query 是否显式声明了include: { author: true }或select字段。Prisma 默认不会把关联表的数据带出来,你得告诉它"我只要 author 的名字"。如果不想让查询结果多出过多信息,用select精确指定字段;如果你知道前端需要哪些字段,select比include更容易控制返回体体积。

例外情况是:如果嵌套数据有时有、有时没有(比如用户可能没填简介字段),在前端渲染时加上安全兜底:

const displayName = post.author?.name ?? "匿名用户";

空值合并运算符??和逻辑或||的区别在于:??只处理null和undefined,不会把空字符串、数字 0 这些"假值"误判成缺失值。这个细节在显示年龄、积分这类可能为 0 的数字时特别重要。

5.2 Prisma 迁移卡住或提示表冲突

有一种经典场景:你在本地migrate dev一切正常,但部署到线上跑migrate deploy时报错,提示某个表已经存在。这通常是迁移记录表和实际数据库不同步导致的。

排查顺序是:先看prisma/migrations的迁移文件,确认线上数据库执行的迁移记录是否和本地一致。如果线上数据库的表已经存在,但_prisma_migrations表里没有记录,最简单的恢复方式是:执行一个"基线迁移"(baseline migration),手动插入一条迁移记录,告诉 Prisma "这个状态已经存在了,不要再执行了"。

还有一个经验:开发环境临时改字段又不想保留迁移记录时,可以用npx prisma migrate reset把数据库重置到初始状态。这个命令会删除所有数据、重新执行所有迁移,只适用于本地或开发环境,线上数据千万别跑这个命令。

5.3 环境变量的边界问题

T3 Stack 对环境变量的处理比较严格。项目里的env.js用 zod 校验了所有环境变量,要求它们必须在运行时存在。这带来一个好处:启动时就能发现配错的环境变量,而不是运行到某个模块才莫名报错。

但有个场景容易踩坑:你本地开发时.env里有NEXTAUTH_SECRET,没问题;但部署到 Vercel 时,如果不把同一个变量配到 Vercel 的环境变量里,build 会成功,但运行时访问会产生异常。建议部署前记着把.env里的所有键在目标平台上复制一份。

如果是团队协作,还可以尝试.env.example文件——把DATABASE_URL、NEXTAUTH_SECRET这类变量的键名放进去,值留空的或一个假值,让新伙伴一眼看到"项目需要配置哪些环境变量",避免新人各种问。

5.4 热更新失效:数据不刷新的处理思路

React Query 的缓存策略是"用前先用缓存",所以当你做了 mutation(比如发了新帖子),列表不会自动更新。你需要主动让 React Query 重新拉取列表数据。

t3code 项目的处理方式是在 mutation 的onSuccess回调里调用路径失效:

const createPost = api.post.create.useMutation({ onSuccess: () => { utils.post.list.invalidate(); }, });

invalidate会通知 React Query 清理对应 key 的缓存,下次组件重新挂载时自动重新拉取最新数据。这个方法比手动维护一个全局状态要干净得多。

如果你不想在每次成功后都手动写这段代码,还可以考虑在 router 的 output 层直接做invalidate。tRPC 提供了middleware的机制,可以在 router 层统一处理"某类操作后自动失效哪些 query"的逻辑。不过这种写法容易理解性变差,我个人的习惯还是"显式优于隐式"——在 mutation 里清楚地写明失效了哪个 query,半年后再看代码也一目了然。

5.5 部署后白屏或样式丢失

如果你部署到 Vercel 后发现页面白屏,可以先看浏览器 console:最常见的报错是Hydration failed because the server rendered HTML didn't match the client。这个错误几乎都是因为服务端渲染的 HTML 和客户端首次渲染不一致导致的,原因通常是:组件里用了Date.now()、随机数、或者 localStorage 的值。解决思路是,把这些"客户端才有的值"放在useEffect里再更新,而不是在首次渲染时读取。

样式丢失这边,如果你确认 Tailwind 在本地正常、线上异常,十有八九是 Tailwind 的内容路径配置不对。tailwind.config.ts里的content数组需要包含你项目里所有用到 Tailwind 类名的文件路径:

content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],

如果你把新组件放到了content没覆盖的目录(比如src/components/ui之外的lib目录下的某个渲染组件),线上构建时会把这些类名"瘦身"掉,导致样式丢失。这是一个很低级但很容易发生的错误,排查时第一个想到它。

6. 个人经验沉淀:t3code 给我带来的三个改变

把这个项目完整走一遍之后,我最大的一个体会是:类型安全不是"额外的开销",而是"省下的时间"。以前写全栈项目,前端和后端各有一份数据模型的心智负担。改字段要两处改、两处沟通,总会出现"字段漏改"这种事。t3code 把数据模型定义在同一个地方之后,改模型、改接口、改前端组件是"同步跳动"的,编译期就暴露错误,运行时省心很多。

第二个改变是我对"文件夹结构"的敬畏。以前我建项目比较随便,utils 目录里堆各种函数,components 目录里所有组件打平。时间一长,没看完的旧代码完全不敢动。t3code 按"页面、服务端逻辑、共享类型、数据库模型"分层的做法,让我养成了一种反射性的组织习惯——新代码放哪不是随手决定,而是先想清楚它属于哪一层。

第三个改变是对"小而美"的认可。t3code 的功能不复杂,但每一步都扎实。这个项目教会我一个道理:学全栈开发不需要一上来就搞微服务、Redis、K8s,把 TypeScript、Next.js、tRPC、Prisma 这一套弄通,已经能撑起很多真实场景的项目了。技术栈的深度在精进,广度是后续的事。

如果你也想照着这条路线走一遍,我建议你从一个小功能开始,不要试图一次把 T3 全部的小细节都摸透。先把"数据库增删改查 + 前端列表展示 + 登录认证"跑通,再逐步加功能。这个过程中遇到的所有报错,翻开这篇文章找对应章节即可。最后再分享一个小技巧:开发过程中定期跑一遍npx tsc --noEmit或者npm run type-check,让类型错误在提交代码前爆炸而不是在运行时爆炸,体验会好非常多。

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

MATLAB版Q-Learning源码实战:从贝尔曼公式到参数调优

简介&#xff1a;文档以王子寻公主小游戏为场景&#xff0c;完整呈现 Q-Learning 算法的 Matlab 实现与逐行注释&#xff0c;适合强化学习初学者对照理解无模型决策流程。案例构建了 16 个位置的网格世界&#xff0c;王子可上下左右移动&#xff0c;设有障碍物、陷阱与终点奖励…

作者头像 李华
网站建设 2026/10/14 17:21:34

科研idea挖掘与落地实用指南:助力学术创新高效产出

刚接触一个新领域&#xff0c;最怕的就是迷失在海量的外国文献里&#xff0c;读了很多篇还是理不清脉络。我曾经也以为“研究现状”只能靠逐篇阅读、手动总结&#xff0c;直到发现了一些能生成“知识图谱”的神器。它们能让你像开了上帝视角一样&#xff0c;瞬间看清一个领域的…

作者头像 李华
网站建设 2026/10/14 17:20:55

数据包寻址链路:MAC、IP、DNS与CDN全解析

如果你正打算认真啃一遍计算机网络基础&#xff0c;我的建议是别一头扎进教科书里背七层模型&#xff0c;而是沿着一条实际问题链条走下来&#xff1a;一个数据包从网卡出发&#xff0c;如何靠 MAC 找到直连设备&#xff0c;如何靠 IP 跨网寻路&#xff0c;如何靠 DNS 把域名翻…

作者头像 李华
网站建设 2026/10/14 17:19:56

大浪淘沙见真章:湘楚有才单招,湖南单招赛道本土标杆的成长之路

一、浪潮奔涌,谁在深耕,谁在浮沉近年来,随着职业教育改革持续深化、湖南高职分类考试招生规模逐年扩大,单招这条升学通道,正被越来越多的考生家庭视为一条务实而重要的人生路径。需求升温,市场也随之热闹起来。各路机构竞相涌入,宣传话术一个比一个响亮,"保过""…

作者头像 李华