Redux: توصيات لأفضل الممارسات المتعلقة بمنشئي الإجراءات ومخفضات السرعة والمحددات

تم إنشاؤها على ٢٢ ديسمبر ٢٠١٥  ·  106تعليقات  ·  مصدر: reduxjs/redux

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

حتى هنا هي أفكاري.

استخدم المحددات في كل مكان

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

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

لذا فإن توصيتي الأولى هي - استخدام المحددات المشتركة _ في كل مكان- حتى عند الوصول إلى البيانات بشكل متزامن (على سبيل المثال ، تفضل myValueSelector(state) على state.myValue ). هذا يقلل من احتمالية وجود متغيرات خاطئة تؤدي إلى قيم غير محددة دقيقة ، ويبسط التغييرات على هيكل متجرك ، إلخ.

افعل _المزيد _ من المبدعين في العمل و _ _ _ _ _ _ _ في مخفضات _

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

  1. يمكن لمنشئي الإجراءات أن يكونوا غير متزامنين من خلال استخدام برمجيات وسيطة مثل redux-thunk . نظرًا لأن تطبيقك سيتطلب غالبًا تحديثات غير متزامنة لمتجرك - فإن بعض "منطق الأعمال" سينتهي به الأمر في أفعالك.
  2. عمل المبدعين (أكثر دقة thunks يمكن استخدام عودتهم) محددات مشتركة لأن لديهم الوصول إلى دولة كاملة. لا يمكن للمخفّضات لأن لديهم فقط حق الوصول إلى عقدتهم.
  3. باستخدام redux-thunk ، يمكن لمنشئ إجراء واحد إرسال إجراءات متعددة - مما يجعل تحديثات الحالة المعقدة أبسط ويشجع على إعادة استخدام الكود بشكل أفضل.

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

  1. في علبة التروس. كل مخفض (إضافة ، تحرير ، إزالة) مسؤول عن تحديث القائمة _ وكذلك_ البيانات الوصفية.
  2. في وجهات النظر (الحاوية / المكون). كل عرض يستدعي إجراءً (إضافة أو تعديل أو إزالة) يكون مسؤولاً أيضًا عن استدعاء إجراء updateMetadata . هذا النهج فظيع لأسباب واضحة (نأمل).
  3. في صانعي العمل. يقوم كل منشئ إجراء (إضافة أو تحرير أو إزالة) بإرجاع thunk الذي يرسل إجراءً لتحديث القائمة ثم إجراء آخر لتحديث البيانات الوصفية.

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

اكتب اختبارات "البط" التي تركز على الإجراءات والمحددات

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

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

discussion

التعليق الأكثر فائدة

dtinth @ denis- redux-saga ، ربما لم أوضح أنني ضد فكرة جعل ActionCreators ينمو ويصبح أكثر تعقيدًا بمرور الوقت.

مشروع Redux-saga هو أيضًا محاولة لفعل ما تصفه dtinth ولكن هناك

ربما يمكنك إلقاء نظرة على هذه النقطة من المناقشة الأصلية التي أدت إلى مناقشة Redux saga: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

حالة الاستخدام لحلها

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

الطريقة "النجسة":

هذا ما يبدو أن bvaughn يفضله

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

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

طريقة "حساب كل شيء من الأحداث الأولية":

هذا ما يفضله @ denis-

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

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

طريقة الملحمة المبسطة.

تستخدم لعبة redux-saga المولدات وقد تبدو معقدة بعض الشيء إذا لم تكن معتادًا على ذلك ولكن مع تطبيق مبسط ، يمكنك كتابة شيء مثل:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

الملحمة هي ممثل الدولة الذي يتلقى الأحداث وقد ينتج عنه تأثيرات. هنا يتم تنفيذه كمخفض غير نقي لإعطائك فكرة عما هو عليه ولكنه في الواقع ليس في مشروع redux-saga.

تعقيد القواعد قليلاً:

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

هل يمكنك أن ترى كيف ستصبح الشفرة فوضوية في جميع عمليات التنفيذ الثلاثة بمرور الوقت حيث يصبح الإعداد أكثر تعقيدًا؟

طريقة إعادة الملحمة

مع redux-saga وقواعد الإعداد المذكورة أعلاه ، ستكتب شيئًا مثل

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

أعتقد أنه يحل حالة الاستخدام هذه بطريقة أبسط بكثير من الحلول المذكورة أعلاه. إذا كنت مخطئا ، من فضلك أعطني تطبيقك الأبسط :)

لقد تحدثت عن الشفرة غير النقية ، وفي هذه الحالة لا يوجد شوائب في تنفيذ Redux-saga لأن تأثيرات أخذ / وضع البيانات هي في الواقع بيانات. عندما يُطلق على take () أنه لا يتم تنفيذه ، فإنه يُرجع واصفًا للتأثير المراد تنفيذه ، وفي مرحلة ما يبدأ المترجم الفوري ، لذلك لا تحتاج إلى أي محاكاة وهمية لاختبار الملاحم. إذا كنت مطورًا وظيفيًا يقوم بـ Haskell ، ففكر في Free / IO monads.


في هذه الحالة يسمح بما يلي:

  • تجنب تعقيد actionCreator واجعله يعتمد على getState
  • اجعل ما هو ضمني أكثر وضوحا
  • تجنب اقتران المنطق المستعرض (مثل الإعداد أعلاه) بمجال عملك الأساسي (إنشاء المهام المطلوبة)

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

أمثلة:

  • يمكن أن يؤدي "TIMELINE_SCROLLED_NEAR-BOTTOM" إلى "NEXT_PAGE_LOADED"
  • قد يؤدي "REQUEST_FAILED" إلى "USER_DISCONNECTED" إذا كان رمز الخطأ هو 401.
  • يمكن أن يؤدي "HASHTAG_FILTER_ADDED" إلى "CONTENT_RELOADED"

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

ال 106 كومينتر

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

رائع. يا لها من سهو بالنسبة لي _لا_ أن أذكر ذلك. نعم! نحن نستخدم غير قابل للتغيير! كما قلت ، من الصعب تخيل _لا_ استخدامه لأي شيء جوهري.

bvaughn إحدى المناطق التي ناضلت معها هي مكان رسم الخط الفاصل بين العناصر الثابتة وغير القابلة للتغيير. يتيح لك تمرير كائنات غير قابلة للتغيير إلى المكونات استخدام أدوات تزيين / مزج نقية بسهولة شديدة ، ولكن بعد ذلك ينتهي بك الأمر برمز IM قابل للتغيير في مكوناتك (وهو ما لا يعجبني). لقد استسلمت للتو وفعلت ذلك حتى الآن ، لكنني أظن أنك تستخدم المحددات في طرق العرض () بدلاً من الوصول المباشر إلى طرق Immutable.js؟

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

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

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

استخدم المحددات في كل مكان

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

استخدم ImmutableJS

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

أنجز المزيد أثناء العمل

bvaughn يجب أن تنظر حقًا إلى هذا المشروع: https://github.com/yelouafi/redux-saga
عندما بدأت بالمناقشة حول sagas (مفهوم الواجهة الخلفية في البداية) لـ yelouafi كان حل هذا النوع من المشاكل. في حالتي ، حاولت أولاً استخدام الملاحم أثناء توصيل مستخدم على متن تطبيق موجود.

1) كيف تختبر صانعي العمل لديك؟ أحب نقل أكبر قدر ممكن من المنطق إلى وظائف نقية ومتزامنة لا تعتمد على الخدمات الخارجية لأنها أسهل في الاختبار.

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

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

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

