ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

tRPC 非 JSON 内容类型实战:以 minimal-content-types 示例解析 FormData 与二进制文件上传的端到端类型安全实现

tRPC 非 JSON 内容类型实战:以 minimal-content-types 示例解析 FormData 与二进制文件上传的端到端类型安全实现 tRPC 非 JSON 内容类型实战以 minimal-content-types 示例解析 FormData 与二进制文件上传的端到端类型安全实现【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本篇指南以 examples/minimal-content-types 示例工程为主体讲解 tRPC 如何处理默认 JSON 之外的内容类型——FormData、File及其他二进制输入。读完后你能掌握如何在服务端用z.instanceof(FormData)与octetInputParser声明非 JSON 输入并在类型系统内消费它们如何在客户端零额外配置地通过httpLink发送File/FormData以及如何完整运行、构建和端到端验证这套示例。示例定位为什么需要 non-JSON content typestRPC 默认收发 JSON 可序列化的数据。但文件上传、表单提交等场景天然产生FormData、File、Blob这类无法或不适合JSON 序列化的输入。官方文档非 JSON 内容类型将其列为 tRPC 服务端的一等能力服务端会根据请求的Content-Type头自行解析请求体把二进制内容转成可在 procedure 内消费的ReadableStream。examples/minimal-content-types就是为这一能力准备的最小可运行 React 示例README 明确给出了两条基线信息依赖Node 18因为使用了全局fetch及FormData/File/Blob等 Web API客户端是一个 Vite React 应用服务端是独立 HTTP 服务通过类型AppRouter实现端到端类型检查。项目结构与运行方式示例采用 npm workspaces 组织两个子包目录结构如下examples/minimal-content-types/ ├── client/ # Vite React 前端 │ └── src/ │ ├── App.tsx # 客户端入口组件创建 tRPC client │ ├── SendFileButton.tsx # 发送 File 的按钮 │ ├── SendMultipartFormDataButton.tsx # 发送 FormData 的按钮 │ └── utils/trpc.ts # createTRPCReactAppRouter() ├── server/ │ └── index.ts # API handler standalone HTTP 服务器 ├── test/smoke.test.ts # Playwright 冒烟测试 ├── playwright.config.ts └── package.json根 package.json 中的脚本把两个子包编排在一起-w即 workspaces{ scripts: { build: run-s build:server build:client, dev: run-p dev:*, start: run-p start:*, test:e2e: playwright test, test-dev: start-server-and-test dev 3000 test:e2e } }按 README 的指引运行# 开发模式Node 18 npm i npm run dev # 生产构建 npm run build npm run startdev会并行启动 Vite 客户端vite.config.ts 中固定port: 3000与服务端监听2022端口见下文。README 还提示可以直接编辑 TS 文件观察端到端类型检查如何即时生效——这正是本示例的教学价值所在。服务端实现两种非 JSON 输入的声明与消费完整服务端代码见 server/index.ts核心是一个包含两条 mutation 的 routerimport { initTRPC } from trpc/server; import { createHTTPServer } from trpc/server/adapters/standalone; import { octetInputParser } from trpc/server/http; import cors from cors; import { z } from zod; const t initTRPC.create(); const publicProcedure t.procedure; const router t.router; const appRouter router({ // 1) FormData 输入期望输入已整体加载到内存 formData: publicProcedure .input(z.instanceof(FormData)) .mutation(async ({ input }) { const object {} as Recordstring, unknown; for (const [key, value] of input.entries()) { if (value instanceof File) { object[key] { name: value.name, type: value.type, size: value.size, text: await value.text(), }; } else { object[key] value; } } console.log(FormData: , object); return { text: ACK, data: object }; }), // 2) File / 二进制输入octetInputParser 产出 ReadableStream file: publicProcedure.input(octetInputParser).mutation(async ({ input }) { const chunks []; const reader input.getReader(); while (true) { const { done, value } await reader.read(); if (done) break; chunks.push(value); } const content Buffer.concat(chunks).toString(utf-8); console.log(File: , content); return { text: ACK, data: content }; }), }); // 只导出类型定义给客户端不暴露实现 export type AppRouter typeof appRouter; // standalone 适配器直接创建 HTTP 服务器 createHTTPServer({ middleware: cors(), router: appRouter, createContext() { return {}; }, onError(opts) { console.error(Error, opts.error); }, }).listen(2022);这里有三个关键设计点值得展开1. FormData 输入z.instanceof(FormData)即校验器z.instanceof(FormData)直接把「输入必须是FormData实例」写进了 zod schema因此opts.input在 handler 内被推断为FormData类型可以直接调用entries()逐项处理。示例对每个条目做了instanceof File判别文件项只读取元信息name/type/size并调用value.text()取出内容非文件项如name、occupation原样存入返回对象。源码注释特别标注了这类输入的语义——should expect the input to be loaded into memory即FormData 在 tRPC 解析后是完整驻留内存的超大表单需要自行评估内存占用。2. 二进制输入octetInputParser与ReadableStreamoctetInputParser从trpc/server/http导出。从源码看packages/server/src/http.ts 仅做一行转发export * from ./trpc/server/http;而 packages/server/src/trpc/server/http.ts 明确导出了octetInputParser及其配套类型OctetInput、FileLike、UtilityParser。tRPC 会把多种 octet 内容类型Blob、Uint8Array、File等统一转换成ReadableStream所以file这条 mutation 里input就是一个标准的 Web 流通过input.getReader()循环read()直到done再用Buffer.concat(chunks)拼装出完整内容。这种流式消费方式意味着服务端不需要一次性把整个文件读进内存是处理大文件上传的关键前提。3. standalone 适配器与端口约定示例没有套 Express/Fastify而是用 createHTTPServertrpc/server/adapters/standalone直接起一个 Node HTTP 服务并.listen(2022)middleware: cors()处理跨域因为 Vite 客户端跑在 3000 端口属于跨源请求。onError钩子统一打印错误——在更大应用中可以在此接日志/告警。另请注意 server/package.json 所在工作区的职责划分server 子包只依赖trpc/server、cors、zod不依赖任何框架而 client/src/utils/trpc.ts 直接import type { AppRouter } from ../../../server跨包引用服务端类型——这就是 README 所说edit the ts files to see the type checking in action的机制类型错误会在客户端包中直接报出。客户端实现httpLink 原生支持非 JSON 内容类型客户端入口在 App.tsxconst [trpcClient] useState(() trpc.createClient({ links: [ httpLink({ url: http://localhost:2022, }), ], }), ); return ( trpc.Provider client{trpcClient} queryClient{queryClient} QueryClientProvider client{queryClient} SendMultipartFormDataButton / SendFileButton / /QueryClientProvider /trpc.Provider );要点是httpLink开箱即支持非 JSON 内容类型。按官方文档的说明如果你的客户端只用httpLink现有配置无需任何改动即可发送File/FormData。本示例的 links 配置正是最简形态——只有一个url没有 transformer、没有 batch 逻辑。两个按钮组件演示了两种 mutation 调用方式SendMultipartFormDataButton.tsx在onClick中构造FormData塞入两个字符串字段和一个Filenew File([hi bob], bob.txt, { type: text/plain })然后mutation.mutate(fd)一把发出SendFileButton.tsxinput typefile选择后直接mutation.mutate(file)把原始File作为输入。const mutation trpc.file.useMutation(); // ... const file e.target.files.item(0)!; mutation.mutate(file);链接选型提醒批处理 link 需要 splitLink虽然本示例只用了httpLink但结合官方文档 www/docs/server/non-json-content-types.md 可以得出一个重要的工程约束并非所有 link 都支持非 JSON 内容类型。如果你同时使用httpBatchLink或httpBatchStreamLink批处理 link 会把多个操作合并成一个 JSON 请求二进制输入无法参与需要用splitLinkisNonJsonSerializable按内容类型分流createTRPCClientAppRouter({ links: [ splitLink({ condition: (op) isNonJsonSerializable(op.input), true: httpLink({ url }), false: httpBatchLink({ url }), }), ], });另外两条文档给出的配套规则也值得记住若服务端配置了transformer客户端对应 link 也必须定义transformerTS 会强制检查服务端框架如 Express不要在 tRPC 接管路由之前解析请求体否则会触发Failed to parse body as XXX错误——示例用 standalone 适配器完全绕开了这个问题这也是它作为minimal示例的又一价值。端到端验证示例自带 Playwright 冒烟测试 test/smoke.test.tstest(go to /, async ({ page }) { await page.goto(/); await page.waitForSelector(texttRPC user); });配合package.json中的test-dev/test-start脚本start-server-and-test dev 3000 test:e2e先等 3000 端口Vite 客户端就绪后再跑 E2E覆盖开发模式与生产构建两条路径。手动验证时开发模式下访问http://localhost:3000即可看到 Send FormData 与 Send File 两个控件点击后服务端控制台会打印FormData: .../File: ...客户端 mutation 返回{ text: ACK, data: ... }。小结examples/minimal-content-types用不到 200 行代码覆盖了 tRPC 非 JSON 内容类型全链路能力服务端写法客户端写法源码依据FormData 输入.input(z.instanceof(FormData))mutation.mutate(formData)server/index.tsFile / 二进制输入.input(octetInputParser)以ReadableStream流式消费mutation.mutate(file)server/index.ts类型贯通export type AppRouter typeof appRoutercreateTRPCReactAppRouter()client/src/utils/trpc.ts跨源请求standalone 适配器 cors()中间件httpLink指向http://localhost:2022client/src/App.tsx需要牢记的适用前提Node 18全局 fetch 与 Web 文件 API、FormData输入整体驻留内存、二进制输入以流式消费、批处理 link 需splitLink分流。以此为骨架你可以在自己的项目里替换 standalone 适配器Express/Fastify/Next.js 等而保持同样的输入声明与客户端调用方式不变。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表