tRPC+Prisma初体验,不得不说全栈开发体验太赞了。

前言

前段时间狼叔在年终总结中提到了tRPC,并且给了很高的评价,引起了我的好奇。于是我最近研究了一下,写了几个小demo后,发现体验真的太棒了,下面给大家分享一下我这几天的学习成果,让大家简单了解一下tRPC。

下面是狼叔文章里的截图

tRPC是什么

先看官网首页的一张动图,也可以自己去这个地址体验一下,体验完你可能就知道tRPC用来干嘛的了。

tRPC(TypeScript RPC)是一个用于在 TypeScript 应用程序中构建类型安全的 API 的工具。它允许开发者创建端到端类型安全的API,这意味着客户端和服务器之间的通信可以完全通过 TypeScript 类型进行验证和推断。

tRPC 通过结合远程过程调用(RPC)的概念与 TypeScript 的强类型系统,消除了编写单独的API类型定义或运行时验证代码的需要。客户端可以直接像调用本地函数一样调用服务器上的函数,而无需担心参数类型或返回值类型是否匹配。

主要特点包括:

  1. 类型安全:客户端与服务器端代码共享相同的类型定义,保证了API请求和响应的类型安全。
  2. 无需定义REST路由:不需要手动设置RESTful API路由,tRPC 会自动为你提供基于函数的端点。
  3. 轻量级:tRPC 是一个轻量级库,它不需要依赖大型框架。
  4. 前后端集成:tRPC 可以与现代前端框架(如 Next.js、React、Vue等)深度集成,提供更加流畅的开发体验。

总的来说,tRPC 提供了一种简洁、高效且类型安全的方式来构建应用程序的后端API,并能轻松与 TypeScript 前端代码集成。

上面是chatgpt说的,通过我这几天实践,补充一个真实有用的场景,关注我的人应该知道我在开发一套全栈后台管理系统,在开发的过程有个很恶心的地方,就是后端定义过一遍模型,前端因为是ts项目,为了类型安全,请求那里还要定义一遍模型,这个开发体验很差,为了解决这个问题,我还自己写了一个插件,通过后端swagger数据生成前端service请求,有了tRPC就不担心这个问题了,后面有时间把fluxy-admin的后端使用tRPC重构一下。

搭建框架

前言

下面给大家分享一下如何在前端主流框架中使用tRPC,包括React、Next、Vue3框架,搭建好的模版项目已经上传到了github,大家有需要自取。

因为是多项目,所以项目管理采用了pnpm workspace方案。

pnpm workspacepnpm 包管理器为多包仓库(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>();

添加QueryProviderTRPCProviderRootProvider

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做一个全栈应用,做完后会出一个全栈应用开发系列教程,敬请期待吧。

仓库地址:github.com/dbfu/trpc-t...

相关推荐
Fan_web5 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常6 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
杨哥带你写代码1 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries2 小时前
读《show your work》的一点感悟
后端
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
A尘埃2 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23072 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code2 小时前
(Django)初步使用
后端·python·django