首页 / 资讯中心 / 文章详情

Rematch 插件体系详解:五大官方插件与插件扩展机制

Rematch 插件体系详解:五大官方插件与插件扩展机制 ★ FEATURED ARTICLE
前端【免费下载链接】rematchThe Redux Framework项目地址https://gitcode.com/gh_mirrors/re/rematch点击查看免费下载Rematch 是建立在 Redux 之上的状态管理框架其最大的工程特征之一就是通过插件Plugin机制把 Immer、状态持久化、Effect 加载状态、记忆化选择器等常用能力从核心库中解耦出去。本文基于仓库中 插件总览文档 和 插件 API 参考 展开逐一讲解rematch/immer、rematch/select、rematch/persist、rematch/loading、rematch/updated五个官方插件的兼容性、配置参数与真实用法并结合 核心包源码 剖析插件在 store 初始化各阶段是如何被加载、校验和执行的最后给出一个自定义插件的完整骨架。读完本文你可以独立完成官方插件的安装配置并具备基于onReducer、onModel、onStoreCreated等钩子编写自有插件的能力。插件是什么在 Rematch 生命周期中注入扩展点官方插件总览页docs/plugins/index.md的定位是插件用于扩展 Rematch 的功能它们是可选的但非常有用。除了 Rematch 官方团队维护的插件外你也可以在 npm 上搜索社区插件或者自己编写一个。一个插件本质上是一个对象按照 docs/api-reference/plugins.md 的定义它可以包含以下属性config{models, redux}允许插件向 store 中追加额外的 model或覆盖 Redux 的配置项如combineReducers、middlewares这两个属性的形状与init方法接收的配置一致exposed{[string]: ((rematchStore, ...args) any) | object}在onModel和onStoreCreated钩子执行之前向 store 挂载额外属性用于插件之间的通信createMiddleware(bag) Redux.Middleware创建需要访问 Rematch 内部bag的自定义 middleware如果不需要 bag也可以直接把 middleware 放进config.redux.middlewaresonReducer(reducer, modelName, bag) reducer | void当某个 model 的基础 reducer 被创建时执行返回新的 reducer 即可整体覆盖 Rematch 生成的那个onRootReducer(reducer, bag) reducer | void当 store 的根 reducer 被创建时执行同样可以用返回值覆盖onModel(namedModel, rematchStore) void当某个 model 的 reducers 与 dispatcher 全部就绪后对每个 model 执行一次每次动态addModel时也会触发适合收集、改写 model 的 reducers/effects 或生成新属性onStoreCreated(rematchStore, bag) rematchStore | void最后一个钩子在 Rematch Store 完全就绪后执行可以返回一个新的 store 来覆盖原有 store通常用来向 store 上追加属性或函数TypeScript 项目中记得同步更新 store 的类型声明。这与核心包的类型定义完全对应。在 packages/core/src/types.ts 中Plugin接口由PluginConfig、PluginExposed以及PluginHooks组成PluginHooks恰好声明了onStoreCreated、onModel、onReducer、onRootReducer、createMiddleware五个可选钩子。插件配置如何参与 store 初始化插件并不是事后挂载的而是直接参与配置的组装。查看 packages/core/src/config.tscreateConfig在填充默认值之后会遍历config.plugins对每个声明了config的插件把它config.models浅合并进全局 models把config.redux中的initialState、reducers、enhancers、middlewares依次并入 Redux 配置combineReducers和createStore则以用户配置优先、插件配置兜底的方式取用。合并逻辑由同文件中的merge函数实现——原始对象优先插件提供的值作为补充。也就是说loading、updated 这类额外 model 型插件正是通过config.models把自身状态注入 store 的。此外packages/core/src/bag.ts 中的createRematchBag为插件暴露了一个forEachPlugin(method, fn)工具它遍历所有插件并对实现了method钩子的插件逐个执行回调。从源码结构看各生命周期的钩子就是围绕这个遍历函数被依次调用的这就是bag这一名字的由来——它是 Rematch 内部信息models、reduxConfig、effects 等对插件的统一出口。插件钩子有强制校验并非任意字段都能出现在插件对象上。packages/core/src/validate.ts 中的validatePlugin会对onStoreCreated、onModel、onReducer、onRootReducer、createMiddleware逐一检查只要定义了就必须是函数否则在开发环境下抛出类似Plugin onModel must be a function的错误。配套的测试见 validatePlugins.test.ts而插件整体行为的集成测试见 plugins.test.ts。五大官方插件总览官方维护的插件及其一句话定位引自 docs/plugins/index.md插件包定位文档rematch/immer用 immer 包裹 reducers让你以可变方式安全地修改状态最终得到不可变状态docs/plugins/immer.mdrematch/select为 model 提供基于 Reselect 的记忆化 selectorsdocs/plugins/select.mdrematch/persistredux-persist v6 的封装提供自动的 Redux 状态持久化docs/plugins/persist.mdrematch/loading为 effects 自动维护加载状态免去手工维护loading: truedocs/plugins/loading.mdrematch/updated记录 model 的 effect 上次触发时间的时间戳工具docs/plugins/updated.md所有插件都遵循版本与rematch/core主版本对齐的兼容约定rematch/corerematch/immerrematch/selectrematch/persistrematch/loadingrematch/updated1.x.x1.x.x2.x.x1.x.x1.x.x1.x.x2.x.x2.x.x3.x2.x.x2.x.x2.x.x下面逐个展开。rematch/immer让 reducer 直接改 state安装npm install immer rematch/immer需要同时安装 immer 运行时和插件本体。immerPlugin([config])插件接受一个可选参数config包含两个属性whiteliststring[]model 名称数组只让列表内 model 的 reducers 被 immer 包裹blackliststring[]model 名称数组列表内 model 的 reducers不被包裹。不提供 config 时所有 model 的 reducers 都会被包裹。用法reducer 可以可变地写在 Immer 中reducer 通过直接修改 draft 来得到下一个不可变状态既不需要返回新状态Rematch 也不会强制你返回见 docs/plugins/immer.md。// filename: todo.ts import { createModel } from rematch/core import { RootModel } from ./models export const todo createModelRootModel()({ state: [ { todo: Learn typescript, done: true }, { todo: Try immer, done: false }, ], reducers: { done(state) { // mutable changes to the state state.push({ todo: Tweet about it, done: false }) state[1].done true }, }, })// filename: models.ts import { Models } from rematch/core import { todo } from ./todo export interface RootModel extends ModelsRootModel { todo: typeof todo } export const models: RootModel { todo }// filename: store.ts import immerPlugin from rematch/immer import { init, RematchDispatch, RematchRootState } from rematch/core import { models, RootModel } from ./models export const store initRootModel({ models, // add immerPlugin to your store plugins: [immerPlugin()], }) export type Store typeof store export type Dispatch RematchDispatchRootModel export type RootState RematchRootStateRootModel源码印证白名单/黑名单如何生效packages/immer/src/index.ts 的实现非常薄恰好是用插件钩子做 reducer 拦截的标准范例function wrapReducerWithImmer(reducer: Redux.Reducer) { return (state: any, payload: any): any { if (state undefined) return reducer(state, payload) return produce(state, (draft: any) reducer(draft, payload)) } } const immerPlugin (config?: ImmerPluginConfig): PluginTModels, TExtraModels ({ onReducer(reducer, model) { if ( !config || (!config.whitelist !config.blacklist) || (config.whitelist config.whitelist.includes(model)) || (config.blacklist !config.blacklist.includes(model)) ) { return wrapReducerWithImmer(reducer) } return undefined }, })两个细节值得注意包裹发生在state undefined时直接透传让 reducer 自己产出初始状态否则用 immer 的produce(state, draft reducer(draft, payload))执行——你的 reducer 对draft做的任何可变操作最终都会以不可变更新的形式写回 store白/黑名单判断以model 名称为粒度config.whitelist.includes(model)这与文档中allows defining on a model level的表述一致immer 插件的测试覆盖了该行为packages/immer/test/immer.test.ts。rematch/select为 model 装配记忆化选择器安装npm install rematch/select注意与rematch/core1.x / 2.x 对应的分别是 select 插件的 2.x / 3.x跨大版本时请参照上表选择。selectPlugin([config])config 支持两个属性见 docs/plugins/select.mdsliceState(rootState, model) modelState自定义从根状态中取出当前 model 状态的函数默认假设 store 是普通 JS 对象。当你的 store 基于 Immutable.js 的 Map 时需要覆盖它selectorCreator(selector, combiner) outputSelector默认使用 Reselect 的createSelector可以换成接口兼容的其他库例如按 props 做缓存的方案。1. 添加插件// store.js import { init } from rematch/core; import selectPlugin from rematch/select; import * as models from ./models; init({ models, // add selectPlugin to your store plugins: [selectPlugin()], });// store.ts import selectPlugin from rematch/select; import { init, RematchDispatch, RematchRootState } from rematch/core; import { models, RootModel } from ./models; export const store initRootModel({ models, plugins: [selectPlugin()], }) export type Store typeof store; export type Dispatch RematchDispatchRootModel; export type RootState RematchRootStateRootModel;2. 在 model 中定义 selectors一个 selector 是形如(rootState, props) value的函数为了性能我们实际想要的是记忆化 selector——只有依赖的数据变化时才重新计算。model 通过selectors属性定义它们该属性是一个函数接收三个参数并返回 selector 集合slice有两种用法传入从当前 model 状态取值的函数(modelState value) memoizedSelector返回一个记忆化 selector相当于创建简单记忆化 selector 的快捷方式传入根状态(rootState) modelState返回当前 model 的状态切片createSelector创建记忆化 selector 的函数默认就是 Reselect 的createSelectorhasProps为每组新的props创建一份新的 selector 缓存它包裹整个 selector 工厂形成高阶 selector。还有三个使用细节selector 内部this绑定到当前 model 的 selectors每个 selector 都接受models参数可以访问其他 model 的 selector向 selector 传递props要小心Reselect 的结果缓存是按单次缓存设计的跨多个组件实例共享带 props 的 selector 时需要自行处理。3. 完整示例const model { name: cart, state: [ { price: 42.0, amount: 3, productId: 2 }, ], selectors: (slice, createSelector, hasProps) ({ // creates a simple memoized selector based on the cart state total() { return slice((cart) cart.reduce((a, b) a b.price * b.amount, 0)); }, // uses createSelector method to create more complex memoized selector totalWithShipping() { return createSelector( slice, // shortcut for (rootState) rootState.cart (rootState, props) props.shipping, (cart, shipping) cart.reduce((a, b) a b.price * b.amount, shipping) ); }, // refers to the other selector from this model doubleTotal() { return createSelector( this.totalWithShipping, (totalWithShipping) totalWithShipping * 2 ); }, // accesses selector from a different model productsPopularity(models) { return createSelector( slice, models.popularity.pastDay, // gets pastDay selector from popularity model (cart, hot) cart.sort((a, b) hot[a.productId] hot[b.productId]) ); }, // uses hasProps function, which returns new selector for each given lowerLimit prop expensiveFilter: hasProps(function (models, lowerLimit) { return slice((items) items.filter((item) item.price lowerLimit)); }), // uses expensiveFilter selector with lowerLimit set to 20.00 wouldGetFreeShipping() { return this.expensiveFilter(20.0); }, }), };4. 在应用中使用 selectorsrematch/select会向 store 上追加一个select属性有两种使用方式方式一作为函数调用。select(mapSelectToStructure: (select) object)会惰性创建一个 structured selectorReselect 的createStructuredSelector接口const selection store.select((models) ({ total: models.cart.total, eligibleItems: models.cart.wouldGetFreeShipping, })); // it can be used as mapStateToProps connect(selection)(MyComponent); // or connect((state) ({ contacts: state.contacts.collection, ...selection(state), }))(MyComponent);方式二作为对象访问。select本身是一个对象结构为{ [modelName]: { [selectorName]: (state) any } }其中的 selector 是普通函数可以在应用任何位置调用const moreThan50 store.select.cart.expensiveFilter(50.0); console.log(moreThan50(store.getState())); const mapStateToProps (state) ({ items: moreThan50(state), });实战配方RecipesImmutable.js 根状态如果你的 store 使用 Immutable.js 的 Map需要配置sliceState用Map.get()取切片selectPlugin({ sliceState: (rootState, model) rootState.get(model.name), });按 props 缓存应对高频重算在仪表盘或大量外部值参与计算的场景带 props 的 selector 被多个组件共享时会因为缓存大小只有 1 而频繁重算。社区库 re-reselect 按 props 维度缓存 selector可以通过selectorCreator接入import createCachedSelector from re-reselect; selectPlugin({ selectorCreator: createCachedSelector, });替代思路用 listener reducer 主动追踪selectors 的懒惰性意味着它只能依赖其他 model 的公开惰性接口slice永远只能访问当前 model。另一个公开接口是 store 发出的 actions——在某些应用中用 listener reducer 主动响应其他 model 的变化可能更合适reducers: { selectedGroup/change: (state, id) ({ ...state, list: id ? state.unfilteredList.filter((p) p.group id) : state.unfilteredList, }), },rematch/persist自动化的状态持久化rematch/persist是 redux-persist v6 的封装提供自动的 Redux 状态持久化。安装npm install rematch/persistpersistPlugin(persistConfig, [nestedPersistConfig, persistStoreConfig, callback])四个参数见 docs/plugins/persist.mdpersistConfigPersistConfig与 redux-persist 的persistReducer的 config 兼容用于创建被持久化的根 reducerkey、storage等选项直接沿用 redux-persist 的语义nestedPersistConfig{ [modelName]: PersistConfig }为个别 model 配置 Nested Persist——从 model 名到该 model 持久化配置的映射persistStoreConfigPersistorOptions与 redux-persist 的persistStore的 config 兼容callback() voidrehydration 完成后回调。配置 storeimport persistPlugin from rematch/persist; import { init } from rematch/core; import storage from redux-persist/lib/storage; const persistConfig { key: root, storage, }; init({ plugins: [persistPlugin(persistConfig)], });Persist Gate等待 rehydration在 React 中可以用 redux-persist 提供的PersistGate组件在异步数据从 storage 加载完成前显示加载指示器。rematch/persist额外导出了getPersistor用于获取 persistor 实例import { getPersistor } from rematch/persist; import { PersistGate } from redux-persist/lib/integration/react; const persistor getPersistor(); const Root () ( PersistGate persistor{persistor} divapp/div /PersistGate );仓库中的 persist-react 示例 演示了完整的持久化流程packages/persist/test/persist.test.ts 则验证了持久化与 rehydration 的行为。rematch/loadingeffects 的自动化加载状态rematch/loading为 effects 自动维护加载指示器让你不必手动维护loading: true之类的状态设计灵感来自 dva-loading。安装npm install rematch/loadingloadingPlugin([config])config 的属性见 docs/plugins/loading.mdnamestring?loading model 在 store 中的键名例如命名为custom后状态位于state.custom默认loadingasNumberboolean?默认用布尔值追踪正在运行的 effectsstate.loading.global true设为true后改为计数state.loading.global 5。已废弃请使用typetypenumber | boolean | full默认布尔值之外full可以追踪 effect 的 Promise 是否 resolve 成 Error、是否 loading、是否成功 resolvenumber则追踪执行次数whiteliststring[]?只为列表内的 effects 启用插件blackliststring[]?列表内的 effects 不启用插件其余全部启用。blacklist与whitelist都接受modelName/effectFunctionName格式的完整 effect 名例如count/addOne。两者都不提供时插件对所有 effect 生效。状态结构假设 store 中有 modelcountloading 状态的结构为{ global: true, models: { count: true }, effects: { count: { addOne: true } } }global任意 model 的任意 effect 正在加载时为truemodels.countcountmodel 下任意 effect 正在加载时为trueeffects.count.addOnecount的addOneeffect 正在执行时为true。配置 store含 TypeScript 类型扩展// filename: count.ts import { createModel } from rematch/core import { RootModel } from ./models export const count createModelRootModel()({ state: 0, reducers: { increment(state, payload: number) { return state payload }, }, effects: (dispatch) ({ async incrementAsync(payload: number, state) { dispatch.count.increment(payload) }, }), })// filename: models.ts import { Models } from rematch/core import { count } from ./count export interface RootModel extends ModelsRootModel { count: typeof count } export const models: RootModel { count }默认布尔值配置// filename: store.ts import loadingPlugin, { ExtraModelsFromLoading } from rematch/loading import { init, RematchDispatch, RematchRootState } from rematch/core import { models, RootModel } from ./models type FullModel ExtraModelsFromLoadingRootModel export const store initRootModel, FullModel({ models, plugins: [loadingPlugin()], }) export type Store typeof store export type Dispatch RematchDispatchRootModel export type RootState RematchRootStateRootModel, FullModel改用计数init的第二个泛型与插件配置都要同步换成{ type: number }// filename: storeAsNumber.ts type FullModel ExtraModelsFromLoadingRootModel, { type: number } export const store initRootModel, FullModel({ models, plugins: [loadingPlugin({ type: number })], })改用 detailed 的 loading/error/success 信息// filename: storeAsFull.ts type FullModel ExtraModelsFromLoadingRootModel, { type: full } export const store initRootModel, FullModel({ models, plugins: [loadingPlugin({ type: full })], })ExtraModelsFromLoading这个工具类型把 loading 模型的状态形状并入RematchRootState的第二个泛型TExtraModels——回看 packages/core/src/types.ts 中RematchRootState的定义可以看到根状态类型是用户 models 的状态与插件额外 models 的状态的交集这正是插件向类型系统注入状态通道的机制。React 中使用默认布尔值import React from react import { useSelector } from react-redux import { RootState } from ./store export const App () { const isCountLoading useSelector( (rootState: RootState) rootState.loading.models.count ) if (isCountLoading) return divLOADING.../div return divData succesfully loaded/div }full模式解构出 loading/success/errorexport const App () { const { loading, success, error } useSelector( (rootState: RootState) rootState.loading.models.count ) if (loading) return divLOADING.../div if (error) return div{(error as Error).name}/div return divData succesfully loaded/div }number模式用次数判断export const App () { const countCalledTimes useSelector( (rootState: RootState) rootState.loading.models.count ) if (countCalledTimes 0) return divLOADING.../div return divData succesfully loaded/div }三种模式的行为分别由 loading-asBoolean.test.ts、loading-asNumber.test.ts、loading-asFull.test.ts 三个测试文件覆盖。rematch/updated记录 effect 上次触发时间rematch/updated在 effect 被触发时维护时间戳主要用于 effect 优化例如在一段时间窗口内阻止昂贵的 fetch 请求对 effect 做节流throttle。安装npm install rematch/updatedupdatedPlugin([config])config 的属性见 docs/plugins/updated.mdnamestringupdated 状态在 store 中的键名默认updatedblackliststring[]不按 model 维度追踪的 model 名列表dateCreator() any默认返回new Date()可以换成 moment 等自定义实现如() moment()。状态结构假设 modelcount有fetchOne、fetchTwo两个 effectsupdated 状态结构为{ count: { fetchOne: 2020-12-13T20:48:34.935Z, // Date when fetchOne effect was last fetched fetchTwo: 2020-12-13T20:40:34.935Z , // Date when fetchTwo effect was last fetched } }配置 store// store.js import updatedPlugin from rematch/updated; import { init } from rematch/core; import * as models from ./models; init({ models, plugins: [updatedPlugin()], });TypeScript 项目通过ExtraModelsFromUpdated扩展根状态类型且可以与 loading 插件的类型叠加// store.ts import updatedPlugin, { ExtraModelsFromUpdated } from rematch/updated; import { init, RematchDispatch, RematchRootState } from rematch/core; import { models, RootModel } from ./models; type FullModel ExtraModelsFromUpdatedRootModel; // 与 loading 插件同时使用时 // type FullModel ExtraModelsFromLoadingRootModel ExtraModelsFromUpdatedRootModel export const store initRootModel, FullModel({ models, plugins: [updatedPlugin()], }); export type Store typeof store; export type Dispatch RematchDispatchRootModel; export type RootState RematchRootStateRootModel, FullModel;在视图中使用定义带 effects 的 modelexport const count { ..., effects: { async fetchOne() {}, async fetchTwo() {}, } }读取时间戳const state store.getState(); // or just connect() on react-redux console.log(state.updated.count.fetchOne); console.log(state.updated.count.fetchTwo);典型用法是dispatch 前先读一次state.updated.count.fetchOne若距今时间短于阈值则跳过本次请求由此实现文档所述的时间窗口去重/节流效果。动手写自己的插件官方插件总览页docs/plugins/index.md明确指出如果找不到需要的插件你可以自己写一个参考 Plugins API 即可。官方插件源码本身就是最好的模板——immer 插件只用了一个onReducer钩子packages/immer/src/index.ts而 select 插件则展示了exposedonModelonStoreCreated的组合select属性先通过exposed占位onModel阶段为每个 model 生成 selectorsonStoreCreated阶段把它们组装到 store 上从源码结构可以这样推断。一个覆盖全部钩子的插件骨架引自 docs/api-reference/plugins.mdconst plugin { config: { redux: { combineReducers: customCombineReducers, }, models: { extra: extraModel, }, }, exposed: { select: {} }, createMiddleware: (rematchBag) (store) (next) (action) { // do something here return next(action); }, onReducer(reducer, modelName, bag) { // do something }, onRootReducer(reducer, bag) { // do something }, onModel(namedModel, rematchStore) { // do something }, onStoreCreated(rematchStore, bag) { // do something }, };编写时需要注意两条来自源码的约束执行时机分层config合并发生在createConfig阶段packages/core/src/config.tsexposed属性先于onModel/onStoreCreated执行类型定义中PluginExposed与钩子并列于 types.tsonStoreCreated是最后一个钩子——选错钩子会导致拿不到预期的 store/model 数据钩子必须是函数validatePlugin的校验validate.ts在开发环境下就会抛错测试用例 validatePlugins.test.ts 覆盖了各钩子的错误提示类型层面initTModels, TExtraModels的第二个泛型用于声明插件注入的额外 model 类型RematchDispatch、RematchRootState也会据此推导——这是 loading、updated 等插件能同时提供运行时状态与编译期类型的原因。小结Rematch 的插件体系把store 装配过程中的每一个可替换节点都开放了出来配置合并config、store 属性注入exposed、中间件createMiddleware、reducer 拦截onReducer/onRootReducer、model 后处理onModel和 store 终态加工onStoreCreated。五个官方插件分别示范了其中最有代表性的组合immer 用onReducer换掉 reducer 语义select 用钩子链生成store.selectpersist/loading/updated 则通过额外 model 向 store 注入持久化、加载与时间戳状态。理解这套机制后无论是选择官方插件还是参照 plugins.test.ts 的模式编写自有插件都有了清晰的落点。赞分享前端【免费下载链接】rematchThe Redux Framework项目地址https://gitcode.com/gh_mirrors/re/rematch点击查看免费下载相关推荐Vue CLI 核心插件体系解析插件机制与九大官方插件实战指南Vue CLI 核心插件体系解析插件机制与九大官方插件实战指南 Vue CLI本仓库为 vue cli 的镜像采用插件化架构你在 vue create前端开发工具构建工具终极Mina插件生态系统详解从官方工具到第三方扩展的完整指南终极Mina插件生态系统详解从官方工具到第三方扩展的完整指南 Mina作为一款闪电般快速的部署和服务器自动化工具其强大的插件生态系统是实现高效开发与部署的关运维Hydra 插件体系完全指南插件发现机制与四大插件类型详解Hydra 插件体系完全指南插件发现机制与四大插件类型详解 Hydra 通过一套轻量而严谨的插件体系实现功能扩展让框架可以从本地运行 基础扫描扩展到开发工具后端CLI上一篇whereami API接口详解Python模块化调用的完整指南下一篇RePKG高效上手指南从入门到精通的Wallpaper Engine资源处理工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站