الثبات في JavaScript باستخدام Redux

نشرت: 2022-03-11

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

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

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

يسمح لنا استخدام الحالات غير القابلة للتغيير بكتابة رمز يمكنه معرفة ما إذا كانت الحالة قد تغيرت بسرعة ، دون الحاجة إلى إجراء مقارنة متكررة على البيانات ، والتي عادة ما تكون أسرع بكثير.

ستغطي هذه المقالة التطبيقات العملية لـ Redux عند إدارة الحالة من خلال منشئي الإجراءات والوظائف النقية والمخفضات المركبة والإجراءات غير النقية مع Redux-saga و Redux Thunk وأخيراً استخدام Redux مع React. ومع ذلك ، هناك الكثير من البدائل لـ Redux ، مثل مكتبات MobX و Relay و Flux.

لماذا يستعيد؟

يتمثل الجانب الرئيسي الذي يفصل Redux عن معظم حاويات الحالة الأخرى مثل MobX و Relay ومعظم التطبيقات الأخرى المستندة إلى Flux في أن Redux له حالة واحدة لا يمكن تعديلها إلا من خلال "الإجراءات" (كائنات JavaScript العادية) ، والتي يتم إرسالها إلى متجر Redux. تحتوي معظم مخازن البيانات الأخرى على الحالة الموجودة في مكونات React نفسها ، مما يسمح لك بالحصول على مخازن متعددة و / أو استخدام حالة قابلة للتغيير.

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

تدفق الإحياء.

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

 import Immutable from 'seamless-immutable' const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { return state.concat(action.payload) } return state // note that a reducer MUST return a value } // somewhere else... store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user

يتيح التعامل في الوظائف البحتة لـ Redux دعم العديد من حالات الاستخدام التي لا يسهل إجراؤها عمومًا مع الحالة الطفرية ، مثل:

  • السفر عبر الزمن (العودة بالزمن إلى حالة سابقة)
  • التسجيل (تتبع كل إجراء لمعرفة سبب حدوث طفرة في المتجر)
  • البيئات التعاونية (مثل محرّر مستندات Google ، حيث تكون الإجراءات كائنات جافا سكريبت عادية ويمكن إجراء تسلسل لها وإرسالها عبر السلك وإعادة تشغيلها على جهاز آخر)
  • الإبلاغ عن الأخطاء بسهولة (فقط أرسل قائمة الإجراءات المرسلة ، وأعد تشغيلها للحصول على نفس الحالة بالضبط)
  • العرض المُحسَّن (على الأقل في الأطر التي تعرض DOM الظاهري كدالة للحالة ، مثل React: نظرًا لعدم قابلية التغيير ، يمكنك بسهولة معرفة ما إذا كان هناك شيء ما قد تغير من خلال مقارنة المراجع ، بدلاً من المقارنة العودية للكائنات)
  • اختبر مخفضاتك بسهولة ، حيث يمكن بسهولة اختبار الوحدات الصافية

منشئو العمل

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

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

 export function usersFetched(users) { return { type: 'USERS_FETCHED', payload: users, } } export function usersFetchFailed(err) { return { type: 'USERS_FETCH_FAILED', payload: err, } } // reducer somewhere else... const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function usersFetchedReducer(state = initialState, action) { if (action.type === 'USERS_FETCHED') { return Immutable(action.payload) } return state // note that a reducer MUST return a value }

استخدام الإعادة مع المكتبات الثابتة

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

