发布时间:2026/9/6 19:48:12
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),仅供参考

相关新闻

2026/9/6 19:43:12

Teable 私有化部署快速教程:五分钟跑起一套数据协作平台

Teable 私有化部署快速教程:五分钟跑起一套数据协作平台 【免费下载链接】teable ✨ AI Spreadsheet for Business 项目地址: https://gitcode.com/GitHub_Trending/te/teable Teable 是构建在 PostgreSQL 之上的开源数据协作平台,表面上是一张熟…

2026/9/6 19:43:12

基于STM32的智能鱼缸控制系统开发实践:从硬件选型到远程监控

简介:这是一份基于STM32的智能鱼缸控制系统完整设计与实现资料,适合具备一定电子和编程基础、对嵌入式与物联网感兴趣的初学者、中级开发者及养鱼爱好者。系统以STM32F103C8T6为控制核心,集成温度、水位、光照、浊度和pH检测传感器&#xff0…

2026/9/6 20:33:15

MaxKB 部署教程:10 分钟跑通你的第一个知识库问答

MaxKB 部署教程:10 分钟跑通你的第一个知识库问答 【免费下载链接】MaxKB 🔥 MaxKB is an open-source platform for building enterprise-grade agents. 强大易用的开源企业级智能体平台。 项目地址: https://gitcode.com/GitHub_Trending/ma/MaxKB …

2026/9/6 20:33:15

GNOS-II不均匀DDM海面高度反演:重跟踪与误差修正全流程

简介:围绕风云三号E星GNOS-II载荷的GNSS-R海面高度反演研究,提供配套论文复现资料,面向卫星遥感、海洋测高及GNSS-R技术科研人员。内容以数据预处理、物理模型反演、随机森林与卷积神经网络实现为主线,对比北斗BDS与GPS反射信号性…

2026/9/6 20:33:15

MAKINO加工中心换刀臂更换全流程:从故障判断到调试验证

简介:针对Makino数控机床换刀臂更换维护的实操型PPT,由设备维修人员整理,面向数控机床维护工程师、机修技师及自动化产线技术员。资源以图文步骤形式,覆盖拆前清洁备料、断电断气锁定挂牌、护罩与换刀门拆除、连接件标记、锁紧背帽…

2026/9/6 20:33:15

Makino换刀臂更换全流程:拆装、调整与验证指南

简介:针对Makino高端数控机床换刀臂更换的PPT技术资料,面向数控设备维修人员、机修工程师及车间技术骨干。内容以图文形式完整呈现换刀臂更换全流程,包括机床清理、断电断气等安全措施、外围护罩与内部护板拆卸、连接件标记、花键套及轴承抽取…

2026/9/6 20:28:14

食品微生物检验质量控制全流程详解:关键环节与实操要点

简介:这是一份食品微生物检验质量控制的专题PPT,面向食品检验实验室人员、质量管理岗及相关专业学生,帮助系统理解微生物检验所需的国内外标准规范与操作要点。资源为单个PPT文件,约1.5MB,内容依次讲解ISO 7218、SN/T …

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 11:40:10

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/6 19:33:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/6 10:19:40

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…