如果你想成为一个 Typescript 全栈工程师,那么你可能需要关注一下 tRPC 框架。
本文总共会接触到以下主要技术栈。
不是介绍 tRPC 吗,怎么突然出现这么多技术栈。好吧,主要这些技术栈都与 typescript 相关,并且在 trpc 的示例应用中都或多或少使用到,因此也是有必要了解一下。
在线体验地址:TRPC demo
End-to-end typesafe APIs(端到端类型安全)
在介绍相关技术前,不妨思考一个问题。
当 进行网络请求和 API 调用时,你是否知道本次请求的参数类型以及返回的响应数据类型?知道了请求的数据类型与响应的数据类型,会为得到的 json 数据定义 type/interface,使其有更好的类型提示?还是会在 any 类型下获取属性,但由于没有类型提示,导致写错个单词,最终提示 Cannot read properties of undefined (reading 'xxx')?
对于大部分前端应用而言,类型往往常被忽略的,这就导致不知道这个请求的提交参数、响应结果有什么数据字段。举个 axios 发送 post 请求的例子
这是一个 post 请求用于实现登录的,但是这个响应数据 data 没有任何具体提示(这里的提示是 vscode 记录用户最近输入的提示),这时候如果一旦对象属性拼写错误,就会导致某个数据没拿到,从而诱发 bug。同理提交的请求体 body 不做约束,万一这个请求还有验证码 code 参数,但是我没写上,那请求就会失败,这是就需要通过调试输出,甚至需要抓包比对原始数据包,其过程可想而知。
最主要的是没有类型约束的情况下,非常容易出现访问某个对象属性不存在,js 开发者肯定经常遇到如下错误提示。
Cannot read properties of undefined (reading 'xxx')
有太多时候就是因为没有类型,无形间诱发 bug,这也是很多做 api 接口都常常忽视的一点。
因此我个人所认为的未来 Web 框架形态是要满足的前提就是前后端类型统一,即可以将后端的类型无缝的给前端使用,反之同理。而像 Next、Nuxt 这样的全栈框架便是趋势所向。
当然 axios 是可以通过泛型的方式拿到 data 的数据类型提示,就如下图所示。
但这样为了更好的类型提示,无形之间又增加了工作量,我需要定义每个接口的 Response 与 Body 类型,就极易造成开发疲惫,不愿维护代码。而本次所要介绍的技术栈 tRPC 就能够帮你省去重复的类型定义的一个 web 全栈框架。
tRPC
tRPC 是一个基于 TypeScript 的远程过程调用框架,旨在简化客户端与服务端之间的通信过程,并提供高效的类型安全。它允许您使用类似本地函数调用的方式来调用远程函数,同时自动处理序列化和反序列化、错误处理和通信协议等底层细节。
借官方 Feature
- Automatic type-safety(自动类型安全)
- Snappy DX(敏捷高效的开发者体验)
- Is framework agnostic (不依赖于特定框架)
- Amazing autocompletion(出色的自动补全功能)
- Light bundle size(轻量级打包大小)
什么时候该使用 tRPC
这个问题非常好,因为我在了解到 tRPC,并参阅了一些基本示例与实践一段时间后发现 trpc 和 http 的应用场景可以说非常相似,完全可以使用 trpc 来替代 http,只不过写法上从 发送 http 请求 ⇒ 调用本地函数(这在后面会演示到)。
而 trpc 又以类型安全与高效著称,如果你的 Web 应用的程序是基于 typescript,并且需要有高效的性能,那么 tRPC 就是一个很好的选择。
tRPC 可以作为 REST/GraphQL 的替代品,如果前端与后端共享代码的 TypeScript monorepo,trpc 则可以无需任何类型转换,也不太会有心智负担。
请记住,tRPC 只有当您在诸如 Next、Nuxt、SvelteKit、SolidStart 等全栈项目中使用 TypeScript 时,tRPC 才会发挥其优势。
tRPC 如何进行接口调用
一图胜千言,你可以点击 这里 在线体验一下 tRPC,并且查看其目录结构,以及调用方式。下面我一步步讲解如何进行接口调用。
定义服务端
这里以 Next.js 的目录结构而定。创建 server/trpc.ts
,如下代码。分别导出 router, middleware, procedure
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const router = t.router
export const middleware = t.middleware
export const publicProcedure = t.procedure
创建项目(根)路由文件 pages/api/trpc/[trpc].ts
import * as trpc from '@trpc/server'
import { publicProcedure, router } from './trpc'
const appRouter = router({
greeting: publicProcedure.query(() => 'hello tRPC!'),
})
export type AppRouter = typeof appRouter
此时已经定义好了一个路由地址 api/trpc/[trpc].ts
(这里 endpoint(端点)会在客户端中使用到),以及 greeting
函数,服务端的工作就暂且完毕。
创建客户端
创建 utils/trpc.ts
文件,代码如下
import { httpBatchLink } from '@trpc/client'
import { createTRPCNext } from '@trpc/next'
import type { AppRouter } from '../pages/api/trpc/[trpc]'
function getBaseUrl() {
if (typeof window !== 'undefined') {
// In the browser, we return a relative URL
return ''
}
// When rendering on the server, we return an absolute URL
// reference for vercel.com
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`
}
// assume localhost
return `http://localhost:${process.env.PORT ?? 3000}`
}
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: getBaseUrl() + '/api/trpc',
}),
],
}
},
})
在 _app.tsx
包装一下
import type { AppType } from 'next/app'
import { trpc } from '../utils/trpc'
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />
}
export default trpc.withTRPC(MyApp)
有了这个对象后,我们就可以开始尽情调用服务端所定义好了函数了。
当你导入 trpc 并输入 trpc.
时,将会提示出服务端定义好的 greeting
函数,如下图所示。
此时通过 const result = trpc.greeting.useQuery()
便可调用 greeting
函数,其中 result.data
便可拿到 'hello tRPC!'
信息。
这个过程发生了什么?
不妨此时打开控制台面板,看看请求
不难看出,调用 greeting 函数本质是向 /api/trpc/greeting
发送了 http 请求,并且携带参数 batch 和 input,虽然我们暂时还没有传。默认 input 为 。
要支持传递参数,首先需要在服务端定义传递参数的类型(会有 Zod 对参数效验),这样客户端才有对应的类型提示。然后调用 greeting 函数时,通过通过函数参数的形式来传递请求参数。
举例说明,比如说我们将 appRouter 改写成这样,通过 input 参数指定了 useQuery
需要传递一个 name
为字符串且不为空的对象。
import z from 'zod'
const appRouter = router({
greeting: publicProcedure
.input(
z.object({
name: z.string().nullish(),
}),
)
.query(({ input }) => {
return {
text: `hello ${input?.name ?? 'world'}`,
}
}),
})
调用 trpc.greeting.useQuery({ name: 'kuizuo' })
发送的请求的 query 参数则变为
不仅于此,你如果同时调用了多次 greeting 函数,如
const result1 = trpc.greeting.useQuery({ name: 'kuizuo1' })
const result2 = trpc.greeting.useQuery({ name: 'kuizuo2' })
const result3 = trpc.greeting.useQuery({ name: 'kuizuo3' })
tRPC 会将这三次函数调用合并成一次 http 请求,并且得到的响应本文也是以多条数据的形式返回
分别输出三者 result 也没有任何问题。
这是 tRPC 的一个特性:请求批处理,将同时发出的请求(调用)可以自动组合成一个请求。
useMutation() | tRPC
tRPC 同样也支持 post 请求,例如
服务端代码
const appRouter = router({
createUser: publicProcedure.input(z.object({ name: z.string() })).mutation(req => {
const user: User = {
name: req.input.name,
}
return user
}),
})
客户端代码
export default function IndexPage() {
const mutation = trpc.createUser.useMutation()
// ERROR!
// mutation.mutate({ name: 'kuizuo' });
const handleCreate = () => {
mutation.mutate({ name: 'kuizuo' })
}
return (
<div>
<button onClick={handleCreate} disabled={mutation.isLoading}>
Create
</button>
{mutation.error && <p>Something went wrong! {mutation.error.message}</p>}
</div>
)
}
这里需要注意 mutate
方法无法在外层直接调用,否则将会提示
Unhandled Runtime Error
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
主要防止这个组件被其他组件调用,此时自动调用 mutate 函数,导致不可控且循环调用的情况,因此需要通过一个事件(比如点击事件)来触发。
此时请求变为 post 请求,并且携带的参数也以 body 形式传递。
通过 useQuery 和 useMutation 就能够用 tRPC 实现最基本的 CRUD。此外还有 useInfiniteQuery 可以用作类似无限下拉查询,类似 SWR 无限加载。useQueries 批量查询,使用 Subscriptions 进行订阅 WebSocket 等等。
tRPC 针对 react 项目的查询主要依赖于 @tanstack/react-query