ضع في اعتبارك مثال الكود التالي لمشكلة ستواجهها بدون مكتبة لحمايتك:

 const initialState = [] function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { state.push(action.payload) // NOTE: mutating action!! return state } return state // note that a reducer MUST return a value }

في مثال الكود هذا ، سيتم كسر السفر عبر الزمن لأن الحالة السابقة ستكون الآن هي نفسها الحالة الحالية ، ومن المحتمل ألا يتم تحديث (أو إعادة تصيير) المكونات النقية لأن المرجع إلى الحالة لم يتغير على الرغم من أن البيانات يحتوي على تغيرات ، والطفرات أصعب بكثير في التفكير فيها.

بدون مكتبة ثابتة ، نفقد جميع المزايا التي يوفرها Redux. لذلك يوصى بشدة باستخدام مكتبة مساعدة ثابتة ، مثل immutable.js أو السلس غير القابل للتغيير ، خاصة عند العمل في فريق كبير بأيدٍ متعددة تلامس الشفرة.

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

غير قابل للتغيير

Immutable.js هي مكتبة ، تم إنشاؤها بواسطة Facebook ، بأسلوب وظيفي أكثر على هياكل البيانات ، مثل الخرائط والقوائم والمجموعات والتسلسلات. تقوم مكتبتها المكونة من هياكل البيانات الثابتة غير القابلة للتغيير بأقل قدر ممكن من النسخ بين الحالات المختلفة.

الايجابيات:

  • المشاركة الهيكلية
  • أكثر كفاءة في التحديثات
  • أكثر كفاءة في الذاكرة
  • لديه مجموعة من الأساليب المساعدة لإدارة التحديثات

سلبيات:

  • لا يعمل بسلاسة مع مكتبات JS الحالية (مثل ، Lodash ، ramda)
  • يتطلب التحويل من وإلى (toJS / fromJS) ، خاصة أثناء الجفاف / الجفاف والتقديم

سلس وغير قابل للتغيير

السلس غير القابل للتغيير عبارة عن مكتبة للبيانات غير القابلة للتغيير والتي تتوافق مع الإصدارات السابقة على طول الطريق إلى ES5.

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

الايجابيات:

  • يعمل بسلاسة مع مكتبات JS الحالية (مثل ، Lodash ، ramda)
  • لا حاجة إلى رمز إضافي لدعم التحويل
  • يمكن تعطيل الشيكات في عمليات الإنتاج ، مما يؤدي إلى زيادة الأداء

سلبيات:

  • لا توجد مشاركة هيكلية - يتم نسخ الكائنات / المصفوفات ضحلة ، مما يجعلها أبطأ لمجموعات البيانات الكبيرة
  • ليس كذاكرة فعالة

الإعادة والمخفضات المتعددة

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

 import { combineReducers } from 'redux' import currentUserReducer from './currentUserReducer' import postsListReducer from './postsListReducer' export default combineReducers({ currentUser: currentUserReducer, postsList: postsListReducer, })

باستخدام الكود أعلاه ، يمكن أن يكون لديك مكون يعتمد على currentUser آخر يعتمد على postsList . يؤدي هذا أيضًا إلى تحسين الأداء حيث أن أي مكون منفرد سيشترك فقط في أي فرع (فروع) من الشجرة يتعلق به.

الإجراءات غير النقية في إعادة

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

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

مكتبتان شائعتان للبرامج الوسيطة هما Redux Thunk و Redux-saga. تمت كتابة Redux Thunk بأسلوب حتمي ، بينما تمت كتابة Redux-saga بأسلوب وظيفي. دعنا نقارن الاثنين.

إحياء ثانك

يدعم Redux Thunk الإجراءات غير النقية داخل Redux باستخدام thunks ، وهي وظائف تعيد وظائف أخرى قادرة على السلسلة. لاستخدام Redux-Thunk ، يجب عليك أولاً تحميل البرامج الوسيطة Redux Thunk في المتجر:

 import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' const store = createStore( myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R )

يمكننا الآن تنفيذ إجراءات غير نقية (مثل إجراء استدعاء API) عن طريق إرسال thunk إلى متجر Redux:

 store.dispatch( dispatch => { return api.fetchUsers() .then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action) .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError } )

من المهم ملاحظة أن استخدام thunks يمكن أن يجعل من الصعب اختبار التعليمات البرمجية الخاصة بك ويجعل من الصعب التفكير من خلال تدفق التعليمات البرمجية.

إعادة الملحمة

تدعم Redux-saga الإجراءات غير النقية من خلال ميزة ES6 (ES2015) تسمى المولدات ومكتبة من المساعدين الوظيفيين / الصافين. إن الشيء العظيم في المولدات هو أنه يمكن استئنافها وإيقافها مؤقتًا ، كما أن عقد API الخاص بهم يجعل من السهل للغاية اختبارها.

دعونا نرى كيف يمكننا تحسين قابلية القراءة والاختبار لطريقة thunk السابقة باستخدام الملاحم!

أولاً ، لنقم بتركيب البرامج الوسيطة Redux-saga على متجرنا:

 import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './rootReducer' import rootSaga from './rootSaga' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount the middleware to the store const store = createStore( rootReducer, applyMiddleware(sagaMiddleware), ) // run our saga! sagaMiddleware.run(rootSaga)

لاحظ أنه يجب استدعاء وظيفة run(..) مع الملحمة لبدء التنفيذ.

لنقم الآن بإنشاء قصتنا:

 import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use export function *fetchUsers(action) { try { const users = yield call(api.fetchUsers) yield put(usersFetched(users)) } catch (err) { yield put(usersFetchFailed(err)) } } export default function *rootSaga() { yield takeEvery('USERS_FETCH', fetchUsers) }

لقد حددنا وظيفتين للمولد ، إحداهما تجلب قائمة المستخدمين و rootSaga . لاحظ أننا لم api.fetchUsers مباشرة ولكن بدلًا من ذلك قدمناها في كائن استدعاء. هذا لأن Redux-saga يعترض كائن الاستدعاء وينفذ الوظيفة المضمنة فيه لإنشاء بيئة نقية (فيما يتعلق بالمولدات الخاصة بك).

ينتج rootSaga استدعاء واحد لوظيفة تسمى takeEvery, والتي تتخذ كل إجراء يتم إرساله بنوع من USERS_FETCH وتستدعي fetchUsers saga بالإجراء الذي تم اتخاذه. كما نرى ، فإن هذا يخلق تأثيرًا جانبيًا يمكن التنبؤ به للغاية لـ Redux ، مما يجعل من السهل اختباره!

