Redux Toolkit كأداة لتطوير Redux الفعال

صورة
حاليًا ، يتم تطوير حصة الأسد من تطبيقات الويب القائمة على إطار React باستخدام مكتبة Redux. هذه المكتبة هي أشهر تطبيق لهيكل FLUX ، وعلى الرغم من عدد من المزايا الواضحة ، إلا أن لها عيوبًا كبيرة جدًا ، مثل:


  • تعقيد و "فعل" الأنماط الموصى بها للكتابة وتنظيم الكود ، مما يستلزم عددًا كبيرًا من الألواح.
  • عدم وجود عناصر تحكم مدمجة للسلوك غير المتزامن والآثار الجانبية ، مما يؤدي إلى الحاجة إلى اختيار الأداة المناسبة من مجموعة متنوعة من الوظائف الإضافية التي كتبها مطورو الطرف الثالث.

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


في هذه المقالة ، سنتحدث عن الأدوات الرئيسية المضمنة في Redux Toolkit ، وكذلك ، باستخدام مثال لجزء من تطبيقنا الداخلي ، نوضح لك كيفية استخدامها في التعليمات البرمجية الموجودة.


باختصار عن المكتبة


ملخص أدوات Redux:


  • قبل الإصدار ، كانت المكتبة تسمى redux-starter-kit ؛
  • تم إطلاق الإصدار في نهاية أكتوبر 2019 ؛
  • يتم دعم المكتبة رسميًا بواسطة مطوري Redux.

وفقًا للمطورين ، تقوم مجموعة Redux Toolkit بالوظائف التالية:


  • يساعدك على البدء بسرعة باستخدام Redux.
  • يبسط العمل مع المهام النموذجية ورمز رد ؛
  • يسمح لك باستخدام أفضل ممارسات Redux افتراضيًا ؛
  • يقدم الحلول التي تقلل من عدم الثقة في المراجل.

يوفر Redux Toolkit مجموعة من كلاهما المصممان خصيصًا ويضيف عددًا من الأدوات المجربة التي يتم استخدامها بشكل شائع مع Redux. يسمح هذا النهج للمطور بتحديد كيفية استخدام الأدوات والتطبيقات المستخدمة في تطبيقها. في سياق هذه المقالة ، سنلاحظ أي القروض التي تستخدمها هذه المكتبة. لمزيد من المعلومات والتبعيات الخاصة بـ Redux Toolkit ، انظر وصف حزمة @ reduxjs / toolkit .


أهم الميزات التي توفرها مكتبة Redux Toolkit هي:


  • #configureStore - وظيفة مصممة لتبسيط عملية إنشاء وتكوين التخزين ؛
  • #createReducer - وظيفة تساعد على الوصف الدقيق والواضح وإنشاء المخفض ؛
  • #createAction - تُرجع وظيفة مُنشئ الإجراء الخاص بالسلسلة المحددة من نوع الإجراء ؛
  • #createSlice - يجمع بين وظائف createAction و createReducer ؛
  • createSelector هي وظيفة من مكتبة Reselect ، والتي أعيد تصديرها لسهولة الاستخدام.

تجدر الإشارة أيضًا إلى أن Redux Toolkit متكاملة تمامًا مع TypeScript. لمزيد من المعلومات ، راجع قسم الاستخدام باستخدام TypeScript في الوثائق الرسمية.


تطبيق


فكر في استخدام مكتبة Redux Toolkit كمثال على جزء من تطبيق React Redux المستخدم بالفعل.
المذكرة. علاوة على ذلك ، سيتم عرض الكود المصدر بدون استخدام مجموعة أدوات Redux واستخدامها ، مما سيتيح تقييم الجوانب الإيجابية والسلبية لاستخدام هذه المكتبة بشكل أفضل.


مهمة


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


إنشاء التخزين


بدا الإصدار الأولي من الكود المصدري الذي ينشئ المستودع كما يلي:


import { createStore, applyMiddleware, combineReducers, compose, } from 'redux'; import thunk from 'redux-thunk'; import * as reducers from './reducers'; const ext = window.__REDUX_DEVTOOLS_EXTENSION__; const devtoolMiddleware = ext && process.env.NODE_ENV === 'development' ? ext() : f => f; const store = createStore( combineReducers({ ...reducers, }), compose( applyMiddleware(thunk), devtoolMiddleware ) ); 

