> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/next.js-kai-fa-zhi-nan/66.-shi-zhan-pian-trpc-yu-lei-xing-an-quan.md).

# 66. 实战篇 ❘ tRPC 与类型安全

### 前言

我们先从 RPC 开始说起。

RPC（Remote Procedure Call），中文译为“**远程过程调用**”，主要用于在远程计算机之间执行操作。它允许一个程序调用另一个程序，就像调用本地服务一样。

其实这个介绍已经很精炼的概括了 RPC，但对于初学者尤其是前端同学，这个介绍可能依然有些不明所以。

其实提 RPC 一定要讲分布式，对于大型网站而言，子系统部署在不同的服务器上，但又需要相互协作，于是诞生了 RPC 这个技术概念。它主要解决 2 个问题：

1. 分布式系统的服务之间的调用问题
2. 远程调用时，最好能够像本地调用一样方便，让调用者感知不到远程调用的逻辑

> 想想 Server Actions，它的本质其实就是 RPC，调用的时候就像本地调用，实际上是客户端调用服务端

至于底层是使用 HTTP 还是 Socket 那是 RPC 框架的事情。

而 [tRPC](https://trpc.io/) 是一个基于 TypeScript 的 RPC 框架，不过我们使用 tRPC 倒不是要解决分布式问题，而是为了解决客户端和服务端之间共享类型的问题。引用 tRPC 首页的介绍图：

![v10-dark-landscape.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10b5a472cf5a403bbb4f19f0273c7f55~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2954\&h=1408\&s=4017690\&e=gif\&f=487\&b=131211)

左边是服务端代码，右边是客户端代码，当你修改了服务端的请求参数字段，TypeScript 立刻就在客户端代码提示出了字段错误。这种客户端和服务端之间的类型共享就叫做端到端类型安全（End-to-end typesafe）。

本篇为大家讲解 Next.js App Router 如何集成 tRPC。

> 不过我个人建议：如果你之前没有用过或喜欢 tRPC，那就不要学了。末尾会给解释。

### tRPC 与 Next.js

#### 1. 项目初始化

初始化项目：

```bash
npx create-next-app@latest
```

选择 TypeScript、App Router：

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43bcab9815264e05a02a16b6fde2bd22~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1566\&h=390\&s=140599\&e=png\&b=1e1e1e)

安装 tRPC 依赖项：

```bash
npm install @trpc/server@next @trpc/client@next @trpc/react-query@next @trpc/next@next @tanstack/react-query@latest zod
```

#### 2. 定义接口

新建 `server/trpc.ts`，代码如下：

```javascript
import { initTRPC } from "@trpc/server";
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.create();
// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;
```

新建 `server/index.ts`，代码如下：

```javascript
import { z } from "zod";
import { procedure, router } from "./trpc";

export const appRouter = router({
  getTodos: procedure.query(() => {
    return {
      todos: ["运动", "冥想", "阅读"],
    };
  }),
});

// export type definition of API
export type AppRouter = typeof appRouter;
```

这里就是具体定义 trpc 方法的地方，我们声明了一个 `getTodos` 方法。

新建 `app/api/trpc/[trpc]/route.ts`，代码如下：

```javascript
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server";

function handler(req: Request) {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => ({}),
  });
}
export { handler as GET, handler as POST };
```

此时访问 <http://localhost:3000/api/trpc/getTodos>，就可以查看到接口数据：

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d546049f33d4946abaa9f6f2eb78c76~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1452\&h=274\&s=47794\&e=png\&b=f9f9f9)

#### 3. tRPC Client

新建 `app/_trpc/client.ts`，代码如下：

```javascript
import { type AppRouter } from "@/server";
import { createTRPCReact } from "@trpc/react-query";

export const trpc = createTRPCReact<AppRouter>({});
```

新建 `app/_trpc/Provider.tsx`，代码如下：

```jsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import React, { useState } from "react";

import { trpc } from "./client";

export default function Provider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({}));
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: "http://localhost:3000/api/trpc",
        }),
      ],
    })
                               );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}
```

