news 2026/9/25 5:48:29

Rematch 插件 API 深度指南:用 config、exposed 与五个钩子扩展 Redux 行为

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rematch 插件 API 深度指南:用 config、exposed 与五个钩子扩展 Redux 行为
  • 前端

【免费下载链接】rematch

The Redux Framework

项目地址:https://gitcode.com/gh_mirrors/re/rematch
点击查看免费下载

本文基于 Rematch 官方 API 文档 Plugins 展开,系统讲解 Rematch 插件机制的六个组成部分(config、exposed、createMiddleware、onReducer、onRootReducer、onModel、onStoreCreated),并结合 packages/core 的源码与官方插件实现,说明每个属性在 store 创建流程中的真实执行时机、参数含义与返回值语义。读完后,你能够独立编写一个完整插件,理解插件如何注入模型、覆盖 reducer 链、向 store 暴露方法,以及 TypeScript 泛型如何为插件扩展的类型提供保障。

插件机制在 Rematch 中的作用

插件(Plugin)是 Rematch 提供的一套标准扩展点,用于在不修改核心代码的前提下扩展功能:覆盖 Redux 配置、向 store 添加额外模型、注入自定义中间件,甚至替换最终生成的 store。官方文档明确指出,插件"可以覆盖配置、添加新模型、甚至整体替换 store",其设计目标是让 Rematch 团队维护的官方插件(immer、select、persist、loading、updated、typed-state)与社区插件共用同一套机制。

从源码结构看,一个 Rematch store 的创建链路为:

  1. init()接收用户配置,调用createConfig()补全默认值;
  2. createConfig()在生成配置的过程中合并所有插件的config声明(模型、redux 配置);
  3. createRematchStore()构建 Rematch "bag"(内部工具包),收集createMiddleware产物,创建 Redux store,逐个模型执行onModel,最后以onStoreCreated收尾。

init的入口在 packages/core/src/index.ts,它只做两件事:createConfig(initConfig || {})和createRematchStore(config)。插件正是插在这两个阶段的各个环节中的。

插件对象的完整属性

根据文档,插件是一个对象,可以包含以下属性。类型定义见 packages/core/src/types.ts:Plugin接口继承自PluginHooks(四个钩子 +createMiddleware),并额外允许config与exposed两个属性。

config:向 store 注入模型与 Redux 配置

config是一个形如{ models, redux }的对象,形状与 init 方法接受的配置一致(分别参见 Models 与 Redux 文档)。它的两个用途:

  • models:让插件向 store 注入额外模型。这是 Rematch 插件添加"基础设施状态"的正规方式,例如@rematch/updated插件就是通过config.models注入一个名为updated的模型来记录各 effect 的最后执行时间,见 packages/updated/src/index.ts。
  • redux:覆盖 Redux 层配置,如combineReducers、initialState、reducers、middlewares、enhancers等。

config的合并逻辑在 packages/core/src/config.ts 中实现,具体规则是:

子项合并方式源码行为
config.models浅合并,用户模型优先config.models = merge(config.models, plugin.config.models)
config.redux.initialState浅合并,用户优先merge(original, pluginValue)
config.redux.reducers/rootReducers浅合并,用户优先同上
config.redux.enhancers/middlewares追加[...config, ...pluginValue]数组拼接
config.redux.combineReducers/createStore替换(用户优先)config.X \|\| plugin.config.X

其中merge是一个浅合并函数,"给原对象(用户配置)以优先权"(merge 实现:return extra ? { ...extra, ...original } : original)。这意味着如果用户在init中定义了与插件同名的模型或 reducer key,用户定义会覆盖插件的注入。middlewares与enhancers则是多插件累加关系,每个插件的中间件都会加入 Redux 的中间件链。

单元测试验证了这些行为:插件注入单个/多个模型("should add a model" / "should add multiple models")以及initialState的合并("should merge plugin configs into configs"),见 packages/core/test/plugins.test.ts。

exposed:向 store 暴露插件方法

exposed是一个{ [string]: ((rematchStore, ...args) => any) | object }形式的映射,用于在 store 上挂属性,实现插件与插件之间、插件与业务代码之间的通信。文档特别强调:exposed的执行早于onModel与onStoreCreated钩子,所以在钩子里可以直接使用暴露出来的属性。

