جافاسكريبت - الإيجابيات/السلبيات لاستخدام redux-saga مع مولدات ES6 مقابل redux-thunk مع async/await في ES2017
هناك الكثير من الحديث عن الطفل الجديد في مدينة ريدوكس الآن، redux-saga/redux-saga. إنه يستخدم دوال المولد للاستماع إلى الإجراءات/إرسالها.
قبل أن أفهم هذا تمامًا، أود أن أعرف إيجابيات وسلبيات استخدام redux-saga بدلاً من الطريقة أدناه حيث أستخدم redux-thunk مع async/await.
قد يبدو المكوّن هكذا، أرسل الإجراءات كالمعتاد.
import { login } from 'redux/auth';
class LoginForm extends Component {
onClick(e) {
e.preventDefault();
const { user, pass } = this.refs;
this.props.dispatch(login(user.value, pass.value));
}
render() {
return (<div>
<input type="text" ref="user" />
<input type="password" ref="pass" />
<button onClick={::this.onClick}>Sign In</button>
</div>);
}
}
export default connect((state) => ({}))(LoginForm);
ثم تبدو أفعالي شيئًا مثل هذا:
// auth.js
import request from 'axios';
import { loadUserData } from './user';
// define constants
// define initial state
// export default reducer
export const login = (user, pass) => async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
let { data } = await request.post('/login', { user, pass });
await dispatch(loadUserData(data.uid));
dispatch({ type: LOGIN_SUCCESS, data });
} catch(error) {
dispatch({ type: LOGIN_ERROR, error });
}
}
// more actions...
// user.js
import request from 'axios';
// define constants
// define initial state
// export default reducer
export const loadUserData = (uid) => async (dispatch) => {
try {
dispatch({ type: USERDATA_REQUEST });
let { data } = await request.get(`/users/${uid}`);
dispatch({ type: USERDATA_SUCCESS, data });
} catch(error) {
dispatch({ type: USERDATA_ERROR, error });
}
}
// more actions...
الإجابة رقم 1
في redux-saga، ما يعادل المثال أعلاه سيكون
export function* loginSaga() {
while(true) {
const { user, pass } = yield take(LOGIN_REQUEST)
try {
let { data } = yield call(request.post, '/login', { user, pass });
yield fork(loadUserData, data.uid);
yield put({ type: LOGIN_SUCCESS, data });
} catch(error) {
yield put({ type: LOGIN_ERROR, error });
}
}
}
export function* loadUserData(uid) {
try {
yield put({ type: USERDATA_REQUEST });
let { data } = yield call(request.get, `/users/${uid}`);
yield put({ type: USERDATA_SUCCESS, data });
} catch(error) {
yield put({ type: USERDATA_ERROR, error });
}
}
أول شيء يجب ملاحظته هو أننا نستدعي دوال الـ API باستخدام الشكل yield call(func, ...args). call لا ينفذ التأثير، بل ينشئ مجرد كائن عادي مثل {type: 'CALL', func, args}. يتم تفويض التنفيذ إلى وسيط redux-saga الذي يتولى تنفيذ الدالة واستئناف الـ generator بنتيجتها.
الميزة الرئيسية هي أنه يمكنك اختبار المولد خارج Redux باستخدام فحوصات المساواة البسيطة
const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
)
لاحظ أننا نقوم بمحاكاة نتيجة استدعاء واجهة البرمجة التطبيقات ببساطة عن طريق حقن البيانات المحاكية في طريقة next للمكرر. محاكاة البيانات أبسط بكثير من محاكاة الوظائف.
الشيء الثاني الذي يجب ملاحظته هو الاستدعاء لـ yield take(ACTION). يتم استدعاء الثانكس بواسطة منشئ الإجراء على كل إجراء جديد (مثل LOGIN_REQUEST). أي أن الإجراءات يتم دفعها باستمرار إلى الثانكس، ولا يملك الثانكس أي سيطرة على متى يتوقف عن التعامل مع تلك الإجراءات.
في redux-saga، المولدات تسحب الإجراء التالي. أي أنها تتحكم في متى تستمع لبعض الإجراءات ومتى لا تستمع لها. في المثال أعلاه، تم وضع تعليمات التدفق داخل حلقة while(true)، لذلك ستستمع لكل إجراء وارد، مما يحاكي إلى حد ما سلوك الدفع في thunk.
النهج القائم على السحب يسمح بتنفيذ تدفقات تحكم معقدة. لنفترض على سبيل المثال أننا نريد إضافة المتطلبات التالية
معالجة إجراء تسجيل خروج المستخدم
عند أول تسجيل دخول ناجح، يعيد الخادم رمزًا يُنتهي بعد فترة مخزنة في حقل
expires_in. سيتعين علينا تحديث التفويض في الخلفية عند كلexpires_inميلي ثانيةخذ في الاعتبار أنه عند انتظار نتيجة استدعاءات الـ API (سواء تسجيل الدخول الأولي أو التحديث) قد يقوم المستخدم بتسجيل الخروج في هذه الأثناء.
كيف ستقوم بتنفيذ ذلك باستخدام الـ thunks؛ مع توفير تغطية اختبار كاملة لكامل العملية؟ هذا هو كيف قد يبدو باستخدام Sagas:
function* authorize(credentials) {
const token = yield call(api.authorize, credentials)
yield put( login.success(token) )
return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
let token = yield call(authorize, {name, password})
while(true) {
yield call(delay, token.expires_in)
token = yield call(authorize, {token})
}
}
function* watchAuth() {
while(true) {
try {
const {name, password} = yield take(LOGIN_REQUEST)
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password)
])
// user logged out, next while iteration will wait for the
// next LOGIN_REQUEST action
} catch(error) {
yield put( login.error(error) )
}
}
}
في المثال أعلاه، نحن نعبر عن متطلب التزامن الخاص بنا باستخدام race. إذا فاز take(LOGOUT) في السباق (أي أن المستخدم نقر على زر تسجيل الخروج). سيقوم السباق تلقائيًا بإلغاء مهمة الخلفية authAndRefreshTokenOnExpiry. وإذا كانت authAndRefreshTokenOnExpiry محجوزة في منتصف استدعاء call(authorize, {token})، فسيتم أيضًا إلغاؤها. ينتقل الإلغاء إلى الأسفل تلقائيًا.
يمكنك العثور على نسخة تجريبية قابلة للتشغيل للتدفق أعلاه
الإجابة رقم 2
سأضيف تجربتي في استخدام الساجا في نظام الإنتاج بالإضافة إلى إجابة مؤلف المكتبة الشاملة إلى حد ما.
احترافي (باستخدام الساجا):
قابلية الاختبار. من السهل جدًا اختبار الساجات لأن call() تُعيد كائنًا خالصًا. عادةً ما يتطلب اختبار الثانكس تضمين mockStore داخل الاختبار الخاص بك.
يأتي redux-saga مع الكثير من الدوال المساعدة المفيدة المتعلقة بالمهام. يبدو لي أن مفهوم الساجا هو إنشاء نوع من العامل/الخيط الخلفي لتطبيقك، الذي يعمل كقطعة مفقودة في بنية ريأكت-ريدوكس (يجب أن تكون الدوال المنشئة للإجراءات والمخفضات دوال نقية). مما يقودنا إلى النقطة التالية.
توفر الساجات مكانًا مستقلاً للتعامل مع جميع التأثيرات الجانبية. عادةً ما يكون من الأسهل تعديلها وإدارتها مقارنة بإجراءات الثانك في تجربتي.
ضد :
بنية المولد.
العديد من المفاهيم لتعلمها.
استقرار واجهة برمجة التطبيقات. يبدو أن redux-saga لا يزال يضيف ميزات (مثل القنوات؟) والمجتمع ليس كبيرًا. هناك قلق من أن المكتبة قد تقوم بتحديث غير متوافق مع الإصدارات السابقة في يوم من الأيام.
الإجابة رقم 3
أود فقط أن أضيف بعض التعليقات من تجربتي الشخصية (باستخدام كل من sagas و thunk):
الملحميات رائعة للاختبار:
- لا تحتاج إلى محاكاة الوظائف المغلفة بالتأثيرات
- لذلك فإن الاختبارات نقية وقابلة للقراءة وسهلة الكتابة
- عند استخدام الساجاس، عادةً ما تُعيد منشئو الإجراءات كائنات حرفية عادية. كما أنه من الأسهل اختبارها والتحقق منها على عكس وعود الثانك.
الملحمات أكثر قوة. كل ما يمكنك فعله في منشئ إجراءات thunk واحد يمكنك أيضًا فعله في ملحمة واحدة، ولكن ليس العكس (أو على الأقل ليس بسهولة). على سبيل المثال:
- انتظر تنفيذ إجراء/إجراءات (
take) - إلغاء الروتين الحالي (
cancel،takeLatest،race) - يمكن لعدة روتينات الاستماع إلى نفس الإجراء (
take،takeEvery، ...)
توفر Sagas أيضًا وظائف مفيدة أخرى، والتي تعمم بعض أنماط التطبيقات الشائعة:
القنواتللاستماع إلى مصادر الأحداث الخارجية (مثل WebSockets)- نموذج التفرع (
fork،spawn) - خانق
- ...
الساجاز هي أداة عظيمة وقوية. ومع ذلك، مع القوة تأتي المسؤولية. عندما ينمو تطبيقك، يمكنك بسهولة أن تضيع في معرفة من ينتظر تنفيذ الإجراء، أو ما يحدث بالضبط عندما يتم تنفيذ بعض الإجراءات. من ناحية أخرى، الثانك أبسط وأسهل للفهم. اختيار أحدهما يعتمد على العديد من الجوانب مثل نوع وحجم المشروع، وما أنواع التأثيرات الجانبية التي يجب أن يتعامل معها مشروعك، أو تفضيل فريق التطوير. في أي حال، فقط اجعل تطبيقك بسيطًا ويمكن التنبؤ بتصرفاته.
الإجابة رقم 4
تحديث في يوليو 2020:
خلال الستة عشر شهرًا الماضية، ربما يكون التغيير الأكثر لفتًا للانتباه في مجتمع React هو خطافات React.
وفقًا لما ألاحظه، من أجل الحصول على توافق أفضل مع المكونات الوظيفية والهوكس، تميل المشاريع (حتى الكبيرة منها) إلى استخدام:
- هوك + ثانك غير متزامن (يجعل الهوك كل شيء مرنًا جدًا بحيث يمكنك فعليًا وضع الثانك غير المتزامن في المكان الذي تريد واستخدامه كدوال عادية، على سبيل المثال، لا يزال بإمكانك كتابة الثانك في action.ts ثم استخدام useDispatch() لتشغيل الثانك: https://stackoverflow.com/a/59991104/5256695)،
- استخدام الطلب,
- GraphQL/Apollo
useQueryuseMutation - مكتبة جلب البيانات في رياكت
- خيارات شائعة أخرى لمكتبات جلب البيانات/استدعاء واجهات برمجة التطبيقات، الأدوات، أنماط التصميم، وما إلى ذلك
بالمقارنة، لا يوفر redux-saga فائدة كبيرة في معظم الحالات العادية لمكالمات API مقارنة بالأساليب المذكورة أعلاه في الوقت الحالي، بينما يزيد من تعقيد المشروع عن طريق إدخال العديد من ملفات/مولدات saga (وأيضًا لأن الإصدار الأخير v1.1.1 من redux-saga كان في 18 سبتمبر 2019، وهو منذ وقت طويل).
ولكن مع ذلك، redux-saga يوفر بعض الميزات الفريدة مثل تأثير السباق والطلبات المتوازية. لذلك، إذا كنت بحاجة إلى هذه الوظائف الخاصة، فإن redux-saga لا يزال خيارًا جيدًا.
المنشور الأصلي في مارس 2019:
مجرد بعض الخبرة الشخصية:
من حيث أسلوب الترميز وسهولة القراءة، فإن أحد أهم المزايا لاستخدام redux-saga في الماضي هو تجنب جحيم الاستدعاءات الخلفية في redux-thunk - فلا يحتاج المرء إلى استخدام العديد من التعشيقات then/catch بعد الآن. ولكن الآن مع شعبية async/await thunk، يمكن أيضًا كتابة التعليمات البرمجية غير المتزامنة بأسلوب متزامن عند استخدام redux-thunk، وهو ما يمكن اعتباره تحسنًا في redux-thunk.
قد يحتاج المرء إلى كتابة المزيد من التعليمات البرمجية الأساسية عند استخدام redux-saga، خاصة في Typescript. على سبيل المثال، إذا أراد شخص ما تنفيذ دالة fetch غير متزامنة، يمكن تنفيذ معالجة البيانات والأخطاء مباشرة في وحدة thunk واحدة في action.js باستخدام إجراء FETCH واحد فقط. ولكن في redux-saga، قد يحتاج المرء إلى تعريف الإجراءات FETCH_START و FETCH_SUCCESS و FETCH_FAILURE وجميع فحوصات النوع المتعلقة بها، لأن إحدى ميزات redux-saga هي استخدام هذا النوع الغني من آلية "الرمز" لإنشاء التأثيرات وتوجيه متجر redux لتسهيل الاختبار. بالطبع، يمكن للمرء كتابة saga بدون استخدام هذه الإجراءات، ولكن ذلك سيجعلها مشابهة لل-thunk.
من حيث هيكلية الملفات، يبدو أن redux-saga أكثر وضوحًا في العديد من الحالات. يمكن للمرء بسهولة العثور على كود متعلق بالعمليات غير المتزامنة في كل ملف sagas.ts، ولكن في redux-thunk، سيحتاج المرء إلى رؤيته في الملفات الخاصة بالإجراءات.
قد يكون الاختبار السهل ميزة أخرى مهمة في redux-saga. هذا أمر مريح حقًا. ولكن هناك شيء واحد يحتاج إلى توضيح وهو أن اختبار redux-saga "call" لن ينفذ الاستدعاء الفعلي لـ API أثناء الاختبار، وبالتالي سيكون من الضروري تحديد نتيجة نموذجية للخطوات التي قد تُستخدم بعد استدعاء API. لذلك، قبل الكتابة في redux-saga، سيكون من الأفضل التخطيط للـ saga وملف sagas.spec.ts المقابل لها بالتفصيل.
توفر Redux-saga أيضًا العديد من الميزات المتقدمة مثل تشغيل المهام بشكل متوازي، ومساعدات التزامن مثل takeLatest/takeEvery، fork/spawn، والتي تعتبر أقوى بكثير من thunks.
في الختام، شخصيًا، أود أن أقول: في العديد من الحالات العادية وتطبيقات الحجم الصغير إلى المتوسط، استخدم أسلوب async/await مع redux-thunk. سيوفر عليك الكثير من أكواد القالب/الإجراءات/الأنواع، ولن تحتاج إلى التنقل بين العديد من ملفات sagas.ts المختلفة والحفاظ على شجرة sagas محددة. ولكن إذا كنت تطور تطبيقًا كبيرًا يحتوي على منطق غير متزامن معقد جدًا وتحتاج إلى ميزات مثل نمط التزامن/التوازي، أو إذا كان لديك طلب مرتفع على الاختبار والصيانة (خصوصًا في تطوير الاختبارات-driven)، فإن redux-sagas قد ينقذ حياتك.
على أي حال، redux-saga ليست أكثر صعوبة وتعقيداً من redux نفسه، وليس لديها ما يُسمى بمنحنى تعلم حاد لأنها تمتلك مفاهيم أساسية وواجهات برمجة تطبيقات محدودة جيدًا. قد يفيدك قضاء وقت قليل في تعلم redux-saga يومًا ما في المستقبل.