3) إذا أرسل منشئو عملك عدة مرات لإحداث تأثير ، فهل يعني ذلك أن حالتك غير صالحة لفترة وجيزة؟ (أفكر هنا في إرسالات متعددة متزامنة ، وليست تلك التي يتم إرسالها بشكل غير متزامن في وقت لاحق)

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

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

الأسباب الرئيسية لاستخدام ثابت (في عيني) ليست الأداء أو السكر النحوي للتحديثات. السبب الأساسي هو أنه يمنعك (أو أي شخص آخر) من _ بطريق الخطأ_ تغيير حالتك الواردة داخل المخفض. هذا أمر محظور ولسوء الحظ من السهل القيام به باستخدام كائنات JS العادية.

bvaughn يجب أن تنظر حقًا إلى هذا المشروع: https://github.com/yelouafi/redux-saga
عندما بدأت بالمناقشة حول sagas (مفهوم الواجهة الخلفية في البداية) لـ yelouafi كان حل هذا النوع من المشاكل. في حالتي ، حاولت أولاً استخدام الملاحم أثناء توصيل مستخدم على متن تطبيق موجود.

لقد قمت بالفعل بفحص هذا المشروع من قبل :) على الرغم من أنني لم أستخدمه بعد. يبدو أنيقًا.

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

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

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

لكن كيف يبدو اختبارك؟ شيء من هذا القبيل؟

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

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

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

مع مخفض:

1) أحاول إجراءً ما في تطبيقي.
2) يوجد خطأ في علبة التروس ، مما يؤدي إلى الحالة الخاطئة.
3) أقوم بإصلاح الخلل في المخفض وحفظه
4) يحمّل السفر عبر الزمن المخفض الجديد ويضعني في الحالة التي كان ينبغي أن أكون فيها.

بينما مع منشئ العمل

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

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

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

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

ماذا تقصد بالحالة العابرة في Reduxbvaughn و sompylasar ؟ إما انتهاء الإرسال أو رميه. إذا ألقى فالحالة لا تتغير.

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

إذا تم تحديث الشجرة بأكملها ولكن ليس بطريقة مناسبة (مثل حالة لا تستطيع React عرضها) ، فهذا يعني أنك لم تقم بعملك بشكل صحيح :)

في Redux ، الحالة الحالية هي اعتبار أن الإرسال الفردي هو حد المعاملة.

ومع ذلك ، فأنا أفهم اهتمام winstonewert الذي يبدو أنه يريد إرسال إجراءين بشكل متزامن في نفس المعاملة. لأن actionCreators يرسل أحيانًا إجراءات متعددة ويتوقع أن يتم تنفيذ جميع الإجراءات بشكل صحيح. إذا تم إرسال إجراءين ثم فشل الإجراء الثاني ، فسيتم تطبيق الإجراء الأول فقط ، مما يؤدي إلى حالة يمكننا اعتبارها "غير متسقة". ربما يريد winstonewert أنه في حالة فشل إرسال الإجراء الثاني ، فإننا نتراجع عن الإجراءين.

winstonewert لقد قمت بتطبيق شيء من هذا القبيل في https://github.com/stample/atom-react/blob/master/src/atom/atom.js
أردت أيضًا معالجة أخطاء العرض: إذا تعذر عرض الحالة بنجاح ، فقد أردت التراجع عن حالتي لتجنب حظر واجهة المستخدم. لسوء الحظ ، حتى الإصدار التالي ، قامت React بعمل سيء للغاية عندما تتسبب طرق العرض في حدوث أخطاء ، لذا لم تكن مفيدة كثيرًا ولكنها قد تكون في المستقبل.

أنا متأكد تمامًا من أنه يمكننا السماح لمتجر بقبول إرسالات مزامنة متعددة في معاملة باستخدام برمجية وسيطة.

ومع ذلك ، لست متأكدًا من أنه سيكون من الممكن التراجع عن الحالة في حالة حدوث خطأ في العرض ، حيث إن متجر redux قد "التزم" بالفعل عندما نحاول تقديم حالته. في إطار العمل الخاص بي ، يوجد رابط "beforeTransactionCommit" أستخدمه لتشغيل العرض وللتراجع في النهاية عن أي خطأ في العرض.

gaearon أتساءل عما إذا كنت تخطط لدعم هذا النوع من الميزات وما إذا كان ذلك ممكنًا مع واجهة برمجة التطبيقات الحالية.

يبدو لي أن الاشتراك المتراكم لا يسمح بإجراء معاملة حقيقية ولكنه يقلل فقط من عدد العروض. ما أراه هو أن المتجر "يلتزم" بعد كل إرسال حتى لو تم تشغيل مستمع الاشتراك مرة واحدة فقط في النهاية

لماذا نحتاج إلى دعم كامل للمعاملات؟ لا أعتقد أنني أفهم حالة الاستخدام.

gaearon لست متأكدًا بعد ولكن سأكون سعيدًا بمعرفة المزيد عن winstonewert usecase.

الفكرة هي أنه يمكنك إجراء dispatch([a1,a2]) وإذا فشل a2 ، فإننا نعود إلى الحالة السابقة قبل إرسال a1.

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

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

ومع ذلك ، عندما يفشل العرض ، أليس من المنطقي محاولة التراجع إلى الحالة الأخيرة التي لا يفشل فيها العرض بدلاً من محاولة إحراز تقدم في حالة غير قابلة للعرض؟

هل سيعمل مُحسِّن المخفض البسيط؟ على سبيل المثال

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

مع ذلك ، يمكنك إرسال مجموعة إلى المتجر. يقوم المُحسِّن بفك ضغط العمل الفردي وتغذيته لكل مخفض.

Ohhgaearon لم أكن أعرف ذلك. لم ألاحظ وجود مشروعين متميزين يحاولان حل حالة استخدام متشابهة تمامًا بطرق مختلفة:

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

gaearon أوتش ،


يمثل منشئو العمل كود نجس

ربما لم يكن لدي الكثير من الخبرة العملية مع Redux مثل معظم الأشخاص ، ولكن للوهلة الأولى ، لا بد لي من الاختلاف مع "القيام بالمزيد من المبدعين في العمل والقيام بقدر أقل في المخفضات" ، لقد أجريت مناقشة مماثلة داخل شركة.

في Hacker Way: إعادة التفكير في تطوير تطبيقات الويب في Facebook حيث يتم تقديم نمط Flux ، فإن المشكلة نفسها التي تؤدي إلى اختراع Flux هي رمز إلزامي.

في هذه الحالة ، يعتبر Action Creator الذي يقوم بـ I / O هو هذا الرمز الضروري.

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

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

بمعنى ما ، يمثل Action Creators رمزًا غير نقي ، بينما يمثل Reducers (و Selectors) رمزًا خالصًا . لقد اكتشف الناس في هاسكل أنه من الأفضل أن يكون لديك كود أقل نقية وأكثر نقاءً .

على سبيل المثال ، في مشروعي الجانبي (باستخدام Redux) ، أستخدم واجهة برمجة تطبيقات التعرف على الكلام في webkit. يصدر حدث onresult أثناء حديثك. هناك خياران - أين تتم معالجة هذه الأحداث؟

  • اجعل منشئ الإجراء يعالج الحدث أولاً ، ثم أرسله إلى المتجر.
  • فقط أرسل كائن الحدث إلى المتجر.