每个 exposed 项的取值规则(源码在 packages/core/src/rematchStore.ts 的addExposed):

  • 若项是函数:store 上得到的是一个包装函数,调用时rematchStore会作为第一个参数自动传入,其余参数透传;
  • 若项是对象:store 上得到的是Object.create(plugin.exposed[key])—— 基于该对象的原型创建的新对象,而非直接引用,因此插件可以在后续钩子(如onModel)中往这个对象上安全地写属性而不影响插件内部定义。

类型层面,types.ts 用ObjectNotAFunction这个约束型({ [k: string]: any } & ({ bind?: never } | { call?: never }))确保对象不能带有bind/call等函数特征,ExposedFunction则声明了函数必须接收RematchStore作为第一个参数。

典型案例是@rematch/select插件:它通过exposed: { select, sliceState, selectorCreator }把select工厂暴露出去(packages/select/src/index.ts),随后在onModel中往select[model.name]上逐个模型地挂选择器,最后在onStoreCreated里执行store.select = select。这一系列操作能成立,正依赖于 exposed 先于两个钩子执行。

createMiddleware:需要访问 bag 的自定义中间件

createMiddleware的签名是(bag) => Redux.Middleware。与其他中间件的区别在于它可以拿到 Rematch 的内部工具包bag(见下文),从而访问bag.effects、bag.models、bag.reduxConfig等内部状态。文档同时提示:如果不需要 bag,也可以直接把中间件放到config.redux.middlewares(即插件的config或init的redux配置)里,走更简单的路径。

执行位置在 rematchStore.ts:

// collect middlewares from plugins bag.forEachPlugin('createMiddleware', (createMiddleware) => { bag.reduxConfig.middlewares.push(createMiddleware(bag)) })

所有插件返回的中间件会 push 进reduxConfig.middlewares,与config.redux.middlewares注入的中间件一起,在createReduxStore中统一经Redux.applyMiddleware(...)组合(packages/core/src/reduxStore.ts)。

核心自带的一个"参考实现"是 effects 中间件(createEffectsMiddleware):它先执行 reducer 分支(next(action)),再执行对应的 effect 并返回其结果。测试用例 "should add middleware" 用一个把所有 action payload 改写为 100 的中间件验证了插件中间件确实进入了 action 处理链(plugins.test.ts)。

onReducer:包装单个模型的 reducer

签名:(reducer, modelName, bag) => reducer | void。在 Rematch 为每个模型构建"base reducer"(即把模型所有reducers合并、并先跑baseReducer的组合函数)之后触发;返回新 reducer 则整体替换 Rematch 生成的版本。

调用点位于模型 reducer 构建流程末尾(packages/core/src/reduxStore.ts):

bag.forEachPlugin('onReducer', (onReducer) => { reducer = onReducer(reducer, model.name, bag) || reducer }) bag.reduxConfig.reducers[model.name] = reducer

|| reducer的写法明确了语义:返回 falsy(void/undefined)时保持原 reducer,返回函数时替换。注意此时传入的 reducer 已经包好了模型的baseReducer与reducers映射,插件拿到的是"模型级别的最终 reducer"。

@rematch/persist是典型用法:onReducer按modelName查找嵌套持久化配置,命中则返回persistReducer(reducerConfig, reducer),未命中返回undefined(packages/persist/src/index.ts)。

onRootReducer:包装根 reducer

签名:(reducer, bag) => reducer | void,触发时机在根 reducer 组装完毕之后。Rematch 先用combineReducers(用户或插件可覆盖)合并所有模型 reducer,再套上rootReducers包装层,最后交给插件链(createRootReducer):

bag.forEachPlugin('onRootReducer', (onRootReducer) => { rootReducer = onRootReducer(rootReducer, bag) || rootReducer })

@rematch/persist用它把整个根 reducer 换成persistReducer(persistConfig, rootReducer),实现全量状态持久化(packages/persist/src/index.ts)。对比onReducer按模型粒度拦截、onRootReducer对整个 state 树拦截,可以清楚区分两者的适用场景:嵌套持久化(部分模型)与根级持久化(整个 store)。

onModel:模型就绪后介入

签名:(namedModel, rematchStore) => void,返回值为 void。文档说明其触发时机是"整个模型装配完成——reducer 与 dispatcher 均已就绪",并且每次动态添加模型(store.addModel)时同样会执行。典型用途:收集模型 reducer/effects 信息、改写它们或创建新属性。

