جافاسكريبت - لماذا نحتاج إلى وسيط (middleware) لتدفق غير متزامن في Redux؟
وفقًا للوثائق، "بدون الوسطاء، يدعم مخزن 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، كما توصي الوثائق؟
تعديل: لقد بحثت في مستودع Redux عن أدلة، ووجدت أن منشئي الإجراءات كان من المطلوب أن يكونوا دوال نقية في الماضي. على سبيل المثال، إليك مستخدم يحاول تقديم شرح أفضل لتدفق البيانات غير المتزامن:
عامل إنشاء الإجراء نفسه لا يزال دالة نقية، لكن دالة الثنك التي يعيدها لا تحتاج أن تكون نقية، ويمكنها القيام باستدعاءاتنا غير المتزامنة
لم يعد من الضروري أن تكون منشئو الإجراءات نقية. لذلك، كان من الضروري بالتأكيد وجود وسيط thunk/promise في الماضي، لكن يبدو أن هذا لم يعد الحال الآن؟
الإجابة رقم 1
ما الخطأ في هذا النهج؟ لماذا أريد استخدام Redux Thunk أو Redux Promise، كما توصي الوثائق؟
لا يوجد خطأ في هذا النهج. إنه فقط غير ملائم في تطبيق كبير لأنك ستحتاج إلى مكونات مختلفة تؤدي نفس الإجراءات، وقد ترغب في تثبيت بعض الإجراءات مؤقتًا، أو الاحتفاظ ببعض الحالة المحلية مثل معرفات الزيادة التلقائية بالقرب من منشئي الإجراءات، وما إلى ذلك. لذلك، من الأسهل فقط من منظور الصيانة استخراج منشئي الإجراءات إلى وظائف منفصلة.
يمكنك قراءة إجابتي على “كيفية إرسال إجراء في Redux مع مهلة” للحصول على شرح أكثر تفصيلاً.
البرمجيات الوسيطة مثل Redux Thunk أو Redux Promise تمنحك فقط "سكر بناء الجملة" لإرسال الثانكس أو الوعود، ولكنك لست مضطرًا لاستخدامها.
لذا، بدون أي برنامج وسيط، قد يبدو منشئ الإجراء الخاص بك هكذا
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
ولكن مع وسيط Thunk يمكنك كتابتها هكذا:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
لذلك لا يوجد فرق كبير. أحد الأمور التي أحبها في النهج الأخير هو أن المكون لا يهتم بأن منشئ الإجراءات غير متزامن. فهو يستدعي dispatch بشكل طبيعي فقط، ويمكنه أيضًا استخدام mapDispatchToProps لربط منشئ الإجراء بمختصر صغير، وما إلى ذلك. المكونات لا تعرف كيف تم تنفيذ منشئي الإجراءات، ويمكنك التبديل بين أساليب غير متزامنة مختلفة (Redux Thunk، Redux Promise، Redux Saga) دون تغيير المكونات. من ناحية أخرى، في النهج الأول الصريح، تعرف مكوناتك بالضبط أن استدعاءً معينًا غير متزامن، ويحتاج إلى تمرير dispatch عن طريق بعض الاتفاقية (على سبيل المثال، كمعامل متزامن).
فكر أيضًا في كيفية تغير هذا الكود. لنفترض أننا نريد أن يكون لدينا دالة تحميل بيانات ثانية، وأن ندمجها في منشئ إجراءات واحد.
مع النهج الأول، نحتاج إلى أن نكون واعين بنوع منشئ الإجراء الذي نستدعيه:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
مع Redux Thunk، يمكن لمنشئي الإجراءات dispatch نتيجة منشئي إجراءات آخرين دون الحاجة للتفكير فيما إذا كانت تلك الإجراءات متزامنة أم غير متزامنة:
// action creators
function loadSomeData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(userId) {
return dispatch => Promise.all(
dispatch(loadSomeData(userId)), // just dispatch normally!
dispatch(loadOtherData(userId)) // just dispatch normally!
);
}
// component
componentWillMount() {
this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}
بهذا النهج، إذا أردت لاحقًا أن يقوم منشئو الإجراءات الخاصون بك بالنظر في حالة Redux الحالية، يمكنك ببساطة استخدام الوسيطة الثانية getState التي يتم تمريرها إلى الثانكس دون تعديل الكود المستدعي على الإطلاق:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
}
إذا كنت بحاجة إلى تغييره ليكون متزامنًا، يمكنك أيضًا القيام بذلك دون تغيير أي من كود الاستدعاء:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
لذلك فإن فائدة استخدام الوسائط الوسطية مثل Redux Thunk أو Redux Promise هي أن المكونات لا تكون على علم بكيفية تنفيذ منشئي الإجراءات، وما إذا كانوا يهتمون بحالة Redux، وما إذا كانوا متزامنين أم غير متزامنين، وما إذا كانوا ينادون منشئي إجراءات آخرين أم لا. العيب هو قليل من التحويل غير المباشر، لكننا نعتقد أن الأمر يستحق ذلك في التطبيقات الحقيقية.
أخيرًا، Redux Thunk وأصدقاؤه هو مجرد نهج واحد ممكن للطلبات غير المتزامنة في تطبيقات Redux. نهج آخر مثير للاهتمام هو Redux Saga الذي يتيح لك تعريف شياطين طويلة الأمد («sagas») التي تأخذ الإجراءات عند ورودها، وتحولها أو تقوم بالطلبات قبل إخراج الإجراءات. هذا ينقل المنطق من منشئي الإجراءات إلى sagas. قد ترغب في تجربته، ثم لاحقًا اختيار ما يناسبك أكثر.
بحثت في مستودع Redux عن أدلة، ووجدت أن منشئي الإجراءات كان يُطلب منهم أن يكونوا دوال نقيّة في الماضي.
هذا غير صحيح. الوثائق قالت هذا، لكن الوثائق كانت خاطئة.
لم يكن مطلقًا مطلوبًا أن تكون منشئي الإجراءات دوال نقية.
قمنا بتصحيح الوثائق لتعكس ذلك.
الإجابة رقم 2
أنت لا تفعل.
لكن... يجب أن تستخدم redux-saga :)
إجابة دان أبراموف صحيحة بشأن redux-thunk لكن سأتحدث قليلاً أكثر عن redux-saga الذي يشبهه إلى حد كبير ولكنه أقوى.
الأمر مقابل الإعلاني
- DOM: jQuery إجرائي/React وصفي
- المونادات: الإدخال/الإخراج إلزامي/الحر إعلاني
- تأثيرات Redux:
redux-thunkإجرائي/redux-sagaوصفي
عندما يكون لديك دالة مؤجلة (thunk) بين يديك، مثل موناد IO أو وعد (promise)، لا يمكنك بسهولة معرفة ما الذي ستفعله بمجرد تنفيذها. الطريقة الوحيدة لاختبار دالة مؤجلة هي تنفيذها، ومحاكاة الموزع (أو العالم الخارجي بأكمله إذا كانت تتفاعل مع المزيد من الأشياء...).
إذا كنت تستخدم المحاكيات، فأنت لا تقوم بالبرمجة الوظيفية.
عند النظر من خلال عدسة الآثار الجانبية، تُعدّ الاختبارات الوهمية علامة على أن الكود الخاص بك غير نقي، وفي نظر المبرمج الوظيفي، دليل على أن هناك خطأ ما. بدلاً من تنزيل مكتبة لمساعدتنا في التحقق من أن الجبل الجليدي سليم، ينبغي علينا الإبحار حوله. سألني شخص ملتزم بشدة بـ TDD/Java ذات مرة عن كيفية القيام بالاختبارات الوهمية في Clojure. الإجابة هي أننا عادة لا نفعل ذلك. في الغالب نرى ذلك كإشارة إلى أننا بحاجة لإعادة هيكلة كودنا.
مصدر
الملحمة (كما تم تنفيذها في redux-saga) هي إعلانية ومثل الـ Free monad أو مكونات React، فهي أسهل بكثير في الاختبار بدون أي محاكاة.
انظر أيضًا هذا المقال:
في البرمجة الوظيفية الحديثة، لا ينبغي لنا كتابة برامج — يجب أن نكتب أوصافًا للبرامج، والتي يمكننا بعد ذلك فحصها وتحويلها وتفسيرها حسب الرغبة.
(في الواقع، Redux-saga يشبه الهجين: التدفق إجرائي ولكن التأثيرات إعلانية)
الارتباك: الأفعال/الأحداث/الأوامر...
هناك الكثير من الارتباك في عالم الواجهة الأمامية حول كيفية ارتباط بعض مفاهيم الخلفية مثل CQRS/EventSourcing و Flux/Redux، ويرجع ذلك أساسًا إلى أننا في Flux نستخدم مصطلح "الإجراء" الذي يمكن أن يمثل أحيانًا كل من الكود الإلزامي (LOAD_USER) والأحداث (USER_LOADED). أعتقد أنه مثل EventSourcing، يجب عليك فقط إرسال الأحداث.
استخدام الساغاس في الممارسة
تخيل تطبيقًا يحتوي على رابط إلى ملف مستخدم. الطريقة المألوفة للتعامل مع هذا مع كل وسيط ستكون:
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 })
);
}
ريدوكس-ساجا
<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.
يتيح استخدام takeLatest التعبير عن أنك مهتم فقط بالحصول على بيانات آخر اسم مستخدم تم النقر عليه (معالجة مشاكل التزامن في حال نقر المستخدم بسرعة على العديد من أسماء المستخدمين). هذا النوع من الأمور صعب مع thunks. كان بإمكانك استخدام takeEvery إذا لم ترغب في هذا السلوك.
تحافظ على صانعي الأفعال نقية. لاحظ أنه لا يزال من المفيد الاحتفاظ بـ actionCreators (في sagas put والمكونات dispatch)، حيث قد يساعدك ذلك في إضافة تحقق من الأفعال (assertions/flow/typescript) في المستقبل.
يصبح كودك أكثر قابلية للاختبار بشكل كبير حيث إن التأثيرات وصفية
أنت لست بحاجة بعد الآن لتحفيز مكالمات شبيهة بـ RPC مثل actions.loadUser(). واجهة المستخدم الخاصة بك تحتاج فقط لإرسال ما قد حدث. نحن لا نطلق سوى الأحداث (دائمًا بصيغة الماضي!) وليس الإجراءات بعد الآن. هذا يعني أنه يمكنك إنشاء "بط" أو سياقات محددة مستقلة، وأن الساجا يمكن أن تعمل كنقطة الترابط بين هذه المكونات المعيارية.
هذا يعني أن وجهات نظرك أسهل في الإدارة لأنها لم تعد بحاجة بعد الآن لاحتواء تلك الطبقة الترجمية بين ما حدث وما ينبغي أن يحدث نتيجة لذلك
على سبيل المثال، تخيل عرض تمرير غير محدود. CONTAINER_SCROLLED يمكن أن يؤدي إلى NEXT_PAGE_LOADED، لكن هل من حقًا مسؤولية الحاوية القابلة للتمرير أن تقرر ما إذا كان ينبغي علينا تحميل صفحة أخرى أم لا؟ ثم يجب أن تكون على دراية بأمور أكثر تعقيدًا مثل ما إذا كانت الصفحة الأخيرة قد تم تحميلها بنجاح أم لا، أو إذا كان هناك بالفعل صفحة تحاول التحميل، أو إذا لم تعد هناك عناصر متبقية للتحميل؟ لا أعتقد ذلك: من أجل أقصى قدر من إعادة الاستخدام، يجب على الحاوية القابلة للتمرير فقط وصف أنها قد تم تمريرها. تحميل الصفحة هو "تأثير تجاري" لهذا التمرير.
قد يجادل البعض بأن المولدات يمكنها بطبيعتها إخفاء الحالة خارج متجر Redux باستخدام المتغيرات المحلية، ولكن إذا بدأت في تنسيق أشياء معقدة داخل الثانكس من خلال بدء المؤقتات وما إلى ذلك فستواجه نفس المشكلة على أي حال. وهناك تأثير select الذي يسمح الآن بالحصول على بعض الحالة من متجر Redux الخاص بك.
يمكن السفر عبر الزمن باستخدام الساجاس كما أنها تتيح تسجيل تدفق معقد وأدوات تطوير يتم العمل عليها حالياً. إليك بعض تسجيلات تدفق غير متزامنة بسيطة التي تم تنفيذها بالفعل:
فصل
الملحمات لا تستبدل فقط الدوال الوسيطة في Redux. إنها تأتي من الأنظمة الخلفية/الأنظمة الموزعة/مصادر الأحداث.
من المفهوم الخاطئ الشائع جدًا أن الساجات موجودة فقط لاستبدال ثانكس في Redux بتحسين قابلية الاختبار. في الواقع، هذه مجرد تفاصيل تنفيذية في redux-saga. استخدام التأثيرات الإعلانية أفضل من الثانكس من حيث قابلية الاختبار، لكن يمكن تنفيذ نمط الساجا على الكود الإلزامي أو الإعلاني.
في المقام الأول، الساغا هي قطعة من البرامج تسمح بتنسيق المعاملات طويلة الأمد (الاتساق النهائي)، والمعاملات عبر سياقات محدودة مختلفة (مصطلحات تصميم مدفوع بالمجال).
لتبسيط هذا لعالم الواجهة الأمامية، تخيل أن هناك ويدجيت1 وويدجيت2. عندما يتم النقر على زر ما في ويدجيت1، يجب أن يكون له تأثير على ويدجيت2. بدلاً من ربط الويدجيتين معًا (أي ويدجيت1 يرسل حدثًا يستهدف ويدجيت2)، ويدجيت1 يرسل فقط أن زره تم النقر عليه. ثم تقوم الساغا بالاستماع لهذا النقر على الزر وبعدها تحديث ويدجيت2 عن طريق إرسال حدث جديد يكون ويدجيت2 على علم به.
هذا يضيف مستوى من التجريد غير الضروري للتطبيقات البسيطة، ولكنه يجعل من السهل توسيع التطبيقات المعقدة. يمكنك الآن نشر widget1 و widget2 إلى مستودعات npm مختلفة بحيث لا يضطران إلى معرفة بعضهما البعض أبداً، دون الحاجة لمشاركتهما سجلًا عالميًا للإجراءات. الويدجتين الآن عبارة عن سياقات محددة يمكن أن تعيش بشكل منفصل. إنهما لا يحتاجان لبعضهما البعض ليكونا متسقين ويمكن إعادة استخدامهما في تطبيقات أخرى أيضًا. الساجا هي نقطة الربط بين الويدجتين التي تنسقهما بطريقة ذات معنى لعملك.
بعض المقالات الجيدة عن كيفية هيكلة تطبيق Redux الخاص بك، والتي يمكنك استخدام Redux-saga فيها لأسباب تتعلق بفصل المكونات:
- http://jaysoo.ca/2016/02/28/organizing-redux-application/
- http://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://github.com/slorber/scalable-frontend-with-elm-or-redux
حالة استخدام ملموسة: نظام الإشعارات
أريد أن تكون مكوناتي قادرة على تشغيل عرض الإشعارات داخل التطبيق. لكنني لا أريد أن تكون مكوناتي مرتبطة بشكل كبير بنظام الإشعارات الذي له قواعده الخاصة (حد أقصى 3 إشعارات معروضة في نفس الوقت، ترتيب الإشعارات في قائمة الانتظار، زمن العرض 4 ثوانٍ وما إلى ذلك...).
لا أريد أن تقرر مكونات JSX الخاصة بي متى ستظهر/تختفي الإشعارات. أنا فقط أعطيها القدرة على طلب إشعار، وأترك القواعد المعقدة داخل الساجا. هذا النوع من الأشياء صعب جدًا تنفيذه باستخدام الثانكس أو البروميسات.
لقد وصفت هنا كيفية القيام بذلك باستخدام الساجا
لماذا يُسمّى ساگا؟
مصطلح ساگا يأتي من عالم الباك إند. لقد قدمت ياسين (مؤلف Redux-saga) في البداية لهذا المصطلح في مناقشة طويلة.
في البداية، تم تقديم هذا المصطلح من خلال ورقة بحثية، وكان من المفترض استخدام نمط الملحمة للتعامل مع التوافق النهائي في المعاملات الموزعة، ولكن تم توسيع استخدامه ليشمل تعريفًا أوسع من قبل مطوري الواجهة الخلفية بحيث يغطي الآن أيضًا نمط "مدير العمليات" (بطريقة ما، نمط الملحمة الأصلي هو شكل متخصص من مدير العمليات).
اليوم، يعتبر مصطلح "ساجا" مربكًا لأنه يمكن أن يصف شيئان مختلفان. كما هو مستخدم في redux-saga، فهو لا يصف طريقة للتعامل مع المعاملات الموزعة، بل طريقة لتنسيق الإجراءات في تطبيقك. كان من الممكن أيضًا أن يُسمى redux-saga باسم redux-process-manager.
انظر أيضًا:
- مقابلة مع ياسين عن تاريخ Redux-saga
- كيلا بايت: توضيح نمط الساجا
- رحلة مايكروسوفت CQRS: ملحمة عن الملحمات
- الرد المتوسط لياسين
بدائل
إذا لم تكن تحب فكرة استخدام المولدات ولكنك مهتم بنمط الملحمة وخصائص فصلها، يمكنك أيضًا تحقيق نفس الشيء باستخدام 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
}))
);
بعض الموارد المفيدة لـ redux-saga
- ريدوكس-ساج مقابل ريدوكس-ثانك مع async/await
- إدارة العمليات في Redux Saga
- من منشئي الإجراءات إلى الساجات
- لعبة الثعبان تم تنفيذها باستخدام Redux-saga
2017 ينصح
- لا تفرط في استخدام Redux-saga لمجرد استخدامه. مكالمات API القابلة للاختبار فقط لا تستحق الجهد.
- لا تقم بإزالة الثانكس من مشروعك في معظم الحالات البسيطة.
- لا تتردد في إرسال الثنكس في
yield put(someActionThunk)إذا كان ذلك منطقيًا.
إذا كنت خائفًا من استخدام Redux-saga (أو Redux-observable) ولكنك تحتاج فقط إلى نمط الفصل، تحقق من redux-dispatch-subscribe: فهو يتيح الاستماع إلى العمليات الإرسالية وتشغيل عمليات إرسالية جديدة في المستمع.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});
الإجابة رقم 3
الإجابة القصيرة: يبدو لي كنهج معقول تمامًا لمشكلة التزامن. مع بعض التحفظات.
كان لدي تسلسل أفكار مشابه جدًا عندما كنت أعمل على مشروع جديد بدأناه للتو في عملي. كنت من كبار المعجبين بنظام ريدوكس الكلاسيكي الأنيق لتحديث المتجر وإعادة عرض المكونات بطريقة تبقى بعيدة عن جوهر شجرة مكونات ريأكت. كان يبدو لي غريبًا أن أتدخل في تلك الآلية الأنيقة dispatch للتعامل مع العمليات غير المتزامنة.
انتهى بي المطاف باختيار نهج مشابه جدًا لما لديك هناك في مكتبة قمت بفصلها من مشروعنا، والتي أطلقنا عليها اسم react-redux-controller.
انتهى بي الأمر بعدم اتباع النهج الدقيق الذي لديك أعلاه لعدة أسباب:
- بالطريقة التي كتبتها بها، تلك الدوال المرسلة لا تمتلك وصولاً إلى المخزن. يمكنك التملص من ذلك إلى حد ما بجعل مكونات واجهة المستخدم الخاصة بك تمرر كل المعلومات التي تحتاجها دالة الإرسال. لكني سأجادل بأن هذا يربط تلك المكونات المنظرية بمنطق الإرسال بشكل غير ضروري. والأكثر إشكالية، لا توجد طريقة واضحة لدالة الإرسال للوصول إلى الحالة المحدّثة في الاستمراريات غير المتزامنة.
- وظائف الإرسال لها إمكانية الوصول إلى
dispatchنفسها عبر النطاق اللفظي. هذا يقيّد الخيارات لإعادة الهيكلة بمجرد أن يصبح بيانconnectفوضويًا -- ويبدو أنه غير عملي مع مجرد تلك الطريقة الواحدةupdate. لذلك تحتاج إلى نظام يسمح لك بتجميع تلك الوظائف المرسلة إذا قمت بتقسيمها إلى وحدات منفصلة.
معًا، عليك أن تقوم بتركيب نظام ما للسماح بحقن dispatch والمتجر في دوال الإرسال الخاصة بك، جنبًا إلى جنب مع معلمات الحدث. أنا أعرف ثلاث طرق معقولة لهذا الحقن الاعتمادي:
- يقوم redux-thunk بذلك بطريقة وظيفية، عن طريق تمريرها إلى thunks الخاصة بك (مما يجعلها ليست thunks بالضبط، وفقًا لبعض التعريفات). لم أعمل مع نهج الوسيط الأخرى
dispatch، لكن أفترض أنها أساسًا نفسها. - يقوم react-redux-controller بذلك باستخدام كوروتين. كمكافأة، يمنحك أيضًا الوصول إلى "المحددات"، وهي الدوال التي قد تكون قد مررت بها كأول وسيط إلى
connect، بدلاً من الاضطرار للعمل مباشرة مع المتجر الخام والمطبع. - يمكنك أيضًا القيام بذلك بطريقة البرمجة الشيئية عن طريق حقنها في سياق
this، من خلال مجموعة متنوعة من الآليات الممكنة.
تحديث
يتبادر إلى ذهني أن جزءًا من هذا اللغز هو حدّ من حدود react-redux. الحجة الأولى لـ connect تحصل على لقطة من الحالة، ولكن ليس على dispatch. الحجة الثانية تحصل على dispatch ولكن ليس على الحالة. لا تحصل أي من الحجتين على thunk يغلق على الحالة الحالية، ليكون قادرًا على رؤية الحالة المحدثة في وقت الاستمرار/التابع.
الإجابة رقم 4
هدف أبراموف - وهدف الجميع في المثالية - هو ببساطة حصر التعقيد (والاستدعاءات غير المتزامنة) في المكان الذي يكون فيه الأكثر ملاءمة وقابلية لإعادة الاستخدام.
أين أفضل مكان للقيام بذلك في تدفق بيانات Redux القياسي؟ ماذا عن:
- المخفضات? مستحيل. يجب أن تكون دوال نقية بدون تأثيرات جانبية. تحديث المخزن أمر جاد ومعقد. لا تلوثه.
- مكونات العرض الغبية؟ بالتأكيد لا. لديهم اهتمام واحد: العرض والتفاعل مع المستخدم، ويجب أن تكون بسيطة قدر الإمكان.
مكونات الحاوية؟ ممكنة، لكنها ليست الأمثل. ذلك منطقي بما أن الحاوية هي مكان نغلف فيه بعض التعقيدات المتعلقة بالعرض ونتفاعل مع المخزن، لكن:
- الحاويات تحتاج إلى أن تكون أكثر تعقيدًا من المكونات البسيطة، لكنها لا تزال تتحمل مسؤولية واحدة: توفير الروابط بين العرض والحالة/المخزن. منطقك غير المتزامن هو مسألة منفصلة تمامًا عن ذلك.
- بوضعه في حاوية، ستقوم بقفل منطق الـ async الخاص بك داخل سياق واحد، مرتبط بذات الوقت بعرض أو أكثر/طرق. فكرة سيئة. من الناحية المثالية، يجب أن يكون كل شيء قابل لإعادة الاستخدام، ومنفصل تمامًا عن العروض.
- (مثل جميع القواعد، قد يكون هناك استثناء إذا كان لديك منطق ربط قائم على الحالة يمكن إعادة استخدامه عبر سياقات متعددة، أو إذا كنت تستطيع بطريقة ما تعميم جميع حالتك إلى شيء مثل مخطط GraphQL متكامل. حسنًا، هذا قد يكون رائعًا. لكن... معظم الوقت يبدو أن الروابط تنتهي لتكون محددة بالسياق/العرض بشكل كبير.)
الإجراءات والوسائط الوسيطة التي تقوم بتفسيرها؟ لماذا لا؟! للمبتدئين، إنها الخيار الرئيسي الوحيد المتبقي لدينا. :-) من الناحية المنطقية، يعد نظام الإجراءات منطق تنفيذ مفصول يمكنك استخدامه من أي مكان. لديه إمكانية الوصول إلى المخزن ويمكنه إرسال المزيد من الإجراءات. لديه مسؤولية واحدة وهي تنظيم تدفق التحكم والبيانات حول التطبيق، ومعظم الإجراءات غير المتزامنة تتناسب تمامًا مع ذلك.
ماذا عن منشئي الإجراءات؟ لماذا لا نقوم بالعمليات غير المتزامنة هناك، بدلاً من القيام بها في الإجراءات نفسها، وفي الوسيط أيضًا؟
- أولًا والأهم، المبدعون ليس لديهم وصول إلى المخزن، كما هو الحال مع البرمجيات الوسيطة. هذا يعني أنه لا يمكنك إرسال إجراءات طارئة جديدة، ولا يمكنك القراءة من المخزن لتكوين العمليات غير المتزامنة الخاصة بك، وما إلى ذلك.
الإجابة رقم 5
للإجابة على السؤال الذي تم طرحه في البداية:
لماذا لا يمكن لمكوّن الحاوية استدعاء واجهة برمجة التطبيقات غير المتزامنة، ثم إرسال الإجراءات؟
ضع في اعتبارك أن تلك المستندات خاصة بـ Redux، وليست Redux مع React. مخازن Redux المتصلة بمكونات React يمكنها القيام تمامًا بما تقول، ولكن مخزن Redux العادي بدون أي وسائط لا يقبل وسائط لـ dispatch سوى الكائنات البسيطة المعتادة.
بدون وسيط، يمكنك بالطبع أن تفعل ذلك
const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));
لكنها حالة مشابهة حيث يتم تضمين اللامزامنة حول Redux بدلاً من أن يتم التعامل معها بواسطة Redux. لذلك، الوسائط الوسطية تسمح باللامزامنة عن طريق تعديل ما يمكن تمريره مباشرة إلى dispatch.
مع ذلك، أعتقد أن جوهر اقتراحك صحيح. هناك بالتأكيد طرق أخرى يمكنك من خلالها التعامل مع اللاتزامن في تطبيق Redux + React.
أحد فوائد استخدام البرمجيات الوسيطة هو أنه يمكنك الاستمرار في استخدام منشئي الإجراءات بشكل طبيعي دون القلق بشأن كيفية ربطهم بالضبط. على سبيل المثال، باستخدام redux-thunk، سيبدو الكود الذي كتبته كثيرًا مثل
function updateThing() {
return dispatch => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
}
const ConnectedApp = connect(
(state) => { ...state },
{ update: updateThing }
)(App);
الذي لا يبدو مختلفًا تمامًا عن الأصل — فقط تم خلطه قليلًا — وconnect لا يعرف أن updateThing هي (أو تحتاج أن تكون) غير متزامنة.
إذا كنت ترغب أيضًا في دعم الوعود (promises) أو الملاحظات (observables) أو السفن (sagas) أو منشئي الإجراءات المخصصين والمعلنين للغاية، فإن Redux يمكنه القيام بذلك ببساطة من خلال تغيير ما تمرره إلى dispatch (أي ما تعيده من منشئي الإجراءات). لا حاجة للتلاعب بمكونات React (أو استدعاءات connect).
الإجابة رقم 6
حسنًا، لنبدأ أولاً برؤية كيف يعمل الوسط البرمجي، فهذا يجيب إلى حد ما على السؤال، هذا هو كود مصدر دالة applyMiddleWare في 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);
}
- لاحظ أن كل برنامج وسيط سيتم تزويده بالوظائف
dispatchوgetStateكوسائط مسماة.
حسنًا، هذه هي الطريقة التي يقدم بها Redux-thunk نفسه كأحد أكثر الوسائط الوسيطة استخدامًا في Redux:
وسيط Redux Thunk يتيح لك كتابة منشئي الإجراءات الذين يعيدون دالة بدلًا من إجراء. يمكن استخدام الثنك لتأخير إرسال إجراء ما، أو لإرساله فقط إذا تم استيفاء شرط معين. الدالة الداخلية تستلم طرق المتجر dispatch و getState كمعاملات.
كما ترى، سيُرجع دالة بدلاً من إجراء، وهذا يعني أنه يمكنك الانتظار واستدعاؤها في أي وقت تريد لأنها دالة...
فما هو بحق الجحيم الثانك؟ هكذا تم تقديمه في ويكيبيديا:
في برمجة الحاسوب، الثانك هو روتين فرعي يُستخدم لإدخال حساب إضافي في روتين فرعي آخر. تُستخدم الثانكات بشكل أساسي لتأجيل عملية الحساب حتى تكون هناك حاجة إليها، أو لإدراج عمليات في بداية أو نهاية الروتين الفرعي الآخر. لديها مجموعة متنوعة من التطبيقات الأخرى في توليد كود المترجم وفي البرمجة المعيارية.
نشأ المصطلح كمشتق هزلي من كلمة "think".
الثنك هو دالة تغلف تعبيرًا لتأجيل تقييمه.
//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;
لذا انظر كيف أن المفهوم سهل وكيف يمكن أن يساعدك في إدارة إجراءاتك غير المتزامنة...
هذا شيء يمكنك العيش بدونه، لكن تذكر أنه في البرمجة هناك دائمًا طرق أفضل وأنظف وأدق للقيام بالأشياء...



