我们先从 RPC 开始说起。
RPC(Remote Procedure Call),中文译为“远程过程调用”,主要用于在远程计算机之间执行操作。它允许一个程序调用另一个程序,就像调用本地服务一样。
其实这个介绍已经很精炼的概括了 RPC,但对于初学者尤其是前端同学,这个介绍可能依然有些不明所以。
其实提 RPC 一定要讲分布式,对于大型网站而言,子系统部署在不同的服务器上,但又需要相互协作,于是诞生了 RPC 这个技术概念。它主要解决 2 个问题:
想想 Server Actions,它的本质其实就是 RPC,调用的时候就像本地调用,实际上是客户端调用服务端
至于底层是使用 HTTP 还是 Socket 那是 RPC 框架的事情。
而 tRPC 是一个基于 TypeScript 的 RPC 框架,不过我们使用 tRPC 倒不是要解决分布式问题,而是为了解决客户端和服务端之间共享类型的问题。引用 tRPC 首页的介绍图:
左边是服务端代码,右边是客户端代码,当你修改了服务端的请求参数字段,TypeScript 立刻就在客户端代码提示出了字段错误。这种客户端和服务端之间的类型共享就叫做端到端类型安全(End-to-end typesafe)。
本篇为大家讲解 Next.js App Router 如何集成 tRPC。
不过我个人建议:如果你之前没有用过或喜欢 tRPC,那就不要学了。末尾会给解释。
初始化项目:
npx create-next-app@latest
选择 TypeScript、App Router:
安装 tRPC 依赖项:
npm install @trpc/server@next @trpc/client@next @trpc/react-query@next @trpc/next@next @tanstack/react-query@latest zod
新建 server/trpc.ts
,代码如下:
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
,代码如下:
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
,代码如下:
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,就可以查看到接口数据:
新建 app/_trpc/client.ts
,代码如下:
import { type AppRouter } from "@/server";
import { createTRPCReact } from "@trpc/react-query";
export const trpc = createTRPCReact<AppRouter>({});
新建 app/_trpc/Provider.tsx
,代码如下:
"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
,代码如下:
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
,代码如下:
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",
}),
],
});
当需要服务端渲染的时候,导入 app/_trpc/serverclient
调用方法。
修改 app/page.tsx
,代码如下:
import { serverClient } from "@/app/_trpc/serverclient";
export default async function Home() {
const todos = await serverClient.getTodos();
return <div>{JSON.stringify(todos)}</div>;
}
浏览器效果如下:
当客户端调用接口的时候,导入 app/_trpc/client
调用方法。
新建 app/todos/page.js
,代码如下:
"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>
);
}
浏览器效果如下:
首先,正如 tRPC 官方网站首页的介绍,tRPC 的最大特点是解决了全栈应用端到端类型安全的问题:
但是 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 的 Issue,而 tRPC 的作者在 5 月给出的回应是:
也就是说,因为 RSC 和 Server Actions 解决了不少创建 trpc 时要解决的问题,所以作者也不知道人们到底要怎么用 tRPC。
最后,tRPC 学习门槛高。毕竟 rpc 并不是一个小概念,它涉及的内容很多,使用 tRPC 还要重新学习 API 比如如何做校验、做鉴权、做缓存、做跨域、错误处理等等,对于没有经验的人又要踩上一批坑。
所以我个人觉得如果你之前没有用过或喜欢 tRPC,那就不用学了,觉得 tRPC 听起来帅就更没必要了。
但这并不是说 tRPC 一点用也没有,实际上,tRPC 官方给出了结合 Next.js Server Actions 的教程。因为 tRPC 本身的 API 做的不错,所以使用 tRPC 定义 Server Actions 可以使用 tRPC 的如输入验证、身份验证和授权、输出验证、数据转换器等功能。
个人意见,仅供参考,欢迎留言讨论