执行点在 enhanceModel:

function enhanceModel(rematchStore, bag, model) { createEffectDispatcher(rematchStore, bag, model) bag.forEachPlugin('onModel', (onModel) => { onModel(model, rematchStore) }) }

注意rematchStore此时已带完整的dispatch[modelName][reducer|effect]结构。@rematch/updated插件展示了onModel的高级用法:遍历rematch.dispatch[name],用isEffect标记识别 effect dispatcher,把每个 effect 的 dispatch 函数包一层——原函数执行后(Promise 完成时)再 dispatchupdated模型的onUpdate,从而自动记录 effect 的最后触发时间,全程无需用户手写任何代码(packages/updated/src/index.ts)。

onModel还支撑@rematch/select的选择器装配:为每个模型初始化select[model.name],把model.selectors(对象或工厂函数)解析为惰性 getter,避免模型间选择器互相引用时的初始化顺序问题(packages/select/src/index.ts)。

onStoreCreated:最后的钩子,可整体替换 store

签名:(rematchStore, bag) => rematchStore | void。它在 Rematch store 完全就绪后运行,是插件链的最后一环。文档说明:返回新 store 则替换 Rematch 创建的 store,通常用于给 store 添加额外属性或函数;用 TypeScript 写此类插件时,务必同步更新 store 的类型定义,否则新增属性没有类型。

执行与替换逻辑在 rematchStore.ts:

bag.forEachPlugin('onStoreCreated', (onStoreCreated) => { rematchStore = onStoreCreated(rematchStore, bag) || rematchStore }) return rematchStore

|| rematchStore意味着返回 falsy 时保留原 store;而多个插件都会依次拿到"当前最新版本"的 store 并可以再次替换。测试用例 "plugins should be able to set a value in store" 直接在钩子里给 store 赋值store.returned = 42验证了这条路径(plugins.test.ts)。

钩子的执行顺序与 Rematch "bag"

综合源码,一次init()中插件相关逻辑的完整时序为:

  1. config 合并阶段(config.ts):遍历config.plugins,合并plugin.config.models与plugin.config.redux,然后执行validatePlugin;
  2. bag 构建与中间件收集(rematchStore.ts):创建bag,push 核心 effects 中间件,再依次调用各插件的createMiddleware;
  3. 模型 reducer 构建(reduxStore.ts):每个模型的组合 reducer 完成后触发onReducer;
  4. 根 reducer 构建(reduxStore.ts):触发onRootReducer;随后createReduxStore组装 enhancers/createStore,得到 Redux store;
  5. exposed 注入(rematchStore.ts):addExposed先于模型装配执行,保证onModel/onStoreCreated中可用;
  6. 模型装配(rematchStore.ts):两轮遍历——先prepareModel(注入 reducer dispatchers),再enhanceModel(生成 effect dispatchers 并触发onModel);分两轮是为了让循环引用的模型在 effects 中可通过解构互相访问(源码注释说明了这一点);
  7. 收尾:依次触发onStoreCreated,返回最终 store。

贯穿其中的bag是 RematchBag 类型,由 bag.ts 创建,包含:

  • models:所有 NamedModel 数组(模型名内嵌);
  • reduxConfig:完整 Redux 配置;
  • forEachPlugin(method, fn):遍历所有插件并对实现了指定钩子的插件回调——所有钩子都通过它派发,保证插件按plugins数组声明顺序执行;
  • effects:action type 到 effect 函数的全局映射(effects 中间件依赖它)。

文档中"bag"的称呼即来源于此:它是"Rematch 内部可用值的集合",故意对最终用户隐藏,只开放给插件的createMiddleware、onReducer、onRootReducer、onStoreCreated。

一个完整插件示例

下面把文档中的插件骨架补全为可运行的形态,并附上与源码行为对应的注释:

