[javascript] لماذا نحتاج إلى برامج وسيطة للتدفق المتزامن في Redux؟


2 Answers

لم تكن.

ولكن ... يجب عليك استخدام ريدو ساغا :)

إن رد دان أبراموف صحيح حول إعادة redux-thunk لكني سأتحدث أكثر قليلاً عن موضوع إعادة التدوير المشابه تمامًا ولكنه أكثر قوة.

حتمية VS التعريفي

  • DOM : jQuery أمر حتمي / React هو declarative
  • Monads : IO أمر حتمي / مجاني هو التعريفي
  • تأثيرات Redux : redux-thunk أمر حتمي / redux-saga

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

إذا كنت تستخدم mocks ، فأنت لا تقوم ببرمجة وظيفية.

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

Source

الملاحم (كما تم تنفيذها في redux-saga ) هي تعريفية ومثل عناصر Monad أو React المجانية ، فهي أسهل بكثير لاختبار دون أي وهمية.

انظر أيضا هذا article :

في FP الحديثة ، لا ينبغي لنا أن نكتب برامج - يجب أن نكتب أوصاف البرامج ، والتي يمكننا بعد ذلك الاستنباط ، والتحول ، وتفسيرها حسب الإرادة.

(في الواقع ، فإن Redux-saga يشبه الهجين: التدفق أمر حتمي ولكن التأثيرات توضيحية)

الارتباك: الإجراءات / الأحداث / الأوامر ...

هناك الكثير من الارتباك في عالم الواجهة الأمامية حول كيفية ارتباط بعض مفاهيم الخلفية مثل CQRS / EventSourcing و Flux / Redux ، ويرجع ذلك في الغالب إلى أنه في Flux نستخدم مصطلح "action" الذي يمكن أن يمثل في بعض الأحيان كلا من الكود الحتمي ( LOAD_USER ) والأحداث ( USER_LOADED ). أنا أؤمن أنه مثل التزود بالأحداث ، يجب عليك فقط إرسال الأحداث.

باستخدام sagas في الممارسة

تخيل تطبيقًا يحتوي على رابط إلى ملف شخصي للمستخدم. والطريقة الاصطلاحية للتعامل مع كل من middlewares سيكون:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

هذه الملحمة تترجم إلى:

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

كما ترون ، هناك بعض مزايا redux-saga .

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

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

يصبح الرمز الخاص بك قابلاً للاختبار بشكل أكبر حيث أن التأثيرات تقريبية

لا تحتاج بعد الآن إلى تشغيل مكالمات تشبه rpc مثل actions.loadUser() . واجهة المستخدم الخاصة بك تحتاج فقط إلى إرسال ما حدث. نحن نطلق فقط الأحداث (دائمًا في الزمن الماضي!) وليس الإجراءات بعد الآن. هذا يعني أنه يمكنك إنشاء "ducks" decoupled أو سياقات Bounded وأن الملحمة يمكن أن تكون بمثابة نقطة اقتران بين هذه المكونات المعيارية.

هذا يعني أنه من الأسهل إدارة آرائك لأنها لا تحتاج بعد الآن إلى احتواء طبقة الترجمة بين ما حدث وما يجب أن يحدث كتأثير

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

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

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

فصل

Sagas ليست فقط استبدال thonds redux. أنها تأتي من الخلفية / توزيع النظم / مصادر الحدث.

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

في المقام الأول ، الملحمة هي قطعة من البرامج التي تسمح بتنسيق المعاملات طويلة الأمد (الاتساق في نهاية المطاف) ، والمعاملات عبر سياقات حدودية مختلفة (بلغة تصميم مدفوعة بالنطاق).

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

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

بعض المقالات الجيدة حول كيفية بناء تطبيق Redux ، حيث يمكنك استخدام Redus-saga لأسباب الفصل:

A usecase ملموسة: نظام الإخطار

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

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

لقد وصفت here كيف يمكن القيام بذلك مع الملحمة

لماذا تسمى "ساغا"؟

