前言
前段时间狼叔在年终总结中提到了tRPC,并且给了很高的评价,引起了我的好奇。于是我最近研究了一下,写了几个小demo后,发现体验真的太棒了,下面给大家分享一下我这几天的学习成果,让大家简单了解一下tRPC。
下面是狼叔文章里的截图
tRPC是什么
先看官网首页的一张动图,也可以自己去这个地址体验一下,体验完你可能就知道tRPC用来干嘛的了。
tRPC(TypeScript RPC)是一个用于在 TypeScript 应用程序中构建类型安全的 API 的工具。它允许开发者创建端到端类型安全的API,这意味着客户端和服务器之间的通信可以完全通过 TypeScript 类型进行验证和推断。
tRPC 通过结合远程过程调用(RPC)的概念与 TypeScript 的强类型系统,消除了编写单独的API类型定义或运行时验证代码的需要。客户端可以直接像调用本地函数一样调用服务器上的函数,而无需担心参数类型或返回值类型是否匹配。
主要特点包括:
- 类型安全:客户端与服务器端代码共享相同的类型定义,保证了API请求和响应的类型安全。
- 无需定义REST路由:不需要手动设置RESTful API路由,tRPC 会自动为你提供基于函数的端点。
- 轻量级:tRPC 是一个轻量级库,它不需要依赖大型框架。
- 前后端集成:tRPC 可以与现代前端框架(如 Next.js、React、Vue等)深度集成,提供更加流畅的开发体验。
总的来说,tRPC 提供了一种简洁、高效且类型安全的方式来构建应用程序的后端API,并能轻松与 TypeScript 前端代码集成。
上面是chatgpt说的,通过我这几天实践,补充一个真实有用的场景,关注我的人应该知道我在开发一套全栈后台管理系统,在开发的过程有个很恶心的地方,就是后端定义过一遍模型,前端因为是ts项目,为了类型安全,请求那里还要定义一遍模型,这个开发体验很差,为了解决这个问题,我还自己写了一个插件,通过后端swagger数据生成前端service请求,有了tRPC就不担心这个问题了,后面有时间把fluxy-admin的后端使用tRPC重构一下。
搭建框架
前言
下面给大家分享一下如何在前端主流框架中使用tRPC,包括React、Next、Vue3框架,搭建好的模版项目已经上传到了github,大家有需要自取。
因为是多项目,所以项目管理采用了pnpm workspace方案。
pnpm workspace
是pnpm
包管理器为多包仓库(monorepo)提供的特性。在一个 monorepo 中,可能有多个项目(或称"工作区"/"workspaces"),它们共享相同的代码库和
node_modules
文件夹。
pnpm workspaces
允许你在这些项目之间有效地管理依赖关系,并可以一次性执行跨多个项目的操作。
tRPC和vite react
创建代码目录
新建一个文件夹
sh
mkdir trpc-demo
cd trpc-demo
pnpm init
在项目根目录下创建pnpm-workspace.yaml
文件
yaml
packages:
- packages/*
在项目根目录下创建packages文件夹,后面项目都放到这个文件夹下
sh
mkdir packages
创建server端也就是tRPC项目
在packages下创建trpc文件夹,并初始化项目
sh
cd packages
mkdir trpc
pnpm init
全局安装trpc依赖
安装依赖这一块有一些坑,按照官方文档这样安装,会导致一些奇怪的ts问题,我被这个问题卡了一些时间,所以这里我给版本限制一下。
sh
# -w 表示按照到全局,这样每个项目都可以共用这两个依赖
pnpm add -w @trpc/server@10.45.0 @trpc/client@10.45.0
在packages/trpc
目录下创建index.ts
,创建一个最简单的tRPC服务
ts
import { initTRPC } from '@trpc/server';
import { createHTTPServer } from '@trpc/server/adapters/standalone';
const t = initTRPC.create();
// 定义一个路由
const appRouter = t.router({
hello: t.procedure.query(() => {
return {
name: 'hello',
};
}),
});
// 创建一个服务,这里使用的是trpc自带的,也可以使用express,koa等
const server = createHTTPServer({
router: appRouter,
});
// 监听7001端口
server.listen(7001);
// 导出路由类型,可以在客户端使用,这一步很关键,不然在客户端中无法获得ts类型推断
export type AppRouter = typeof appRouter;
可以使用nodemon启动node服务,更改文件后,自动生效,很方便。
sh
# -r --filter trpc 表示在trpc目录下安装nodemon依赖
pnpm add nodemon -D -r --filter trpc
在packages/trpc/package.json
文件中添加dev运行命令
运行npm run dev
测试一下,可以正常运行
使用vite创建react项目
sh
cd packages
npm create vite
需要选择typescript
因为客户端需要使用trpc项目里的AppRouter类型,所以需要把trpc项目作为依赖
sh
pnpm add trpc -r --filter vite-react
项目依赖出现这个,表示安装成功了
在项目里,封装一个trpc请求客户端,这里使用到了trpc项目里导出的AppRouter类型,客户端类型推断主要依赖于这个。
vite-react/src/utils/trpc.ts
ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { AppRouter } from 'trpc/index';
// 定义一个trpc客户端
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:7001',
}),
],
});
在代码中测试一下
运行前后端项目,发现请求报错了,这是因为接口跨域了,有两个方案解决跨域问题,第一个是trpc项目中使用cors,第二个是前端反向代理。
trpc使用cors解决跨域问题
安装cors依赖
sh
pnpm add cors -r --filter trpc
pnpm add @types/cors -D -r --filter trpc
给服务添加cors中间件
重新运行项目,发现可以请求成功了
使用前端反向代理方式解决跨域问题
改造文件vite.config.ts文件
改造utils/trpc.ts文件
给大家看一下trpc一个优化
我们在项目里重复请求接口
可以看到,trpc会把短时间内多次请求合并成一个请求,统一发送,可以减轻服务器压力。
注意不是只会合并相同的请求,而是短时间内所有请求。
如果不想合并,把utils/trpc里的httpBatchLink改成httpLink就行了
使用@tanstack/react-query
让请求变得简单
正常的开发过程中,经常需要把请求的数据渲染到页面上,但是如果和useState配合使用,就没有类型推断了,这时候我们可以使用@tanstack/react-query
库,trpc可以配合react-query一起使用。
安装依赖,依然锁定了版本
sh
pnpm add @tanstack/react-query@4.36.1 @trpc/react-query@10.45.0 -r --filter vite-react
改造utils/trpc.ts文件
ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from 'trpc/index';
export const trpc = createTRPCReact<AppRouter>();
添加QueryProvider
、TRPCProvider
、RootProvider
QueryProvider
ts
// src/providers/query.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
},
},
});
const QueryProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default QueryProvider;
export { queryClient };
TRPCProvider
ts
// src/providers/trpc.tsx
import { httpBatchLink } from '@trpc/client';
import { trpc } from "../utils/trpc";
import { queryClient } from "./query";
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: '/api',
})
],
});
const TRPCProvider = ({ children }: { children: React.ReactNode }) => {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
{children}
</trpc.Provider>
);
};
export default TRPCProvider;
RootProvider
ts
// src/providers/index.tsx
import QueryProvider from "./query";
import TRPCProvider from "./trpc";
const RootProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryProvider>
<TRPCProvider>{children}</TRPCProvider>
</QueryProvider>
);
};
export default RootProvider;
在main.tsx里面使用RootProvider
可以正常类型推断了,并且也不需要自己处理请求异常和loading的状态了。
trpc引入Prisma操作数据库
prisma是一个数据库的orm库,使用起来比较简单,并且和tRPC很契合。
在trpc项目里安装Prisma依赖
sh
pnpm add prisma -D -r --filter trpc
依赖安装成功后,执行下面命令初始化prisma目录,这里使用的是sqlite本地数据库,prisma支持大部分主流数据库。
sh
npx prisma init --datasource-provider sqlite
上面命令执行完成后,项目里会多一个prisma目录,和schema.prisma文件,往schema.prisma添加我们的模型。
prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
执行下面命令把模型生成到数据库中
sh
npx prisma migrate dev --name init
改造路由方法,对外提供查询用户方法
改完路由后,前端项目就报错了,再也不用担心后端改完东西,前端不知道了。
使用npx prisma studio
命令,可以在线看到prisma模型数据,手动往User表里插入一条数据。
改造前端项目,这里有准确的类型提示,真的太爽了。
假如前端给后端传参数,并且需要做参数校验怎么做呢,这个也很简单,tRPC也可以配合zod库执行参数校验。
在trpc项目中安装zod依赖
sh
pnpm add zod -r --filter trpc
添加一个通过用户id查询用户的方法
客户端中调用,可以提前知道数据格式有没有问题。
在vue3项目中使用
进入packages目录,使用npm create vite
命令创建vue3项目。
sh
cd packages
npm create vite
项目创建完成后,安装依赖
sh
pnpm add @tanstack/vue-query@4.36.1 trpc -r --filter vite-vue3
改造main.ts文件
添加utils/trpc.ts文件
ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { AppRouter } from 'trpc';
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: '/api',
}),
],
});
创建一个store.ts文件
ts
import { useQuery } from '@tanstack/vue-query';
import { trpc } from './utils/trpc';
export const useUsers = () =>
useQuery({
queryKey: ['users'],
queryFn: () => trpc.queryUser.query(),
});
改造app.vue文件
类型推断也能正常使用
在nextjs中使用
在我看来tPRC和next框架才是最佳搭档,国外很多人使用这两个做独立应用。
安装依赖
sh
pnpm add @trpc/next@10.45.0 trpc -r --filter next
添加utils/trpc.ts文件
ts
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from 'trpc';
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: `http://localhost:7001`,
}),
],
};
},
// 表示服务端渲染
ssr: true,
});
改造src/pages/_app.tsx文件,给App添加withTRPC高级组件
tsx
import '@/styles/globals.css'
import { trpc } from '@/utils/trpc'
import type { AppProps } from 'next/app'
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default trpc.withTRPC(App);
改造src/pages/index.tsx文件
tsx
import { trpc } from '@/utils/trpc';
export default function Home() {
const { data: users } = trpc.user.getUser.useQuery('hello');
return (
<ul>
{(users || []).map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
使用npm run dev
启动项目,别忘记启动trpc项目
看一下上面截图,因为是服务端渲染,所以返回的html里已经调完接口给元素渲染出来了。但是下面还是请求了,并且报错了,下面的请求是react-query
库会默认请求,并且在失败的时候会重试。报错是因为浏览器调用的,跨域了。服务端渲染那次掉接口是node调用的,所以不存在跨域问题。想解决这个问题,给下面几个东西关掉就行了。
在next中提供tRPC服务
了解过next框架的同学应该知道,next框架是允许写接口的,和以前的php有点像。既然next支持写接口,那肯定也可以使用tRPC对外提供服务。
添加src/server/index.ts文件,把tRPC迁移到当前项目中
ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
// 定义一个路由
export const appRouter = t.router({
queryUser: t.procedure.query(() => {
// 这里可以继续使用prisma
return [
{
name: 'tom',
},
];
}),
});
// 导出路由类型,可以在客户端使用,这一步很关键,不然在客户端中无法获得ts类型推断
export type AppRouter = typeof appRouter;
添加src/pages/api/trpc/[trpc].ts文件,对外暴露trpc服务
ts
/**
* This file contains tRPC's HTTP response handler
*/
import {appRouter} from '@/server';
import * as trpcNext from '@trpc/server/adapters/next';
export default trpcNext.createNextApiHandler({
router: appRouter,
});
改造/src/utils/trpc.ts文件
ts
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server';
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: `http://localhost:3000/api/trpc`,
}),
],
queryClientConfig: {
defaultOptions: {
queries: {
// 默认会发送一次请求
refetchOnMount: false,
// 页面获取焦点后从新请求
refetchOnWindowFocus: false,
// 关闭重试
retry: false,
},
},
},
};
},
ssr: true,
});
启动项目,并关掉前面tRPC项目,发现项目能正常运行,也就是说next项目前后端项目是一体的,和jsp、asp、php差不多。
实战
前言
上面给大家分享了如何搭建项目,下面带着大家实战实现CURD,让大家更深入的了解tRPC。
改造trpc项目
前面搭建的trpc项目结构,不太适合正常项目开发,所以需要给目录结构整理一下。
把prisma和trpc封装一下
ts
// utils/prisma.ts
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
ts
// utils/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const procedure = t.procedure;
添加一个路由文件夹,后面把路由都放在里面
sh
mkdir routers
添加一个user路由,并加一个查询全部用户的方法
ts
import z from 'zod';
import {prisma} from '../utils/prisma';
import {procedure, router} from '../utils/trpc';
export const userRouter = router({
list: procedure
.input(
z.object({
name: z.string().nullish(),
})
)
.query(async ({input}) => {
// 使用contains来模糊搜索
const where = input?.name
? {
name: {
contains: input.name,
},
}
: {};
const users = await prisma.user.findMany({
where,
});
return users;
}),
});
改造index.ts文件,把user路由添加进去,支持路由嵌套
使用prisma-extension-pagination
插件快速实现后端分页
安装依赖
sh
pnpm add prisma-extension-pagination -r --filter trpc
改造utils/prisma.ts文件
ts
import { PrismaClient } from '@prisma/client';
import { pagination } from 'prisma-extension-pagination';
export const prisma = new PrismaClient().$extends(pagination());
user路由中添加分页方法
添加创建方法
这里注意一下,前面查询用的是query
,而这次用的是mutation
,这是为了让前端在某个方法中调用,query默认会调用一次,而创建方法需要主动调用,mutation可以解决这个问题。
添加更新方法
添加删除方法
改造前端项目
改造vite-react项目,实现前端增删改查。
表格页
tsx
// packages/vite-react/src/pages/user/index.tsx
import { Button, Input, Space, Table, message } from 'antd';
import { useState } from 'react';
import { trpc } from '../../utils/trpc';
import NewAndEditForm from './newAndEditForm';
function UserPage() {
const [value, setValue] = useState<string>();
const [tablePagination, setTablePagination] = useState({
current: 1,
pageSize: 10
});
const [open, setOpen] = useState(false);
const [editRecord, setEditRecord] = useState<{ id: number, name: string, email: string } | null>();
const { isLoading, data, refetch } = trpc.user.page.useQuery({
name: value,
limit: tablePagination.pageSize,
page: tablePagination.current,
});
const { mutate } = trpc.user.remove.useMutation({
onSuccess: () => {
message.success('操作成功');
refetch();
}
});
const [columns] = useState([{
dataIndex: 'id',
title: 'id',
}, {
dataIndex: 'name',
title: 'name',
}, {
dataIndex: 'email',
title: 'email',
},
{
dataIndex: 'id',
title: '操作',
render: (id: number, record: any) => (
<Space>
<Button
type='link'
onClick={() => {
setOpen(true);
setEditRecord(record);
}}
>
编辑
</Button>
<Button
type='link'
danger
onClick={() => {
mutate({ id });
}}
>
删除
</Button>
</Space>
)
}]);
const search = (value: string) => {
setValue(value);
}
const tableChange = (pagination: any) => {
setTablePagination(pagination);
}
return (
<Space
direction="vertical"
style={{ width: '100%', padding: '40px 20px', boxSizing: 'border-box' }}
size="large"
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button type='primary' onClick={() => setOpen(true)}>新建</Button>
<Input.Search enterButton onSearch={search} style={{ width: 400 }} />
</div>
<Table
rowKey="id"
columns={columns}
loading={isLoading}
dataSource={data?.users}
onChange={tableChange}
pagination={{
...tablePagination,
total: data?.pageInfo.totalCount,
}}
/>
<NewAndEditForm
open={open}
onClose={() => {
setOpen(false);
setEditRecord(null);
}}
onSuccess={() => {
refetch();
}}
editRecord={editRecord}
/>
</Space>
)
}
export default UserPage;
新建和编辑弹框表单
tsx
packages/vite-react/src/pages/user/newAndEditForm.tsx
import { Form, Input, Modal, message } from 'antd';
import React, { useEffect } from 'react';
import { trpc } from '../../utils/trpc';
interface Props {
open: boolean;
onSuccess: () => void;
onClose: () => void;
editRecord: any;
}
const NewAndEditForm: React.FC<Props> = ({
open,
onSuccess,
editRecord,
onClose,
}) => {
const onSuccessHandle = () => {
message.success('操作成功');
onClose();
onSuccess();
form.resetFields();
}
const { mutate: createMutate, isLoading: createLoading } = trpc.user.create.useMutation({
onSuccess: onSuccessHandle,
});
const { mutate: editMutate, isLoading: editLoading } = trpc.user.update.useMutation({
onSuccess: onSuccessHandle,
});
const [form] = Form.useForm();
useEffect(() => {
if (open) {
form.setFieldsValue(editRecord);
}
}, [open, editRecord, form]);
// antd的表单和trpc配合的不是很好,需要自己定义类型,不能通过FormItem下面的元素自动推导出values类型,可以自己封装一个Form组件
const save = (values: any) => {
if (editRecord) {
editMutate({ ...editRecord, ...values })
} else {
createMutate(values);
}
}
return (
<Modal
title="hello"
open={open}
onCancel={() => {
onClose();
form.resetFields();
}}
onOk={() => {
form.submit();
}}
confirmLoading={createLoading || editLoading}
>
<Form
form={form}
labelCol={{ span: 5 }}
wrapperCol={{ span: 16 }}
onFinish={save}
>
<Form.Item rules={[{ required: true }]} label="姓名" name="name">
<Input />
</Form.Item>
<Form.Item rules={[{ required: true }]} label="邮箱" name="email">
<Input />
</Form.Item>
</Form>
</Modal>
);
}
export default NewAndEditForm;
效果展示
小结
前端代码比较简单,我就不一一解释了。不过有几个点需要说明一下:
- useQuery里面的参数如果有变化,自动会发送请求,也可以调用refetch方法手动发送请求。
- useMutation里面可以添加onSuccess方法,接口调用成功后,会触发onSuccess方法。
- 可以主动调用上面mutate方法,并且可以传参数
最后
这篇文章主要给大家分享一下我最近搭建的几个项目模版,并且写了一个简单小demo,让大家先快速了解tPRC。
我最近在学next.js,后面打算用next和tRPC做一个全栈应用,做完后会出一个全栈应用开发系列教程,敬请期待吧。