ذهبت مع الرقم الثاني: فقط أرسل كائن الحدث الخام إلى المتجر.

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

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

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

dtinth @ denis- redux-saga ، ربما لم أوضح أنني ضد فكرة جعل ActionCreators ينمو ويصبح أكثر تعقيدًا بمرور الوقت.

مشروع Redux-saga هو أيضًا محاولة لفعل ما تصفه dtinth ولكن هناك

ربما يمكنك إلقاء نظرة على هذه النقطة من المناقشة الأصلية التي أدت إلى مناقشة Redux saga: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

حالة الاستخدام لحلها

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

الطريقة "النجسة":

هذا ما يبدو أن bvaughn يفضله

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

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

طريقة "حساب كل شيء من الأحداث الأولية":

هذا ما يفضله @ denis-

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

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

طريقة الملحمة المبسطة.

تستخدم لعبة redux-saga المولدات وقد تبدو معقدة بعض الشيء إذا لم تكن معتادًا على ذلك ولكن مع تطبيق مبسط ، يمكنك كتابة شيء مثل:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

الملحمة هي ممثل الدولة الذي يتلقى الأحداث وقد ينتج عنه تأثيرات. هنا يتم تنفيذه كمخفض غير نقي لإعطائك فكرة عما هو عليه ولكنه في الواقع ليس في مشروع redux-saga.

تعقيد القواعد قليلاً:

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

هل يمكنك أن ترى كيف ستصبح الشفرة فوضوية في جميع عمليات التنفيذ الثلاثة بمرور الوقت حيث يصبح الإعداد أكثر تعقيدًا؟

طريقة إعادة الملحمة

مع redux-saga وقواعد الإعداد المذكورة أعلاه ، ستكتب شيئًا مثل

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

أعتقد أنه يحل حالة الاستخدام هذه بطريقة أبسط بكثير من الحلول المذكورة أعلاه. إذا كنت مخطئا ، من فضلك أعطني تطبيقك الأبسط :)

لقد تحدثت عن الشفرة غير النقية ، وفي هذه الحالة لا يوجد شوائب في تنفيذ Redux-saga لأن تأثيرات أخذ / وضع البيانات هي في الواقع بيانات. عندما يُطلق على take () أنه لا يتم تنفيذه ، فإنه يُرجع واصفًا للتأثير المراد تنفيذه ، وفي مرحلة ما يبدأ المترجم الفوري ، لذلك لا تحتاج إلى أي محاكاة وهمية لاختبار الملاحم. إذا كنت مطورًا وظيفيًا يقوم بـ Haskell ، ففكر في Free / IO monads.


في هذه الحالة يسمح بما يلي:

  • تجنب تعقيد actionCreator واجعله يعتمد على getState
  • اجعل ما هو ضمني أكثر وضوحا
  • تجنب اقتران المنطق المستعرض (مثل الإعداد أعلاه) بمجال عملك الأساسي (إنشاء المهام المطلوبة)

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

أمثلة:

  • يمكن أن يؤدي "TIMELINE_SCROLLED_NEAR-BOTTOM" إلى "NEXT_PAGE_LOADED"
  • قد يؤدي "REQUEST_FAILED" إلى "USER_DISCONNECTED" إذا كان رمز الخطأ هو 401.
  • يمكن أن يؤدي "HASHTAG_FILTER_ADDED" إلى "CONTENT_RELOADED"

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

slorber هذا مثال ممتاز! نشكرك على الوقت الذي أمضيته في شرح مزايا وعيوب كل نهج بوضوح. (حتى أنني أعتقد أن ذلك يجب أن يُدرج في المستندات).

اعتدت على استكشاف فكرة مماثلة (أطلق عليها اسم "مكونات العمال"). في الأساس ، هو مكون React لا يعرض أي شيء ( render: () => null ) ، ولكنه يستمع إلى الأحداث (على سبيل المثال من المتاجر) ويؤدي إلى آثار جانبية أخرى. ثم يتم وضع هذا المكون العامل داخل مكون جذر التطبيق. مجرد طريقة أخرى مجنونة للتعامل مع الآثار الجانبية المعقدة. : stuck_out_tongue:

الكثير من النقاش هنا بينما كنت نائمًا.

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

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


قال winstonewert ، "أعتقد أن طريقتي في التفكير في إعادة التأكيد تصر على أن المتجر دائمًا في حالة صالحة."
سأل slorber ، "ماذا تقصد بالحالة العابرة في Reduxbvaughn و sompylasar ؟ إما أن ينتهي الإرسال ، أو يتم رميها. إذا حدث ذلك ، فلن تتغير الحالة."

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

تخيل أن متجر التطبيقات الخاص بك يحتوي على بعض العناصر القابلة للبحث: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . قام المستخدم بإدخال نص عامل التصفية "e" ولذا تحتوي برمجية البحث الوسيطة على مصفوفة من المعرفين 1 و 3. تخيل الآن أن المستخدم 1 (Alex) تم حذفه - إما استجابة لإجراء مستخدم محليًا أو تحديثًا لبيانات بعيدة لم يعد يحتوي على سجل المستخدم هذا. في الوقت الذي يقوم فيه المخفض الخاص بك بتحديث مجموعة المستخدمين ، سيكون متجرك غير صالح مؤقتًا - لأن إعادة البحث ستشير إلى معرف لم يعد موجودًا في المجموعة. بمجرد تشغيل البرامج الوسيطة مرة أخرى ، ستصحح الحالة غير الصالحة. يمكن أن يحدث هذا النوع من الأشياء في أي وقت ترتبط عقدة من شجرتك بعقدة أخرى.


قال slorber : "لا أحب هذا الأسلوب لأنه يجعل منشئ الإجراء مقترنًا بشكل كبير بتخطيط عرض التطبيق. يفترض أن actionCreator يعرف بنية شجرة حالة واجهة المستخدم لاتخاذ قرارها."

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

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

تم تعديله من أجل توضيح النقطة الأخيرة.

راجع للشغل slorber هنا مثال على ما كان

لنفترض أن ولايتك بها العديد من العقد. تقوم إحدى هذه العقد بتخزين الموارد المشتركة. (أعني بكلمة "مشتركة" الموارد التي تم تخزينها مؤقتًا محليًا والوصول إليها من خلال صفحات متعددة داخل التطبيق الخاص بك.) هذه الموارد المشتركة لها أدوات إنشاء ومخفضات الإجراءات الخاصة بها ("البط"). عقدة أخرى تخزن المعلومات لصفحة تطبيق معينة. تحتوي صفحتك أيضًا على بطة خاصة بها.

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

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

ربما يريد winstonewert أنه في حالة فشل إرسال الإجراء الثاني ، فإننا نتراجع عن الإجراءين.

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

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

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

يبدو أن OP يفضل صانعي الإجراءات الذين يرسلون إجراءات أصغر تسمح بالحالات غير الصالحة العابرة التي لا أحبها.

ليس تماما. أنا أفضل الإجراءات الفردية عندما يتعلق الأمر بعقدة صانع الإجراء الخاص بك في شجرة الدولة. ولكن إذا كان "إجراء" مستخدم مفاهيمي واحد يؤثر على عقد متعددة من شجرة الحالة ، فستحتاج إلى إرسال إجراءات متعددة. يمكنك استدعاء كل إجراء بشكل منفصل (والذي أعتقد أنه _bad_) أو يمكن أن يكون لديك منشئ إجراء واحد يقوم بإرسال الإجراءات (طريقة redux-thunk ، والتي أعتقد أنها _أفضل _ لأنها تخفي هذه المعلومات من طبقة العرض الخاصة بك).

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

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

