大家好,欢迎来到今天的“自动化生存指南”讲座。我是你们的主讲人,一个在代码世界里摸爬滚打多年的资深老兵。
今天我们要聊的话题,非常具体,也非常痛苦——在 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 实时显示进度条。
我们需要三个主要组件:
- Server(后端): 运行 tRPC Router 和脚本逻辑。
- Script(触发器): 被调用的独立脚本(可以是 Python, Go, 或者另一个 Node.js 文件)。
- 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;
注意上面的代码,这里我们定义了两个核心点:
triggerBackup:这是 UI 和脚本之间的接口。前端调它,它告诉“后端系统”去干活。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>
);
}
这就是魔力所在!
- 输入安全:你绝对不可能传错参数,因为
triggerBackup.mutate里的参数是编译器帮你填好的。 - 输出安全:
jobQuery.data里的字段是自动推导的。如果你去掉了数据库里的某个字段,代码会直接报错。 - 实时性:
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}`);
}
});
总结:为什么这很重要?
我们回顾一下,通过这次改造:
- 开发体验:你不再需要写文档来描述“这个 API 需要 JSON,包含 name 和 age”。
- 类型安全:你在写脚本时,如果改了参数,编译器会阻止你提交代码。
- 调试效率:错误信息从“Network Error”变成了具体的 Zod 验证错误或业务逻辑错误。
- 用户体验:UI 从“黑盒”变成了“白盒”,进度条、状态提示一目了然。
这就是 tRPC 在 React 自动化工作流中的力量。它不仅仅是一个库,它是现代全栈开发的“液压系统”,把数据的输入、处理、输出紧紧捏在了一起。
记住,不要为了用技术而用技术。但在 React 全栈开发里,tRPC 是那个能让你少写 50% 错误代码,多写 50% 舒服代码的神器。
现在,去把你的那些控制台日志和手写的 fetch 调用全部干掉吧。祝你的自动化工作流永远顺滑,Bug 永远不沾身!