- 前端
【免费下载链接】rematch
The Redux Framework
本文基于 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 的创建链路为:
init()接收用户配置,调用createConfig()补全默认值;createConfig()在生成配置的过程中合并所有插件的config声明(模型、redux 配置);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()中插件相关逻辑的完整时序为:
- config 合并阶段(config.ts):遍历
config.plugins,合并plugin.config.models与plugin.config.redux,然后执行validatePlugin; - bag 构建与中间件收集(rematchStore.ts):创建
bag,push 核心 effects 中间件,再依次调用各插件的createMiddleware; - 模型 reducer 构建(reduxStore.ts):每个模型的组合 reducer 完成后触发
onReducer; - 根 reducer 构建(reduxStore.ts):触发
onRootReducer;随后createReduxStore组装 enhancers/createStore,得到 Redux store; - exposed 注入(rematchStore.ts):
addExposed先于模型装配执行,保证onModel/onStoreCreated中可用; - 模型装配(rematchStore.ts):两轮遍历——先
prepareModel(注入 reducer dispatchers),再enhanceModel(生成 effect dispatchers 并触发onModel);分两轮是为了让循环引用的模型在 effects 中可通过解构互相访问(源码注释说明了这一点); - 收尾:依次触发
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/persist | onReducer+onRootReducer+onStoreCreated | 包装 redux-persist:onReducer处理嵌套持久化,onRootReducer包装根 reducer,onStoreCreated中创建 persistor |
| @rematch/select | exposed+onModel+onStoreCreated | 基于 reselect 为模型生成惰性、可互相引用的store.select[model] |
| @rematch/updated | config.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
相关推荐
conda 插件开发指南:使用 conda_pre_commands 钩子扩展 conda 命令前置行为
conda 插件开发指南:使用 conda_pre_commands 钩子扩展 conda 命令前置行为 本篇技术指南围绕 conda 插件体系中的 conda
包管理器CLIPostGraphile 原始插件 API 实战:用 Graphile Engine 钩子深度扩展你的 GraphQL Schema
PostGraphile 原始插件 API 实战:用 Graphile Engine 钩子深度扩展你的 GraphQL Schema 本篇技术指南以 PostG
后端API网关micro插件钩子系统:深度定制编辑器行为
micro插件钩子系统:深度定制编辑器行为 你是否曾因编辑器功能固化而无法实现特定工作流?作为一款现代终端文本编辑器,micro通过强大的插件钩子系统(Plug
开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考