☰
t3code:用tRPC打通前后端类型安全的全栈实践
2026/10/27 7:27:20 网站建设 项目流程

最近和几个全栈方向的朋友聊项目,不止一个人提到 t3code 这个词。一个说他们在新项目里用它做基建,另一个在技术吐槽群里抱怨团队从 REST 迁移过来之后的联调习惯全变了。t3code 实际上对应的就是 The T3 Stack 这套全栈技术组合的代码实践代号——TypeScript、Tailwind CSS、tRPC 加上 Next.js,目标是让类型安全从数据库一路贯通到浏览器端的每一个组件里。

我见过太多团队的情况:前后端都用 TypeScript,但接口类型各维护一份,后端改了字段名不吭声,前端跑起来才发现整个页面崩了。t3code 想解决的就是这个问题——让类型定义成为真正的单一事实来源,而不是靠人去同步。这篇文章我会从技术选型逻辑、全链路运行机制、可复现的搭建过程、类型安全的实际收益,以及我在真实项目里踩过的几个坑,把一个 t3code 风格的项目完整拆给你。适合正在选型全栈方案、或者对 tRPC 感兴趣但一直没动手的开发者,也适合想理解"为什么新项目默认用这套"的团队技术负责人。

1. t3code 的命名逻辑与 T3 原子精神:为什么偏偏是这三个 T

1.1 TypeScript:类型不是约束,是留给未来的文档

很多人把 TypeScript 当成"多写几行类型注解的 JavaScript",这个理解没什么大错,但放到 t3code 的语境里就太浅了。t3code 对 TypeScript 的定位不是编译期检查器,而是整个系统的信息主干——类型定义不是写给编译器看的,是写给下一个改代码的人看的。

我举个实际感受。一个 t3code 项目里,你定义一个 createUser 的 procedure,输入是 email 和 password,输出是 user 对象。这个时候 TypeScript 的类型信息会流向两个方向:向后,它会约束数据库查询的结果映射;向前,它会决定前端调用时编辑器能补全出哪些字段。也就是说,类型写在服务端,却直接变成了客户端开发者看到的所有提示。这种"一次定义,两头受益"的体验,只有在整套技术栈都以 TypeScript 为轴心的时候才转得起来。

这也是为什么 t3code 不接受 JavaScript。不是 TypeScript 高人一等,而是整个类型隧道机制建立在 TypeScript 的推断能力之上。你一旦去掉类型,tRPC 的魔法就消失了大半,剩一个远程函数调用工具,价值就大打折扣了。

1.2 Tailwind:样式与组件同文件,消除上下文切换

Tailwind 排在第二个 T,不是因为样式方案多漂亮,而是它和 t3code 的组件化开发节奏契合得非常好。在传统 CSS 方案里,写一个组件往往要在三个文件之间来回跳:结构、样式、逻辑。Tailwind 的做法是直接用原子类把样式钉在 JSX 上,组件改到哪里,样式就跟到哪里。

这个特性和 tRPC 的体验是一致的——相关的东西待在一起。tRPC 把后端逻辑和前端调用放在同一个思维模型里,Tailwind 把样式和组件放在同一个视觉单元里,它们都在做同一件事:降低上下文切换成本。

加上 Tailwind 的设计令牌机制,你不需要为每个新项目重新发明一套颜色和间距系统。在 t3code 的快速原型场景里,这是实打实的效率提升——不用再等样式方案定稿,先把页面结构搭出来,原子类直接可用。我实测一个中等复杂度的后台页面,纯 Tailwind 写下来大概能比手写 CSS 快一倍,而且后期调整间距和配色只需要改类名,不需要翻样式表找嵌套层级。

1.3 tRPC:打通服务端和客户端的那条类型隧道

第三个 T 是 t3code 的灵魂。tRPC 的全称是 TypeScript Remote Procedure Call,它解决的是我在开头说的那个割裂问题:前端调后端接口时,类型信息能不能不靠手写、不靠生成器,直接通过语言本身推导过去?

REST 方案里,后端给一个 GET /user/1 的响应,前端拿到的是运行时才知道结构的 JSON,类型安全全靠手写的 interface 或者手工维护的 OpenAPI 生成物。GraphQL 好一些,有 schema 作为契约,但你需要学 query 语法、配 resolver、维护 codegen 流程。tRPC 直接绕过了这一整套复杂度——你在服务端定义一个函数(它叫 procedure),前端就能像一个普通异步函数一样调用它,输入和输出的类型自动跟随,不用写 schema、不用跑 codegen、不用定义路由字符串。