bvaughn أوه ، آسف

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

تشير أفضل ممارسات Flux إلى أن الإجراء يجب أن "يصف إجراء المستخدم ، وليس واضعًا". ألمحت مستندات Flux أيضًا إلى المكان الذي من المفترض أن تأتي منه هذه الإجراءات:

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

في الأساس ، الإجراءات هي حقائق / بيانات تصف "ما حدث" وليس ما يجب أن يحدث. يمكن للمتاجر فقط أن تتفاعل مع هذه الإجراءات بشكل متزامن ومتوقع وبدون أي آثار جانبية أخرى. يجب التعامل مع جميع الآثار الجانبية الأخرى في منشئي العمل (أو sagas: wink :).

مثال

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

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

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

بافتراض أن الإجراءات يمكن أن تصل إلى المتجر فقط نتيجة لإجراءات المستخدم أو استجابة الخادم ، يمكننا إنشاء 5 إجراءات.

  • SCOREBOARD_VIEW (نتيجة نقر المستخدم على زر عرض لوحة النتائج)
  • SCOREBOARD_FETCH_SUCCESS (نتيجة الاستجابة الناجحة من الخادم)
  • SCOREBOARD_FETCH_FAILURE (نتيجة استجابة الخادم لخطأ)
  • SCOREBOARD_CLOSE (نتيجة نقر المستخدم على زر الإغلاق)
  • MESSAGE_BOX_CLOSE (نتيجة نقر المستخدم على زر الإغلاق في مربع الرسالة)

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

الطريقة الوحيدة لتغيير شجرة الحالة هي إرسال فعل ، كائن يصف ما حدث. - README ل Redux

يتم لصقهم مع صانعي الحركة هؤلاء:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

ثم يمكن لكل جزء من المتجر (تحكمه مخفضات) أن يتفاعل مع هذه الإجراءات:

| جزء من المخزن / المخفض | السلوك |
| --- | --- |
| عرض | قم بتحديث الرؤية إلى الوضع الصحيح على SCOREBOARD_VIEW ، خطأ على SCOREBOARD_CLOSE و SCOREBOARD_FETCH_FAILURE |
| لوحة النتائج قم بتحديث الرؤية إلى صحيح على SCOREBOARD_VIEW ، خطأ على SCOREBOARD_FETCH_* |
| البيانات | تحديث البيانات داخل المتجر على SCOREBOARD_FETCH_SUCCESS |
| messageBox | قم بتحديث الرؤية إلى "true" وقم بتخزين الرسالة على SCOREBOARD_FETCH_FAILURE ، وقم بتحديث الرؤية إلى false على MESSAGE_BOX_CLOSE |

كما ترى ، يمكن أن يؤثر إجراء واحد على أجزاء كثيرة من المتجر. يتم إعطاء المتاجر وصفًا عالي المستوى لإجراء ما (ماذا حدث؟) بدلاً من أمر (ماذا تفعل؟). نتيجة ل:

  1. من الأسهل تحديد الأخطاء.

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

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

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

  1. يمكن اختبار منشئي ومخفضات العمل بشكل منفصل.

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

بنفس الطريقة ، يمكن ببساطة اختبار مخفضات السرعة ما إذا كانت تتفاعل بشكل صحيح مع الإجراءات من العالم الخارجي.

(سيظل اختبار التكامل مفيدًا جدًا.)

لا داعى للقلق. :) أقدر التوضيح الإضافي. في الواقع يبدو أننا نتفق هنا. بالنظر إلى المثال الخاص بك منشئ الإجراء ، viewScoreboard ، يبدو إلى حد كبير مثل مثال صانع الإجراء fetchAndProcessThing ، فوقه مباشرة.

يمكن اختبار منشئي ومخفضات العمل بشكل منفصل.

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

ولكن إذا كان "إجراء" مستخدم مفاهيمي واحد يؤثر على عقد متعددة من شجرة الحالة ، فستحتاج إلى إرسال إجراءات متعددة.

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

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

لقد كنا نتبع نمطًا من استخدام ثوابت النوع الفريدة لكل حزمة "بط"

لاحظ أننا لا نؤيدها في أي مكان في المستندات ؛-) دون أن نقول إنها سيئة ، ولكنها تعطي الأشخاص بعض الأفكار الخاطئة أحيانًا حول Redux.

وبالتالي ، فإن المخفض يستجيب فقط للإجراءات التي يرسلها صانعو الإجراءات الأشقاء

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

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

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

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

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

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

  • بأسلوب الأمر ، يتم إعطاء المتجر "ما يجب فعله" ، على سبيل المثال SHOW_MESSAGE_BOX أو SHOW_ERROR
  • في النمط التفاعلي ، يُمنح المتجر "حقيقة ما حدث" ، على سبيل المثال DATA_FETCHING_FAILED أو USER_ENTERED_INVALID_THING_ID . المتجر يتفاعل وفقًا لذلك.

في المثال السابق ، ليس لدي منشئ إجراء SHOW_MESSAGE_BOX أو showError('Invalid thing id="'+id+'"') ، لأن هذا ليس حقيقة. هذا أمر.

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

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

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

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

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

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


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

من الشائع أيضًا أن تأخذ مكونات متعددة البيانات من نفس المتجر. من الشائع أيضًا أن يعتمد مكون واحد على بيانات من أجزاء متعددة من المتجر. ألا يبدو هذا كثيرًا مثل التغليف السيئ؟ لكي تصبح نمطيًا حقًا ، ألا يجب أن يكون مكون React أيضًا داخل حزمة "duck"؟ ( تقوم هندسة Elm بذلك .)

تجعل React واجهة المستخدم الخاصة بك تفاعلية (ومن هنا جاءت تسميتها) من خلال التعامل مع البيانات من متجرك على أنها حقيقة. لذلك لا يتعين عليك إخبار وجهة نظرك "بكيفية تحديث واجهة المستخدم".

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

نشكرك على الوقت الذي قضيته في الكتابة ومشاركة أفكارك ،dtinth. نشكرك أيضًا على

من الشائع أيضًا أن تأخذ مكونات متعددة البيانات من نفس المتجر. من الشائع أيضًا أن يعتمد مكون واحد على بيانات من أجزاء متعددة من المتجر. ألا يبدو هذا كثيرًا مثل التغليف السيئ؟

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

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

بالمناسبة ، يعد مربع الرسائل مثالًا جيدًا على المكان الذي أفضل أن يكون لدي منشئ إجراء منفصل لعرضه. في الغالب لأنني أريد أن أمضي وقتًا عندما تم إنشاؤه حتى يمكن رفضه تلقائيًا (ومنشئ الإجراء هو المكان الذي تسميه غير النقي Date.now() ) ، لأنني أريد إعداد مؤقت لرفضه ، تريد رفض هذا المؤقت ، وما إلى ذلك ، لذلك سأعتبر أن صندوق الرسائل هو الحالة التي يكون فيها "تدفق العمل" مهمًا بما يكفي لتبرير أفعاله الشخصية. ومع ذلك ، ربما يمكن حل ما وصفته بطريقة أكثر أناقة من خلال https://github.com/yelouafi/redux-saga.

لقد كتبت هذا في دردشة Discord Reactiflux في البداية ، ولكن طُلب مني لصقه هنا.

