tRPC 在 React 自动化工作流中的应用:实现从脚本触发到 UI 反馈的端到端类型校验

大家好,欢迎来到今天的“自动化生存指南”讲座。我是你们的主讲人,一个在代码世界里摸爬滚打多年的资深老兵。

今天我们要聊的话题,非常具体,也非常痛苦——在 React 自动化工作流中,如何让你的脚本和 UI 说话,并且像外交官一样优雅,而不是像没头苍蝇一样乱撞。

想象一下这个场景:你写了一个超级复杂的 Python 脚本,或者一个 Node.js Worker,它能自动部署、处理图片、生成报表。你把它扔到服务器上,运行它,然后呢?你只能打开终端,Ctrl+C,看那行“Success”或者“Error”,然后手动刷新浏览器,看看 UI 上有没有变。如果出错了,你还得去查日志,去问 AI,去跟你的队友拍桌子。

这太糟糕了,朋友。这就像是你开着一辆法拉利,结果每次出门都得靠两条腿跑。

这时候,tRPC 就登场了。它不是那种只会生成文档的工具,它是你的数字高速公路。它让前端、后端、甚至你的脚本之间,通过 TypeScript 类型系统进行无缝沟通。

今天,我们就来解剖这个“从脚本触发到 UI 反馈”的完整生物链。

第一部分:痛苦的根源——为什么我们要死磕类型?

在 tRPC 出现之前,前后端通信就像是在玩“你画我猜”。前端定义了一个接口 interface CreateJob { name: string; config: number },后端也定义了一个接口 interface CreateJob { name: string; config: number }。它们长得一模一样,对吧?

但是,当你写代码时,你是不是经常手滑,把 config 写成了 configu?或者在 fetch 的时候漏掉了一个参数?前端报错,后端报错,两者相隔十万八千里,你根本不知道是谁搞错了。

tRPC 的核心哲学只有一句话:相信你的代码,因为它自己会说话。

在 tRPC 的工作流里,你在后端定义一次 procedure.input(z.object(...)),前端自动就能得到一个完美的 TypeScript 接口。如果后端说“我只要 id”,前端就绝对不能传 name,编译器会直接给你一个红波浪线,告诉你“你想干嘛?我没这玩意儿!”

这就是我们要构建的自动化工作流的基石:端到端类型校验

第二部分:架构蓝图——构建“神经中枢”

首先,我们要建立一个 tRPC Router。这个 Router 是整个系统的“外交官”。它负责接收请求、验证数据、调用业务逻辑,然后把结果打包好发出去。

我们的目标是一个长任务的自动化场景:用户在 UI 上点击“开始备份”,这会触发一个后台脚本运行,脚本在后台处理数据,最后 UI 实时显示进度条。

我们需要三个主要组件:

  1. Server(后端): 运行 tRPC Router 和脚本逻辑。
  2. Script(触发器): 被调用的独立脚本(可以是 Python, Go, 或者另一个 Node.js 文件)。
  3. Client(前端): React 组件,负责展示和接收反馈。

第三部分:定义“契约”——Router 与 Schema

好,我们开始干活。首先,我们需要在后端定义我们的契约。这里我们使用 zod 作为验证库,它是 tRPC 的最佳拍档。

// app/api/trpc/[trpc].ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
import { prisma } from '@/lib/prisma'; // 假设你用了 Prisma

// 1. 初始化 tRPC,开启“信任模式”
const t = initTRPC.create();

// 2. 创建中间件:统一处理错误和 Zod 验证
const middleware = t.middleware(({ next, input, error }) => {
  try {
    // 如果输入数据不符合 schema,这里会抛错
    return next({ ctx: { input } });
  } catch (err) {
    if (err instanceof ZodError) {
      // 把 Zod 的验证错误转换成 tRPC 错误
      return error({
        code: 'BAD_REQUEST',
        message: '输入数据格式不对!',
        cause: err.errors,
      });
    }
    throw err;
  }
});