ملحمة مصطلح تأتي من العالم الخلفي. في البداية قدمت ياسين (مؤلف كتاب ريدكس ساغا) لهذا المصطلح في مناقشة طويلة .

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

اليوم ، مصطلح "saga" مربك لأنه يمكن أن يصف شيئين مختلفين. نظرًا لأنه يُستخدم في عملية red--saga ، فإنه لا يصف طريقة للتعامل مع المعاملات الموزعة ، ولكنه وسيلة لتنسيق الإجراءات في تطبيقك. كما يمكن أن يطلق على redux-saga اسم redux-process-manager .

أنظر أيضا:

البدائل

إذا لم تعجبك فكرة استخدام المولدات ولكنك مهتمًا بنمط الحكاية وخصائص فصلها ، يمكنك أيضًا تحقيق نفس الشيء باستخدام redux-observable التي redux-observable والتي تستخدم اسم epic لوصف النموذج نفسه ، ولكن باستخدام RxJS. إذا كنت على دراية بـ Rx ، فسوف تشعر أنك في المنزل.

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

بعض موارد مفيدة ملحمة

2017 ينصح

  • لا تفرط في استخدام Redux-saga لمجرد استخدامه. لا يمكن إجراء مكالمات API القابلة للاختبار فقط.
  • لا تقم بإزالة أجزاء thunks من مشروعك لمعظم الحالات البسيطة.
  • لا تتردد في إرسال yield put(someActionThunk) في yield put(someActionThunk) إذا كان من المنطقي.

إذا كنت خائفًا من استخدام Redus-saga (أو Redux-observable) ولكنك تحتاج فقط إلى نمط الفصل ، تحقق من redux-dispatch-subscribe : فهو يسمح بالاستماع إلى الإرسالات وإطلاق رسائل جديدة في المستمع.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
Question

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

على سبيل المثال ، تخيل واجهة مستخدم بسيطة: حقل وزر. عندما يقوم المستخدم بدفع الزر ، يتم ملء الحقل بالبيانات من خادم بعيد.

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

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

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

ما هو الخطأ في هذا النهج؟ لماذا أرغب في استخدام Redux Thunk أو Redux Promise ، كما تقترح الوثائق؟

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

لا يزال منشئ الفعل نفسه عبارة عن وظيفة نقية ، ولكن وظيفة thunk التي تعرضها لا تحتاج إلى أن تكون ، ويمكنها إجراء مكالمات غير متزامنة

لم يعد مطلوبًا من منشئي الأعمال أن يكونوا محضين. لذلك ، كان من المؤكد بالتأكيد ، thunk / وعد الوسيطة في الماضي ، ولكن يبدو أن هذا لم يعد هو الحال؟




إن هدف أبراموف - ومن وجهة نظر الجميع - هو ببساطة لتغليف التعقيد (والمكالمات غير المتزامنة) في المكان المناسب .

أين هو أفضل مكان للقيام بذلك في البيانات القياسية من Redux؟ كيف حول:

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



حسنًا ، لنبدأ في معرفة كيفية عمل البرامج الوسيطة أولاً ، والتي تجيب تمامًا على السؤال ، هذه هي وظيفة رمز التطبيق mediumMidWare في Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

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

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Note that each middleware will be given the dispatch and getState functions as named arguments.

OK, this is how Redux-thunk as one of the most used middlewares for Redux introduce itself:

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.

So as you see, it will return a function instead an action, means you can wait and call it anytime you want as it's a function...

So what the heck is thunk? That's how it's introduced in Wikipedia:

In computer programming, a thunk is a subroutine used to inject an additional calculation into another subroutine. Thunks are primarily used to delay a calculation until it is needed, or to insert operations at the beginning or end of the other subroutine. They have a variety of other applications to compiler code generation and in modular programming.

The term originated as a jocular derivative of "think".

A thunk is a function that wraps an expression to delay its evaluation.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

So see how easy the concept is and how it can help you manage your async actions...

That's something you can live without it, but remember in programming there are always better, neater and proper ways to do things...




Related