减速器组织-进一步


我们在这里要覆盖什么?


我们将概述过去两年中我的Redux / NGRX应用程序中减速器的演变。 从香草switch-case ,到通过键从对象选择减速器,最后解决基于类的减速器。 我们不仅要谈论如何,而且要谈论为什么。


如果您对在Redux / NGRX中处理过多样板感兴趣,则可能需要查看本文

如果您已经熟悉从地图技术中选择减速器,请考虑直接跳到基于类的减速器

香草开关盒


因此,让我们看一下在服务器上异步创建实体的日常任务。 这次我建议我们描述如何创建新的绝地武士。


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, // List of our jedi data: [], error: undefined, } const reducerJedi = (state = reducerJediInitialState, action) => { switch (action.type) { case actionTypeJediCreateInit: return { ...state, loading: true, } case actionTypeJediCreateSuccess: return { loading: false, data: [...state.data, action.payload], error: undefined, } case actionTypeJediCreateError: return { ...state, loading: false, error: action.payload, } default: return state } } 

老实说,我从未在生产中使用过这种减速器。 我的推理是三方面的:


  • switch-case引入了一些张紧,泄漏的管道,我们可能会忘记及时修补。 如果不立即return ,我们总是会忘记break ,我们总是会忘记添加default ,我们必须将它添加到每个reducer中。
  • switch-case本身有一些样板代码,不添加任何上下文。
  • switch-case为O(n), 种类为 。 它本身并不是一个可靠的论据,因为Redux无论如何都不是很出色,但是它使我内心的完美主义者发了疯。

Redux官方文档建议采取的合乎逻辑的下一步是从一个对象中逐个选择一个减速器。


通过键从对象选择减速器


这个想法很简单。 每个状态转换都是状态和动作的函数,并且具有对应的动作类型。 考虑到每个动作类型都是一个字符串,我们可以创建一个对象,其中每个键都是一个动作类型,每个值都是一个转换状态的函数(reducer)。 然后,当我们收到一个新动作时,可以通过键从该对象中选择一个必需的缩减器,即O(1)。


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJediMap = { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), } const reducerJedi = (state = reducerJediInitialState, action) => { // Pick a reducer by action type const reducer = reducerJediMap[action.type] if (!reducer) { // Return state unchanged if we did not find a suitable reducer return state } // Run suitable reducer if found one return reducer(state, action) } 

这里很酷的事情是, reducerJedi内部的逻辑对于任何减速器都保持不变,这意味着我们可以重复使用它。 甚至还有一个小库,叫做redux-create-reducer ,它正是这样做的。 它使代码看起来像这样:


 import { createReducer } from 'redux-create-reducer' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJedi = createReducer(reducerJediInitialState, { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), }) 

漂亮又漂亮,是吗? 尽管这还有些警告:


  • 对于复杂的reducer,我们必须留下很多评论,描述reducer的功能以及原因。
  • 庞大的减速器图很难阅读。
  • 每个减速器只有一种对应的动作类型。 如果我想为几个动作运行相同的reducer怎么办?

基于类的还原器成为我在黑夜王国中的亮点。


基于类的减速器


这次让我从这种方法的原因开始:


  • Class的方法将成为我们的约简方法,并且方法具有名称,这是有用的元信息,在90%的情况下,我们可以放弃注释。
  • 可以修饰类的方法,这是一种易于阅读的声明式方式,可匹配动作和归约器。
  • 我们仍然可以使用引擎盖下的动作图来提高O(1)的复杂度。

如果这听起来像是您的合理理由清单,那就让我们深入研究吧!


首先,我想定义我们想要得到的结果。


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { // Take a look at "Class field delcaratrions" proposal, which is now at Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } 

现在,当我们看到要达到的目标时,就可以逐步实现它。


步骤1. 操作装饰器。


我们在这里要做的是接受任何数量的动作类型,并将它们存储为元信息,以供类的方法稍后使用。 为此,我们可以利用反射元数据 polyfill,它将元数据功能带到Reflect对象。 之后,此装饰器将仅将其参数(操作类型)作为元数据附加到方法。


 const METADATA_KEY_ACTION = 'reducer-class-action-metadata' export const Action = (...actionTypes) => (target, propertyKey, descriptor) => { Reflect.defineMetadata(METADATA_KEY_ACTION, actionTypes, target, propertyKey) } 

步骤2.在reducer类之外创建一个reducer函数


众所周知,每个化简器都是一个纯函数,它接受一个状态和一个动作并返回一个新状态。 好的,类也是一个函数,但是ES6类不能在没有new情况下被调用,无论如何,我们必须使用几种方法在类中创建一个实际的reducer。 因此,我们需要以某种方式对其进行转换。


我们需要一个函数来使用我们的类,遍历每种方法,收集具有操作类型的元数据,构建一个reducer映射,并从该reducer映射中创建最终的reducer。


这是我们检查类的每个方法的方式。


 const getReducerClassMethodsWthActionTypes = (instance) => { // Get method names from class' prototype const proto = Object.getPrototypeOf(instance) const methodNames = Object.getOwnPropertyNames(proto).filter( (name) => name !== 'constructor', ) // We want to get back a collection with action types and corresponding reducers const res = [] methodNames.forEach((methodName) => { const actionTypes = Reflect.getMetadata( METADATA_KEY_ACTION, instance, methodName, ) // We want to bind each method to class' instance not to lose `this` context const method = instance[methodName].bind(instance) // We might have many action types associated with a reducer actionTypes.forEach((actionType) => res.push({ actionType, method, }), ) }) return res } 

现在,我们要将接收到的集合处理为化简图。


 const getReducerMap = (methodsWithActionTypes) => methodsWithActionTypes.reduce((reducerMap, { method, actionType }) => { reducerMap[actionType] = method return reducerMap }, {}) 

因此最终功能可能看起来像这样。


 import { createReducer } from 'redux-create-reducer' const createClassReducer = (ReducerClass) => { const reducerClass = new ReducerClass() const methodsWithActionTypes = getReducerClassMethodsWthActionTypes( reducerClass, ) const reducerMap = getReducerMap(methodsWithActionTypes) const initialState = reducerClass.initialState const reducer = createReducer(initialState, reducerMap) return reducer } 

我们可以像这样将其应用于我们的ReducerJedi类。


 const reducerJedi = createClassReducer(ReducerJedi) 

步骤3.将它们合并在一起。


 // We move that generic code to a dedicated module import { Action, createClassReducer } from 'utils/reducer-class' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { // Take a look at "Class field delcaratrions" proposal, which is now at Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } export const reducerJedi = createClassReducer(ReducerJedi) 

后续步骤


这是我们错过的地方:


  • 如果同一动作对应几种方法怎么办? 当前的逻辑不能解决这个问题。
  • 我们可以增加沉浸感吗?
  • 如果我使用基于类的操作怎么办? 如何传递动作创建者而不是动作类型?

所有带有其他代码示例和示例的代码都由reducer-class覆盖。


我必须说将类用于减速器不是一个原始想法。 @amcdnl早就提出了很棒的ngrx动作 ,但似乎他现在正专注于NGXS ,更不用说我想要更严格的类型输入和与Angular特定的逻辑的分离。 这是reducer-class和ngrx-actions之间的主要区别的列表。


如果您喜欢为减速器使用类的想法,则可能希望为动作创建者做同样的事情。 看一下flux-action-class

希望您发现了一些对您的项目有用的东西。 随时向我传达您的反馈! 我非常感谢任何批评和疑问。

Source: https://habr.com/ru/post/zh-CN439914/


All Articles