كنت أفكر في نفس الأشياء كثيرًا مؤخرًا. أشعر أن تحديثات الحالة مقسمة إلى ثلاثة أجزاء.

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

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

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

لا أعرف ما هو الحل لهذه المشاكل ، ولست متأكدا إذا كان هناك واحد حتى الآن

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

هذا هو موضوع مثير للاهتمام! إن معرفة مكان وضع الكود في تطبيق redux يمثل مشكلة أعتقد أننا جميعًا نواجهها. تعجبني فكرة CQRS المتمثلة في مجرد تسجيل الأشياء التي حدثت.
لكن يمكنني رؤية عدم تطابق في الأفكار هنا لأن أفضل الممارسات في CQRS ، AFAIK هي بناء حالة غير معيارية من الأحداث / الأحداث التي تستهلكها الآراء بشكل مباشر. ولكن في عملية الإعادة ، فإن أفضل الممارسات هي بناء حالة طبيعية تمامًا واستخلاص بيانات وجهات نظرك من خلال المحددات.
إذا قمنا ببناء حالة غير طبيعية قابلة للاستهلاك مباشرة بواسطة طريقة العرض ، فأعتقد أن المشكلة التي يريدها المخفض للبيانات في مخفض آخر تختفي (لأن كل مخفض يمكنه فقط تخزين جميع البيانات التي يحتاجها دون الاهتمام بالتطبيع). ولكن بعد ذلك نواجه مشكلات أخرى عند تحديث البيانات. ربما هذا هو جوهر المناقشة؟

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

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

jayesbe قادمة من خلفية برمجة كائنية التوجه ، قد تجد أنها https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptively-bad/.

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

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

هذا هو المكان الذي يأتي فيه Redux: لتنظيم حالة التطبيق والجمع بين الإجراءات والتحول المقابل لحالة التطبيق.

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

هدفي هو إنشاء تطبيق واحد / بنية واحدة يمكن استخدامها (باستخدام قيم التكوين وحدها) لإنشاء 100 تطبيق. حالة الاستخدام التي يجب أن أتعامل معها هي التعامل مع حل برمجي ذي علامة بيضاء قيد الاستخدام من قبل العديد من العملاء .. كل منهم يتصل بالتطبيق الخاص به.

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

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

مثال..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

لقد أنشأت مثيلًا واحدًا لهذا الكائن ووضعته في السياق .. من خلال توسيع موفر Redux

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

ثم استخدمت هذا المزود على هذا النحو

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

أخيرًا في المكون الخاص بي ، استخدمت تطبيقي ..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

ومن ثم فإن منشئ الإجراء هو مجرد وظيفة رد إلى منطق عملي.

                      app.login(value, actions.login);

يبدو أن هذا الحل يعمل بشكل جيد في الوقت الحالي على الرغم من أنني بدأت فقط في المصادقة.

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

لقد توصلت إلى ما أشعر أنه حل وسط مثير للاهتمام .. وأعتقد أنه يتعامل معه بشكل جيد بما فيه الكفاية ولكنه قد لا يفي بمعايير حشود البرمجة الوظيفية.

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

لقد أنشأت مثيلًا واحدًا لهذا الكائن ووضعته في السياق .. من خلال توسيع موفر Redux

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

ومع ذلك ، أتجنب تمديد Provider لأن هذا هش. ليست هناك حاجة لذلك: تدمج React سياق المكونات ، لذا يمكنك فقط لفها بدلاً من ذلك.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

إنها في الواقع أسهل القراءة من وجهة نظري ، وهي أقل هشاشة.

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

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

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

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

ثم في mapStateToProps:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

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

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

لذا ، بشكل عام ، فإن مقاربتك منطقية تمامًا.

: +1: شكرا هذا يساعد. أوافق على التحسين في MyProvider. سوف أقوم بتحديث الكود الخاص بي للمتابعة. واحدة من أكبر المشاكل التي واجهتها عندما تعلمت Redux لأول مرة كانت الفكرة الدلالية لـ "Action Creators" .. لم تندهش حتى ساوتهم بالأحداث. بالنسبة لي كان نوعًا من الإدراك مثل .. هذه أحداث يتم إرسالها.

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

واحدة من أكبر المشاكل التي واجهتها عندما تعلمت Redux لأول مرة كانت الفكرة الدلالية لـ "Action Creators" .. لم تندهش حتى ساوتهم بالأحداث. بالنسبة لي كان نوعًا من الإدراك مثل .. هذه أحداث يتم إرسالها.