إذا نظرت بعناية إلى الكود أعلاه ، يمكنك أن ترى سلسلة طويلة من الإجراءات التي يجب إكمالها حتى يتم تكوين التخزين بالكامل. تحتوي مجموعة أدوات Redux Toolkit على أداة مصممة لتبسيط هذا الإجراء ، وهي وظيفة configStore.


وظيفة تكوين


تتيح لك هذه الأداة الجمع بين المخفضات تلقائيًا ، وإضافة برامج وسيطة Redux (تتضمن افتراضيًا ميزة redux-thunk) ، وكذلك استخدام امتداد Redux DevTools. تقبل الدالة configStore كائنًا ذو الخصائص التالية كمعلمات إدخال:


  • المخفض - مجموعة من المخفضات المخصصة ،
  • الوسيطة - معلمة اختيارية تحدد مجموعة من الوسيطة المصممة للاتصال بالمستودع ،
  • devTools - معلمة نوع منطقي تتيح لك تمكين امتداد Redux DevTools المثبت في المستعرض (القيمة الافتراضية صحيحة) ،
  • preloadedState - معلمة اختيارية تحدد الحالة الأولية للمستودع ،
  • معززات - معلمة اختيارية تحدد مجموعة من مكبرات الصوت.

للحصول على قائمة البرامج الوسيطة الأكثر شيوعًا ، يمكنك استخدام getDefaultMiddleware ذات الوظيفة الخاصة ، والتي تعد أيضًا جزءًا من Redux Toolkit. تقوم هذه الوظيفة بإرجاع صفيف مع تمكين الوسيطة بشكل افتراضي في مكتبة Redux Toolkit. تختلف قائمة هذه البرامج الوسيطة وفقًا للطريقة التي يتم بها تنفيذ التعليمات البرمجية الخاصة بك. في وضع الإنتاج ، تتكون المصفوفة من عنصر واحد فقط - thunk. في وضع التطوير ، في وقت كتابة هذا التقرير ، يتم تجديد القائمة بالوسيطة التالية:


  • serializableStateInvariant - أداة تم تطويرها خصيصًا للاستخدام في Redux Toolkit ومصممة للتحقق من شجرة الحالة لوجود قيم غير قابلة للتسلسل ، مثل الدوال والوعود والرمز والقيم الأخرى التي ليست بيانات JS بسيطة ؛
  • immutableStateInvariant - برنامج وسيط من الحزمة الثابتة المسترجعة غير القابلة للتغيير ، المصممة للكشف عن الطفرات في البيانات المخزنة في المستودع.

لتحديد قائمة تصاعدية من البرامج الوسيطة ، تقبل الدالة getDefaultMidlleware كائنًا يحدد قائمة البرامج الوسيطة المضمنة وإعدادات لكل منها. يمكن العثور على مزيد من المعلومات حول هذه المعلومات في القسم المقابل من الوثائق الرسمية.


سنقوم الآن بإعادة كتابة قسم الشفرة المسؤول عن إنشاء مستودع التخزين باستخدام الأدوات الموضحة أعلاه. نتيجة لذلك ، حصلنا على ما يلي:


 import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import * as reducers from './reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { ...reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', }); 

باستخدام مثال هذا القسم من التعليمات البرمجية ، يمكنك أن ترى بوضوح أن وظيفة configStore تعمل على حل المشكلات التالية:


  • الحاجة إلى الجمع بين المخفضات ، والاتصال تلقائيًا بـ combineReducers ،
  • الحاجة إلى الجمع بين الوسيطة ، واستدعاء تلقائيا applicationMiddleware.

كما يسمح لك بتمكين امتداد Redux DevTools بشكل أكثر ملاءمة باستخدام وظيفة composeWithDevTools من حزمة التمديد redux-devtools . يشير كل ما سبق إلى أن استخدام هذه الوظيفة يتيح لك جعل الشفرة أكثر إحكاما وفهمًا.


