المستوى 80 الكتابة أو المخفضات: المسار من حالة التبديل إلى فئات

الصورة


ماذا سيكون حول؟


دعنا ننظر إلى تحولات المخفضات في تطبيقات Redux / NGRX على مدار العامين الماضيين. بدءًا من صندوق switch-case البلوط ، تابع التحديد من الكائن حسب المفتاح وينتهي بالفصول الدراسية المزودة بالديكور وبلاك جاك و TypeScript. سنحاول مراجعة ليس فقط تاريخ هذا المسار ، ولكن أيضًا سنجد بعض العلاقة السببية.


إذا قمت أنت أيضًا بطرح أسئلة حول التخلص من الغلاية في Redux / NGRX ، فيمكن أن تكون هذه المقالة ممتعة بالنسبة لك.

إذا كنت تستخدم بالفعل النهج لاختيار مخفض من كائن حسب المفتاح وكنت سئمت منه ، يمكنك على الفور الانتقال إلى "المخفضات المستندة إلى فئة".

حالة التبديل الشوكولاته


عادةً ما تكون switch-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 تقريبا O (ن). هذا ليس مهم جدا في حد ذاته ، لأنه 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 يبقى كما هو لأي مخفض ، ويمكننا إعادة استخدامه. حتى أن هناك مكتبة نانو مسترجعة لتخفيض هذا .


 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 .


نحتاج أنه في هذا الديكور ، يمكننا التمسك بأي عدد من الإجراءات ، وأن يتم حفظ هذه ZhKshny كنوع من المعلومات الوصفية ، والتي يمكن الوصول إليها لاحقًا. للقيام بذلك ، يمكننا استخدام بيانات تعريف 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. تحويل الصف إلى ، في الواقع ، المخفض.


ارسم دائرة ، ارسم ثانية ، والآن سحر قليلًا واحصل على بومة!

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


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


لنبدأ بجمع معلومات التعريف.


 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) 

كيف تعيش على؟


شيء تركناه وراء الكواليس:


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

تحتوي مكتبة فئة مخفض صغيرة على كل هذه الوظائف مع أمثلة إضافية.


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


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

أرجو ألا تضيع الوقت دون جدوى ، وكانت المقالة مفيدة لك على الأقل. يرجى ركلة وانتقاد. سوف نتعلم أن نرمز معا بشكل أفضل.

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


All Articles