ARTICLE DETAIL

资讯详情

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

NgRx Meta-reducers 详解:深入 action→reducer 管线的预处理机制

NgRx Meta-reducers 详解:深入 action→reducer 管线的预处理机制 前端状态管理【免费下载链接】platformReactive State for Angular项目地址https://gitcode.com/gh_mirrors/pl/platform点击查看免费下载Meta-reducers 是 NgRx Store 中拦截并预处理 action 的核心扩展点它让你能在普通 reducer 真正处理 state 之前对每一个 action 进行观察、改写、校验或附加副作用。本文将以官方指南为骨架结合 ngrx/store 在本仓库中的源码实现、示例应用与测试用例系统讲解 meta-reducers 的类型签名、组合顺序、注册方式与典型实战用法帮助你在自己的 Angular 应用中编写出可复用的 Store 中间件。什么是 Meta-reducersngrx/store会把你在ActionReducerMap中声明的各个 reducer 组合成一个单一的顶层 reducer。Meta-reducer 则是这一组合过程中的一个“钩子”你可以把它理解为挂在action → reducer 流水线上的拦截器它允许开发者在普通reducer 被调用之前先行对 action 进行预处理。这一概念与 Redux 社区中的 middleware中间件非常相似——官方文档也明确指出“NgRx 中的 Meta-reducers 与 Redux 中使用的 middleware 类似”。两者的共同点是都位于分发dispatch动作与真正执行状态转换之间都承担日志、审计、变换、校验等横切关注点区别在于 Redux middleware 通过中间件栈显式接管 dispatch而 NgRx meta-reducer 直接包装 reducer 函数本身以函数式组合的方式参与 reducer 的构建。import { StoreModule, ActionReducer, MetaReducer } from ngrx/store; import { reducers } from ./reducers; // console.log all actions export function debug( reducer: ActionReducerany ): ActionReducerany { return function (state, action) { console.log(state, state); console.log(action, action); return reducer(state, action); }; } export const metaReducers: MetaReducerany[] [debug]; NgModule({ imports: [StoreModule.forRoot(reducers, { metaReducers })], }) export class AppModule {}理解 MetaReducer 的类型签名要写对 meta-reducer首先要理解它背后三个相互关联的类型它们都定义在 modules/store/src/models.ts 中ActionReducerT, V接收当前state与一个action返回新state的纯函数。这是普通 reducer 的形态也是 meta-reducer 包装的对象。ActionReducerMapT, V由 key 到ActionReducer的映射对象即StoreModule.forRoot/provideStore传入的 reducer 集合。MetaReducerT, V一个以 reducer 为入参、返回新 reducer 的函数export type MetaReducerT any, V extends Action Action ( reducer: ActionReducerT, V ) ActionReducerT, V;也就是说meta-reducer 本身并不直接处理 state而是“装饰”一个 reducer它接收一个 reducer返回一个同样签名的函数在返回的函数内部你可以决定在调用reducer(state, action)之前或之后做什么。正是因为这种签名多个 meta-reducer 才能像洋葱一样层层嵌套、自由组合。组合顺序为什么是“从右到左”StoreModule.forRoot(reducers, { metaReducers })中的metaReducers配置选项接受一个 meta-reducer 数组。根据官方指南这些 meta-reducer 以从右到左的顺序组合。这一语义在源码中有明确实现。在 modules/store/src/utils.ts 中createReducerFactory会先取出用户配置的 meta-reducers通过compose函数将它们与默认的 reducer factory 组合起来export function createReducerFactoryT, V extends Action Action( reducerFactory: ActionReducerFactoryT, V, metaReducers?: MetaReducerT, V[] ): ActionReducerFactoryT, V { if (Array.isArray(metaReducers) metaReducers.length 0) { (reducerFactory as any) compose.apply(null, [ ...metaReducers, reducerFactory, ]); } // ... }compose的实现使用reduceRight从数组末尾开始逐层包装modules/store/src/utils.tsexport function compose(...functions: any[]) { return function (arg: any) { if (functions.length 0) { return arg; } const last functions[functions.length - 1]; const rest functions.slice(0, -1); return rest.reduceRight((composed, fn) fn(composed), last(arg)); }; }因此假设你配置了metaReducers: [a, b]最终的执行链是a(b(reducer))action 首先经过最右侧的b再进入a最后才到达真正的业务 reducer。理解这一点对调试日志顺序、校验顺序至关重要——左侧的 meta-reducer 处于“最外层”会最先看到 action。同时在 modules/store/src/store_config.ts 中_concatMetaReducers会将 NgRx 内部内置的 meta-reducers来自META_REDUCERStokenmulti 注册与用户通过config.metaReducers提供的数组拼接在一起export function _concatMetaReducers( metaReducers: MetaReducer[], userProvidedMetaReducers: MetaReducer[] ): MetaReducer[] { return metaReducers.concat(userProvidedMetaReducers); }这意味着运行时真正生效的 meta-reducer 集合 内置检查类 meta-reducers 用户自定义 meta-reducers。实战用 Meta-reducer 记录所有 Action官方指南给出的是一个非常实用的场景——用 meta-reducer 为每一个 action 打日志。完整代码如下import { StoreModule, ActionReducer, MetaReducer } from ngrx/store; import { reducers } from ./reducers; // console.log all actions export function debug( reducer: ActionReducerany ): ActionReducerany { return function (state, action) { console.log(state, state); console.log(action, action); return reducer(state, action); }; } export const metaReducers: MetaReducerany[] [debug]; NgModule({ imports: [StoreModule.forRoot(reducers, { metaReducers })], }) export class AppModule {}这段代码的关键点在于debug是一个标准的MetaReducer它接收reducer返回一个新的 reducer 函数。新返回的函数先打印当前state与正在分发的action再调用reducer(state, action)计算下一个状态。metaReducers数组通过StoreModule.forRoot的第二个参数传入。在本仓库的示例应用中这种日志型 meta-reducer 有更完整的工程化实现。参见 projects/example-app/src/app/reducers/index.ts其中的logger使用console.groupCollapsed分组打印prev state、action和next state并且只会在开发模式下启用// console.log all actions export function logger(reducer: ActionReducerState): ActionReducerState { return (state, action) { const result reducer(state, action); console.groupCollapsed(action.type); console.log(prev state, state); console.log(action, action); console.log(next state, result); console.groupEnd(); return result; }; } export const metaReducers: MetaReducerState[] isDevMode() ? [logger] : [];随后在 projects/example-app/src/app/app.module.ts 中通过StoreModule.forRoot(rootReducers, { metaReducers, runtimeChecks: {...} })将其注册到根 Store。注册 Meta-reducers 的完整途径在根 Store 中注册StoreModule.forRoot/provideStoreNgModule 模式使用StoreModule.forRoot(reducers, { metaReducers })对应的配置接口是 modules/store/src/store_config.ts 中的RootStoreConfigexport interface StoreConfigT, V extends Action Action { initialState?: InitialStateT; reducerFactory?: ActionReducerFactoryT, V; metaReducers?: MetaReducer{ [P in keyof T]: T[P] }, V[]; } export interface RootStoreConfigT, V extends Action Action extends StoreConfigT, V { runtimeChecks?: PartialRuntimeChecks; }如果你使用 Angular 的独立组件 / 函数式 provider 模式standalone则用 modules/store/src/provide_store.ts 中导出的provideStoreimport { provideStore, MetaReducer } from ngrx/store; const metaReducers: MetaReducer[] [debug]; bootstrapApplication(AppComponent, { providers: [ provideStore(reducers, { metaReducers }), ], });在_provideStore的实现里config.metaReducers会被注入到USER_PROVIDED_META_REDUCERStoken再与内置的META_REDUCERS经_concatMetaReducers合并后生成最终的 reducer factory。通过META_REDUCERStoken 注入适合库作者如果希望 meta-reducer 由依赖注入提供例如需要在 meta-reducer 工厂中注入SomeService可以使用 NgRx Store API 导出的META_REDUCERSinjection token。该 token 定义在 modules/store/src/tokens.ts/** * Meta reducers defined either internally by ngrx/store or by library authors */ export const META_REDUCERS new InjectionTokenMetaReducer[]( ngrx/store Meta Reducers );注册方式参考 projects/www/src/app/pages/guide/store/recipes/injecting.mdimport { ActionReducer, MetaReducer, META_REDUCERS, } from ngrx/store; import { SomeService } from ./some.service; import * as fromRoot from ./reducers; export function metaReducerFactory(): MetaReducerfromRoot.State { return (reducer: ActionReducerany) (state, action) { console.log(state, state); console.log(action, action); return reducer(state, action); }; } NgModule({ providers: [ { provide: META_REDUCERS, deps: [SomeService], useFactory: metaReducerFactory, multi: true, }, ], }) export class AppModule {}注意multi: true必须保留META_REDUCERS是一个 multi-providerNgRx 自身的运行时检查runtime checks也是通过该 token 注册的见 modules/store/src/runtime_checks.ts 中provideRuntimeChecks对META_REDUCERS的三次multi: true注册。如果省略multi: true会覆盖其他库或 NgRx 内置的 meta-reducers导致行为异常。Feature 级 Meta-reducersmeta-reducers 同样可以作用于单个 feature slice。通过StoreModule.forFeature(key, reducers, config)或provideState(key, reducers, config)传入metaReducers即可。Feature 的组合逻辑在 modules/store/src/store_config.ts 的_createFeatureStore与 modules/store/src/utils.ts 的createFeatureReducerFactory中体现——feature 级 meta-reducers 只会包装该 feature 自己的 reducer不会影响全局 reducer。从源码结构看根 Store 与 feature Store 采用了两条不同的装配管线根 Store 走createReducerFactory(reducerFactory, metaReducers)feature Store 走createFeatureReducerFactory(metaReducers)这也解释了为何二者对 meta-reducers 的接收方式一致、但作用域隔离。内置 Meta-reducers 与运行时检查NgRx 自带三类内置 meta-reducer全部集中在 modules/store/src/meta-reducers 目录并通过 modules/store/src/meta-reducers/index.ts 导出export { immutabilityCheckMetaReducer } from ./immutability_reducer; export { serializationCheckMetaReducer } from ./serialization_reducer; export { inNgZoneAssertMetaReducer } from ./inNgZoneAssert_reducer;immutabilityCheckMetaReducerimmutability_reducer.ts在调用真正的 reducer 之前对 action、之后对返回的 state 执行递归Object.freeze一旦有代码试图原地修改对象在开发模式下立即报错严格模式。serializationCheckMetaReducerserialization_reducer.ts递归遍历 state 与 action检测函数、Date、undefined、null等不可序列化的值并抛出带完整路径如root.todos.0的错误信息。inNgZoneAssertMetaReducerinNgZoneAssert_reducer.ts通过NgZone.isInAngularZone()断言 action 是否在 NgZone 内分发。这些内置 meta-reducer 并不直接暴露为配置项而是由运行时检查runtime checks驱动。在 modules/store/src/runtime_checks.ts 中createActiveRuntimeChecks会根据开发模式给出默认值strictStateImmutability与strictActionImmutability默认开启其余四项序列化、NgZone、action 类型唯一性默认关闭且所有检查在生产构建非isDevMode()中都会被自动禁用。export function createActiveRuntimeChecks( runtimeChecks?: PartialRuntimeChecks ): RuntimeChecks { if (isDevMode()) { return { strictStateSerializability: false, strictActionSerializability: false, strictStateImmutability: true, strictActionImmutability: true, strictActionWithinNgZone: false, strictActionTypeUniqueness: false, ...runtimeChecks, }; } // production: all false }如果你不想用config.metaReducers也可以像示例应用一样通过StoreModule.forRoot的runtimeChecks配置项开启或关闭这些内置 meta-reducer详见 modules/store/src/models.ts 中的RuntimeChecks接口以及指南文档 projects/www/src/app/pages/guide/store/configuration/runtime-checks.md。例如在 projects/example-app/src/app/app.module.ts 中示例应用在注册根 Store 时同时启用了状态/action 序列化检查与 action 类型唯一性检查StoreModule.forRoot(rootReducers, { metaReducers, runtimeChecks: { strictStateSerializability: true, strictActionSerializability: true, strictActionWithinNgZone: false, strictActionTypeUniqueness: true, }, }),需要特别说明的是strictActionTypeUniqueness并不是以 meta-reducer 形式实现的而是通过checkForActionTypeUniqueness()在 Store 初始化阶段扫描全局注册表REGISTERED_ACTION_TYPES发现重复 action 类型时直接抛出异常modules/store/src/runtime_checks.ts。验证与测试Meta-reducers 如何被组装仓库中的测试可以印证上面的全部机制modules/store/spec/modules.spec.ts 中With initial state分组直接以StoreModule.forRoot(reducerMap, { initialState, metaReducers })的形式注册 meta-reducer并分别验证“无 meta-reducer”与“带 noop meta-reducer”两种情况下初始状态都能正确注入。modules/store/spec/runtime_checks_meta_reducers.spec.ts 通过vi.spyOn断言配置strictStateSerializability: true时会创建serializationCheckMetaReducer全部关闭时不会创建序列化与 NgZone 检查而即使不提供任何 runtime checks 配置immutabilityCheckMetaReducer也一定会被创建——这与createActiveRuntimeChecks中“开发模式默认开启不可变性检查”的默认值完全一致。编写 Meta-reducer 的最佳实践综合官方指南与仓库源码编写 meta-reducer 时有几点值得遵循保持纯函数meta-reducer 与 reducer 一样应是纯函数除了观察与转发不应在内部持有可变状态或产生副作用日志这类可观测副作用除外。永远调用链式末端的 reducer无论你在包装函数内做什么最终都必须返回reducer(state, action)的结果否则会中断整个 state 更新管线。不要修改 state / action 引用如果需要在预处理中改写 action应返回一个新的 action 对象并调用reducer(state, newAction)而不是原地修改。利用开发/生产开关像示例应用那样用isDevMode()将调试型 meta-reducer 限制在开发环境避免生产构建的额外开销。注意组合顺序记住metaReducers数组从右到左组合最左侧的 meta-reducer 最先接触 action依赖注入注册的META_REDUCERS会与内置检查一起进入同一个组合管线。小结Meta-reducers 是 NgRx 赋予开发者的“管线级”扩展能力它既可以是StoreModule.forRoot/provideStore配置项里的一个数组也可以是经由META_REDUCERStoken 以multi: true注入的库级扩展其背后由 modules/store/src/utils.ts 的compose与createReducerFactory完成从右到左的函数式组装。NgRx 自身的不可变性检查、序列化检查与 NgZone 断言正是这一机制最典型的应用范例。掌握了 meta-reducers你就掌握了在 action 到达业务 reducer 之前对其做任何横切处理的钥匙。赞分享前端状态管理【免费下载链接】platformReactive State for Angular项目地址https://gitcode.com/gh_mirrors/pl/platform点击查看免费下载相关推荐Magika Rust 库在新模型发布时如何用 sync.sh 同步 model 相关文件Magika Rust 库在新模型发布时如何用 sync.sh 同步 model 相关文件 当 Magika 发布新的检测模型时Rust 库crates.i前端状态管理NgRx Data Entity Reducer实体缓存主 Reducer、集合 Reducer 与 MetaReducer 定制机制详解NgRx Data Entity Reducer实体缓存主 Reducer、集合 Reducer 与 MetaReducer 定制机制详解 Entity Re前端状态管理OpenReplay Tracker NgRx 插件录制并回放 Angular 应用状态的 Meta-Reducer 实战指南OpenReplay Tracker NgRx 插件录制并回放 Angular 应用状态的 Meta Reducer 实战指南 导读 本文围绕 OpenRep可观测性开发工具前端后端上一篇Tachiyomi扩展插件完全指南打造个人专属漫画阅读体验下一篇Rye安装器设计Python解释器下载与安装流程终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表