اختبار Sagas

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

نظرًا لأن القصص الملحمية تنتج كائنات جافا سكريبت عادية ويتم تشغيلها داخل منشئ ، يمكننا بسهولة اختبار أنها تؤدي السلوك الصحيح دون أي سخرية على الإطلاق! put في اعتبارك take call والاستلام والوضع وما إلى ذلك هي مجرد كائنات JavaScript عادية يتم اعتراضها بواسطة البرامج الوسيطة Redux-saga.

 import { take, call } from 'redux-saga/effects' import { expect } from 'chai' import { rootSaga, fetchUsers } from '../rootSaga' describe('saga unit test', () => { it('should take every USERS_FETCH action', () => { const gen = rootSaga() // create our generator iterable expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely }) it('should fetch the users if successful', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const users = [ user1, user2 ] // some mock response expect(gen.next(users).value).to.be.eql(put(usersFetched(users)) }) it('should fail if API fails', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const err = { message: 'authentication failed' } // some mock error expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err)) }) })

العمل مع React

بينما لا يرتبط Redux بأي مكتبة مصاحبة محددة ، إلا أنه يعمل بشكل جيد مع React.js نظرًا لأن مكونات React هي وظائف خالصة تأخذ حالة كمدخلات وتنتج DOM افتراضيًا كإخراج.

React-Redux هي مكتبة مساعدة لـ React و Redux تقضي على معظم العمل الشاق الذي يربط بين الاثنين. لاستخدام React-Redux بشكل أكثر فاعلية ، دعنا ننتقل إلى فكرة مكونات العرض ومكونات الحاوية.

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

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

مكونات العرض ومكونات الحاوية في Redux.

دعنا نكتب مكونًا تقديميًا ونوصله بـ Redux عبر React-Redux:

 const HelloWorld = ({ count, onButtonClicked }) => ( <div> <span>Hello! You've clicked the button {count} times!</span> <button onClick={onButtonClicked}>Click me</button> </div> ) HelloWorld.propTypes = { count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }

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

مكونات الترتيب الأعلى

يوفر React-Redux وظيفة مساعدة تسمى connect( .. ) تقوم بإنشاء مكون ذي ترتيب أعلى من مكون React "dumb" الذي يكون على دراية بـ Redux.

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

إليك كيف تفعل ذلك:

 import { connect } from 'react-redux' const mapStateToProps = state => { // state is the state of our store // return the props that we want to use for our component return { count: state.count, } } const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function // return the props that we want to use for our component return { onButtonClicked: () => { dispatch({ type: 'BUTTON_CLICKED' }) }, } } // create our enhancer function const enhancer = connect(mapStateToProps, mapDispatchToProps) // wrap our "dumb" component with the enhancer const HelloWorldContainer = enhancer(HelloWorld) // and finally we export it export default HelloWorldContainer

لاحظ أننا حددنا وظيفتين ، mapStateToProps و mapDispatchToProps .

mapStateToProps هي وظيفة خالصة لـ (state: Object) تُعيد كائنًا محسوبًا من حالة Redux. سيتم دمج هذا الكائن مع الخاصيات التي تم تمريرها إلى المكون المغلف. يُعرف هذا أيضًا باسم المحدد ، لأنه يحدد أجزاء من حالة Redux ليتم دمجها في دعائم المكون.

mapDispatchToProps هي أيضًا وظيفة خالصة ، ولكنها إحدى (الإرسال: (الإجراء) => الفراغ) التي تُرجع كائنًا محسوبًا من وظيفة الإرسال Redux. سيتم أيضًا دمج هذا الكائن مع الخاصيات التي تم تمريرها إلى المكون المغلف.

الآن لاستخدام مكون الحاوية ، يجب علينا استخدام مكون Provider في React-Redux لإخبار مكون الحاوية بالمخزن الذي يجب استخدامه:

 import { Provider } from 'react-redux' import { render } from 'react-dom' import store from './store' // where ever your Redux store resides import HelloWorld from './HelloWorld' render( ( <Provider store={store}> <HelloWorld /> </Provider> ), document.getElementById('container') )

ينشر مكون Provider المتجر وصولاً إلى أي مكونات فرعية تشترك في متجر Redux ، مع الاحتفاظ بكل شيء في مكان واحد وتقليل نقاط الخطأ أو الطفرة!

بناء الثقة في التعليمات البرمجية مع الاستعادة

من خلال هذه المعرفة المكتشفة حديثًا عن Redux ، ومكتباتها الداعمة العديدة واتصالها الإطاري بـ React.js ، يمكنك بسهولة تحديد عدد الطفرات في تطبيقك من خلال سيطرة الدولة. يتيح لك التحكم القوي في الحالة ، بدوره ، التحرك بشكل أسرع وإنشاء قاعدة أكواد صلبة بثقة أكبر.