修改 `app/layout.tsx`，代码如下：

```javascript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/app/_trpc/Provider";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}
```

新建 `app/_trpc/serverclient.ts`，代码如下：

```javascript
import { appRouter } from "@/server";
import { createCallerFactory } from "@/server/trpc";
import { httpBatchLink } from "@trpc/client";

const createCaller = createCallerFactory(appRouter);

export const serverClient = createCaller({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/api/trpc",
    }),
  ],
});
```

#### 4. 服务端调用示例

当需要服务端渲染的时候，导入 `app/_trpc/serverclient` 调用方法。

修改 `app/page.tsx`，代码如下：

```javascript
import { serverClient } from "@/app/_trpc/serverclient";

export default async function Home() {
  const todos = await serverClient.getTodos();
  return <div>{JSON.stringify(todos)}</div>;
}
```

浏览器效果如下：

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07f1deed87084f3b8ebf63ac632a3d1f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1146\&h=236\&s=31769\&e=png\&b=fefefe)

#### 5. 客户端调用示例

当客户端调用接口的时候，导入 `app/_trpc/client`调用方法。

新建 `app/todos/page.js`，代码如下：

```javascript
"use client";

import { trpc } from "@/app/_trpc/client";

export default function page() {
  const getTodos = trpc.getTodos.useQuery();

  return (
    <main>
      <div>{JSON.stringify(getTodos, null, "\t")}</div>
    </main>
  );
}

```

浏览器效果如下：

![29.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/668246f72f2e4fbb819f9f8fdda49ab8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1512\&h=315\&s=125303\&e=gif\&f=13\&b=fdfdfd)

### 不一定非要用 tRPC

首先，正如 tRPC 官方网站首页的介绍，tRPC 的最大特点是解决了全栈应用端到端类型安全的问题：

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1196109fc53a4844b179e8cdc593ac4f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2826\&h=592\&s=239839\&e=png\&b=ffffff)

但是 Next.js 的 Server Actions 已经解决了这一问题（实际上 Server Actions 和 tRPC 本质都是 RPC），当 Server Actions 搭配 TypeScript 的时候，已经能够给出准确的类型。

而且 Next.js App Router 已经出来 2 年了，tRPC 官方至今没有给出权威的接入 App Router 的教程（以上接入的教程更多是参考业界的实践总结而来）。

为什么至今没有呢？于是就有人发起了 [docs: Provide examples using tRPC with Next.js app router](https://github.com/trpc/trpc/issues/5277) 的 Issue，而 tRPC 的作者在 5 月给出的回应是：

![截屏2024-07-04 21.44.15.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c9286c440dc41d3855ca4ff51605b43~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2360\&h=796\&s=240004\&e=png\&b=ffffff)

也就是说，因为 RSC 和 Server Actions 解决了不少创建 trpc 时要解决的问题，所以作者也不知道人们到底要怎么用 tRPC。

最后，tRPC 学习门槛高。毕竟 rpc 并不是一个小概念，它涉及的内容很多，使用 tRPC 还要重新学习 API 比如如何做校验、做鉴权、做缓存、做跨域、错误处理等等，对于没有经验的人又要踩上一批坑。

所以我个人觉得如果你之前没有用过或喜欢 tRPC，那就不用学了，觉得 tRPC 听起来帅就更没必要了。

但这并不是说 tRPC 一点用也没有，实际上，tRPC 官方给出了结合 Next.js Server Actions 的[教程](https://trpc.io/blog/trpc-actions)。因为 tRPC 本身的 API 做的不错，所以使用 tRPC 定义 Server Actions 可以使用 tRPC 的如输入验证、身份验证和授权、输出验证、数据转换器等功能。

> 个人意见，仅供参考，欢迎留言讨论

### 参考链接

1. <https://www.youtube.com/watch?v=qCLV0Iaq9zU&t=12s&ab_channel=JackHerrington>
