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