// 3. 公共过程:所有路由都要经过这个过滤器
const protectedProcedure = t.procedure.use(middleware);

export const appRouter = t.router({
  // 路由 A: 触发脚本
  triggerBackup: protectedProcedure
    .input(z.object({
      userId: z.string().uuid(),
      targetPath: z.string(),
    }))
    .mutation(async ({ input }) => {
      // 这里是关键:调用业务逻辑,而不是直接写脚本
      // 我们可以在这里启动一个 Worker 或写入数据库

      console.log(`收到指令:用户 ${input.userId} 正在尝试备份 ${input.targetPath}`);

      // 模拟调用外部脚本或内部 Worker
      await runExternalScript(input);

      return { success: true, jobId: 'job-123' };
    }),

  // 路由 B: 获取进度(轮询模式)
  getJobStatus: protectedProcedure
    .input(z.object({ jobId: z.string() }))
    .query(async ({ input }) => {
      const job = await prisma.job.findUnique({
        where: { id: input.jobId },
      });
      if (!job) throw new TRPCError({ code: 'NOT_FOUND' });
      return job;
    }),
});

export type AppRouter = typeof appRouter;

注意上面的代码,这里我们定义了两个核心点:

  1. triggerBackup:这是 UI 和脚本之间的接口。前端调它,它告诉“后端系统”去干活。
  2. getJobStatus:这是 UI 和脚本之间的“探针”。UI 会不断问这个接口:“那个 job 做完了吗?”

第四部分:连接“孤岛”——脚本如何调用 tRPC?

现在,我们的脚本在哪里?脚本通常运行在 Docker 容器里,或者就是另一台服务器上的一个进程。它怎么知道调用后端的 tRPC 接口呢?

不要用 HTTP 轮询!不要手动拼 JSON!太丑陋了。

我们需要在脚本里创建一个 tRPC Client

// scripts/backup-worker.js
// 假设这是一个独立的 Node.js 脚本,或者是 Python 通过子进程调用的逻辑
import { PrismaClient } from '@prisma/client';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './app-router'; // 引用后端定义的类型

const prisma = new PrismaClient();

// 1. 创建 tRPC 客户端
// 注意:这里需要配置后端服务器的地址,通常在开发环境是 localhost
const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc', // 你的 Next.js 服务地址
      // 这里可以添加 JWT 认证逻辑
    }),
  ],
});

// 2. 定义脚本逻辑
async function runBackup(targetPath) {
  console.log(`[Worker] 开始工作,目标路径: ${targetPath}`);

  // 这里我们模拟处理过程
  for (let i = 0; i <= 100; i += 10) {
    await new Promise(resolve => setTimeout(resolve, 1000)); // 假装在干活
    console.log(`[Worker] 进度: ${i}%`);

    // 关键点:每一步都把进度写回数据库
    // 或者我们可以设计一个 tRPC 路由专门用来更新进度,
    // 但为了简单,我们这里直接操作数据库,UI 去查数据库
    await prisma.job.update({
      where: { id: 'job-123' },
      data: { progress: i, status: 'RUNNING' },
    });
  }

  console.log(`[Worker] 完成!`);
  await prisma.job.update({
    where: { id: 'job-123' },
    data: { status: 'COMPLETED' },
  });
}

// 3. 启动脚本
// 实际上,这个函数会在 AppRouter 的 mutation 中被调用
export async function runExternalScript(input) {
  // 这里我们不仅运行脚本,还返回一个 Promise 给前端
  // 如果是真实的场景,你可能用 Bull/BullMQ 等队列系统来管理这个 Job
  return runBackup(input.targetPath);
}

看懂了吗?在这个脚本里,我们复用了后端的 AppRouter 类型。这意味着,如果你的后端改了 input 的定义,脚本里的代码会立刻报错。这就是端到端类型校验的魅力。

第五部分:UI 的蜕变——从“等待刷新”到“实时感知”

现在,回到我们的 React 应用。以前,我们写这样的代码:

