كما هو الحال في Yandex.Practicum ، فاز تصميم الواجهة الأمامية: رقم بهلواني مع Redux-Saga و postMessage و Jupyter

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



من وما تتكون ورشة العمل من


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

تعتمد الواجهة الأمامية على React.js بالتزامن مع Redux / Redux-Saga ، نستخدم Express للتواصل مع الواجهة الخلفية. الجزء الخلفي من الحزمة في Python (بتعبير أدق ، Django) ، وقاعدة البيانات هي PostgreSQL ، وبالنسبة لبعض المهام ، Redis. باستخدام Redux ، نقوم بتخزين تخزين المعلومات وإرسال الإجراءات التي تتم معالجتها بواسطة Redux و Redux-Saga. تتم معالجة جميع الآثار الجانبية ، مثل طلبات الخادم والمكالمات إلى Yandex.Metrica وإعادة التوجيه ، فقط في Redux-Saga. وتحدث جميع التعديلات في مخفضات Redux.

كيف لا نغفل سجل في iframe الخاص بك


الآن على منصتنا ، التدريب مفتوح في ثلاث مهن: المطور الأمامي ، مطور الويب ، محلل البيانات. ونحن نشهد بنشاط أدوات لكل دورة.

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

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

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


دفتر الطالب نفسه (على اليمين) هو مجرد iframe يؤدي عنوان URL الخاص به إلى دفتر ملاحظات محدد في JupyterHub.

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

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

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

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

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

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

عندما نقر على زر "التحقق من المهام" ، يتم إرسال الحدث المقابل إلى متجر Redux: "فلان وفلان ، لقد تم فحصنا." Redux-Saga يرى الإجراء يصل ويؤدي postMessage في iframe. الآن تنتظر iframe لإعطاء إجابة. في هذه الأثناء ، يرى طالبنا مؤشر التنزيل على زر "التحقق من المهمة" ويفهم أن جهاز المحاكاة لا يعلق ، لكنه "يفكر". وفقط عندما يعود postMessage يقول أن الحفظ قد اكتمل ، يستمر Redux-Saga في العمل ويرسل طلبًا إلى الواجهة الخلفية. يتم فحص المهمة على الخادم - الحل الصحيح أم لا ، إذا ارتكبت أخطاء ، ثم أيها ، وما إلى ذلك ، ويتم تخزين هذه المعلومات بدقة في متجر Redux. ومن هناك ، يقوم البرنامج النصي للواجهة الأمامية بسحبه إلى واجهة الدرس.

هنا هو المخطط الذي صدر في النهاية:



(1) نضغط على الزر "التحقق من المهمة" (تحقق) → (2) نرسل الإجراء CHECK_NOTEBOOK_REQUEST → (3) نرسل إجراء التحقق → (2) نرسل الإجراء SAVE_NOTEBOOK_REQUEST → (3) نلحق الإجراء ونرسل postMessage في الحدث iframe → (4) تلقي رسالة → (5) يتم حفظ دفتر الملاحظات → (4) تلقي حدث من Jupyter API تم حفظه في دفتر الملاحظات وإرسال postMessage دفتر المحفوظة → (1) حدث تلقي → (2) إرسال إجراء SAVE_NOTEBOOK_SUCCESS → (3) نحن التقاط الإجراء وإرسال طلب للتحقق من دفتر الملاحظات → (6) → (7) تحقق من وجود هذا دفتر الملاحظات في قاعدة البيانات → (8) → (7) اذهب إلى رمز دفتر الملاحظات → (5) ارجع الرمز → (7) قم بتشغيل رمز التحقق → (9) ) → (7) حصلنا على قطع الاختيار المتبادل → (6) → (3) نرسل CHECK_NOTEBOOK_SUCCESS العمل → (2) وصولا الى التحقق من وقفت استجابة → (1) رسم نتيجة

دعونا نرى كيف يعمل كل هذا في سياق الكود.

لدينا في الواجهة الأمامية trainer_type_jupyter.jsx - البرنامج النصي للصفحة حيث يتم رسم دفتر ملاحظاتنا.

<div className="trainer__right-column"> {notebookLinkIsLoading ? ( <iframe className="trainer__jupiter-frame" ref={this.onIframeRef} src={notebookLink} /> ) : ( <Spin size="l" mix="trainer__jupiter-spin" /> )} </div> 

