戰(zhàn):端到端類(lèi)型安全的全棧開(kāi)發(fā)實(shí)踐)
做全棧開(kāi)發(fā)這幾年我最大的痛點(diǎn)不是業(yè)務(wù)邏輯寫(xiě)不出來(lái)而是前后端的數(shù)據(jù)類(lèi)型就像兩個(gè)平行世界——后端定義了接口前端還得手動(dòng)維護(hù)一份類(lèi)型定義改一個(gè)字段名要跑兩個(gè)倉(cāng)庫(kù)。T3 Stack 就是沖著這個(gè)痛點(diǎn)來(lái)的而 t3code 則是我在這個(gè)技術(shù)棧下長(zhǎng)期積累的一套完整實(shí)踐總結(jié)從項(xiàng)目初始化、數(shù)據(jù)建模、路由編排到頁(yè)面落地全部圍繞“端到端類(lèi)型安全”四個(gè)字展開(kāi)。這套東西適合誰(shuí)用如果你是剛接觸 Next.js 想做全棧項(xiàng)目、又被各種 ORM 和 API 方案糾結(jié)到失眠的開(kāi)發(fā)者t3code 可以給你一條順暢的捷徑如果你是已經(jīng)寫(xiě)過(guò)幾年接口的老手想見(jiàn)識(shí)下不用手寫(xiě) REST API 也能做前后端交互的思路這篇文章里同樣有值得一看的實(shí)操細(xì)節(jié)。我會(huì)把每一步選型背后的“為什么”也講清楚而不是只丟給你一串命令。1. 整體設(shè)計(jì)與思路拆解為什么是 T3 Stack 而不是傳統(tǒng)前后端分離1.1 T3 到底是什么先說(shuō)清楚這個(gè) T3 不是因?yàn)榘姹咎?hào)而是 TypeScript、Tailwind、tRPC 三個(gè)單詞的縮寫(xiě)組合。T3 Stack 的完整成員除了這三者通常還包括 Next.js、Prisma 和 NextAuth.js核心目標(biāo)是讓整個(gè)全棧項(xiàng)目從數(shù)據(jù)庫(kù)到瀏覽器 UI 全程都由 TypeScript 守住類(lèi)型邊界。傳統(tǒng)做法是前端一個(gè) React 項(xiàng)目、后端一個(gè) Express 項(xiàng)目中間靠 REST API 溝通。這樣拆的代價(jià)是接口文檔必須同步、請(qǐng)求和響應(yīng)類(lèi)型必須手工維護(hù)、部署環(huán)境至少要兩套。T3 Stack 的思路則是用 Next.js 同時(shí)承擔(dān)頁(yè)面渲染和 API 處理用 Prisma 把數(shù)據(jù)庫(kù)表結(jié)構(gòu)同步成 TypeScript 類(lèi)型再用 tRPC 讓前端直接調(diào)用后端函數(shù)全程不需要生成 OpenAPI 文檔也不用在前后端各寫(xiě)一份 interface。t3code 本質(zhì)上就是這個(gè)選型思路的工程化落地。我見(jiàn)過(guò)很多人對(duì) T3 Stack 感興趣但實(shí)際搭項(xiàng)目時(shí)會(huì)在“App Router 還是 Pages Router”、“tRPC 能不能和 Server Action 混用”、“Prisma 到底要不要單獨(dú)抽象一層 service”這些問(wèn)題上糾結(jié)結(jié)果項(xiàng)目搭到一半就泄氣了。1.2 類(lèi)型安全能解決什么問(wèn)題拿一個(gè)最簡(jiǎn)單的“創(chuàng)建文章”功能舉例。傳統(tǒng) REST 流程里前端要記住 POST /api/post 這個(gè)地址還要保證傳參里用title還是name和后端代碼一致。一旦兩邊有一個(gè)字母拼錯(cuò)運(yùn)行時(shí)不報(bào)錯(cuò)數(shù)據(jù)卻進(jìn)了數(shù)據(jù)庫(kù)錯(cuò)誤列。這種 Bug 在開(kāi)發(fā)環(huán)境往往很難第一時(shí)間暴露等到前端頁(yè)面加載不出來(lái)你才意識(shí)到類(lèi)型錯(cuò)了。T3 Stack 的處理方式完全不同。數(shù)據(jù)庫(kù)表用 Prisma Schema 定義生成出來(lái)的Post類(lèi)型全項(xiàng)目通用tRPC 的createPost過(guò)程會(huì)從數(shù)據(jù)庫(kù)層一路推導(dǎo)到前端調(diào)用處如果你傳遞了錯(cuò)誤字段保存代碼的那一刻 TypeScript 就會(huì)亮紅。也就是說(shuō)接口契約不再是文檔而是直接寫(xiě)死在類(lèi)型系統(tǒng)里。t3code 把這類(lèi)實(shí)踐都?xì)w納成了可復(fù)現(xiàn)的模式。我實(shí)操下來(lái)的體會(huì)是類(lèi)型安全不是一次配置就完事它需要你在建表、寫(xiě)路由、封裝組件時(shí)都遵循同一套約定否則強(qiáng)勢(shì)類(lèi)型反而會(huì)變成一堆報(bào)錯(cuò)的麻煩。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 項(xiàng)目初始化create-t3-app 的選項(xiàng)怎么選初始化 T3 項(xiàng)目現(xiàn)在很簡(jiǎn)單一條命令搞定pnpm create t3-applatest如果你更喜歡 npm也可以執(zhí)行npm create t3-applatest交互內(nèi)容是一樣的。需要特別注意的是勾選項(xiàng)因?yàn)?create-t3-app 會(huì)把這幾個(gè)技術(shù)棧一次性裝好后面再補(bǔ)會(huì)比較麻煩。我一般這樣選擇Next.js 搭配 App Router新項(xiàng)目推薦直接用 App RouterPages Router 屬于老項(xiàng)目過(guò)渡場(chǎng)景。TypeScript 必選選框默認(rèn)選中。Tailwind CSS 建議選擇T3 的默認(rèn)樣式方案就是它。trpc 必選如果你跳過(guò)它整個(gè) T3 Stack 最核心的端到端類(lèi)型安全就不存在了。Prisma 建議選擇如果你更習(xí)慣 drizzle 可以手動(dòng)換但 create-t3-app 內(nèi)置的是 Prisma。NextAuth.js 按需選擇需要登錄鑒權(quán)再勾不必提前引入。生成之后目錄結(jié)構(gòu)是這樣的├── prisma │ └── schema.prisma ├── public ├── src │ ├── app │ ├── pages │ ├── server │ │ ├── api │ │ │ └── root.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── styles │ ├── trpc │ │ ├── react.tsx │ │ ├── query.ts │ │ └── server.ts ├── .env ├── next.config.js ├── package.json └── tsconfig.json這里我踩過(guò)一個(gè)小坑腳手架生成的src/server和src/trpc這兩個(gè)目錄是所有類(lèi)型安全的根基盡量別擅自改名。特別是trpc/query.ts這個(gè)文件它是服務(wù)端路由類(lèi)型與客戶(hù)端 Provider 連接的橋梁把它改動(dòng)后編輯器里的智能提示會(huì)略顯混亂。2.2 Prisma 數(shù)據(jù)建模的核心要點(diǎn)T3 Stack 默認(rèn)使用 Prisma數(shù)據(jù)模型寫(xiě)在prisma/schema.prisma里。一個(gè)最簡(jiǎn)單的文章模型長(zhǎng)這樣model Post { id String id default(cuid()) title String content String published Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt }這里需要注意三個(gè)細(xì)節(jié)。第一主鍵用default(cuid())而不是autoincrement()。T3 項(xiàng)目通常部署到 Serverless 環(huán)境自增主鍵在分布式場(chǎng)景下需要額外考慮沖突和協(xié)調(diào)成本而 cuid 生成字符串主鍵幾乎不需要額外配置生成之后也不需要等待主鍵回讀。第二每次修改schema.prisma后一定要執(zhí)行遷移命令pnpm prisma migrate dev這個(gè)命令會(huì)同時(shí)做兩件事生成 SQL 遷移文件并且重新生成 Prisma Client 類(lèi)型。如果你只改表結(jié)構(gòu)而不重新生成 Client前端那邊引用Post類(lèi)型時(shí)會(huì)出現(xiàn)類(lèi)型過(guò)期、字段缺失的詭異現(xiàn)象。第三記得給PrismaClient做單例封裝。Next.js 在開(kāi)發(fā)環(huán)境的熱更新機(jī)制下如果你的代碼直接new PrismaClient()每改一次代碼就會(huì)創(chuàng)建一個(gè)新的數(shù)據(jù)庫(kù)連接實(shí)例連接數(shù)一多數(shù)據(jù)庫(kù)就會(huì)被拖垮。create-t3-app 生成的src/server/db.ts已經(jīng)處理了這個(gè)問(wèn)題你在自己寫(xiě)腳本時(shí)也要注意。2.3 tRPC 路由設(shè)計(jì)模式tRPC 是 T3 Stack 的靈魂。它讓你在函數(shù)級(jí)別調(diào)用后端邏輯不需要寫(xiě) URL不需要手動(dòng)處理 JSON更不需要定義 REST 資源。一個(gè)最簡(jiǎn)單的 tRPC 子路由通常寫(xiě)作import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { z } from zod; export const postRouter createTRPCRouter({ list: publicProcedure .query(async ({ ctx }) { return ctx.db.post.findMany(); }), create: publicProcedure .input(z.object({ title: z.string().min(1), content: z.string(), })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, }, }); }), });publicProcedure表示公開(kāi)可調(diào)用不需要登錄。有鑒權(quán)需求時(shí)用protectedProcedure它會(huì)自動(dòng)注入當(dāng)前會(huì)話用戶(hù)信息。input部分使用 zod 做運(yùn)行時(shí)校驗(yàn)服務(wù)端校驗(yàn)和類(lèi)型推導(dǎo)一起完成前端調(diào)用時(shí)也會(huì)獲得同樣精確的類(lèi)型提示。我的建議是把 router 文件按業(yè)務(wù)模塊拆分比如postRouter、userRouter、commentRouter然后在root.ts里合并export const appRouter createTRPCRouter({ post: postRouter, user: userRouter, comment: commentRouter, });不要在單個(gè) router 文件里塞幾十個(gè) procedure那會(huì)讓調(diào)試體驗(yàn)極其痛苦。2.4 前端調(diào)用的兩種姿勢(shì)T3 項(xiàng)目里調(diào)用 tRPC 有兩種常見(jiàn)方式。Client Component 的方式是通過(guò)api對(duì)象use client; import { api } from ~/trpc/react; export function PostList() { const { data, isLoading } api.post.list.useQuery(); if (isLoading) return div加載中.../div; return ( div {data?.map((post) ( div key{post.id}{post.title}/div ))} /div ); }Server Component 的方式則是直接調(diào)用服務(wù)端函數(shù)比如在 App Router 的page.tsx里import { api } from ~/trpc/server; export default async function HomePage() { const posts await api.post.list(); return div{posts.length} 篇文章/div; }這兩種方式的核心區(qū)別在于api.post.list.useQuery會(huì)經(jīng)過(guò)客戶(hù)端查詢(xún)緩存適合需要響應(yīng)式更新的場(chǎng)景await api.post.list()則在服務(wù)端直接執(zhí)行適合數(shù)據(jù)不需要交互變化的場(chǎng)景。t3code 的準(zhǔn)則是能用 Server Component 取數(shù)就先在服務(wù)端取只有在需要加載態(tài)、緩存更新時(shí)才切換到客戶(hù)端查詢(xún)。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 從零做一個(gè)帶數(shù)據(jù)庫(kù)的全棧功能為了把 t3code 的完整流程走一遍我準(zhǔn)備做一個(gè)“文章發(fā)布器”的簡(jiǎn)單需求支持文章列表展示、支持輸入標(biāo)題和內(nèi)容創(chuàng)建文章。麻雀雖小但數(shù)據(jù)庫(kù)、后端邏輯、前端交互全都覆蓋到了。第一步定義 Prisma 模型。在schema.prisma中準(zhǔn)備一個(gè)Post模型model Post { id String id default(cuid()) title String content String createdAt DateTime default(now()) updatedAt DateTime updatedAt }執(zhí)行遷移pnpm prisma migrate dev --name init_post這一步會(huì)創(chuàng)建數(shù)據(jù)庫(kù)表并同步生成PrismaClient類(lèi)型。開(kāi)發(fā)環(huán)境我一般用本地 SQLite 文件因?yàn)椴恍枰獑为?dú)啟動(dòng)數(shù)據(jù)庫(kù)服務(wù)。部署到線上時(shí)會(huì)切換到 PostgreSQL只需要改環(huán)境變量并重新遷移即可。3.2 編寫(xiě) tRPC API在src/server/api/routers/post.ts中寫(xiě)路由import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ list: publicProcedure.query(({ ctx }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().min(1), }) ) .mutation(({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, }, }); }), });list用了publicProcedure因?yàn)榱斜碚l(shuí)都能看。create用protectedProcedure因?yàn)橹挥械卿浻脩?hù)能發(fā)文。如果項(xiàng)目沒(méi)有啟用 NextAuth那你只能先用publicProcedure頂替但真正上線前要換回來(lái)。3.3 服務(wù)端數(shù)據(jù)綁定App Router 模式下page.tsx默認(rèn)是 Server Component可以直接調(diào)用 tRPC Server APIimport { api } from ~/trpc/server; import { CreatePost} from ./_components/create-post; import { PostList } from ./_components/post-list; export default async function Home() { const posts await api.post.list(); return ( main classNamemx-auto max-w-2xl p-4 h1 classNametext-2xl font-bold文章發(fā)布器/h1 CreatePost / PostList initialPosts{posts} / /main ); }這里我把PostList設(shè)計(jì)成客戶(hù)端組件但把服務(wù)端取到的posts作為初始數(shù)據(jù)傳進(jìn)去這樣頁(yè)面首屏直接就有內(nèi)容客戶(hù)端 hydrate 后又能使用查詢(xún)緩存刷新數(shù)據(jù)。是不是有點(diǎn)冗余對(duì)但這是新手最容易忽略的優(yōu)化點(diǎn)不要一上來(lái)就在客戶(hù)端組件里useQuery拉接口導(dǎo)致頁(yè)面先空白再加載。PostList組件的實(shí)現(xiàn)use client; import { api } from ~/trpc/react; type Post { id: string; title: string; content: string; createdAt: Date; }; export function PostList({ initialPosts }: { initialPosts: Post[] }) { const { data initialPosts } api.post.list.useQuery(undefined, { initialData: initialPosts, }); return ( div classNamemt-6 space-y-4 {data.map((post) ( article key{post.id} classNamerounded border p-4 h2 classNametext-lg font-semibold{post.title}/h2 p{post.content}/p /article ))} /div ); }注意這段代碼里的類(lèi)型Post是我手寫(xiě)的其實(shí)不太符合 t3code 的風(fēng)格。更優(yōu)雅的做法是直接從 Prisma 導(dǎo)入比如import type { Post } from prisma/client真正實(shí)現(xiàn)類(lèi)型單一事實(shí)來(lái)源。3.4 表單提交與緩存刷新創(chuàng)建表單同樣是客戶(hù)端組件use client; import { useRouter } from next/navigation; import { api } from ~/trpc/react; import { useState } from react; export function CreatePost() { const router useRouter(); const [title, setTitle] useState(); const [content, setContent] useState(); const createPost api.post.create.useMutation({ onSuccess: () { router.refresh(); setTitle(); setContent(); }, }); return ( form onSubmit{(e) { e.preventDefault(); createPost.mutate({ title, content }); }} input value{title} onChange{(e) setTitle(e.target.value)} placeholder標(biāo)題 classNamew-full border p-2 / textarea value{content} onChange{(e) setContent(e.target.value)} placeholder正文 classNamemt-2 w-full border p-2 / button classNamemt-2 bg-blue-600 p-2 text-white {createPost.isLoading ? 發(fā)布中... : 發(fā)布} /button {createPost.isError ( p classNametext-red-500發(fā)布失敗請(qǐng)檢查輸入/p )} /form ); }這里的關(guān)鍵點(diǎn)是router.refresh()。因?yàn)榱斜頂?shù)據(jù)的獲取在 Server Component 里單純靠客戶(hù)端useMutation的onSuccess不會(huì)自動(dòng)更新服務(wù)端渲染的列表。調(diào)用router.refresh()會(huì)重新從服務(wù)端拉取當(dāng)前路由的數(shù)據(jù)這樣一來(lái)新發(fā)布的文章才能立刻出現(xiàn)在列表里。3.5 完整功能驗(yàn)證步驟寫(xiě)完代碼后我建議按下列順序做一次完整驗(yàn)證啟動(dòng)開(kāi)發(fā)服務(wù)器pnpm dev打開(kāi)首頁(yè)確認(rèn)能看到空狀態(tài)列表提交一篇文章觀察表單重置、列表新增文章手動(dòng)改一下數(shù)據(jù)庫(kù)里某條數(shù)據(jù)的title刷新頁(yè)面確保列表與數(shù)據(jù)庫(kù)一致這個(gè)流程雖然簡(jiǎn)單但它能驗(yàn)證數(shù)據(jù)庫(kù)、tRPC 后端、Server Component 取數(shù)、Client Mutation 更新四條鏈路都是通的。我見(jiàn)過(guò)太多項(xiàng)目在初期跳過(guò)端到端驗(yàn)證結(jié)果部署時(shí)才暴露問(wèn)題。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 常見(jiàn)錯(cuò)誤速查表現(xiàn)象可能原因排查方向TypeScript 報(bào)錯(cuò)Object literal may only specify known properties傳的參數(shù)和 zod input 類(lèi)型不匹配檢查 zod schema 字段名和前端調(diào)用字段名列表一直顯示舊數(shù)據(jù)Router 緩存未失效在 mutation 的onSuccess里補(bǔ)router.refresh()請(qǐng)求返回 401tRPC procedure 用了protectedProcedure但未登錄先檢查 NextAuth 會(huì)話狀態(tài)部署后 Prisma 找不到數(shù)據(jù)表線上數(shù)據(jù)庫(kù)未執(zhí)行遷移執(zhí)行prisma migrate deployTailwind 樣式不生效內(nèi)容路徑不在 Tailwind content 配置里檢查tailwind.config.ts的content是否覆蓋到該目錄開(kāi)發(fā)時(shí)每次刷新都新建數(shù)據(jù)庫(kù)連接PrismaClient 未做單例看看src/server/db.ts是否用全局變量緩存4.2 Router 刷新失效的深坑我在 t3code 實(shí)踐中遇到最多的坑就是使用 tRPC mutation 后調(diào)用router.refresh()但頁(yè)面數(shù)據(jù)看起來(lái)沒(méi)變。排查了很久發(fā)現(xiàn)根因是 Server Component 里的await api.post.list()和客戶(hù)端組件里的useQuery同時(shí)存在??蛻?hù)端useQuery的緩存優(yōu)先級(jí)高于服務(wù)端刷新結(jié)果所以即使 server 刷新重新渲染了頁(yè)面客戶(hù)端拿到的新數(shù)據(jù)又被舊緩存頂?shù)袅恕=鉀Q方案有兩種第一種列表只用服務(wù)端渲染客戶(hù)端只做表單提交router.refresh()就能穩(wěn)定生效。第二種列表必須用客戶(hù)端useQuery和緩存表單提交后調(diào)用api.post.list.invalidate()主動(dòng)讓緩存失效。不要混用兩種策略這是我踩過(guò)幾次坑后的深刻教訓(xùn)。一個(gè)頁(yè)面只選擇一種數(shù)據(jù)獲取模式理解起來(lái)也輕松。4.3 調(diào)試 tRPC 時(shí)的小技巧tRPC 是類(lèi)型安全體系的核心但也意味著一旦類(lèi)型對(duì)不上錯(cuò)誤信息會(huì)比較難定位。我的調(diào)試順序是先看瀏覽器 Network 面板。tRPC 的請(qǐng)求是一個(gè) POST路徑類(lèi)似/api/trpc/post.create返回 JSON 里會(huì)帶錯(cuò)誤信息。再看服務(wù)端控制臺(tái)。如果 zod 校驗(yàn)失敗服務(wù)端會(huì)輸出更詳細(xì)的堆棧信息。最后看類(lèi)型。如果 TS 報(bào)錯(cuò)說(shuō)某個(gè) property 不存在大概率是 Prisma Schema 改了但沒(méi)重新生成 Client。還有一個(gè)小技巧在 tRPC 的 error 回調(diào)里臨時(shí)輸出整個(gè)錯(cuò)誤對(duì)象而不是直接輸出error.message因?yàn)?tRPC 的TRPCClientError有時(shí)會(huì)帶shape字段直接看 message 可能會(huì)遺漏關(guān)鍵信息。4.4 數(shù)據(jù)庫(kù)遷移的三個(gè)階段開(kāi)發(fā)環(huán)境我用prisma migrate dev它會(huì)重置本地?cái)?shù)據(jù)庫(kù)。這個(gè)命令在開(kāi)發(fā)期很舒服但線上不能這么用線上要用prisma migrate deploy。我見(jiàn)過(guò)有人把migrate dev用于生產(chǎn)數(shù)據(jù)庫(kù)結(jié)果直接把線上數(shù)據(jù)清空或者表結(jié)構(gòu)重做這個(gè)教訓(xùn)很深刻。區(qū)分一下prisma migrate dev開(kāi)發(fā)環(huán)境專(zhuān)用會(huì)執(zhí)行遷移、重新生成 Client、可能 reset 數(shù)據(jù)庫(kù)。prisma migrate deploy一般用于 CI/CD 或線上部署只應(yīng)用遷移文件不會(huì)重置數(shù)據(jù)。T3 項(xiàng)目部署到 Vercel 時(shí)我會(huì)在構(gòu)建命令里加上prisma generate prisma migrate deploy。不過(guò)也要注意Postgres 數(shù)據(jù)庫(kù)需要確保遷移服務(wù)能訪問(wèn)到線上庫(kù)像 Vercel 這種 Serverless 平臺(tái)跑migrate deploy有時(shí)會(huì)有網(wǎng)絡(luò)限制建議在本地執(zhí)行遷移后再推代碼。4.5 依賴(lài)版本陷阱T3 Stack 的生態(tài)更新速度很快尤其要注意 Next.js 和 tRPC 的版本兼容。create-t3-app 腳手架生成的版本通常是最新組合但如果你手動(dòng)升級(jí)某個(gè)包比如把 Next.js 從 14 升到 15tRPC 可能需要跟著調(diào)整 import 路徑。我的建議是生產(chǎn)項(xiàng)目盡量鎖定版本不要隔三岔五無(wú)腦升級(jí)。如果你想體驗(yàn)新功能單獨(dú)開(kāi)分支試別在主線項(xiàng)目上升級(jí)依賴(lài)省得類(lèi)型系統(tǒng)突然炸一片。5. 代碼組織與項(xiàng)目擴(kuò)展建議5.1 server 與 client 的代碼邊界t3code 里最重要的一條組織原則是能放服務(wù)端的代碼不要放客戶(hù)端。具體來(lái)說(shuō)數(shù)據(jù)庫(kù)操作只出現(xiàn)在src/server或 router 的 procedure 內(nèi)部與數(shù)據(jù)庫(kù)直接相關(guān)的類(lèi)型不要在前端冗余定義調(diào)用 tRPC 獲取數(shù)據(jù)時(shí)優(yōu)先考慮 Server Component 直接取數(shù)只有需要交互、狀態(tài)、動(dòng)畫(huà)、實(shí)時(shí)更新時(shí)才移動(dòng)客戶(hù)端這樣設(shè)計(jì)之后整個(gè)項(xiàng)目的邏輯主線會(huì)變得非常清晰data flow 從數(shù)據(jù)庫(kù)到服務(wù)端路由再到頁(yè)面組件每個(gè)環(huán)節(jié)都經(jīng)過(guò)類(lèi)型校驗(yàn)不會(huì)再出現(xiàn)“前端 mock 數(shù)據(jù)跑得通聯(lián)調(diào)后端就崩”的情況。5.2 如何擴(kuò)展新業(yè)務(wù)模塊模塊一多路由文件會(huì)變多這時(shí)候項(xiàng)目容易亂。建議按業(yè)務(wù)域建目錄比如src/server/api/routers/ ├── auth.ts ├── post.ts ├── user.ts └── comment.ts在root.ts里統(tǒng)一聚合。每個(gè) router 內(nèi)部可以重復(fù)使用publicProcedure或protectedProcedure如果需要鑒權(quán)變體可以單獨(dú)封裝一個(gè)帶特定中間件的 procedure而不是在每個(gè)業(yè)務(wù)路由里重復(fù)寫(xiě)邏輯。5.3 部署前要檢查的清單部署 T3 項(xiàng)目前我會(huì)例行檢查下面幾項(xiàng).env文件的數(shù)據(jù)庫(kù)地址、認(rèn)證密鑰是否正確Prisma schema 是否已經(jīng)同步到線上數(shù)據(jù)庫(kù)next build能不能通過(guò)類(lèi)型檢查是否啟用了 NextAuth 的 NEXTAUTH_URL有沒(méi)有把DATABASE_URL誤提交到 Git 倉(cāng)庫(kù)這里有個(gè)很容易踩的坑prisma generate生成的 Client 不一定會(huì)提交到倉(cāng)庫(kù)線上構(gòu)建時(shí)如果沒(méi)執(zhí)行 generate就會(huì)報(bào)找不到 Prisma Client。建議在構(gòu)建命令里加上prisma generate或者把生成命令放到postinstall腳本。5.4 與其他全棧方案的橫向?qū)Ρ扔?t3code 的方法論和別的方案對(duì)比Next.js REST API需要手動(dòng)維護(hù)類(lèi)型和接口文檔適合多端共用 API 的項(xiàng)目。T3 更適合同構(gòu)全棧、快速迭代的單體項(xiàng)目。Next.js GraphQLGraphQL 靈活但迭代成本高需要 resolver、schema、generator 一堆配置。tRPC 在這些方面更輕量因?yàn)樗恍枰~外的 schema 語(yǔ)言直接用 TypeScript 類(lèi)型。Next.js Server ActionsServer Action 很新處理表單提交很方便但復(fù)雜的查詢(xún)、權(quán)限校驗(yàn)、復(fù)用還偏年輕。tRPC 生態(tài)更成熟而且和 React Query 搭配緩存和狀態(tài)管理更順手。這并不意味著 T3 一定最好只是它更適合整天和 TypeScript 打交道、不想在接口溝通上消耗太多精力的開(kāi)發(fā)者。如果你團(tuán)隊(duì)里有純前端、純后端分工明確的人REST 可能更合適。個(gè)人體會(huì)是T3 Stack 最舒服的地方不在于某個(gè)單一庫(kù)而在于它把整個(gè)鏈路擰成一股繩。t3code 的核心也不是復(fù)制粘貼代碼而是理解那種“每個(gè)數(shù)據(jù)變動(dòng)都帶著類(lèi)型”的開(kāi)發(fā)節(jié)奏。最后再分享一個(gè)小技巧當(dāng)你新建一個(gè)業(yè)務(wù)模塊時(shí)按“先改 Schema、再寫(xiě) router、最后接頁(yè)面”的順序推進(jìn)你會(huì)發(fā)現(xiàn)類(lèi)型系統(tǒng)會(huì)幫你找出絕大多數(shù)遺漏的字段這種被編譯器提醒的感覺(jué)比任何測(cè)試都踏實(shí)。