هذا يكمل إنشاء وتكوين التخزين. ننقلها إلى المزود ونستمر.


الإجراءات ، المبدعين العمل ومخفض


الآن دعونا نلقي نظرة على ميزات Redux Toolkit من حيث تطوير الإجراءات ومنشئي الإجراءات ومخفض الحركة. تم تنظيم الإصدار الأولي من التعليمات البرمجية دون استخدام Redux Toolkit كملفات action.js و reduers.js. تبدو محتويات ملف Actions.js كما يلي:


 import * as productReleasesService from '../../services/productReleases'; export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING'; export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED'; export const PRODUCT_RELEASES_FETCHING_ERROR = 'PRODUCT_RELEASES_FETCHING_ERROR'; … export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING'; export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED'; export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR = 'PRODUCT_RELEASE_CREATING_UPDATING_ERROR'; function productReleasesFetching() { return { type: PRODUCT_RELEASES_FETCHING }; } function productReleasesFetched(productReleases) { return { type: PRODUCT_RELEASES_FETCHED, productReleases }; } function productReleasesFetchingError(error) { return { type: PRODUCT_RELEASES_FETCHING_ERROR, error } } … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched(productReleases)) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError(error)) }); } } … export function updateProductRelease( id, productName, productVersion, releaseDate ) { return dispatch => { dispatch(productReleaseUpdating()); return productReleasesService.updateProductRelease( id, productName, productVersion, releaseDate ).then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } } 

محتويات ملف reduers.js قبل استخدام Redux Toolkit:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', updatingState: 'none', error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case productReleases.PRODUCT_RELEASES_FETCHING: return { ...state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { ...state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { ...state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { ...state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { ...state, updatingState: 'success', productReleases: state.productReleases.map(productRelease => { if (productRelease.id === action.productRelease.id) return action.productRelease; return productRelease; }) }; case productReleases.PRODUCT_RELEASE_UPDATING_ERROR: return { ...state, updatingState: 'failed', error: action.error }; default: return state; } } 

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


وظيفة CreateAction


في القسم المحدد من الكود ، يتم استخدام الطريقة القياسية لتعريف إجراء ما في Redux: أولاً ، يتم تعريف الثابت بشكل منفصل يحدد نوع الإجراء ، ثم - وظيفة منشئ الإجراء من هذا النوع. تجمع دالة createAction بين هذين التصريحين في واحد. عند الإدخال ، يأخذ نوع الإجراء ويعيد منشئ الإجراء لهذا النوع. يمكن استدعاء منشئ الإجراء إما بدون وسيطات ، أو مع بعض الوسيطات (الحمولة النافعة) ، والتي سيتم وضعها في حقل الحمولة النافعة للإجراء الذي تم إنشاؤه. بالإضافة إلى ذلك ، يتخطى منشئ الإجراء الدالة toString () ، بحيث يصبح نوع الإجراء هو تمثيل السلسلة الخاص به.


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


 export const productReleasesFetching = createAction('PRODUCT_RELEASES_FETCHING'); export const productReleasesFetched = createAction('PRODUCT_RELEASES_FETCHED'); export const productReleasesFetchingError = createAction('PRODUCT_RELEASES_FETCHING_ERROR'); … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } ... 

وظيفة CreateReducer


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


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


كن حذرًا: إرجاع كائن جديد من دالة يتجاوز التغييرات "القابلة للتغيير". لن يعمل الاستخدام المتزامن لطريقتي تحديث الحالة.


تقبل الدالة createReducer الوسائط التالية كمعلمات إدخال:


  • الحالة الأولية للتخزين
  • كائن يقوم بتكوين مراسلات بين أنواع التصرفات ومخفضات ، كل منها يقوم بمعالجة نوع معين.

باستخدام طريقة createReducer ، نحصل على الكود التالي:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const counterReducer = createReducer(initialState, { [productReleasesFetching]: (state, action) => { state.fetchingState = 'requesting' }, [productReleasesFetched.type]: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, [productReleasesFetchingError]: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … [productReleaseUpdating]: (state) => { state.updatingState = 'requesting' }, [productReleaseUpdated]: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, [productReleaseUpdatingError]: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }); 