بعد النقر فوق الزر "التحقق من الوظيفة" ، يتم استدعاء طريقة handleCheckTasks.

 handleCheckTasks = () => { const {checkNotebook, lesson} = this.props; checkNotebook({id: lesson.id, iframe: this.iframeRef}); }; 

في الواقع ، تعمل handleCheckTasks على استدعاء إجراء Redux باستخدام المعلمات التي تم تمريرها.

 export const checkNotebook = getAsyncActionsFactory(CHECK_NOTEBOOK).request; 

هذا إجراء شائع مصمم من أجل Redux-Saga وأساليب غير متزامنة. هنا تحصل getAsyncActionsFactory على ثلاثة إجراءات:

// utils / store-helpers / async.js

 export function getAsyncActionsFactory(type) { const ASYNC_CONSTANTS = getAsyncConstants(type); return { request: payload => ({type: ASYNC_CONSTANTS.REQUEST, payload}), error: (response, request) => ({type: ASYNC_CONSTANTS.ERROR, response, request}), success: (response, request) => ({type: ASYNC_CONSTANTS.SUCCESS, response, request}), } } 

وفقًا لذلك ، ينشئ getAsyncConstants ثلاثة ثوابت من النموذج * _REQUEST و * _SUCCESS و * _ERROR.

الآن دعونا نرى كيف ستتعامل Redux-Saga مع كل هذا الاقتصاد:

// trainer.saga.js

 function* watchCheckNotebook() { const watcher = createAsyncActionSagaWatcher({ type: CHECK_NOTEBOOK, apiMethod: Api.checkNotebook, preprocessRequestGenerator: function* ({id, iframe}) { yield put(trainerActions.saveNotebook({iframe})); yield take(getAsyncConstants(SAVE_NOTEBOOK).SUCCESS); return {id}; }, successHandlerGenerator: function* ({response}) { const {completed_tests: completedTests} = response; for (let id of completedTests) { yield put(trainerActions.setTaskSolved(id)); } }, errorHandlerGenerator: function* ({response: error}) { yield put(appActions.setNetworkError(error)); } }); yield watcher(); } 

السحر؟ لا شيء غير عادي. كما ترون ، ينشئ createAsyncActionSagaWatcher ببساطة علامة مائية يمكنها معالجة البيانات مسبقًا في الإجراء ، وتقديم طلب على عنوان URL معين ، وإرسال إجراء * _REQUEST ، وإرسال * _SUCCESS و * _ERROR بعد استجابة ناجحة من الخادم. بالإضافة إلى ذلك ، بالطبع ، لكل خيار ، يتم توفير معالجات داخل الساعة.

ربما لاحظت أننا في المعالج الأولي للبيانات ندعو Redux-Saga آخر ، انتظر حتى ينتهي بـ SUCCESS ، وبعد ذلك فقط استمر في العمل. وبالطبع ، فإن iframes لا تحتاج إلى إرسالها إلى الخادم ، لذلك نحن نعطي فقط المعرف.

ألق نظرة فاحصة على وظيفة saveNotebook:

 function* saveNotebook({payload: {iframe}}) { iframe.contentWindow.postMessage(JSON.stringify({ type: 'save-notebook' }), '*'); yield; } 

لقد وصلنا إلى أهم آلية في تفاعل iframe مع الواجهة الأمامية - postMessage. ترسل جزء الشفرة المحدد إجراءً بنوع الحفظ ، والذي تتم معالجته داخل iframe.

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

 define([ 'base/js/namespace', 'base/js/events' ], function( Jupyter, events ) {...}); 

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

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

 window.addEventListener('message', actionListener); 

الآن سوف نقدم معالجتها:

 function actionListener({data: eventString}) { let event = ''; try { event = JSON.parse(eventString); } catch(e) { return; } switch (event.type) { case 'save-notebook': Jupyter.actions.call('jupyter-notebook:save-notebook'); Break; ... default: break; } } 

يتم تجاهل جميع الأحداث التي لا تناسب تنسيقنا بجرأة.

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

 events.on('notebook_saved.Notebook', actionDispatcher); function actionDispatcher(event) { switch (event.type) { case 'select': const selectedCell = Jupyter.notebook.get_selected_cell(); dispatchEvent({ type: event.type, data: {taskId: getCellTaskId(selectedCell)} }); return; case 'notebook_saved': default: dispatchEvent({type: event.type}); } } function dispatchEvent(event) { return window.parent.postMessage( typeof event === 'string' ? event : JSON.stringify(event), '*' ); } 