أود أن أقول أنه لا يوجد مفهوم دلالي لـ Action Creators في Redux على الإطلاق. هناك مفهوم دلالي للأفعال (التي تصف ما حدث وتعادل تقريبًا الأحداث ولكن ليس تمامًا - على سبيل المثال ، انظر المناقشة في # 351). منشئو الإجراء هم مجرد نمط لتنظيم الكود. من الملائم أن يكون لديك مصانع للإجراءات لأنك تريد التأكد من أن الإجراءات من نفس النوع لها بنية متسقة ، ولها نفس التأثير الجانبي قبل إرسالها ، وما إلى ذلك. ولكن من وجهة نظر Redux ، لا يوجد منشئو الإجراءات - إعادة يرى الأعمال فقط.

هو createSelector متاح في رد فعل أصلي؟

يتوفر في Reselect وهو عبارة عن JavaScript عادي بدون تبعيات ويمكن أن يعمل على الويب أو على الخادم الأصلي أو على الخادم وما إلى ذلك.

آه. حسنًا ، فهمت. الأمور أكثر وضوحًا. هتافات.

لا تقم بتداخل الكائنات في mapStateToProps و mapDispatchToProps

واجهت مؤخرًا مشكلة حيث فقدت الكائنات المتداخلة عندما قام Redux بدمج mapStateToProps و mapDispatchToProps (انظر رد فعل / رد فعل إعادة الإرسال # 324). على الرغم من أن gaearon يوفر حلاً يسمح لي باستخدام كائنات متداخلة ، إلا أنه يستمر في القول إن هذا نمط مضاد:

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

@ bvaughn قال

يجب أن تكون المخفضات غبية وبسيطة

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

لقد أربكتني فترة من الزمن ...

لماذا لا يزال يتعين علينا إنشاء ملفات ووظائف المخفض يدويًا؟

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

هذه هي فائدة الحفاظ على غالبية منطق تحديث الحالة في المخفض.

dtinth فقط للتوضيح ، بقولك "منطق تحديث الحالة" هل تقصد "منطق العمل"؟

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

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

sompylasar "منطق الأعمال" و "منطق تحديث الحالة" هما ، imo ، من نفس الشيء.

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

كمثال .. هذا مخفضي النموذجي

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

أفعالي المعتادة:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

يتم تعريف منطق عملي في كائن مخزن في السياق ، أي ..

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

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

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

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

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

هذه هي الطريقة التي نظمت بها "منطق الأعمال" الأساسي الخاص بي. نظرًا لأن حالتي يتم الحفاظ عليها حقًا على خادم خلفي .. فهذا يعمل جيدًا لحالة الاستخدام الخاصة بي.

إن المكان الذي تخزن فيه "منطق العمل" متروك لك حقًا وكيف يناسب حالة الاستخدام الخاصة بك.

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

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

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

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

في الحالات القصوى ، تحقق من ذلك:
"محركات RTS المتزامنة وحكاية Desyncs"ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

في 5 نيسان (أبريل) 2016 ، الساعة 5:54 مساءً ، كتب "John Babak" [email protected] :

هذه هي فائدة الحفاظ على غالبية منطق تحديث الحالة في
مخفض.

dtinth https://github.com/dtinth فقط للتوضيح بالقول
"منطق تحديث الحالة" هل تقصد "منطق الأعمال"؟

-
أنت تتلقى هذا لأنه تم ذكرك.
قم بالرد على هذا البريد الإلكتروني مباشرة أو قم بعرضه على GitHub
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

LumiaSaki إن النصيحة الخاصة بالحفاظ على

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

لما قيمته ، أنتج مخفضاتي باستخدام شيء مثل:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

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

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

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

بالتوسع في النقاط السابقة المتعلقة بمنطق العمل ، أعتقد أنه يمكنك فصل منطق عملك إلى جزأين:

  • الجزء غير القطعي. يستخدم هذا الجزء الخدمات الخارجية أو الشفرة غير المتزامنة أو وقت النظام أو مولد الأرقام العشوائي. في رأيي ، يتم التعامل مع هذا الجزء بشكل أفضل من خلال وظيفة الإدخال / الإخراج (أو منشئ الإجراء). أسميهم العملية التجارية.

ضع في اعتبارك برنامجًا يشبه IDE حيث يمكن للمستخدم النقر فوق زر التشغيل ، وسيقوم بتجميع التطبيق وتشغيله. (هنا أستخدم وظيفة غير متزامنة تأخذ متجرًا ، ولكن يمكنك استخدام redux-thunk بدلاً من ذلك.)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

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

شبيبة
استيرادك من "updeep"

تصدير const مخفض = createReducer ({
// [اسم الإجراء]: الإجراء => currentState => nextState
startCompiling: () => u ({compiling: true}) ،
errorCompiling: ({error}) => u ({compiling: false، compileError: error}) ،
startRunning: ({app}) => u ({
قيد التشغيل: () => التطبيق ،
التحويل البرمجي: خطأ
}) ،
stopRunning: () => u ({تشغيل: خطأ}) ،
discardCompileError: () => u ({compileError: null}) ،
// ...
})
""

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

dtinth Great ، لأن المثال السابق في https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 يبدو مختلفًا تمامًا عما كتبته في https://github.com/reactjs/redux/ Issuecomment / 1171 # issuecomment -205888533 - يقترح إنشاء قطعة من المبدعين في العمل وتمريرهم إلى مخفضات لهم فقط لنشر التحديثات (هذا النهج يبدو خاطئًا بالنسبة لي ، وأنا أتفق مع نفس المشار إليه في https://github.com/reactjs/redux/issues/1171#issuecomment-205865840).

تضمين التغريدة

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

كيف يمكنك أن تضع المنطق المعقد في القيمين وتبقى طاهرين؟

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

jayesbe : آه ... "معقدة" و "نقية" متعامدة. يمكن أن يكون لديك منطقًا أو تلاعبًا شرطيًا معقدًا حقًا داخل المخفض ، وطالما أنه مجرد وظيفة لمدخلاته بدون آثار جانبية ، فإنه لا يزال خالصًا.

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

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

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

jayesbe : لا أعتقد أن أي شخص قال على الإطلاق أن تحريك الإجراءات الأخرى يجب أن يتم في (currentState + action) -> newState .

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

أنا في حيرة من أمري حول ماهية المناقشة في هذه المرحلة ، لأكون صادقًا.

markerikson يبدو أن الموضوع

كما تلاحظ ، كل ما يهم حقًا هو ما يناسبك. لكن إليكم كيفية التعامل مع المشكلات التي سألت عنها.

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

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

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

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

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

هذه ليست الطريقة الموصى بها لاستخدام Redux. طريقة Redux الموصى بها هي أن يكون لديك إجراء CREATE_FOOBAR واحد يسبب كل هذه التغييرات المرغوبة.

winstonewert :

هذه ليست الطريقة الموصى بها لاستخدام Redux. طريقة Redux الموصى بها هي أن يكون لديك إجراء CREATE_FOOBAR واحد يسبب كل هذه التغييرات المرغوبة.

هل لديك مؤشر إلى مكان محدد؟ لأنه عندما كنت أقوم بإجراء بحث لصفحة الأسئلة الشائعة ، كان ما توصلت إليه هو "الأمر يعتمد على" ، مباشرة من دان. راجع http://redux.js.org/docs/FAQ.html#actions -multiple-Actions وهذه الإجابة بواسطة Dan on SO .

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

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

حول القاعدة الوحيدة الصارمة والسريعة التي أتفق معها هي "اللا حتمية في العمل المبدعين ، الحتمية الخالصة في المخفضات". هذا معطى.

بخلاف ذلك؟ ابحث عن شيء يناسبك. كن متسقا. اعرف سبب قيامك بذلك بطريقة معينة. تماشى مع الامر.

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

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

النمط الذي كنت أستخدمه هو:

  • ماذا تفعل : منشئ العمل / العمل
  • كيفية القيام بذلك : البرامج الوسيطة (على سبيل المثال ، البرامج الوسيطة التي تستمع إلى الإجراءات "غير المتزامنة" وتقوم باستدعاء كائن API الخاص بي.)
  • ماذا تفعل بالنتيجة: المخفض

من وقت سابق في هذا الموضوع ، كان بيان دان:

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

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

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

لكن ، بشكل عملي ، افعل ما ينجح.

: +1:

sompylasar أرى خطأ طرقي من خلال وجود هيكل الدولة في

يبدو لي أنه نفس الشيء.

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

في سؤال StackOverflow الذي ذكرته ، يقول:

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

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

أتخيل حالتين متشابهتين ولكن مختلفتين إلى حد ما هنا:

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

  • أن يكون كل من Reducer A و Reducer B يستجيبان لنفس الإجراء ويقومان بتحديث أجزاء الحالة الخاصة بهما بشكل مستقل
  • ضع كل البيانات ذات الصلة في الإجراء حتى يتمكن أي مخفض من الوصول إلى أجزاء أخرى من الحالة خارج الجزء الخاص به
  • أضف مخفضًا آخر عالي المستوى يمسك أجزاء من حالة المخفض A ويسلمه بالحالة الخاصة إلى Reducer B؟
  • أرسل إجراءً مخصصًا لـ Reducer A مع أجزاء الحالة التي يحتاجها ، وإجراء ثانٍ لـ Reducer B مع ما يحتاج إليه؟

2) لديك مجموعة من الخطوات التي تحدث في تسلسل معين ، كل خطوة تتطلب بعض تحديث الحالة أو إجراء وسيط. هل:

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

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

markerikson لذا فإن نصيحتك هي "يعتمد الأمر على الموقف الذي قابلته" ، وكيفية موازنة "منطق العمل" على الإجراءات أو المخفضات أمر متروك لك ، يجب علينا أيضًا الاستفادة من الوظيفة الخالصة قدر الإمكان؟

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

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

تضمين التغريدة

ماذا تفعل بالنتيجة: المخفض

في الواقع ، هذا هو سبب وجود الملاحم: للتعامل مع تأثيرات مثل "إذا حدث هذا ، فيجب أن يحدث ذلك أيضًا"


تضمين التغريدة

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

في الواقع ، لا يُطلب من المبدعين الفعليين أن يكونوا نجسًا أو حتى موجودين.
راجع http://stackoverflow.com/a/34623840/82609

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

نعم ولكن ليس من الواضح جدًا ملاحظة عيوب كل نهج بدون خبرة :) انظر أيضًا تعليقي هنا: https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

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

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

