
ماذا سيكون حول؟
سنتحدث عن عدة طرق (خمسة ، لتكون محددة) ، والحيل ، والتضحيات الدموية لإله المؤسسة ، والتي يبدو أنها تساعدنا في كتابة كود أكثر إيجازًا وتعبيرًا في تطبيقات Redux (و NGRX!) الخاصة بنا. طرق تعاني من العرق والقهوة. من فضلك ركل وانتقد بشدة. سوف نتعلم أن نرمز معا بشكل أفضل.
بصراحة ، في البداية أردت فقط أن أخبر العالم عن مكتبتي الصغيرة الجديدة (35 سطرًا من الكود!) فئة تدفقات الحركة ، لكنني أتطلع إلى العدد المتزايد باستمرار من التعجبات التي سيصبح Habr قريباً Twitter ، والجزء الأكبر منها أتفق معهم ، قررت أن أحاول قراءة قراءة رحبة بعض الشيء. لذلك ، نلتقي 5 طرق لترقية تطبيق Redux الخاص بك!
Boilerplate يخرج
فكر في مثال نموذجي لكيفية إرسال طلب AJAX إلى Redux. دعنا نتخيل أننا نحتاج حقًا إلى قائمة الأختام من الخادم.
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
إذا كنت لا تفهم تمامًا سبب الحاجة إلى مصانع محددات هنا ، فيمكنك القراءة عنها هنا.
أنا لا أعتبر الآثار الجانبية عن علم. هذا موضوع لمقال منفصل مليء بالغضب في سن المراهقة وانتقاد النظام البيئي الحالي: D
هناك عدة نقاط ضعف في هذا الرمز:
- تعتبر مصانع الإجراءات فريدة في حد ذاتها ، لكننا لا نزال نستخدم أنواع الإجراءات.
- عند إضافة كيانات جديدة ، نستمر في تكرار نفس المنطق لضبط علامة
loading
. البيانات التي نخزنها في data
، ونموذجها يمكن أن تختلف اختلافًا كبيرًا من طلب إلى آخر ، لكن مؤشر التنزيل (علامة loading
) سيظل كما هو. - وقت تشغيل التبديل هو (ن) (حسنا ، تقريبا ). هذه في حد ذاتها ليست حجة قوية للغاية ، لأن Redux ، من حيث المبدأ ، لا يتعلق بالأداء. يزعجني أكثر أنه في كل
case
تحتاج إلى كتابة سطرين إضافيين من شفرة التقديم ، ولا يمكن تقسيم هذا switch
بسهولة وبجمال إلى عدة. - هل نحتاج حقًا إلى تخزين حالة الخطأ لكل كيان على حدة؟
- محددات بارد. محددات memoized بارد مضاعفة. يقدمون لنا تجريدًا من جانبنا ، بحيث لا يتعين علينا فيما بعد إعادة نصف الطلب عند تغيير النموذج الخاص به. نحن فقط تغيير المحدد نفسه. ما لا يرضي العين هو مجموعة من المصانع البدائية اللازمة فقط بسبب خصوصيات المذكرات في إعادة البحث .
الطريقة 1: التخلص من أنواع الإجراءات
حسنا ، ليس حقا. نحن فقط نجعل JS تصنعها لنا.
دعونا نفكر لثانية واحدة حول سبب حاجتنا عمومًا إلى أنواع الإجراءات. حسنًا ، من الواضح ، أن نبدأ فرع المنطق المطلوب في المخفض الخاص بنا وتغيير حالة التطبيق وفقًا لذلك. السؤال الحقيقي هو ، هل يجب أن يكون النوع عبارة عن سلسلة؟ ولكن ماذا لو استخدمنا الفصول وقمنا switch
حسب النوع؟
class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } }
يبدو أن كل شيء رائع ، ولكن هناك مشكلة واحدة: فقدنا تسلسل أعمالنا. لم تعد هذه كائنات بسيطة يمكننا تحويلها إلى سلسلة والعكس صحيح. الآن نعتمد على حقيقة أن كل إجراء له نموذجه الفريد الخاص به ، والذي ، في الواقع ، يسمح بتصميم مثل switch
على action.constructor
للعمل. كما تعلم ، أحب حقًا فكرة إجراء تسلسل لأفعالي في سلسلة وإرسالها مع تقرير خطأ ، وأنا لست مستعدًا لرفضه.
لذلك ، يجب أن يحتوي كل إجراء على حقل type
( هنا يمكنك معرفة الإجراءات الأخرى التي يجب أن يكون لكل إجراء يحترم الإجراء). لحسن الحظ ، كل فئة لها اسم يشبه السلسلة. دعنا نضيف type
getter type
كل فصل سيعود اسم هذه الفئة.
class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, }
إنه يعمل بشكل جيد ، لكنني أرغب في التمسك بادئة لكل نوع ، كما يقترح السيد إريك في البط المعياري - استرجاع (أوصي بالنظر إلى مفترق إعادة البط ، وهو أكثر برودة بالنسبة لي). لإضافة بادئة ، سيتعين علينا التوقف عن استخدام اسم الفصل مباشرةً ، وإضافة برنامج آخر. الآن ثابت.
class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, }
دعونا تمشيط هذا الشيء كله قليلا. قم بتقليل لصق النسخ إلى الحد الأدنى وإضافة شرط آخر: إذا كان الإجراء يمثل خطأ ، فيجب أن تكون payload
من النوع Error
.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
في هذه المرحلة ، يعمل هذا الرمز بشكل جيد مع NGRX ، لكن Redux غير قادر على مضغه. يقسم أن العمل يجب أن يكون أشياء بسيطة. لحسن الحظ ، تسمح لنا JS بإرجاع أي شيء تقريبًا من المصمم ، لكننا لسنا بحاجة حقًا إلى سلسلة نماذج أولية بعد إنشاء الإجراء.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
بناءً على الاعتبارات المذكورة أعلاه ، تمت كتابة مكتبة micro-class-class . هناك اختبارات وتغطية اختبار 100٪ ونفس فئة أكشن ستاندارد ActionStandard
الأدوية الجنيسة لتلبية احتياجات TypeScript. يعمل مع كل من TypeScript و JavaScript.
الطريقة 2: نحن لسنا خائفين من استخدام CombineReducers
الفكرة سهلة الخزي: استخدام أدوات الجمع لا تقتصر فقط على مخفضات المستوى الأعلى ، ولكن أيضًا لمزيد من تفصيل المنطق وإنشاء مخفض منفصل loading
.
const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, })
الطريقة الثالثة: تخلص من المفتاح
ومرة أخرى فكرة بسيطة للغاية: بدلاً من switch-case
استخدم كائنًا لتحديد الحقل المطلوب حسب المفتاح. الوصول إلى حقل الكائن بواسطة المفتاح هو O (1) ، ويبدو أنظف قليلاً في رأيي المتواضع.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
دعونا refactor reducerLoading
. الآن ، ومعرفة الخرائط (الكائنات) الخاصة reducerLoading
، يمكننا إرجاع هذه الخريطة من reducerLoading
، بدلاً من إرجاع المخفض بالكامل. يحتمل أن يفتح هذا نطاقًا غير محدود لتوسيع الوظيفة.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
تتحدث الوثائق الرسمية عن Redux أيضًا عن هذا النهج ، ومع ذلك ، ولسبب غير معروف ، ما زلت أرى الكثير من المشاريع التي تستخدم switch-case
. بناءً على الكود من الوثائق الرسمية ، قام السيد موشيه بتجميع مكتبة لنا من أجل createReducer
.
الطريقة الرابعة: استخدام معالج الأخطاء العمومي
لا يتعين علينا مطلقًا الاحتفاظ بالخطأ لكل كيان على حدة. في معظم الحالات ، نريد فقط إظهار الحوار. نفس الحوار مع النص الديناميكي لجميع الكيانات.
إنشاء معالج خطأ عمومي. في أبسط الحالات ، قد يبدو مثل هذا:
class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, })
ثم في تأثيرنا الجانبي سوف نرسل الإجراء ErrorInit
في catch
. قد يبدو مثل هذا عند استخدام redux-thunk :
const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } }
الآن يمكننا التخلص من حقل error
في متجرنا للقطط واستخدام CatsGetError
فقط لتبديل علامة loading
.
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) })
الطريقة 5: فكر قبل Memoizing
دعونا نلقي نظرة على كومة من المصانع للاختيار مرة أخرى.
رميت makeSelectorCatsError
لأنه لم يعد هناك حاجة ، كما وجدنا في الفصل السابق.
const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, )
لماذا نحتاج محددات memoized هنا؟ ما الذي نحاول تحديده بالضبط؟ الوصول إلى حقل الكائن حسب المفتاح ، وهو ما يحدث هنا ، هو O (1). يمكننا استخدام وظائف عادية غير مذكّرة. استخدم المذكرة فقط عندما تريد تغيير البيانات من المتجر قبل إعطائها للمكون.
const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
الحفظ المنطقي في حالة حساب النتيجة على الطاير. على سبيل المثال أدناه ، دعونا نتخيل أن كل قطة هي كائن يحتوي على حقل name
، ونريد الحصول على سلسلة تحتوي على أسماء جميع القطط.
const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), )
الخاتمة
دعونا نرى مرة أخرى من حيث بدأنا:
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
وماذا جاء إلى:
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
أرجو ألا تضيع الوقت دون جدوى ، وكانت المقالة مفيدة لك على الأقل. كما قلت في البداية ، يرجى الركل وانتقد بشدة. سوف نتعلم أن نرمز معا بشكل أفضل.