تنظيم مخفض - اتخاذ خطوة إلى الأمام


ما الذي سنقوم بتغطيته هنا؟


سنقوم بإلقاء نظرة عامة على تطور أجهزة التخفيض في تطبيقات 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 بعض نقاط التوتر والأنابيب المتسربة ، والتي قد ننسى تصحيحها في وقت ما في وقت ما. يمكن أن ننسى دائمًا أن نضع في break إذا لم تفعل return الفورية ، يمكن أن ننسى دائمًا إضافة default ، الذي يتعين علينا إضافته إلى كل المخفض.
  • switch-case تحتوي على بعض التعليمات البرمجية لـ boilerplate نفسها والتي لا تضيف أي سياق.
  • switch-case هو O (ن) ، نوع من . إنها ليست حجة قوية في حد ذاتها لأن Redux ليس مؤدًا للغاية على أي حال ، ولكنه يجعل مني الكمال الداخلي مجنونًا.

الخطوة المنطقية التالية التي تقترح وثائق Redux الرسمية اتخاذها هي اختيار المخفض من كائن حسب المفتاح.


اختيار المخفض من كائن حسب المفتاح


الفكرة بسيطة. كل تحويل حالة هو وظيفة من الحالة والإجراء ولديه نوع الإجراء المقابل. بالنظر إلى أن كل نوع من أنواع الإجراءات عبارة عن سلسلة ، يمكننا إنشاء كائن ، حيث يكون كل مفتاح نوعًا من الإجراءات وكل قيمة هي وظيفة تحول الحالة (مخفض). ثم يمكننا اختيار المخفض المطلوب من هذا الكائن حسب المفتاح ، وهو 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-مخفض ، والتي تفعل ذلك بالضبط. يجعل الرمز يشبه هذا:


 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, }), }) 

جميلة وجميلة ، هاه؟ رغم أن هذا لا يزال يحتوي على بعض المحاذير:


  • في حالة المخفضات المعقدة ، يتعين علينا ترك الكثير من التعليقات التي تصف ما يفعله هذا المخفض ولماذا.
  • خرائط المخفض ضخمة يصعب قراءتها.
  • كل المخفض لديه واحد فقط نوع العمل المقابل. ماذا لو كنت أرغب في تشغيل نفس المخفض لعدة إجراءات؟

أصبح المخفض القائم على الفصل سقفي من الضوء في مملكة الليل.


المخفضات القائمة على الطبقة


هذه المرة اسمحوا لي أن أبدأ بأسباب هذا النهج:


  • ستكون طرق "الفصل" هي أسماء المخفضين والطرق الخاصة بنا ، وهي معلومات تعريف مفيدة ، ويمكننا التخلي عن التعليقات في 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 العكسي للبيانات الوصفية ، والذي يجلب وظائف البيانات الوصفية لعكس الكائن. بعد ذلك ، يقوم هذا الديكور فقط بإرفاق وسيطاته (أنواع الإجراءات) بأسلوب البيانات الوصفية.


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

الخطوة 2. إنشاء وظيفة المخفض من فئة المخفض


كما نعلم ، فإن كل مخفض هو وظيفة نقية تقبل حالة وإجراء وتعيد حالة جديدة. حسنًا ، تعتبر الفئة وظيفة أيضًا ، ولكن لا يمكن استدعاء فئات ES6 بدون new ، وعلينا إنشاء مخفض فعلي خارج الفصل باستخدام عدة طرق على أي حال. لذلك نحن بحاجة إلى تحويله بطريقة أو بأخرى.


نحن بحاجة إلى وظيفة من شأنها أن تأخذ فصلنا ، والسير على الرغم من كل طريقة ، وجمع البيانات الوصفية مع أنواع الإجراءات ، وبناء خريطة المخفض وإنشاء المخفض النهائي من تلك الخريطة المخفض.


إليك كيف يمكننا فحص كل طريقة من الفصل.


 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) 

الخطوات التالية


إليك ما فاتنا:


  • ماذا لو كان نفس الإجراء يتوافق مع عدة طرق؟ المنطق الحالي لا يعالج هذا.
  • هل يمكن أن نضيف immer ؟
  • ماذا لو استخدمت الإجراءات القائمة على الفصل؟ كيف يمكنني أن أجتاز منشئ التصرفات ، وليس من نوع الإجراء؟

كل ذلك مع عينات رمز إضافية وأمثلة مغطاة فئة المخفض .


يجب أن أقول أن استخدام الطبقات لمخفضات ليست فكرة الأصلي. جاء amcdnl بإجراءات ngrx رهيبة منذ فترة طويلة ، لكن يبدو أنه يركز الآن على NGXS ، ناهيك عن رغبتي في كتابة وفصل أكثر صرامة من المنطق الزاوي المحدد. فيما يلي قائمة بالاختلافات الرئيسية بين فئة المخفض وإجراءات ngrx.


إذا كنت تحب فكرة استخدام الفصول لمخفضات السرعة ، فقد ترغب في أن تفعل الشيء نفسه لمنشئي الإجراءات. نلقي نظرة على الطبقة تدفق العمل .

نأمل أن تكون قد وجدت شيئًا مفيدًا لمشروعك. لا تتردد في توصيل ملاحظاتك لي! بالتأكيد أقدر أي نقد وأسئلة.

Source: https://habr.com/ru/post/ar439914/


All Articles