راجع أيضًا https://github.com/slorber/scalable-frontend-with-elm-or-redux

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

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

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

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

يعد هذا أمرًا رائعًا لتمرير البيانات إلى المكونات ، فماذا عن العكس ، عندما ترسل المكونات الإجراءات:

لنفترض على سبيل المثال في تطبيق Todo أنني أقوم بتحديث اسم عنصر Todo ، لذلك أرسل إجراءً يمرر الجزء من العنصر الذي أريد تحديثه ، على سبيل المثال:

dispatch(updateItem({name: <text variable>}));

، وتعريف الإجراء هو:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

والذي بدوره يتم التعامل معه بواسطة المخفض الذي يمكنه ببساطة القيام بما يلي:

Object.assign({}, item, action.updatedData)

لتحديث العنصر.

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

updateItem({description: <text variable>})

عندما يتم تغيير الوصف بدلاً من ذلك.

ولكن هنا يحتاج المكون إلى معرفة كيفية تعريف عنصر Todo في المتجر ، وإذا تغير هذا التعريف ، فأنا بحاجة إلى تذكر تغييره في جميع المكونات التي تعتمد عليه ، والتي من الواضح أنها فكرة سيئة ، أي اقتراحات لهذا السيناريو؟

تضمين التغريدة

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

لذلك قد يكون لدي:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

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

tkswinstonewert يعجبني aproach لتجنب النموذج المعياري ، أستطيع أن أرى أنه يوفر الكثير من الوقت في التطبيقات التي تحتوي على الكثير من الطرز ؛ لكنني ما زلت لا أرى كيف سيفصل هذا المكون عن بنية المتجر ، أعني أنه حتى إذا تم إنشاء الإجراء ، سيظل المكون بحاجة إلى تمرير الحقول المحدثة إليه ، مما يعني أن المكون لا يزال بحاجة إلى معرفة بنية المتجر أليس كذلك؟

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