// 以前写法
const [status, setStatus] = useState('idle');
const handleStart = () => {
  fetch('/api/start', { method: 'POST', body: JSON.stringify({ data }) })
    .then(r => r.json())
    .then(r => setStatus(r.status)); // 啥?status 是什么?看文档去!
};

现在,有了 tRPC,我们拥有了自动补全和类型检查。

// app/components/BackupManager.tsx
import { useState } from 'react';
import { useMutation, useQuery } from '@trpc/react';
import { appRouter } from '@/server/routers/_app';
import superjson from 'superjson'; // tRPC 的序列化插件

export default function BackupManager() {
  // 1. 初始化 trpc hook
  // 假设你已经配置了 <RouterProvider router={router} />

  const [jobId, setJobId] = useState<string | null>(null);

  // 2. 定义 mutation:触发备份
  // 这里,useMutation 会自动推断 input 类型!
  const triggerBackup = useMutation(appRouter.triggerBackup.mutation);

  // 3. 定义 query:查询状态
  // 这里,useQuery 会自动推断返回值类型!
  const jobQuery = useQuery(appRouter.getJobStatus.queryOptions({
    jobId: jobId || 'job-123', // 如果没 job,传个默认的或者 null
  }), {
    enabled: !!jobId, // 只有有 job id 才去查
    refetchInterval: 1000, // 关键:每秒刷新一次,模拟实时
  });

  const handleStart = () => {
    // 试试手滑?把 input 写错,TypeScript 会当场报警
    triggerBackup.mutate({
      userId: 'uuid-123', 
      targetPath: '/data/backup', 
    }, {
      onSuccess: (res) => {
        // 返回值类型也是自动推断的!
        console.log('任务 ID:', res.jobId);
        setJobId(res.jobId);
      },
      onError: (err) => {
        console.error('哎呀,报错了', err);
      }
    });
  };

  return (
    <div className="p-8 border rounded shadow-lg">
      <h2 className="text-xl font-bold mb-4">自动化备份控制台</h2>

      <button 
        onClick={handleStart}
        disabled={triggerBackup.isPending}
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
      >
        {triggerBackup.isPending ? '启动中...' : '开始备份'}
      </button>

      <div className="mt-8">
        {jobQuery.data ? (
          <>
            <p>当前状态: <span className={`font-bold ${jobQuery.data.status === 'COMPLETED' ? 'text-green-500' : 'text-blue-500'}`}>
              {jobQuery.data.status}
            </span></p>
            <p>进度: {jobQuery.data.progress}%</p>

            {/* 进度条 */}
            <div className="w-full bg-gray-200 rounded-full h-4 mt-2">
              <div 
                className="bg-blue-600 h-4 rounded-full transition-all duration-300" 
                style={{ width: `${jobQuery.data.progress}%` }}
              />
            </div>
          </>
        ) : (
          <p className="text-gray-400">等待指令...</p>
        )}
      </div>
    </div>
  );
}

这就是魔力所在!

  1. 输入安全:你绝对不可能传错参数,因为 triggerBackup.mutate 里的参数是编译器帮你填好的。
  2. 输出安全jobQuery.data 里的字段是自动推导的。如果你去掉了数据库里的某个字段,代码会直接报错。
  3. 实时性refetchInterval: 1000 告诉 React,每隔 1 秒就去问后端一次。这就是你的 UI 状态更新的秘诀。

第六部分:高级技巧——中间件与认证

既然是自动化工作流,安全性不能丢。脚本在调用后端时,也需要证明“我是谁”。

在 Next.js App Router 中,我们通常使用 Cookies 来传递 Session。但是,脚本运行在 Node.js 环境下,它没有浏览器,拿不到 Cookies。怎么办?

我们可以用一种很 hack 但很实用的方法:中间件注入 Token

// scripts/worker-client.ts
const token = process.env.WORKER_SECRET_TOKEN; // 假设这是一个在环境变量里配置的强密码

const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc',
      headers: {
        // 关键:我们在 HTTP 头里注入 token
        Authorization: `Bearer ${token}`,
      },
    }),
  ],
});