كما نرى ، فإن استخدام وظائف createAction و createReducer يعمل بشكل أساسي على حل مشكلة كتابة تعليمات برمجية إضافية ، ولكن لا تزال مشكلة إنشاء ثوابت مسبقًا موجودة. لذلك ، فإننا نعتبر خيارًا أكثر قوة يجمع بين إنشاء منشئي الإجراءات ومخفض - وظيفة createSlice.


وظيفة CreateSlice


تقبل الدالة createSlice كائنًا يحتوي على الحقول التالية كمعلمات إدخال:


  • name - مساحة اسم الإجراءات التي تم إنشاؤها ( ${name}/${action.type} ) ؛
  • initialState - الحالة الأولية للمخفض.
  • مخفضات - كائن مع معالجات. يأخذ كل معالج وظيفة مع حالة الوسائط والإجراء ، ويحتوي الإجراء على بيانات في خاصية الحمولة واسم الحدث في خاصية الاسم. بالإضافة إلى ذلك ، من الممكن تغيير البيانات التي تم استلامها من الحدث بشكل مبدئي قبل إدخال المخفض (على سبيل المثال ، إضافة معرف إلى عناصر المجموعة). للقيام بذلك ، بدلاً من وظيفة ، تحتاج إلى تمرير كائن مع المخفض وإعداد الحقول ، حيث يكون المخفض هو وظيفة معالج الإجراء ، والإعداد هو وظيفة معالج الحمولة النافعة التي تُرجع الحمولة النافعة المحدثة ؛
  • extraReducers - كائن يحتوي على مخفضات شريحة أخرى. قد تكون هذه المعلمة مطلوبة إذا كان من الضروري تحديث كائن ينتمي إلى شريحة أخرى. يمكنك معرفة المزيد حول هذه الوظيفة من القسم المقابل في الوثائق الرسمية.

نتيجة الوظيفة هي كائن يسمى "شريحة" ، مع الحقول التالية:


  • اسم - اسم شريحة ،
  • المخفض - المخفض ،
  • الإجراءات - مجموعة من الإجراءات.

باستخدام هذه الوظيفة لحل مشكلتنا ، نحصل على الكود المصدري التالي:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const productReleases = createSlice({ name: 'productReleases', initialState, reducers: { productReleasesFetching: (state) => { state.fetchingState = 'requesting'; }, productReleasesFetched: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, productReleasesFetchingError: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … productReleaseUpdating: (state) => { state.updatingState = 'requesting' }, productReleaseUpdated: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, productReleaseUpdatingError: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }, }); 

الآن سنقوم باستخراج المبدعين الفعليين والمخفض من الشريحة التي تم إنشاؤها.


 const { actions, reducer } = productReleases; export const { productReleasesFetched, productReleasesFetching, productReleasesFetchingError, … productReleaseUpdated, productReleaseUpdating, productReleaseUpdatingError } = actions; export default reducer; 

لم يتغير شفرة المصدر لمُنشئي الإجراءات التي تحتوي على مكالمات واجهة برمجة التطبيقات ، باستثناء طريقة تمرير المعلمات عند إرسال الإجراءات:


 export const fetchProductReleases = () => (dispatch) => { dispatch(productReleasesFetching()); return productReleasesService .getProductReleases() .then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .catch((error) => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })); }); }; … export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => { dispatch(productReleaseUpdating()); return productReleasesService .updateProductRelease(id, productName, productVersion, releaseDate) .then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .catch((error) => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseUpdatingError({ error })); }); 

توضح التعليمة البرمجية أعلاه أن وظيفة createSlice تسمح لك بالتخلص من جزء كبير من النمطي عند العمل مع Redux ، مما يتيح لك ليس فقط جعل الشفرة أكثر إحكاما وإيجازًا وفهمًا ، ولكن أيضًا قضاء وقت أقل في كتابتها.


يؤدي


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


شكرا لاهتمامكم نأمل أن مقالتنا ستكون مفيدة. يمكن الحصول على مزيد من المعلومات حول مكتبة Redux Toolkit من الوثائق الرسمية.

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


All Articles