import { init } from '@rematch/core' const plugin = { // 1. 注入模型 + 覆盖 redux 配置(在 createConfig 阶段合并,用户配置优先) config: { redux: { combineReducers: customCombineReducers, }, models: { extra: extraModel, }, }, // 2. 暴露属性:函数会被包装(store 作为第一参),对象会以原型方式挂到 store 上 exposed: { select: {} }, // 3. 能拿到 bag 的自定义中间件,进入 Redux 中间件链 createMiddleware: (rematchBag) => (store) => (next) => (action) => { // 可访问 rematchBag.effects / rematchBag.models / rematchBag.reduxConfig // 注意必须调用 next(action),否则 action 链会中断 return next(action) }, // 4. 模型 reducer 就绪后触发;返回新函数则替换,返回 void 则保留 onReducer(reducer, modelName, bag) { // do something }, // 5. 根 reducer 就绪后触发;返回新函数则整体替换 onRootReducer(reducer, bag) { // do something }, // 6. 每个模型装配完成后触发(addModel 动态加模型时也会触发) onModel(namedModel, rematchStore) { // do something }, // 7. 最后的钩子;返回新 store 则替换 onStoreCreated(rematchStore, bag) { // do something }, } const store = init({ models: { counter: { state: 0, reducers: { inc: s => s + 1 } } }, plugins: [plugin], })

该示例与文档 plugins 页 给出的骨架一致,注释补充了各属性的实际执行阶段。验证方面,核心测试 packages/core/test/plugins.test.ts 覆盖了:onModel订阅、插件中间件改写 action、config.models注入单个/多个模型、config.redux.initialState合并、以及onStoreCreated向 store 写入属性,共五组场景。

另外提醒一点运行时行为:开发环境(NODE_ENV !== 'production')下,validatePlugin 会校验五个钩子/工厂"若已定义必须是函数",报错信息形如Plugin onStoreCreated must be a function,且所有错误合并后一次性抛出——这要求插件作者不要把这些 key 写成非函数值。

TypeScript 写插件的要点

Plugin类型(types.ts)有三个泛型参数:TModels(业务模型)、TExtraModels(插件注入的额外模型,默认Record<string, never>)、TExposedModels。官方插件的写法是范式:

  • @rematch/updated定义ExtraModelsFromUpdated(updated/src/index.ts)声明自己注入的updated模型,使init<TModels, ExtraModelsFromUpdated<TModels>>后store.getState()自动包含updated字段,store.dispatch.updated.onUpdate也有完整类型;
  • @rematch/select返回Plugin<TModels, TExtraModels>,其exposed中的select在运行时被逐模型填充,store 上的store.select通过onStoreCreated赋值获得。

文档给出的 TypeScript 注意事项依然有效:若插件通过onStoreCreated给 store 挂了新属性(如store.select、store.returned),用户侧应扩展 store 类型声明以获得类型提示——plugins.test.ts中测试注释也点明了这一点(plugins.test.ts)。

官方插件速览:每类钩子的真实用例

官方插件总览见 docs/plugins/index.md,仓库内各插件源码分别位于packages/下。从插件机制视角可以归纳:

插件主要使用的扩展点用途
@rematch/persistonReducer+onRootReducer+onStoreCreated包装 redux-persist:onReducer处理嵌套持久化,onRootReducer包装根 reducer,onStoreCreated中创建 persistor
@rematch/selectexposed+onModel+onStoreCreated基于 reselect 为模型生成惰性、可互相引用的store.select[model]
@rematch/updatedconfig.models+onModel注入updated模型并包装所有 effect dispatcher,自动记录最后触发时间

这也印证了文档的推荐路径:"如果你找不到需要的插件——可以自己写一个",插件 API 页面(即本文依据的 docs/api-reference/plugins.md)就是编写起点,参考 Rematch 团队各插件的源码即可上手。

参考路径

  • 文档:插件 API、init/store 配置、Models、Redux 配置、插件总览
  • 核心实现:init 入口、config 合并、store 装配与钩子派发、reducer 构建、bag 创建、插件校验、类型定义
  • 测试:插件行为测试
  • 前端

【免费下载链接】rematch

The Redux Framework

项目地址:https://gitcode.com/gh_mirrors/re/rematch
点击查看免费下载
上一篇:Dufs错误处理与日志分析终极指南:快速排查服务异常的5个实用技巧
下一篇:Neovim-Qt性能优化指南:如何调校GUI响应速度和内存使用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 5:44:48

Atlas 300V 24G推理卡部署YOLO全流程与踩坑指南

前几天有个做安防项目的朋友发了一张截图给我&#xff0c;问“Atlas 300V 24G到底算不算运算加速卡&#xff1f;能不能拿来跑YOLO&#xff1f;”这个问题我其实被问过很多次。很多人从CUDA那套习惯转过来&#xff0c;第一次接触华为的昇腾设备&#xff0c;容易拿GPU的思维去套A…

作者头像 李华