实战指南)
react-admin 记录级版本追踪useAddRevisionAfterMutation 自动创建修订Revision实战指南【免费下载链接】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导读useAddRevisionAfterMutation是 react-admin 生态中面向记录版本历史Revisions Versioning场景的 Hook它通过注册一个表单保存时的 mutation middleware在create或update操作成功后自动为记录创建一条修订revision从而让每条数据的变更历史被完整记录。本文以该 Hook 为核心先讲透其工作原理与完整接入方式再深入到 react-admin 开源仓库ra-core的源码层拆解 mutation middleware 的注册、链式调用与卸载机制最后结合useGenerateChangeMessage、RevisionsButton等配套能力帮你搭建一套保存即留痕、可对比、可回滚的记录版本追踪方案。注意useAddRevisionAfterMutation属于 react-admin Enterprise Editionreact-admin/ra-core-ee能力本文示例与代码均基于当前仓库文档内容编写接入前请确认你具备对应的商业订阅。一、这个 Hook 解决什么问题在涉及审计、协作或多用户编辑的业务系统中记录被谁改过、改了哪些字段、能否回滚往往是刚需。react-admin 的 revision 能力将这种需求抽象为每次成功保存新建或编辑记录时自动生成一条修订修订中包含变更摘要message、描述description与操作者authorId用户可以在界面上查看修订列表、对比任意两个版本的差异并一键回滚。useAddRevisionAfterMutation解决的就是第 1 步——把创建修订这件事挂接到表单保存的生命周期上。它的官方定位是This hook registers a mutation middleware that automatically creates a revision after a successful create or update operation.换句话说你不需要在表单的onSuccess里手写保存记录 调用创建修订接口的逻辑这个 Hook 会拦截save()调用链把修订元数据随记录一起提交然后在 mutation 成功后自动落库一条修订。二、工作机制middleware 如何自动创建修订理解这个 Hook关键在于理解 react-admin 的mutation middleware保存中间件机制。useAddRevisionAfterMutation本质上是一段由官方实现的 middleware读取修订元数据middleware 从一个名为REVISION_FIELD的常量所指代的特殊字段中读取修订元数据保存前剥离在真正调用dataProvider.create()/dataProvider.update()之前把这个特殊字段从表单数据中移除避免污染实际写入数据库的记录成功后再创建修订在 mutation 成功后用之前剥离出来的元数据创建一条 revision 记录。REVISION_FIELD常量可以从react-admin/ra-core-ee导入import { REVISION_FIELD } from react-admin/ra-core-ee;所以整个调用链是表单保存 → 用户代码把修订元数据塞进REVISION_FIELD字段 → middleware 拦截并剥离该字段 → 正常保存记录 → middleware 在成功后创建修订。三、官方完整接入示例要让修订元数据在保存时搭便车官方推荐的做法是覆盖表单的SaveContext把修订信息注入到save函数中再由useAddRevisionAfterMutation注册的 middleware 自动消费。以下示例展示了如何重写SaveContext以在保存表单时携带修订元数据摘自docs_headless/src/content/docs/useAddRevisionAfterMutation.mdimport React, { ReactNode, useMemo } from react; import { EditBase, SaveContextProvider, useSaveContext, type SaveContextValue, } from ra-core; import { SimpleForm, TextInput, DeleteButton } from my-react-admin-ui-library; import { useAddRevisionAfterMutation, useGenerateChangeMessage, REVISION_FIELD, } from react-admin/ra-core-ee; export const ProductEdit () ( EditBase CreateRevisionOnSave SimpleForm TextInput sourcereference / TextInput sourcecategory / /SimpleForm /CreateRevisionOnSave /EditBase ); const CreateRevisionOnSave ({ children }: { children: ReactNode }) { const originalSaveContext useSaveContext(); useAddRevisionAfterMutation(); const generateChangeMessage useGenerateChangeMessage(); // Wrap the original save function to add the revision data before saving const saveContext useMemoSaveContextValue( () ({ ...originalSaveContext, save: async (record, callbacks) originalSaveContext.save!( { ...record, // Store the revision metadata in a special field that will be removed by the middleware [REVISION_FIELD]: { message: generateChangeMessage({ data: record }), description: , authorId: john, }, }, callbacks ), }), [generateChangeMessage, originalSaveContext] ); return ( SaveContextProvider value{saveContext} {children} /SaveContextProvider ); };示例中的关键步骤步骤代码位置作用注册 middlewareuseAddRevisionAfterMutation()在当前SaveContext中注册保存后自动建修订的中间件读取原始 saveuseSaveContext()拿到EditBase提供的原始保存函数生成变更摘要useGenerateChangeMessage()自动对比新旧记录生成人类可读的变更说明注入修订元数据[REVISION_FIELD]: { message, description, authorId }把修订信息放入特殊字段随save提交覆盖上下文SaveContextProvider value{saveContext}让子表单使用增强后的 saveTip该示例还借助了useGenerateChangeMessageHook根据表单的变更自动生成修订消息message这是官方文档明确推荐的搭配方式。3.1 修订元数据的三个字段注入到REVISION_FIELD中的数据是一个对象包含message变更摘要例如useGenerateChangeMessage生成的Changed reference, category或Initial revisiondescription可选的补充描述示例中为空字符串authorId操作者标识示例中硬编码为john实际项目中应从当前登录用户如useGetIdentity获取。这些元数据被 middleware 剥离后最终会成为 revision 记录的组成部分供RevisionsButton、RevisionListWithDetailsInDialog等界面组件展示。四、源码级解析mutation middleware 是怎么跑起来的useAddRevisionAfterMutation属于 Enterprise 私有包其实现细节不在当前仓库中但它所依赖的 middleware 基础设施完全来自开源的ra-core。读懂下面这段源码你就能彻底明白Hook 一注册、保存时自动生效的底层原理。4.1 注册与卸载useRegisterMutationMiddlewareuseAddRevisionAfterMutation内部本质上是调用useRegisterMutationMiddleware对应文档docs/useRegisterMutationMiddleware.md其实现位于 packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.tsexport const useRegisterMutationMiddleware MutateFunc extends (...args: any[]) any (...args: any[]) any, ( callback: MiddlewareMutateFunc ) { const { registerMutationMiddleware, unregisterMutationMiddleware } useSaveContext(); useEffect(() { if (!registerMutationMiddleware || !unregisterMutationMiddleware) { return; } registerMutationMiddleware(callback); return () { unregisterMutationMiddleware(callback); }; }, [callback, registerMutationMiddleware, unregisterMutationMiddleware]); };要点生命周期绑定组件挂载时注册 middleware卸载时自动注销与组件共存亡稳定性要求useEffect依赖callback若callback引用不稳定会导致反复注册/注销因此官方建议用useCallback包裹 middleware 函数上下文驱动registerMutationMiddleware/unregisterMutationMiddleware都来自useSaveContext()即 middleware 是绑定到当前表单的 SaveContext上的。4.2 核心存储与链式调用useMutationMiddlewaresmiddleware 的存储与调度实现在 packages/ra-core/src/controller/saveContext/useMutationMiddlewares.tsexport const useMutationMiddlewares MutateFunc extends (...args: any[]) any (...args: any[]) any, (): UseMutationMiddlewaresResultMutateFunc { const callbacks useRefMiddlewareMutateFunc[]([]); const registerMutationMiddleware useCallback( (callback: MiddlewareMutateFunc) { callbacks.current.push(callback); }, [] ); const unregisterMutationMiddleware useCallback( (callback: MiddlewareMutateFunc) { callbacks.current callbacks.current.filter(cb cb ! callback); }, [] ); const getMutateWithMiddlewares useCallback((fn: MutateFunc) { const currentCallbacks [...callbacks.current]; return (...args: ParametersMutateFunc): ReturnTypeMutateFunc { let index currentCallbacks.length - 1; const next (...newArgs: any) { index--; if (index 0) { return currentCallbacksindex; } else { return fn(...newArgs); } }; if (currentCallbacks.length 0) { return currentCallbacksindex; } return fn(...args); }; }, []); // ... };机制解读数组存储middleware 以数组useRef形式保存按注册顺序排列闭包快照getMutateWithMiddlewares生成包装函数时先把当前 middleware 列表拷贝到闭包currentCallbacks因此即使保存后组件因跳转而卸载middleware 被注销本次调用仍然完整执行——这正是先取包装函数、再触发 mutation的设计原因洋葱模型每个 middleware 接收(resource, params, next)通过next(...)把控制权交给链上的下一个 middleware最后一个 middleware 的next会调用真正的 mutation 函数dataProvider.create/dataProvider.update类型签名MiddlewareMutateFunc被定义为在原 mutation 参数末尾追加一个next参数的函数类型见同文件末尾的Middleware类型导出。useRegisterMutationMiddleware.spec.tsxpackages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.spec.tsx中有两个关键测试用例印证了上述行为register / unregister 测试注册后调用getMutateWithMiddlewares生成的函数会触发 middleware卸载Toggle 卸载组件后再调用则不再触发乐观副作用下的执行测试即使 middleware 已被注销例如因跳转产生的乐观副作用由于包装函数闭包保留了快照middleware 仍会按预期执行。4.3 控制器如何把 middleware 接进保存链路在ra-core中useMutationMiddlewares被useCreateController与useEditController使用packages/ra-core/src/controller/create/useCreateController.ts第 79-83 行解构registerMutationMiddleware、getMutateWithMiddlewares、unregisterMutationMiddleware并将getMutateWithMiddlewares传给useCreate第 144 行packages/ra-core/src/controller/edit/useEditController.ts第 111-115 行同样解构三个函数并传给useUpdate第 233 行。而在数据层 packages/ra-core/src/dataProvider/useCreate.ts第 194-219 行getMutateWithMiddlewares的作用是把包装函数提前固化getMutateWithMiddlewares: mutateWithMutationMode { if (getMutateWithMiddlewares) { // Immediately get the function with middlewares applied so that even if the middlewares gets unregistered // (because of a redirect for instance), we still have them applied when users have called the mutate function. const mutateWithMiddlewares getMutateWithMiddlewares( customMutationFn ? ... : dataProviderCreate.bind(dataProvider) ); return args { const { resource, ...params } args; return mutateWithMiddlewares(resource, params); }; } return args mutateWithMutationMode(args); },这段注释直接说明了为什么要立即生成带 middleware 的包装函数即使 middleware 因跳转等原因被注销只要用户已经拿到并调用了这个包装函数middleware 依然生效。这正是useAddRevisionAfterMutation能可靠工作的基石——修订创建不会因为保存后的界面跳转而丢失。SaveContext的类型定义见 packages/ra-core/src/controller/saveContext/SaveContext.tsSaveContextValue除了save之外还暴露registerMutationMiddleware与unregisterMutationMiddlewareuseSaveContext()则是对该 Context 的简单读取封装packages/ra-core/src/controller/saveContext/useSaveContext.ts。这就是官方示例中CreateRevisionOnSave组件能够读取原始 save → 覆盖 save → 注入修订数据的完整依据。五、配套能力useGenerateChangeMessage 自动生成变更摘要官方示例中message字段由useGenerateChangeMessage生成其说明文档位于 docs_headless/src/content/docs/useGenerateChangeMessage.md。它通过对比新提交的数据与现有记录自动生成人类可读的变更说明同样属于 Enterprise Edition 能力。参数props.resource?资源名默认取当前资源上下文props.record?原始记录默认取当前记录上下文。返回值一个接收{ data, record?, resource? }并返回本地化消息的函数。消息规则场景返回消息新记录Initial revision无字段被修改No changes单个字段变更Changed [field]多个字段变更Changed [field1], [field2], ...国际化返回的消息通过i18nprovider 完全本地化使用的翻译键为ra-history.on_save.initial_changesInitial revisionra-history.on_save.no_changesNo changesra-history.on_save.one_changeChanged %{field}ra-history.on_save.many_changesChanged %{fields}把useGenerateChangeMessage与useAddRevisionAfterMutation组合就能在用户点击保存的瞬间自动完成生成摘要 → 携带元数据 → 保存记录 → 创建修订的完整闭环。六、revision 生态保存之后的展示与回滚自动创建修订只是版本追踪的上半场。revison 能力的完整链路在 docs/Features.md 的 Revisions Versioning 一节有系统描述记录变更历史、对比任意两个版本、按需回滚到历史状态。当前仓库的 docs/RevisionsButton.md 详细介绍了配套的RevisionsButton组件Enterprisera-history包通常放在Edit页面的actions中与SimpleFormWithRevision搭配使用点击后弹出当前记录的修订列表选中修订即进入 diff 视图设置allowRevert属性后可一键回滚到所选版本从RecordContext读取当前记录、从ResourceContext读取资源并通过dataProvider.getRevisions()拉取修订列表支持diff自定义差异视图、onSelect选择回调、renderName按authorId渲染作者名等自定义属性。RevisionsButton的典型用法摘自 docs/RevisionsButton.mdimport { Edit, SelectInput, TextInput, TopToolbar } from react-admin; import { SimpleFormWithRevision, RevisionsButton, } from react-admin/ra-history; import categories from ./categories; const ProductEditActions () ( TopToolbar RevisionsButton / /TopToolbar ); export const ProductEdit () ( Edit actions{ProductEditActions /} SimpleFormWithRevision TextInput sourcereference / TextInput multiline sourcedescription / TextInput sourceimage / SelectInput sourcecategory choices{categories} / /SimpleFormWithRevision /Edit );在ra-history生态中还提供RevisionListWithDetailsInDialog常放于Edit aside中持续展示修订列表、FieldDiff字段级差异、SmartFieldDiff逐词高亮的字符串差异等组件。可以看到useAddRevisionAfterMutation负责写入修订这些组件负责读取与操作修订两者共同构成完整的版本追踪闭环。七、实战建议与注意事项结合官方文档与ra-core源码接入时有几点建议authorId 从登录态获取示例中硬编码为john生产环境应通过useGetIdentity()等认证 Hook 获取当前用户 id确保修订可追溯到真实操作者保持 middleware 引用稳定useAddRevisionAfterMutation内部依赖useEffect注册 middleware调用组件应保持挂载且不在条件分支中调用避免 middleware 被意外注销save用useMemo缓存官方示例用useMemo包裹新的saveContext依赖generateChangeMessage与originalSaveContext避免每次渲染都生成新的 context 值导致子表单重复渲染REVISION_FIELD 是临时通道不要在前端表单中直接展示或绑定该字段middleware 会在保存前将其剥离它是修订元数据从表单到 middleware 的专用传输通道Enterprise 边界useAddRevisionAfterMutation、REVISION_FIELD、useGenerateChangeMessage以及ra-history组件均属 Enterprise Edition 能力需要通过对应商业订阅获取私有包react-admin/ra-core-ee、react-admin/ra-history后使用。参考文档与源码Hook 官方文档docs_headless/src/content/docs/useAddRevisionAfterMutation.mdmiddleware 注册机制文档docs/useRegisterMutationMiddleware.md变更摘要 Hook 文档docs_headless/src/content/docs/useGenerateChangeMessage.md修订展示组件文档docs/RevisionsButton.md功能总览docs/Features.mdRevisions Versioning 章节middleware 核心实现packages/ra-core/src/controller/saveContext/useMutationMiddlewares.tsmiddleware 注册 Hookpackages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.tsSaveContext 定义packages/ra-core/src/controller/saveContext/SaveContext.ts控制器接线packages/ra-core/src/controller/create/useCreateController.ts、packages/ra-core/src/controller/edit/useEditController.ts数据层包装packages/ra-core/src/dataProvider/useCreate.ts行为测试packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.spec.tsx【免费下载链接】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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考