听起来像魔法,本质上只是 TypeScript 类型推断能力的一个极致用法。tRPC 的 router 本身就是一个类型对象,通过泛型把整个路由树的结构"投影"到客户端。字段名、嵌套结构、输入输出类型,全部在编译期确定。这种设计把"前后端契约"从文档变成了代码本身,类型不匹配的时候,不是运行时才发现,而是编辑器里当场报错。

2. 四大组件协同工作的机制拆解:从一次浏览器请求看全链路

2.1 一次 t3 请求的完整生命周期

很多人第一次用 tRPC 会觉得玄学:前端明明没有引入任何 API 定义文件,怎么就知道后端有哪些接口?其实整个机制走一遍就清晰了。用户在前端点了一个按钮,触发 trpc.todo.getAll.useQuery(),这个调用先是走到 TanStack Query 这个请求层,由 tRPC 客户端生成一个带有特定路径的 HTTP 请求,打到 Next.js 的 API 路由入口(通常是 /api/trpc/*)。

请求到达服务端后,tRPC 的 endpoint handler 根据路径找到对应的 procedure,先做输入校验——t3code 项目里通常用 zod 定义输入 schema,校验不通过直接返回 400;校验通过后执行你的业务逻辑,比如从 Prisma 里查数据库;返回结果经过 superjson 序列化,再通过同一个 HTTP 响应传回浏览器。浏览器端收到数据后,tRPC 客户端把它反序列化,这时候类型信息已经完全还原,你的组件可以直接访问 todo.title 并有完整的自动补全。

这套链路里最妙的一点是:你完全不用关心 HTTP 细节。用 useQuery 拿到的是带 status 的响应式数据对象,loading、error、success 状态都帮你管好了。我一开始也怀疑这种"黑盒"会不会掩盖问题,但实际排查下来,tRPC 的错误信息非常具体,输入校验失败和业务逻辑抛错会分别显示。

2.2 为什么 tRPC 和 React Query 组合起来几乎没有样板代码

传统数据请求怎么写的?useEffect 里 fetch,然后自己管理 isLoading、error、data 三个 state,还要处理竞态和缓存。t3code 里这套被 tRPC 和 React Query 的集成直接替代了。

createTRPCNext 或者 createTRPCReact 生成的客户端,本身就把 React Query 的 hooks 嵌了进去。你写 trpc.todo.list.useQuery() 拿回来一个对象,里面有 data、isLoading、refetch 这些现成属性。写提交操作就用 trpc.todo.create.useMutation(),自动获得 mutate 和 onSettled 回调。

我实际项目里最有体感的是缓存失效。一个 todo 创建完成之后,你需要让列表自动刷新。在传统写法里要手动重新 fetch 一次;在 t3code 里只要在 mutation 的 onSuccess 里写 utils.todo.list.invalidate() 这一行,tRPC 会自动把所有相关的列表 query 标记为失效,下次渲染自动重新拉取。这种体验已经接近"声明式数据同步"了,代码量少很多,出错概率自然也就下来了。

2.3 一份类型定义怎么做到前后端通用

这个问题是 t3code 体系里被问最多的。答案是:类型从服务端 router 通过 TypeScript 泛型推断到客户端,但数据在 HTTP 上传输时要用 superjson 做序列化。

这里有个关键点:tRPC 的类型传递不靠网络,靠语言特性。服务端的 router 对象本身是类型化的,当客户端把 router 的类型作为泛型传给 createTRPCClient 时,TypeScript 就已经把整个调用链的类型结构"复制"到前端了。网络传输的只是 JSON 数据,类型信息在编译期就已经完成对齐。

但 JavaScript 和 TypeScript 的某些类型在 JSON 序列化时会丢信息——最常见的是 Date 对象,JSON.stringify 会把 Date 变成字符串,拿到前端再 new Date() 又容易出错。superjson 就是为了这个场景做的。它能把 Date、Map、Set 这类复杂类型做特殊编码,序列化时保留原始类型信息,反序列化时原样恢复。t3code 默认开启 superjson,你直接在服务端返回 Date 类型,前端拿到的就是一个真正的 Date 对象,可以直接调用 getTime() 之类的方法。

3. 动手搭建一个 t3code 风格的全栈应用:类型安全待办事项的完整链路

3.1 初始化与目录结构

先说环境要求:Node.js 18 以上,建议用 pnpm 做包管理。初始化用一个命令:

npx create-t3-app@latest my-t3-app

安装过程中会有几个选项让你勾选,我建议第一次尝试的人勾上 Next.js、tRPC、Tailwind、Prisma、Zod,这些是 t3code 的核心组合。如果你暂时不想接数据库,可以先去掉 Prisma,用内存数组代替,先把链路跑通。

初始化完成后的目录结构里,几个关键位置得心中有数:

src/pages/api/trpc/[trpc].ts # tRPC 的 HTTP 入口 src/server/api/routers/ # 所有 procedure 的定义位置 src/server/db.ts # Prisma 客户端 src/server/api/trpc.ts # tRPC 上下文和实例创建 src/server/auth.ts # 鉴权相关(如果勾选了 NextAuth)

这里我多说一句,很多人第一次看到 [trpc].ts 这个动态路由文件名会疑惑。它是在 Next.js 的 API Routes 里挂一个通配端点,所有以 /api/trpc 开头、后面跟着路径参数的请求都会进到 tRPC 的 handler 里,由 tRPC 自己根据路径去解析是哪个 procedure。

3.2 定义数据模型和第一个 Router

我用 Prisma 定义一个最简单的 Todo 模型,然后立刻把它变成可调用的接口。

model Todo { id String @id @default(cuid()) title String done Boolean @default(false) createdAt DateTime @default(now()) }

先跑一次数据库迁移:

npx prisma db push

接下来在 routers 目录下新建一个 todo.ts:

import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../trpc"; export const todoRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.todo.findMany({ orderBy: { createdAt: "desc" } }); }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(200) })) .mutation(async ({ ctx, input }) => { return ctx.db.todo.create({ data: { title: input.title } }); }), toggle: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const todo = await ctx.db.todo.findUnique({ where: { id: input.id } }); if (!todo) throw new Error("Todo not found"); return ctx.db.todo.update({ where: { id: input.id }, data: { done: !todo.done }, }); }), delete: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { await ctx.db.todo.delete({ where: { id: input.id } }); return { success: true }; }), });

这段代码值得细看的地方是 publicProcedure 后面直接跟 query 或 mutation,query 负责读,mutation 负责写。输入参数用 zod 定义,zod 的 schema 也是类型,所以 input 在 handler 里直接就带类型提示,不需要额外声明。

然后把 todoRouter 挂进根 router:

import { createTRPCRouter } from "../trpc"; import { todoRouter } from "./todo"; export const appRouter = createTRPCRouter({ todo: todoRouter, }); export type AppRouter = typeof appRouter;

这一个 export type AppRouter 就是整个类型隧道的源头。

3.3 前端消费:带上全部类型补全的调用体验

在任意页面组件里,tRPC 的 hooks 用法非常直接:

import { api } from "~/utils/api"; export default function TodoList() { const { data: todos, isLoading } = api.todo.list.useQuery(); const createMutation = api.todo.create.useMutation({ onSuccess: () => { // 让列表自动刷新 utils.todo.list.invalidate(); }, }); const toggleMutation = api.todo.toggle.useMutation(); if (isLoading) return <div>加载中...</div>; return ( <ul> {todos?.map((todo) => ( <li key={todo.id}> <input type="checkbox" checked={todo.done} onChange={() => toggleMutation.mutate({ id: todo.id })} /> <span>{todo.title}</span> </li> ))} </ul> ); }

这里 api.todo.list 是有完整类型提示的链式调用,api.todo.create.mutate 也要求传入符合 zod schema 的对象,字段少了或类型不对,编辑器当场就会报错。整个待办功能从后端到前端,没有写一条手动的类型声明,也没有定义任何 URL 字符串,所有类型都从服务端 router 自动流向客户端。

4. 类型安全带来什么真金白银的收益:一次需求变更的现场对比

4.1 传统 REST 联调中一次普通改动要碰多少地方

先说一个我踩过无数次的场景。后端接口返回的用户对象里有个 nickname 字段,业务上要改成 displayName。在传统 REST + 手写类型的项目里,这一步的波及面如下:

环节需要做的事风险
后端改数据库映射、改接口返回结构低
前端类型定义找到手写的 interface,逐个字段同步中,容易漏
UI 组件找到所有使用了 nickname 的地方高,全局搜索不一定找得全
联调测试手动验证每个页面高,运行时才发现

改一个字段名,轻则半天,重则让某个页面悄悄白屏。最难受的是,这种错误不会在编译期暴露,必须到运行时、甚至到某些边缘操作时才炸出来。

4.2 同一个改动发生在 t3code 项目里是什么体验

现在把同样的需求放到 t3code 项目里。服务端 todoRouter 的 create 返回类型从 { id, title, done } 改成 { id, title, done, dueDate },你只需要改 Prisma 模型和 procedure 的实现。改完之后,前端所有用到 create 结果的地方,编辑器会立刻高亮所有缺少 dueDate 处理的位置。

如果你把字段名改掉了——比如 title 改成 name,那么前端所有读取 todo.title 的代码会全部变成红色下划线。你可能需要按需求决定是改 UI 还是保留兼容字段,但关键是:不会再出现"漏改一处,运行时才发现"的情况。类型系统帮你把改动清单列得清清楚楚,你只需要逐个确认,而不是担心还有什么是没找到的。

我在公司内部的存量项目里做过一次估算,同样的接口字段变更,t3code 流程下大概只需要传统流程三分之一的时间,而且不需要专门的联调验证环节——编译通过基本就代表契约一致了。

4.3 收益不止在编辑器里:重构、上手和排错都在受益

类型安全的收益很容易被低估,因为它的价值不是某个功能点,而是像基础设施一样渗透在所有日常操作里。我总结了三个在真实项目里体会最深的点。

第一是重构底气。当你要把一个大组件拆成两个,或者把某个业务逻辑从页面挪到服务端时,改动之后跑一遍 TypeScript 编译,编译器会告诉你有哪里没接上。没有这套类型链路的项目里,这种重构通常伴随大量肉眼检查和线上回归。

第二是新人上手速度。新同事加入 t3code 项目,打开编辑器,输入 api. 之后直接看到所有可用的后端方法及其参数类型,根本不用翻接口文档。我实际观察下来,新人从"看完代码"到"能独立加一个接口",大概只需要半天。

第三是错误定位成本。tRPC 的错误带栈信息,zod 的输入校验失败会精确到哪个字段、哪个约束不满足,前端 error 里能看到原因。相比传统接口返回一个笼统的 500 和一个日志页面,排查定位要快得多。

5. 实操中绕不开的坑:我在 t3code 项目里踩过的几个深坑

5.1 服务端组件里不能直接用 tRPC hooks

这是 Next.js App Router 和 t3code 结合时最经典的坑。t3code 默认的 create-t3-app 是 Pages Router,但如果你把 t3code 用在 App Router 项目里,注意:tRPC React 客户端(createTRPCReact)只能在客户端组件里用。原因很简单,它依赖 React Context 和 React Query Provider,而服务端组件里没有这个运行时上下文。

解决方式有两种。一种是把组件顶部写上 "use client",让它变成客户端组件;另一种是在服务端组件里用 tRPC 的服务端调用器(server caller),直接调用 procedure 的 resolve 方法获取数据,再把数据当作 props 传给客户端组件。

import { appRouter } from "~/server/api/root"; import { createTRPCContext } from "~/server/api/trpc"; // 在服务端组件里同步拿数据 const ctx = await createTRPCContext(); const caller = appRouter.createCaller(ctx); const todos = await caller.todo.list();

这里最需要注意的是一个常见误区:服务器组件渲染是在服务端执行的,直接让服务端调用数据库获取数据反而比多一跳 HTTP 请求更高效。但如果你习惯了客户端组件的写法,在服务端组件里直接 import 客户端 tRPC 库,会得到一串极其难读的打包报错,我第一次碰上时以为是我的 tRPC 配置坏了,排了半天才发现是组件模式的问题。

5.2 superjson 对 undefined 的处理方式

superjson 解决了 Date 序列化问题,但它有一个值得注意的限制:不是所有东西都能原样传。最典型的例子是 undefined。superjson 的序列化格式中,undefined 在对象属性里会丢失,传递后变成 null,没法区分"这个字段不存在"和"这个字段值是 null"。此外,Buffer 类型、函数、符号这些本来也不支持序列化,就不多说了。

这个问题在正常业务里不太会遇到,但如果你喜欢用"未定义"来区分状态——比如 optional 字段表示用户没填写——那就要小心。我的建议是尽量用 null 值表达"空状态",不要依赖 undefined。如果你确实需要区分"字段缺失"和"字段为 null",可以考虑在前端手工做一次对象到类型实例的映射。做一个映射函数的开销很小,但能省掉很多诡异的 bug 调试时间。

5.3 zod schema 前后端共用时的边界与陷阱

zod 之所以被 t3code 选作输入校验库,是因为它的 schema 本身就能输出 TS 类型——你定义一次,校验规则和类型都有了,天然适配 t3code 的类型隧道设计。但在实际项目中,zod 的 schema 往往会长得比较复杂,尤其是嵌套对象和可选字段混在一起时。

一个比较隐蔽的坑是:zod 的 refine 和 superRefine 这类自定义校验逻辑,在服务端执行时的前后端表现不一致。比如你用 refine 检查密码复杂度,前端表单也调用同一个 schema 做即时校验,但前端的运行环境可能缺少某些依赖(比如没有你引入的某个正则库),导致校验行为不同。我的处理原则是:基础结构校验(字段是否存在、类型对不对)用共享 schema;业务规则校验(比如"订单金额不能超过账户余额")放到 procedure 内部去查数据库后判断,不要写在 zod 的 refine 里。这样既保证输入校验的类型安全,也不会把业务逻辑耦合到校验库的机制里去。

5.4 部署到 Serverless 环境时的额外关注点

t3code 应用是标准的 Next.js 应用,部署到 Vercel、Deno Deploy 这类 Serverless 平台,多数情况下都可以直接用。但有一个东西要记得处理:数据库连接。Serverless 环境的函数可能会被频繁地冷启动和销毁,如果每次请求都新建一个 Prisma 客户端连接,数据库连接池很容易被打满。

解决办法是全局单例模式。在 src/server/db.ts 里用一个 globalThis 变量持有 PrismaClient 实例,开发环境热更新时也不会重复创建连接。create-t3-app 生成的模板其实已经做了这个处理,但我见过不少人在迁移老项目时忽略了这个点,导致部署到线上后时不时出现"Too many connections"的报错,而且只在并发量波动时出现,排查起来相当头疼。第一次跑 t3code 部署时,建议先开几个并发请求做一次压测,确认数据库连接数稳定再上正式环境。

5.5 tRPC 鉴权的 split 模式为什么要做

t3code 的鉴权默认会区分 publicProcedure 和 protectedProcedure。protectedProcedure 实际上是先走一个中间件,在上下文对象里把当前登录用户信息挂上去。使用 create-t3-app 初始化项目时,如果你选择了 NextAuth,模板会自动把 session 的读取放在中间件里。

很多人一开始不理解为什么 tRPC 不直接在 handler 里读 session,非要抽一个 protectedMiddleware 出来。我从实际项目里体会到的原因是:中间件模式让权限判断和业务逻辑分离,所有需要鉴权的 procedure 都复用同一段逻辑,而且你在 handler 里拿到的 ctx.session.user 已经带类型了,不需要每个 procedure 都重新做一次空值判断。

这个模式在开发效率上的提升很明显,但要注意:如果你用 split middleware 模式——就是服务端和客户端各自持有上下文——务必确保两边的 session 读取方式一致,否则会出现"服务端这边还没登录,客户端那边以为登录了"的类型误判,甚至在客户端控制台看到 session 对象类型和非空断言打架的诡异情况。我在一个多服务部署的项目里就踩过这个坑,最后排查下来是两边的 session 密钥配置不相同导致的。所以如果你做了多环境部署,先检查各环境的环境变量是否完全一致。

6. t3code 的边界:这套技术栈不适合什么场景

说完了优点和坑,我也想讲讲 t3code 不擅长的地方。技术选型最忌讳的就是拿着一把锤子看什么都是钉子,t3code 很强大,但它的应用边界也相当清晰。

6.1 大型公共服务 API 不如 OpenAPI 生态实用

如果你的产品是一个面向第三方开发者开放的公共 API,情况就完全不同了。第三方开发者不可能为了调用你的接口先去学什么是 tRPC、怎么通过 TS 类型推断来消费你的服务。他们需要的是 OpenAPI 文档、标准的 REST 端点、各种语言的 SDK 生成器。tRPC 的设计前提是"调用方和被调方都在同一个 TypeScript 语言生态里",一旦跳出这个生态,它的优势就消失殆尽。

也就是说,t3code 特别适合内部全栈应用、中小型产品、以及前后端都是 TypeScript 团队的自有业务。但如果你要做一个对外开放的服务平台,老老实实用 REST 或 GraphQL 加上完善的文档才是更稳妥的选择。

6.2 团队 TypeScript 基础薄弱时需要额外培训投入

这一点可能有些反直觉。t3code 号称可以减小团队沟通成本,但前提是成员已经吃透了 TypeScript 的泛型和解构特性。如果你的团队大部分人只会写 JavaScript,对泛型一知半解,那么 t3code 的学习曲线比常规的 REST + 手写类型方案更陡。

原因是 tRPC 虽然省掉了接口文档,但它要求你能够理解和阅读类型推断产生的错误信息。没有 TypeScript 基础的人第一次看到"类型 'Never' 上不存在属性 'data'"这类报错时,可能直接就懵了。我建议:如果团队负责 t3code 项目的核心成员里没有一个人熟悉 TypeScript 高阶类型,先花一周时间做 TypeScript 专项培训,再上 t3code 会比较顺利,否则很容易出现"每个人都在照抄模板,但一遇到类型报错就全员卡住"的局面。

6.3 复杂存量项目的渐进式改造难度在哪

t3code 是典型的绿地项目出生证。它的类型隧道机制要求整个链路里每个环节都使用 tRPC 的类型系统,如果你现有项目里有大量历史接口是 REST、部分页面是 jQuery 时代的老代码、中间还有一层不规范的网关,渐进式接入的难度会指数级上升。

不是说你不能把 t3code 嵌入旧项目,而是计算改造成本时要诚实。把一个新的 tRPC 模块挂到现有 Next.js 项目里很简单,但你要让旧前端代码享受到类型安全,就得把原有的数据请求层整体替换一遍。这个替换过程比重新写一个同规模新项目可能还麻烦。所以我的建议是:想清楚改造边界。如果是单个新业务模块,可以在旧项目里引入 t3code;如果是为了整个系统的类型安全,不如直接考虑从核心模块开始重建,而不是一层层包补丁。

6.4 极端性能敏感场景不适合无脑套用

tRPC 调用本身有明显的事件循环开销吗?有。每一个 procedure 从请求进来,到 tRPC router 匹配、zod 校验、调用 handler 函数、superjson 序列化,最后返回响应,中间多了一层封装。

对于绝大多数业务系统——后台管理、内容平台、中小型电商——这个开销完全可以忽略,数据库查询的网络耗时和磁盘 IO 才是大头。但如果你在做的是面向极大规模用户访问的读取型页面,比如高并发的商品详情页,或者对首屏渲染时间极敏感的活动页,t3code 的默认架构可能过于重型。这类场景更适合把高耗时接口拆成提前渲染的静态数据,或者把部分数据缓存移到 CDN 层,而不是每个请求都穿过完整链路。

这不是说 t3code 不能压性能,而是你要知道它默认的施工方式是以开发效率为第一优先级的,性能优化需要你额外去调缓存策略和部署方案。先确认清楚你的瓶颈在哪儿,再决定是否需要为了性能把某条链路从 t3code 里剥离出来。

最后说一点我自己这几年的体会。t3code 初看是一套工具链的组合,用多了你会发现它代表的一种工作方式的转变——后端把接口当函数暴露,前端把调用当地图导航,类型安全像一根隐形轨道,让两边的代码从出生就注定对齐。这套模式不是银弹,但喜欢的人一定会越用越上瘾。如果你正在规划一个新项目,我建议你拿一个非核心模块先试点跑通,把真实的功能和真实的类型链路都走一遍,体验过那份"改一处全报错"的安全感之后,你再决定要不要把它作为团队的默认起点。

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

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

立即咨询