بمعنى آخر ، ما عليك سوى إرسال {type: 'notebook_saved'} لأعلى. هذا يعني أنه تم حفظ دفتر الملاحظات.

لنعد إلى مكوننا:

//trainer_type_jupyter.jsx

 componentDidMount() { const {getNotebookLink, lesson} = this.props; getNotebookLink({id: lesson.id}); window.addEventListener('message', this.handleWindowMessage); } 

عند تركيب المكون ، نطلب من الخادم رابطًا إلى دفتر الملاحظات والاشتراك في جميع الإجراءات التي يمكن أن تطير إلينا:

 handleWindowMessage = ({data: eventString}) => { const {activeTaskId, history, match: {params}, setNotebookSaved, tasks} = this.props; let event = null; try { event = JSON.parse(eventString); } catch(e) { return; } const {type, data} = event; switch (type) { case 'app_initialized': this.selectTaskCell({taskId: activeTaskId}) return; case 'notebook_saved': setNotebookSaved(); return; case 'select': { const taskId = data && data.taskId; if (!taskId) { return } const task = tasks.find(({id}) => taskId === id); if (task && task.status === TASK_STATUSES.DISABLED) { this.selectTaskCell({taskId: null}) return; } history.push(reversePath(urls.trainerTask, {...params, taskId})); return; } default: break; } }; 

هذا هو المكان الذي يتم فيه استدعاء إرسال إجراء setNotebookSaved ، مما سيتيح لـ Redux-Saga مواصلة العمل وحفظ دفتر الملاحظات.

مواطن الخلل في الاختيار


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

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

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

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

قليلا عن كيفية تعديل الواجهة الأمامية لدينا لحل المشكلة. دعنا ننتقل مرة أخرى إلى trainer_type_jupyter.jsx - سنركز على app_initialized وحددها.

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

وهي:

// trainer_type_jupyter.jsx

 selectTaskCell = ({taskId}) => { const {selectCell} = this.props; if (!this.iframeRef) { return; } selectCell({iframe: this.iframeRef, taskId}); }; 

// trainer.actions.js

 export const selectCell = ({iframe, taskId}) => ({ type: SELECT_CELL, iframe, taskId }); 

// trainer.saga.js

 function* selectCell({iframe, taskId}) { iframe.contentWindow.postMessage(JSON.stringify({ type: 'select-cell', data: {taskId} }), '*'); yield; } function* watchSelectCell() { yield takeEvery(SELECT_CELL, selectCell); } 

// custom.js (Jupyter plugin)

 function getCellTaskId(cell) { const notebook = Jupyter.notebook; while (cell) { const tags = cell.metadata.tags; const taskId = tags && tags[0]; if (taskId) { return taskId; } cell = notebook.get_prev_cell(cell); } return null; } function selectCell({taskId}) { const notebook = Jupyter.notebook; const selectedCell = notebook.get_selected_cell(); if (!taskId) { selectedCell.unselect(); return; } if (selectedCell && selectedCell.selected && getCellTaskId(selectedCell) === taskId) { return; } const index = notebook.get_cells() .findIndex(cell => getCellTaskId(cell) === taskId); if (index < 0) { return; } notebook.select(index); const cell = notebook.get_cell(index); cell.element[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); } function actionListener({data: eventString}) { ... case 'select-cell': selectCell(event.data); break; 

يمكنك الآن تبديل الخلايا والتعلم من iframe أنه تم تبديل الخلية.

عند تبديل الخلية ، نغير عنوان URL ونقع في مهمة أخرى. يبقى فقط القيام بالعكس - عند اختيار مهمة أخرى في الواجهة ، قم بتبديل الخلية. سهل:

 componentDidUpdate({match: {params: {prevTaskId}}) { const {match: {params: {taskId}}} = this.props; if (taskId !== prevTaskId) { this.selectTaskCell({taskId}); 

مرجل منفصل للكمال


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

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

• تنتشر شظايا الكود التي تحل المشكلات خلال المشروع. يتم إجراء التواصل مع iframes من كل من Redux-Saga ومن المكون ، وهو بالتأكيد غير مثالي.

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

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

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

ربما الأفكار تأتي معك؟

Source: https://habr.com/ru/post/ar453876/


All Articles