然后在后端的中间件里验证:

// app/api/trpc/[trpc].ts
const middleware = t.middleware(async ({ next, ctx }) => {
  // 1. 从 ctx (Headers) 获取 token
  const token = ctx.headers?.authorization?.replace('Bearer ', '');

  if (!token || token !== process.env.WORKER_SECRET_TOKEN) {
    throw new TRPCError({ code: 'UNAUTHORIZED', message: '你是谁?' });
  }

  return next({ ctx: { ...ctx, token } });
});

export const appRouter = t.router({
  triggerBackup: protectedProcedure ... // 只有带 token 的请求才能进这里
});

这样,你的脚本就是一个“合法公民”,而你的 React UI 呢?它可以通过 Session Cookie 访问。完美!

第七部分:WebSocket——如果 UI 也要“躺着等”怎么办?

虽然每秒 refetchInterval 很好用,但如果你有成千上万个用户,每一秒都发一个 HTTP 请求,服务器会哭的。

这时候,tRPC 的流式传输能力就派上用场了。我们可以利用 WebSocket 或者 SSE (Server-Sent Events)。

不过,在 Next.js 的 tRPC 设置中,为了保持简单和类型安全,很多团队依然选择“长轮询”或者“短期 WebSocket”。但如果你真的想要 WebSocket,tRPC 也支持。

假设你用 Next.js API Route 包装 tRPC,你可以直接在路由里开启 WebSocket 支持。

// app/api/trpc/[trpc].ts
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import { WebSocketServer } from 'ws';
import { createContext } from '@/server/context';
import { appRouter } from '@/server/routers/_app';

const wss = new WebSocketServer({ port: 3001 });

applyWSSHandler({
  wss,
  router: appRouter,
  createContext,
});

然后,前端 React 组件里,我们可以用 useMutation 来监听流式返回。

但这属于进阶玩法了。对于大多数自动化场景,每秒 1 次的 HTTP 轮询已经足够快且简单了。除非你在做像股票交易那样毫秒级更新的东西,否则别给自己挖坑。

第八部分:错误处理的艺术

在自动化工作流中,报错是常态。脚本可能会断网,数据库可能会挂。

tRPC 提供了一个强大的 onError 钩子。我们可以在 Server 端记录日志,在前端优雅地展示。

// Server 端
export const appRouter = t.router({
  triggerBackup: t.procedure.mutation(async ({ input }) => {
    try {
      // ... 你的逻辑
      await riskyOperation();
    } catch (e) {
      // 把错误抛给前端,带上详细的堆栈信息
      throw new TRPCError({
        code: 'INTERNAL_SERVER_ERROR',
        message: '备份失败',
        cause: e, // 前端会拿到这个对象
      });
    }
  }),
});

// Client 端
triggerBackup.mutate(data, {
  onError: (err) => {
    console.error(err.cause); // 打印完整的堆栈,不用去服务器查日志了
    toast.error(`备份失败: ${err.message}`);
  }
});

总结:为什么这很重要?

我们回顾一下,通过这次改造:

  1. 开发体验:你不再需要写文档来描述“这个 API 需要 JSON,包含 name 和 age”。
  2. 类型安全:你在写脚本时,如果改了参数,编译器会阻止你提交代码。
  3. 调试效率:错误信息从“Network Error”变成了具体的 Zod 验证错误或业务逻辑错误。
  4. 用户体验:UI 从“黑盒”变成了“白盒”,进度条、状态提示一目了然。

这就是 tRPC 在 React 自动化工作流中的力量。它不仅仅是一个库,它是现代全栈开发的“液压系统”,把数据的输入、处理、输出紧紧捏在了一起。

记住,不要为了用技术而用技术。但在 React 全栈开发里,tRPC 是那个能让你少写 50% 错误代码,多写 50% 舒服代码的神器。

现在,去把你的那些控制台日志和手写的 fetch 调用全部干掉吧。祝你的自动化工作流永远顺滑,Bug 永远不沾身!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注