sompylasar صحيح ، أفعل ذلك ، أنا أترجم بيانات api / الراحة إلى بنية متجري ، ولكن لا يزال الشخص الوحيد الذي يجب أن يعرف بنية المتجر هو المخفضات والمحددات الصحيحة؟ سبب مشاركتي في تحديد موقعهم ، مشكلتي تتعلق بالمكونات / الآراء التي أفضلها ألا تحتاج إلى معرفة هيكل المتجر في حال قررت تغييره لاحقًا ، ولكن كما هو موضح في المثال الخاص بي ، فهم بحاجة إلى معرفة الهيكل حتى يتمكنوا من ذلك أرسل البيانات الصحيحة ليتم تحديثها ، لم أجد طريقة أفضل للقيام بذلك :(.

dcoellarb قد تفكر في وجهات نظرك كمدخلات لبيانات من نوع معين (مثل سلسلة أو رقم ، ولكن كائن منظم مع الحقول). لا يعكس كائن البيانات هذا بالضرورة بنية المتجر. يمكنك وضع كائن البيانات هذا في إجراء. هذا لا يقترن بهيكل المتجر. يجب أن يقترن كل من المتجر والعرض بهيكل الحمولة النافعة.

sompylasar منطقي ،

ربما يجب أن أضيف أيضًا أنه يمكنك جعل الإجراءات أكثر نقاءً باستخدام redux-saga . ومع ذلك ، تكافح redux-saga للتعامل مع أحداث غير متزامنة ، لذا يمكنك اتخاذ هذه الفكرة خطوة إلى الأمام باستخدام RxJS (أو أي مكتبة FRP) بدلاً من redux-saga. إليك مثال باستخدام KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

مرحبا @ frankandrobot ،

تكافح redux-saga للتعامل مع الأحداث غير المتزامنة

ما الذي تعنيه بهذا؟ أليس redux-saga مصنوعًا للتعامل مع الأحداث غير المتزامنة والآثار الجانبية بطريقة أنيقة؟ ألق نظرة على https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

لا IceOnFire . في المرة الأخيرة التي قرأت فيها مستندات redux-saga ، كان التعامل مع مهام سير العمل المعقدة غير المتزامنة أمرًا صعبًا. انظر ، على سبيل المثال: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
قالت (ما زالت تقول؟) شيئًا ما

سنترك باقي التفاصيل للقارئ لأنها بدأت في التعقيد

قارن ذلك بطريقة FRP: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
يتم التعامل مع سير العمل بالكامل بشكل كامل. (في بعض الأسطر التي يمكنني إضافتها.) علاوة على ذلك ، لا تزال تحصل على معظم فائدة redux-saga (كل شيء هو وظيفة خالصة ، معظمها اختبارات وحدة سهلة).

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

مرحبا @ frankandrobot ،

شكرا على توضيحك. لا أرى الاقتباس الذي ذكرته ، ربما تطورت المكتبة (على سبيل المثال ، أرى الآن تأثير cancel لم أره من قبل).

إذا كان المثالان (الملحمة و FRP) يتصرفان بنفس الطريقة تمامًا ، فأنا لا أرى فرقًا كبيرًا: أحدهما عبارة عن سلسلة من الإرشادات داخل كتل try / catch ، بينما الآخر عبارة عن سلسلة من الأساليب في التدفقات. نظرًا لقلة خبرتي في التدفقات ، أجد مثال الملحمة أكثر قابلية للقراءة ، وأكثر قابلية للاختبار حيث يمكنك اختبار كل yield واحدًا تلو الآخر. لكنني متأكد تمامًا أن هذا يرجع إلى عقلي أكثر من التقنيات.

على أي حال ، أود أن أعرف رأي yelouafi في هذا الشأن.

bvaughn هل يمكنك الإشارة إلى أي مثال لائق لاختبار الإجراء ، المخفض ، المحدد في نفس الاختبار كما تصف هنا؟

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

مرحبا @ morgs32 😄

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

كنت في الأساس أشير إلى أنه بدلاً من كتابة اختبارات للإجراءات والمخفضات في عزلة ، يمكن أن يكون أكثر فاعلية في كتابتها معًا ، مثل:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

كانت هذه مجرد ملاحظتي الخاصة بعد استخدام Redux لبضعة أشهر في مشروع. إنها ليست توصية رسمية. YMMV. 👍

"أنجز المزيد في إبداعات العمل وأقل في المخفّضات"

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

أولا ، آسف لغتي الإنجليزية. لكن لدي بعض الآراء المختلفة.

خياري هو fat المخفض ، thin منشئو الإجراءات.

يقوم منشئو الإجراء الخاص بي فقط __النشر__ بالإجراءات (غير متزامن ، مزامنة ، غير متزامن تسلسلي ، متوازي غير متزامن ، متوازي غير متزامن في حلقة من أجل حلقة) بناءً على بعض __البرمجيات الوسيطة الوعدية__.

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

نعم ، هناك بعض الحالات المتقاطعة التي تعني A تعتمد على B, C .. و B, C هي بيانات غير متزامنة. يجب أن نستخدم B,C لملء أو تهيئة A. ولهذا السبب أستخدم crossSliceReducer .

حول __أنجز المزيد في إبداعات العمل وأقل في المخفّضات__.

  1. إذا كنت تستخدم redux-thunk أو ما إلى ذلك ، نعم. يمكنك الوصول إلى الحالة الكاملة داخل صانعي الإجراء بواسطة getState() . هذا اختيار. أو يمكنك إنشاء بعض __crossSliceReducer__ ، حتى تتمكن من الوصول إلى الحالة الكاملة أيضًا ، يمكنك استخدام بعض شرائح الحالة لحساب حالتك الأخرى.

حول __اختبار الوحدة__

reducer __وظيفة نقية__. لذلك من السهل إجراء الاختبار. في الوقت الحالي ، أقوم فقط باختبار مخفضاتي ، لأنها أهم من الجزء الآخر.

لاختبار action creators ؟ أعتقد أنهم إذا كانوا "سمينين" ، فربما ليس من السهل إجراء الاختبار. خاصة __منشئو الإجراءات المتزامنة__.

أنا أتفق معك mrdulin وهذا الآن هو الطريق الذي ذهبت إليه أيضًا.

تضمين التغريدة يبدو أن البرمجيات الوسيطة هي المكان المناسب لوضع منطقك النجس.
لكن بالنسبة إلى مخفض منطق الأعمال لا يبدو أنه المكان المناسب. سينتهي بك الأمر بإجراءات "اصطناعية" متعددة لا تمثل ما يطلبه المستخدم ولكن ما يتطلبه منطق عملك.

الكثير من الخيارات الأبسط هو مجرد استدعاء بعض الوظائف / توابع الفئة من البرمجيات الوسيطة:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

تضمين التغريدة

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

حالتي ضد استخدام المحددات في كل مكان ، حتى بالنسبة لأجزاء تافهة من الحالة التي لا تتطلب حفظًا أو أي نوع من تحويل البيانات:

  • ألا يجب أن تكتشف اختبارات الوحدة للمخفضات بالفعل خصائص الحالة التي تمت كتابتها بشكل خاطئ؟
  • لا تحدث التغييرات في هيكل المتجر كثيرًا في تجربتي ، وغالبًا في مرحلة زيادة المشروع. في وقت لاحق ، إذا قمت بتغيير state.stuff.item1 إلى state.stuff.item2 فستبحث في الشفرة وتغيرها في كل مكان - تمامًا مثل تغيير اسم أي شيء آخر حقًا. إنها مهمة شائعة ولا تتطلب تفكيرًا للأشخاص الذين يستخدمون IDEs على وجه الخصوص.
  • استخدام المحددات في كل مكان هو نوع من التجريد الفارغ. بسيطة من الدولة بشكل مباشر. بالتأكيد ، يمكنك الحصول على الاتساق من خلال وجود واجهة برمجة التطبيقات هذه للوصول إلى الحالة ، لكنك تتخلى عن البساطة.

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

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

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

IceOnFire ليس هناك ما يمكن الاستفادة منه إذا لم تكن العمليات الحسابية باهظة الثمن أو لم يكن تحويل البيانات مطلوبًا.

قد تكون طرق Getter ممارسة شائعة في Java ولكن أيضًا الوصول إلى POJOs مباشرة في JS.

تضمين التغريدة

لماذا توجد واجهة برمجة تطبيقات بين المتجر وكود إعادة الإرسال الآخر؟

المحددات عبارة عن واجهة برمجة تطبيقات عامة (قراءة) للمخفض ، والإجراءات عبارة عن واجهة برمجة تطبيقات عامة (كتابة) لأمر المخفض. هيكل المخفض هو تفاصيل التنفيذ.

تُستخدم المحددات والإجراءات في طبقة واجهة المستخدم ، وفي طبقة الملحمة (إذا كنت تستخدم redux-saga) ، وليس في المخفض نفسه.

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

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

timotgl نعم ، المحددات ليست إلزامية. لكنهم يقومون بممارسة جيدة للاستعداد للتغييرات المستقبلية في كود التطبيق ، مما يجعل من الممكن إعادة بنائها بشكل منفصل:

  • كيف يتم تنظيم جزء معين من الدولة وكتابته (القلق المخفض ، مكان واحد)
  • كيف يتم استخدام هذا الجزء من الحالة (مخاوف واجهة المستخدم والآثار الجانبية ، العديد من الأماكن التي يتم فيها الاستعلام عن نفس الحالة)

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

sompylasar شكرا

قد تكون هذه مهمة غير تافهة

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

timotgl مثال بسيط يمكنني مشاركته بشكل عام:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

لا يهم هذا المستخدم المخفض سواء كان ما يتم تخزينه state, valueOrError أو أي شيء آخر. المكشوفة هي سلسلة الحالة (التعداد) ، وكثيرًا ما يستخدم الزوجان الشيكات على تلك الحالة والقيمة والخطأ.

لقد جئت عبر "محدد السباغيتي" ، حيث تسببت المحددات المتداخلة لكل قطعة فرعية تافهة من الحالة في حدوث ارتباك تام.

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

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

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

timotgl :

على سبيل المثال ، يمكنك إرسال الإجراءات مباشرة من مكون متصل ، عن طريق تنفيذ this.props.dispatch({type : "INCREMENT"}) . ومع ذلك ، فإننا لا نشجع ذلك ، لأنه يجبر المكون على "معرفة" أنه يتحدث إلى Redux. هناك طريقة أكثر اصطلاحًا للتفاعل لفعل الأشياء وهي تمرير مُنشئي الإجراءات المقيدة ، بحيث يمكن للمكون فقط استدعاء this.props.increment() ، ولا يهم ما إذا كانت هذه الوظيفة منشئ إجراء Redux مقيد أم لا. لأسفل من قبل أحد الوالدين ، أو وظيفة وهمية في الاختبار.

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

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

لذا لا ، ليس عليك استخدام المحددات ، لكنها ممارسة معمارية جيدة.

قد ترغب في قراءة رسالتي Idiomatic Redux: استخدام محددات إعادة التحديد للتغليف والأداء .

markerikson أنا لا أجادل ضد المحددات بشكل عام ، أو المحددات للحسابات باهظة الثمن. ولم أنوي أبدًا تمرير الإرسال نفسه إلى أحد المكونات :)

كانت وجهة نظري أنني أختلف مع هذا الاعتقاد:

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

حول المثال الخاص بك مع state.some.deeply.nested.field ، يمكنني رؤية قيمة وجود محدد لتقصير هذا. selectSomeDeeplyNestedField() هو اسم وظيفة واحد مقابل 5 خصائص يمكن أن أخطئ فيها.

من ناحية أخرى ، إذا اتبعت هذا المبدأ التوجيهي للحرف ، فأنت تقوم أيضًا بعمل const selectSomeField = state => state.some.field; أو حتى const selectSomething = state => state.something; ، وفي مرحلة ما النفقات العامة (الاستيراد والتصدير والاختبار) للقيام بذلك باستمرار لم يعد يبرر (القابلة للنقاش) السلامة والنقاء في رأيي. إنه حسن النية ولكن لا يمكنني التخلص من الروح العقائدية للمبدأ التوجيهي. أود أن أثق بالمطورين في مشروعي لاستخدام المحددات بحكمة وعند الاقتضاء - لأن تجربتي حتى الآن كانت أنهم يفعلون ذلك.

في ظل ظروف معينة ، استطعت أن أرى سبب رغبتك في الخطأ في جانب السلامة والاتفاقيات. شكرًا على وقتك ومشاركتك ، أنا سعيد جدًا بشكل عام لأننا استعدنا.

بالتأكيد. FWIW ، هناك مكتبات محددات أخرى ، وكذلك أغلفة حول إعادة التحديد . على سبيل المثال ، يتيح لك https://github.com/planttheidea/selectorator تحديد مسارات مفاتيح التدوين النقطي ، ويقوم بإجراء المحددات الوسيطة لك داخليًا.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات