80级覆盖或简化:从切换用例到类的路径

图片


那会是什么?


让我们看看过去几年我的Redux / NGRX应用程序中的reducers的变形。 从Oak switch-case ,继续通过键从对象中进行选择,最后是带有装饰器,二十一点和TypeScript的类。 我们不仅会尝试回顾这条路径的历史,而且还会发现一些因果关系。


如果您以及我在Redux / NGRX中询问如何处理样板的问题,那么本文对您可能会很有趣。

如果您已经使用了通过键从对象中选择减速器的方法并受够了,则可以立即切换到“基于类的减速器”。

巧克力开关盒


通常switch-caseswitch-case香草的,但是在我看来,这严重地区别于所有其他类型的switch-case

因此,让我们看一下异步创建实体(例如Jedi)的典型问题。


 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 = (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 。 我想相信我甚至列出了这样做的原因:


  • switch-case太容易破坏:您可以忘记插入break ,也可以忽略default
  • switch-case太冗长了。
  • switch-case 几乎 switch-case O(n)。 这本身不是很重要,因为 Redux本身并不夸耀惊人的性能,但是这个事实激怒了我内在的美丽鉴赏家。

Redux官方文档提供了梳理所有这些的逻辑方法-通过键从对象中选择一个减速器。


通过键从对象选择减速器


这个想法很简单-每个状态更改都可以通过状态和动作的功能来描述,每个这样的功能都有一个与其对应的特定键(动作中的type字段)。 因为 type是一个字符串,没有什么可以阻止我们为所有这样的函数找出一个对象,其中键是type ,而值是纯状态转换函数(缩减器)。 在这种情况下,当新操作到达根减速器时,我们可以通过键(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) => { //    `type`  const reducer = reducerJediMap[action.type] if (!reducer) { //   ,        return state } //        return reducer(state, action) } 

最可口的是, reducerJedi内部的逻辑对于任何reducer都是相同的,我们可以重用它。 甚至还有一个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, }), }) 

似乎什么也没发生。 的确,一勺蜂蜜并非没有一桶焦油:


  • 对于复杂的异径管,我们必须留下评论,因为 这种方法没有提供开箱即用的方式来提供一些说明性的元信息。
  • 带有大量化简键和键的对象无法很好地读取。
  • 每个异径管只有一把钥匙。 但是,如果您想为多个动作游戏运行同一个减速器,该怎么办?

当我转向基于班级的减速器时,我差点幸福的泪水,下面我将解释原因。


基于类的减速器


包子:


  • 类方法是我们的​​简化器,方法有名称。 只是非常简单的元信息,可以说明该减速器的功能。
  • 可以修饰类方法,这是链接化简器及其相应动作(即动作,而不仅仅是一个动作!)的一种简单的声明方式。
  • 在后台,您可以使用所有相同的对象获得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 { //     "Class field delcaratrions",    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.装饰器@Action


我们需要在该装饰器中粘贴任意数量的动作游戏,并将这些服务另存为一些元信息,以便以后访问。 为此,我们可以使用修补Reflect的出色的polyfill 反射元数据


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

第2步。实际上,将类变成化简器。


画一个圆,画一秒钟,现在再加上一点魔法,然后得到一只猫头鹰!

众所周知,每个化简器都是一个纯函数,它接受当前状态和操作并返回新状态。 当然,一个类是一个函数,但并不完全是我们需要的一个函数,如果没有new则无法调用ES6类。 通常,我们需要以某种方式对其进行转换。


因此,我们需要一个函数,该函数将采用当前类,遍历其每个方法,收集具有操作类型的元信息,收集具有reducer的对象并从该对象创建最终的reducer。


让我们从收集元信息开始。


 const getReducerClassMethodsWthActionTypes = (instance) => { //       const proto = Object.getPrototypeOf(instance) const methodNames = Object.getOwnPropertyNames(proto).filter( (name) => name !== 'constructor', ) //             const res = [] methodNames.forEach((methodName) => { const actionTypes = Reflect.getMetadata( METADATA_KEY_ACTION, instance, methodName, ) //     `this`    const method = instance[methodName].bind(instance) //  ,         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步。我们看看结果如何。


 //       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 { //     "Class field delcaratrions",    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-actions库,但是似乎他现在已经对其评分并切换到NGXS 。 另外,我想要更严格的键入并以特定于Angular功能的形式重置镇流器。 这里列出了reducer-class和ngrx-actions之间的主要区别。


如果您喜欢减速器类的概念,那么您可能还希望使用类进行操作。 看一下flux-action-class

希望您不要白白浪费时间,并且这篇文章至少对您有用。 请踢并批评。 我们将学习一起更好地编码。

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


All Articles