译者 | 刘涛
审校 | 重楼
在近期关注的技术趋势中,一种名为tRPC的技术架构引起了我的注意,它被广泛应用于包括T3在内的多种现代技术堆栈中。然而,对于tRPC的本质及其广受欢迎的原因,我并不清楚。
我开始研究和学习它。我不知道它的意思或目的是什么。所以,我深入研究了RPC、gRPC和其他技术来寻找答案。
我发现tRPC是一种用于设计API的类型安全架构风格。但该定义只是冰山一角,仅仅揭示了其深层内涵的一小部分。
在本文中,我期望能够更为深入地探究这座冰山的根源所在,明晰 tRPC 究竟是何种事物。本文针对 tRPC 展开了深度阐释,涵盖了我们为何需要它以及如何对其进行使用等方面。
请注意,作为这篇文章的撰写者,我是基于自身已有的研究成果,首次与你一同探究tRPC。此次探索主要面向初学者及新学者,现在就让我们一同深入其中。
先决条件
- 中级JavaScript知识
- 基本的TypeScript知识
- 中级React知识
- Fetch和REST API的使用经验
- 使用终端或控制台的经验
- 使用NPM及其命令的经验
- 使用CORS以及连接前端/后端的经验
- 热衷于学习新知识
你可以在此处找到本文的GitHub库和所有其他资源。
目录
- 什么是tRPC?
- 为什么我们需要tRPC?
- 如何使用tRPC
- 结论
什么是tRPC?
tRPC是一个基于TypeScript的类型安全库,它利用RPC API设计来处理API请求并交付响应。
RPC代表远程过程调用。我们的tRPC建立在RPC之上。RPC是一种设计API(如REST)的架构风格。使用RPC,你可以摆脱Fetch和REST API。
顾名思义,tRPC在RPC架构设计上添加了一个类型安全层。传统上,我们使用REST API。它包含GET、POST、PULL等请求类型。在tRPC中,没有请求类型。
每个对tRPC后端的请求都会通过查询系统,并根据输入和查询从tRPC后端获得响应。
相反,tRPC和react-query提供了内置函数来处理你的请求。每个请求都会得到相同的处理。这取决于API端点是否接受输入、输出、修改等。
使用REST时,你会创建一个名为/api 的主文件夹,并在其中创建路由文件。但对于tRPC,你不需要包含许多文件的文件夹。你只需要几个内置函数和一个简化的react-query(反应查询)系统。
你无需使用fetch()、处理输出等。tRPC使用表示特定查询的URL进行操作,你将很快会看到。
为什么我们需要tRPC?
tRPC确保RPC类型的安全。这表示客户端无法向服务器发送与其预期不匹配的数据类型。例如,客户端无法为基于数字的属性传递字符串。
如果客户端尝试这样做,系统将立即返回错误提示-无效类型。若数据类型不匹配,集成开发环境(IDE)和浏览器将同步抛出错误。
类型安全是确保JavaScript应用程序稳定性和可靠性的关键要素。tRPC框架正是利用TypeScript的强类型特性,极大地简化了后端路由的创建和操作执行过程。
tRPC的实现依赖于名为Zod的库。它为构建每个路由的数据模式提供了支持。所谓模式,是定义了属性且链接到每个属性的等效数据类型的对象。
例如,在需要用户详细信息的API路由中,开发人员将在后端定义一个对象,并利用Zod为该对象的每个属性指定相应的数据类型。
在前端,tRPC负责验证用户或API请求所提供的数据是否与后端注册的数据类型相匹配,从而在前端和后端之间实现了类型安全的集成。
接下来,我们将探讨tRPC、Zod以及其他相关库如何在演示项目中协同工作,以实现这一类型安全的集成。
如何使用tRPC
tRPC提供了一种高效的方式来快速搭建Express服务器并开发tRPC路由和查询。其简洁的API设计使得开发过程变得直观且易于上手。
在传统的Wb应用架构中,客户端(前端)和服务器端(后端)通常是分离的。本文示例将遵循这种架构模式,分别构建前后端系统。
让我们首先使用React创建客户端,使用Express+CORS创建服务器端来连接它们。
文件夹结构
首先,创建一个名为tRPC Demo的主目录。在该主目录中,再创建另一个名为trpclibrary的子目录,用于分离客户端和服务器端代码,并为后续作为统一库进行管理做准备。
在trpclibrary目录中,你将很快放置服务器端(Express)和客户端(React)的代码。
在tRPC Demo根目录中,插入带有以下代码的package.json文件,作用是连接所有的子文件夹,并允许通过单一命令同时运行客户端和服务器端。
{
"name": "trpclibrary",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"scripts": {
"start": "concurrently \"wsrun --parallel start\""
},
"workspaces": [
"trpclibrary/*"
],
"devDependencies": {
"concurrently": "^5.2.0",
"wsrun": "^5.2.0"
}
}
Root Directory package.json file
在根目录中配置完package.json文件后,下一步是在trpclibrary目录中搭建Express服务器。
专业提示:使用终端时,可以通过cd 命令进入文件夹并执行命令。假设你当前位于根目录中,则可以使用cd .\trpclibrary命令进入trpclibrary目录。VS Code的集成终端也同样可以用于此操作。
为了快速初始化服务器,我们将使用 npx create-mf-app 启动命令。该命令会生成一个预定义的项目模板,显著节省初始设置时间。
Server-Side Setup
你可能会遇到错误提示,指出你没有安装Express或其他库。不用担心—你很快就会逐一安装所有必需的库。
成功搭建服务器后,我们将继续使用React和相同的命令在同一个trpclibrary目录下创建客户端。
Client-Side Setup
你的React客户端已经准备就绪。然而,你可能会遇到与模块和包相关的各种错误。因此,让我们首先下载它们。
我正在使用yarn,我建议你也使用它。在trpcDemo根目录下执行yarn命令。
提示:你可以使用cd ..命令来退出当前目录,并进入外部目录。
你的服务器端或客户端,亦或是两者均可能缺少TypeScript配置文件。因此,我建议在两个目录下分别执行npx tsc --init命令以进行安装。这样做将有助于初始化配置文件,确保项目能够顺利运行。
TS Configuration File Initialization
当前,你需要将tRPC、CORS和Zod下载到项目的服务器端。
截至2024年7月2日,@trpc/server包的最新版本为10.45.2。请务必注意,即使是客户端的tRPC包也应保持与服务器端相同的版本,即10.45.2。
Installing Zod, CORS, and @trpc/server to the Server-Side
接下来,你需要为客户端安装@trpc/client,@trpc/react-query、@tanstack/react-query,@trpc/server和Zod。你可以使用相同的“yarn add”命令进行安装。
这次就不提供截图了,你可以参考前面的步骤尝试下载安装。
至此,我们已经完成了大部分安装和设置工作。以下是你的文件夹结构应当呈现的外观:
tRPC Demo
├── trpclibrary
│ ├── client-side (React App Folder)
│ ├── server-side (Express Server Folder)
└── package.json
Folder Structure
tRPC设置
在本节中,我们将执行以下任务:
- 利用Context机制来创建tRPC实例。
- 构建tRPC的路由系统并配置查询接口。
- 设定tRPC的基本URL地址。
- 配置跨源资源共享(CORS)策略。
首先,在服务器端目录的index.ts文件中创建一个tRPC实例,根据文档说明,每个应用程序只应启动一个实例。
利用该tRPC实例创建一个路由器。该路由器帮助你注册路由,以便API请求到达时能够进行处理。
路由是你处理请求并返回响应的地方。它是连接到Base URL的API端点。
例如,http://localhost:3005/api/hello这个地址描述了名为hello的API端点,以及用于调用该API端点的Base URL api。
import { initTRPC } from "@trpc/server";import *
as trpcExpress from "@trpc/server/adapters/express";
const createContext = ({}: trpcExpress.CreateExpressContextOptions) =>
({});type Context = Awaited>;
const trpc = initTRPC.context().create();
你需要将以上代码段放置在index.ts 文件中现有模板代码的上方,具体位置应在所有import语句之后,且位于声明app和port变量声明之前。同时,该代码段应位于express 模块的导入语句之下。
看!我使用了@trpc/server包中的initTRPC函数成功创建了一个tRPC实例。我们将依托此实例,来处理与后端相关的所有操作事务。
此外,我在tRPC路由器中添加了一个Context。这是tRPC的一项功能。它允许你将数据库连接和身份验证信息等详细资料输入其中。
tRPC能够在所有tRPC过程之间共享Context。它作为一个信息存储和传递的场所,有助于避免代码冗余,并保持代码的整洁有序。 到目前为止,你已经使用了Context初始化了tRPC实例。接下来,你将对路由器进行编码—因此,请将以下代码添加到之前代码的下方:
import zod from "zod";
const appRouter = trpc.router({
hello: trpc.procedure
.input(
zod.object({
name: zod.string(),
})
)
.query(({ input }) => {
return {
name: input.name,
};
}),});
export type AppRouter = typeof appRouter;
最后,你引入了Zod库,同时,你还创建了一个名为hello的API端点,该端点通过使用input()方法接受输入,并将用户的API请求与此端点定义的Zod对象进行匹配。
通过这段代码,Zod和tRPC都期望前端传递的对象中包含一个基于字符串的名为name的属性。
你获取输入,对其进行解构,并在query()方法中对其进行处理。这些操作都是tRPC流程的一部分。在tRPC的流程之间,Context会被共享。
正如我之前所述,你将在任何地方都需要tRPC实例。我用它来创建一个路由器来存储和注册路由(API端点),并对其进行处理。
在router()过程中,你可以创建无限数量的路由。它类似于一个路由处理程序,每个端点都是一个对象,每个路由都作为一个属性存在。
你将要求过程构建器提供对query()、input()等过程的访问权限。
现在,是时候配置 Base URL了。在此阶段,你将使用@trpc/server库中的Express适配器来设置 Base URL。
将以下代码放在index.ts文件中,具体位置应在app.get()路由处理函数之前:
app.use(
"/api",
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}));
/api表示你的 Base URL。所有路由均位于/api URL之上。目前,你的hello API端点已配置为http://localhost:3005/api/hello。
接下来,让我们尝试使用浏览器进行测试。你是否还记得我曾要求你在tRPC Demo根目录下创建一个包含预设代码的package.json文件吗?
该文件旨在实现服务器端和客户端的库式一体化运行。请将终端切换至根目录,并执行yarn start命令以启动服务器和客户端。启动完成后,请访问网址http://localhost:3005/api/hello。
tRPC Invalid Type Error
你是否遇到了错误提示?如果提示显示为“无效类型”,那么你走在正确的道路上。看,这就是tRPC为我们提供的帮助所在。
在上面的代码中,当我以用户身份向hello API端点发送API请求时,我没有传递tRPC期望该端点的任何对象或值。
tRPC期望的是一个包含一个基于字符串的属性名为name的对象,并且需要为其提供一个值。当我没有提供它时,tRPC就会限制我的访问权限。这正是tRPC的出色之处。
“这一切都很好,但是现在怎么办?”你必须将前端与服务器端连接起来,以便发送包含预期数据的对象。
至于服务器端,还有一件事情需要注意,那就是CORS(跨源资源共享)!设置CORS非常简单。请在index.ts文件中找到Express框架的初始化代码,这个代码是Express模板自带的。然后,在该代码中插入以下代码行:
import cors from "cors";
app.use(cors());
在你的index.ts文件中,请仔细查找有关端口设置与应用配置变量的声明部分。
一旦添加相关代码行,可能会出现错误提示,那是因为你尚未安装CORS的类型定义。请前往终端并在服务器端目录下安装@types/cors。
@types/cors download.
CORS已准备就绪且安全。服务器端已搭建完毕!现在,让我们尝试使用各自的库来实现服务器端和客户端的连接。
在我们转向客户端之前,我需确定我们在同一个页面上。到目前为止,你已经创建了一个tRPC实例,形成了一个路由器,设定了基本URL,并通过可选的Context对API端点进行测试。
以上所有配置和代码均已在服务器端的index.ts文件中完成。让我们转到客户端并攻克本教程的最后一部分。
客户端
我们已经下载了所需的包。接下来,我们将在客户端目录下的/src子目录中创建一个trpc.ts文件。该文件将负责处理前端发出的查询和请求。
在服务器端,你已经创建了一个 tRPC 实例来构建路由器和其他组件。现在,你需要在客户端进行相同的操作。为此,你需要使用@trpc/react-query来创建一个客户端 tRPC 实例。
此外,为了将客户端 tRPC 实例与服务器端实例相连接,你必须导入服务器端的 tRPC 实例及其类型定义。
要导入服务器端 tRPC 实例,请在服务器端的package.json文件中添加一个main 属性。这样,当你在客户端导入服务器端文件夹时,它将自动将index.ts文件设置为入口点。
Server-Side package.json file.
设置该属性后,你便可以通过终端将tRPC实例导入客户端。对我来说,在我的package.json文件中,位于服务器端目录下的后端模块称为server-side,版本为1.0.0。
因此,我将在客户端终端中执行yarn add server-side@1.0.0命令。这一安装过程可能看起来颇为熟悉,因为它正是开发者构建库的常规方式。
此命令应将你的服务器端文件夹作为包添加至客户端节点模块目录中。你可以通过查看客户端的package.json文件来验证这一点。
Client-Side package.json file.
它应当将你的服务器端包名列为依赖项。
换句话说,你已经在客户端应用程序中安装了服务器端包。现在,你可以导入服务器端tRPC,并将其作为库来使用。
回想之前,我们在服务器端创建路由器时,特意添加了额外的导出AppRout类型语句。我们之所以这么做,是因为必须在客户端导入AppRouter类型,这样才能在客户端使用服务器端的tRPC实例。
以下是trpc.ts文件目前的代码结构:
import { createTRPCReact } from
"@trpc/react-query";
import type { AppRouter } from "server-side";
export const trpc = createTRPCReact();
trpc.ts file.
使用此代码,你已经利用了服务器端tRPC实例的特征成功的构建了客户端tRPC实例。
接下来,我们将在/src目录下创建一个名为AppComponent.tsx的新文件。
该文件将负责存放主应用组件,它将从trpc.ts文件中导入tRPC客户端实例,并利用该实例调用hello API端点。
import React from "react";
import { trpc } from "./trpc";
AppComponent.tsx import statements.
鉴于你已经成功创建了tRPC客户端实例,你现在可以访问该客户端的全部API端口,并利用useQuery( )方法向这些API端口发起请求。
import React from "react";import { trpc } from
"./trpc";
const AppComponent = () => {
const userQuery = trpc.hello.useQuery({ name: "Afan" });
return (
{JSON.stringify(userQuery.data?.name)}
);};
export default AppComponent;
Entire AppComponent.tsx file.
如果你还记得,hello API端点需要一个带有字符串类型值的name属性的对象。因此,你将使用带有值的useQuery( )方法传递对象,以避免tRPC的参数不匹配问题。
在JSX代码中,你将利用JSON.stringify( )方法解构API端点返回的API响应,并由API端点访问结果。
你的AppComponent.tsx文件是一个标准的React组件,因此需要将其导入到主App.tsx文件中。在客户端,App.tsx文件相当于服务器端的index.ts。
对于App.tsx文件,你将遵循类似的配置流程。首先,从trpc.ts文件中导入客户端tRPC实例。然后,设置 Base URL并配置React Query。
你将从TanStack导入React Query,从./trpc.ts文件中导入trpc,从@trpc/client导入httpBatchLink,从React导入useState,从AppComponent.tsx文件中导入AppComponent。
// Default Import Statements
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.scss";
// Add the following Import Statements
import { useState } from "react";
import { trpc } from "./trpc";
import { httpBatchLink } from "@trpc/client";
import AppComponent from "./AppComponent";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
你将逐一使用每个导入语句,无需担心关于未使用导入对象的错误。接着,仿照tRPC的做法,创建一个React Query客户端实例。
const client = new QueryClient();
完成此操作后,你需要设置Base URL。这一步骤将在主应用程序的功能部分进行。
此外,请将位于App函数下的代码语句移至React Query客户端声明语句之后,放置在App函数的顶部位置。
const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Failed to find the root element");
const root = ReactDOM.createRoot(rootElement as HTMLElement);
然后,你需要从App函数中移除默认的JSX超文本标记语言代码。你可以放心地安全删除App函数中的所有超文本标记语言内容。
接下来,你需要为客户端设置Base URL。每当前端进行API端点调用时,都会使用这个Base URL。它应该与你在服务器端设置的Base URL保持一致。
请将App功能代码从HTML中替换为以下Base URL代码:
const App = () => {
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// Base URL
url: "http://localhost:3005/api",
}),
],
})
);
return <>;
};
你的App.tsx文件,如下所示:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.scss";
import { useState } from "react";
import { trpc } from "./trpc";
import { httpBatchLink } from "@trpc/client";
import AppComponent from "./AppComponent";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const client = new QueryClient();
const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Failed to find the root element");
const root = ReactDOM.createRoot(rootElement as HTMLElement);
const App = () => {
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// Base URL
url: "http://localhost:3005/api",
}),
],
})
);
return <>;};
root.render();
我尚未详细探讨return语句的具体用法。所以,就让我们现在来进行这项讨论。我们不允许return语句为空。
该语句将显示API端点返回的数据,这些数据应是通过在AppComponent.tsx组件文件中调用useQuery( )方法提交的字符串。
return语句主要供包装器及AppComponent 组件使用。若组件和页面需要采用 tRPC、React Query 等技术,则必须使用这些库的 Providers 来打包 AppComponent 组件。
return (
// tRPC Provider
{/* React Query Provider */}
{/* HTML React Component */}
);
现在,你将使用React Query打包AppComponent组件,并在该文件中传递你通过调用QueryClient( )创建的React Query客户端实例。然后,你将利用tRPC Provider打包React Query Provider。
The tRPC Provider 需要React Query客户端和带有Base URL的tRPC客户端。因此,我们也将提供该信息。
一旦你传递了所需信息并确保代码与我们的代码相匹配,你就可以访问http://localhost:3000并查看输出。该页面将显示你通过hello API端点传递的数据。
注意:你应该在tRPC Demo目录下运行yarn start命令,打开localhost端口以查看输出结果。
输出图像
我们已经准备就绪。tRPC允许我们从前端调用hello API端点。它优先考虑类型安全,并采用TypeScript来避免数以百万计的其他JavaScript可能带来的问题。
你可以在Route处理程序中添加更多路由和API端点,比如hello。这就像给对象添加新属性一样简单。tRPC就是这样让你的开发工作变得更加轻松。
结论
tRPC是一个类型安全的RPC样式库。它将RPC与TypeScript深度集成,旨在消除REST、fetch( )和其他创建和调用API的技术所带来的问题。
它作为REST和Fetch的替代方案。我将在可预见的未来继续使用它。
译者介绍
刘涛,51CTO社区编辑,某大型央企系统上线检测管控负责人。
文章标题:What is the tRPC Library? Explained with a Demo Project (freecodecamp.org),作者:Afan Khan