十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

react-admin 表单 Mutation Middleware 深度解析:用 `useRegisterMutationMiddleware` 拦截 create/update 保存流程

react-admin 表单 Mutation Middleware 深度解析:用 `useRegisterMutationMiddleware` 拦截 create/update 保存流程 前端UI组件【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址https://gitcode.com/gh_mirrors/re/react-admin点击查看免费下载react-admin 的 Creation创建与 Edition编辑页面表单在提交时会调用dataProvider.create()/dataProvider.update()完成数据持久化。useRegisterMutationMiddleware允许你在表单组件树中注册中间件函数像洋葱一样包裹住这次主 mutation从而在数据真正到达 dataProvider 之前对参数进行转换、增删关联数据、补充日志等高级处理。读完本文你将掌握中间件的签名约定、洋葱式链式调用原理、注册与清理的生命周期约束并能独立实现一个提交时把图片转成 base64的实战组件。一、中间件机制要解决的问题在常规 react-admin 开发中表单保存逻辑往往需要在提交前后插入自定义代码典型场景包括转换传给主 mutation 的数据例如把File对象转为 base64、把嵌套对象扁平化后再提交在 mutation 被调用前更新参数例如根据表单内容动态补充meta、附加审计信息创建、更新或删除关联数据例如下单时同时为新客户创建一条独立的 customers 记录并让订单只引用该客户的 id追加性能日志或统计埋点。react-admin 通过中间件函数把上述代码织入保存流程中间件拥有与底层 mutationcreate或update完全相同的参数外加一个next函数next用于调用 mutation 生命周期中的下一个环节。所有中间件构成一条调用链最后由链尾触发真正的 dataProvider 调用。二、中间件函数签名一个中间件函数必须遵循如下签名类型定义见 packages/ra-core/src/controller/saveContext/useMutationMiddlewares.tsconst middleware async (resource, params, next) { // Do something before the mutation const result await next(resource, params); // Do something after the mutation // Always return the result return result; };resource当前资源的名称字符串params将传给底层 mutation 的参数对象其具体结构取决于 mutation 类型见下文next下一个中间件或链尾的原始 mutation 函数必须使用与原始 mutation 相同的参数调用它返回值必须返回next()的结果否则后续处理拿不到 mutation 返回的 data。params的类型随 mutation 不同而不同对应 packages/ra-core/src/types.ts 中的定义Mutation 类型params 结构说明create{ data, meta }data为待创建记录PartialTmeta为可选的元数据update{ id, data, previousData }额外包含id与修改前的整条记录previousData可用于差异计算在 TypeScript 项目中react-admin 还导出了CreateParams、UpdateParams、CreateMutationFunction、UpdateMutationFunction等类型方便为中间件标注精确类型。三、用useRegisterMutationMiddleware注册中间件3.1 基本用法定义中间件函数后用useRegisterMutationMiddleware在组件内注册它。以一个 create 中间件为例示例来自 docs/useRegisterMutationMiddleware.mdimport * as React from react; import { useRegisterMutationMiddleware, CreateParams, CreateMutationFunction } from react-admin; const MyComponent () { const createMiddleware async ( resource: string, params: CreateParams, next: CreateMutationFunction ) { // Do something before the mutation // Call the next middleware const result await next(resource, params); // Do something after the mutation // Always return the result return result; }; const memoizedMiddleware React.useCallback(createMiddleware, []); useRegisterMutationMiddleware(memoizedMiddleware); // ... };随后把该组件渲染为页面控制器组件Create或Edit的**后代descendant**即可。只要MyComponent保持挂载状态react-admin 就会用createMiddleware包裹每一次dataProvider.create()调用。3.2 为什么必须提供稳定引用stable referenceuseRegisterMutationMiddleware的源码packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.ts体现了两个关键约束export const useRegisterMutationMiddleware (callback) { const { registerMutationMiddleware, unregisterMutationMiddleware } useSaveContext(); useEffect(() { if (!registerMutationMiddleware || !unregisterMutationMiddleware) { return; } registerMutationMiddleware(callback); return () { unregisterMutationMiddleware(callback); }; }, [callback, registerMutationMiddleware, unregisterMutationMiddleware]); };注册发生在useEffect中卸载时通过 effect 的 cleanup 自动注销effect 的依赖数组包含callback每当callback引用变化旧中间件会被注销、新中间件被注册。因此文档明确要求用useCallback或useMemo保持函数引用稳定避免每次渲染都产生注销再注册的抖动也避免因引用不稳定导致的注销逻辑混乱。四、中间件链的执行原理洋葱式调用useRegisterMutationMiddleware本身并不维护中间件链它只是把中间件注册到当前表单的SaveContext上。真正的链式编排发生在useMutationMiddlewarespackages/ra-core/src/controller/saveContext/useMutationMiddlewares.ts中const callbacks useRefMiddleware[]([]); const registerMutationMiddleware useCallback((callback) { callbacks.current.push(callback); }, []); const unregisterMutationMiddleware useCallback((callback) { callbacks.current callbacks.current.filter(cb cb ! callback); }, []); const getMutateWithMiddlewares useCallback((fn) { // 在闭包中快照当前 callbacks避免调用方组件卸载后丢失中间件 const currentCallbacks [...callbacks.current]; return (...args) { let index currentCallbacks.length - 1; const next (...newArgs) { index--; if (index 0) { return currentCallbacksindex; } else { return fn(...newArgs); // 链尾调用原始 mutation } }; if (currentCallbacks.length 0) { return currentCallbacksindex; } return fn(...args); }; }, []);从源码可以提炼出三个要点LIFO 顺序getMutateWithMiddlewares从数组尾部index length - 1开始取中间件即最后注册的中间件最先执行后进先出LIFO洋葱模型每个中间件先执行next()之前的代码调用next()后链内侧的中间件以及最终 mutation依次执行再回到外层中间件执行next()之后的代码形成前处理 → 深层调用 → 后处理的嵌套结构快照语义currentCallbacks [...callbacks.current]在生成包裹函数时立即复制中间件列表。这意味着即使中间件在 mutation 执行过程中因乐观更新如重定向被注销已经开始的这次调用仍会完整走完中间件链。这一点有专门的测试用例验证见 packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.spec.tsx。4.1 中间件是如何接入 create/update 的在 controller 层useCreateController与useEditController都通过useMutationMiddlewares()拿到registerMutationMiddleware、unregisterMutationMiddleware与getMutateWithMiddlewares见 packages/ra-core/src/controller/create/useCreateController.ts 与 packages/ra-core/src/controller/edit/useEditController.ts并把getMutateWithMiddlewares作为选项传给useCreate/useUpdate。在 data provider 层useCreate会利用该选项把原始 mutation 包裹进中间件链packages/ra-core/src/dataProvider/useCreate.tsgetMutateWithMiddlewares: mutateWithMutationMode { if (getMutateWithMiddlewares) { // 立即应用中间件即使中间件随后被注销如发生重定向本次调用仍生效 const mutateWithMiddlewares getMutateWithMiddlewares( customMutationFn ? (resource, params) customMutationFnWithDataProviderResult(resource, params) : dataProviderCreate.bind(dataProvider) ); return args { const { resource, ...params } args; return mutateWithMiddlewares(resource, params); }; } return args mutateWithMutationMode(args); },注意这里默认链尾绑定的正是dataProviderCreate即dataProvider.create(resource, params)本身。整条链路可以概括为表单提交 → save() → useCreate/useUpdate 的 mutation → getMutateWithMiddlewares → 中间件 N → ... → 中间件 1 → dataProvider.create()/update()五、实战示例提交时把图片转为 base64文档提供了一个完整的ThumbnailInput组件它在提交时把ImageInput中的 blob 文件转成 base64再更新主资源记录docs/useRegisterMutationMiddleware.md。import { useCallback } from react; import { CreateMutationFunction, ImageInput, Middleware, useRegisterMutationMiddleware } from react-admin; const ThumbnailInput () { const middleware useCallback(async (resource, params, next) { const b64 await convertFileToBase64(params.data.thumbnail); // 更新将要发送给 dataProvider 的参数 const newParams { ...params, data: { ...params.data, thumbnail: b64 } }; return next(resource, newParams); }, []); useRegisterMutationMiddleware(middleware); return ImageInput sourcethumbnail /; }; const convertFileToBase64 (file: { rawFile: File; src: string; title: string; }) new Promise((resolve, reject) { // 若文件 src 是 blob url则必须转为 b64 if (file.src.startsWith(blob:)) { const reader new FileReader(); reader.onload () resolve(reader.result); reader.onerror reject; reader.readAsDataURL(file.rawFile); } else { resolve(file.src); } });注意原文档示例代码中{ ...params, data: { ...data, thumbnail: b64 } }的data应指向params.data上文已按正确语义写出。把ThumbnailInput当作普通 Input 用在创建表单中即可const PostCreate () ( Create SimpleForm TextInput sourcetitle / TextInput sourcebody multiline / ThumbnailInput / /SimpleForm /Create );假设表单提交时的值为{ data: { thumbnail: { rawFile: { path: avatar.jpg }, src: blob:http://localhost:9010/c925dc18-5918-4782-8087-b2464896b8f9, title: avatar.jpg } } }中间件会把 blob url 转成data:image/jpeg;base64,...最终dataProvider.create收到的参数为{ data: { thumbnail: { title: avatar.jpg, src: data:image/jpeg;base64,... } } }这个例子的核心价值在于中间件在 dataProvider 之前拦截并改写参数而ImageInput组件自身不需要感知任何转换逻辑实现了关注点分离。六、进阶场景创建/更新关联数据useMutationMiddlewares的源码注释给出了一个更复杂的实战模式表单同时包含订单字段与新客户字段中间件先调用useCreate创建 customers 记录再改写订单参数引用新客户 idpackages/ra-core/src/controller/saveContext/useMutationMiddlewares.tsconst CustomerForm props { const [createCustomer] useCreateCustomer(); const middleware useCallback(async (resource, params, next) { const { data } params; const { user, ...orderData } data; const { data: newCustomer } await createCustomer(customers, { data: user }); const orderDataWithCustomer { ...orderData, customerId: newCustomer.id }; next(resource, { data: orderDataWithCustomer }); }, [createCustomer]); useRegisterMutationMiddleware(middleware); return ( TextInput sourceuser.email / TextInput sourceuser.firstName / TextInput sourceuser.lastName / / ); };在该模式下createCustomer通过useCreate执行不受当前中间件链影响它没有传入getMutateWithMiddlewares因此不会产生递归调用中间件内可安全地await关联写入再改写参数继续向下传递。七、生命周期与清理机制useRegisterMutationMiddleware的生命周期管理可归纳如下注册时机组件挂载后useEffect注册到 SaveContext未处于Create/Edit等提供 SaveContext 的控制器内时registerMutationMiddleware为undefinedhook 静默跳过注册见源码第 18-20 行卸载清理组件卸载时 effect cleanup 调用unregisterMutationMiddleware中间件从链中移除保证不会出现内存泄漏或幽灵中间件引用稳定性callback引用变化会触发注销旧 注册新因此文档明确要求使用useCallback保持引用稳定快照执行一次已经开始已通过getMutateWithMiddlewares生成包裹函数的 mutation 调用即使在执行途中中间件被注销例如乐观更新触发重定向导致组件卸载仍会按快照走完整个链。SaveContext的值接口packages/ra-core/src/controller/saveContext/SaveContext.ts中同时暴露了registerMutationMiddleware与unregisterMutationMiddleware这也意味着在自定义控制器或自定义上下文中可以手动管理中间件注册。八、测试验证仓库为这一机制提供了完整的单元测试packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.spec.tsx覆盖了两个核心行为注册与注销正确性挂载中间件后触发 save中间件被调用、原 mutation 执行卸载中间件后再次 save中间件不再被调用乐观副作用下的快照执行模拟注册中间件 → 触发 save 前卸载中间件如乐观重定向的场景确认已开始的调用仍按快照执行中间件链。如果你要为自己的中间件编写测试也可以参考该 spec用useMutationMiddlewares()构造SaveContextProvider的 value用getMutateWithMiddlewares手动包裹一个 jest mock 的 save 函数即可在无真实 dataProvider 的情况下验证链式调用。九、小结与使用建议useRegisterMutationMiddleware是 react-admin 表单保存链路中的钩子点机制。使用时请记住几条关键约定中间件必须调用next并返回其结果否则 mutation 生命周期中断用useCallback保持中间件引用稳定并把组件放在Create/Edit控制器的后代位置链的执行顺序是 LIFO后注册先执行多个中间件按洋葱模型嵌套参数改写请基于params.data派生新对象不要原地修改以保证update场景下previousData与传给 dataProvider 的数据各归其位涉及关联数据写入时在中间件内使用不带getMutateWithMiddlewares的useCreate/useUpdate避免递归包裹。掌握这套机制后你可以把表单数据转换、关联写入、审计日志等横切逻辑从页面组件中剥离出来做成可复用的智能输入组件让业务代码更内聚、更可测试。赞分享前端UI组件【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址https://gitcode.com/gh_mirrors/re/react-admin点击查看免费下载相关推荐3步拦截React Router导航表单未保存、权限不足场景全解决3步拦截React Router导航表单未保存、权限不足场景全解决 你是否遇到过用户误触导航导致表单内容丢失或者未登录用户直接访问需要权限的页面React前端路由终极指南go-grpc-middleware拦截器链式调用顺序深度解析终极指南go grpc middleware拦截器链式调用顺序深度解析 在构建Golang gRPC服务时拦截器Interceptor是实现横切关注点的后端微服务可观测性mpv.net终极指南如何在Windows上打造专业级媒体播放体验mpv.net终极指南如何在Windows上打造专业级媒体播放体验 mpv.net是一款基于经典mpv播放器开发的Windows媒体播放器它继承了mpv的强音视频桌面应用上一篇Homepage消息队列异步处理与事件驱动架构下一篇wechat-versions项目快速入门5分钟学会使用GitHub Actions自动抓取微信版本创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表