Wie bei Yandex.Practicum gewann das Front-End-Desync: eine akrobatische Nummer mit Redux-Saga, postMessage und Jupyter

Mein Name ist Artyom Nesmiyanov, ich bin ein Full-Stack-Entwickler bei Yandex.Practicum, ich beschäftige mich hauptsächlich mit dem Frontend. Wir glauben, dass es möglich und notwendig ist, Programmierung, Datenanalyse und anderes digitales Handwerk mit Vergnügen zu studieren. Und fang an zu lernen und mach weiter. Jeder Entwickler, der sich selbst nicht aufgegeben hat, „macht weiter“. Wir auch. Daher nehmen wir Arbeitsaufgaben als Lernformat wahr. Und einer der letzten hat mir und den Jungs geholfen, besser zu verstehen, in welche Richtung wir unseren Frontend-Stack entwickeln sollen.



Aus wem und woraus besteht der Workshop?


Unser Entwicklungsteam ist äußerst kompakt. Es gibt nur zwei Leute im Backend, im Frontend - vier, wenn man mich für einen vollen Stack hält. Von Zeit zu Zeit verstärken sich Jungs von Yandex.Tutorial bei uns. Wir arbeiten mit zweiwöchigen Sprints an Scrum.

Unser Frontend basiert auf React.js in Verbindung mit Redux / Redux-Saga. Wir verwenden Express, um mit dem Backend zu kommunizieren. Der Backend-Teil des Stacks befindet sich in Python (genauer gesagt Django), die Datenbank ist PostgreSQL und für einige Aufgaben Redis. Mit Redux speichern wir Informationsspeicher und senden Aktionen, die von Redux und Redux-Saga verarbeitet werden. Alle Nebenwirkungen wie Serveranforderungen, Aufrufe von Yandex.Metrica und Weiterleitungen werden nur in Redux-Saga verarbeitet. Alle Datenänderungen erfolgen in Redux-Reduzierern.

So ĂĽbersehen Sie ein Protokoll in Ihrem Iframe nicht


Auf unserer Plattform sind Schulungen in drei Berufen möglich: Front-End-Entwickler, Webentwickler und Datenanalyst. Und wir sägen aktiv Werkzeuge für jeden Kurs.

Für den sechsmonatigen Kurs " Data Analyst " haben wir einen interaktiven Simulator erstellt, in dem wir Benutzern den Umgang mit dem Jupyter-Notizbuch beibringen. Dies ist eine coole Hülle für interaktives Computing, die von Datenwissenschaftlern zu Recht geliebt wird. Alle Vorgänge in der Umgebung werden im Notebook ausgeführt, jedoch auf einfache Weise - in einem Notebook (wie ich es später nennen werde).

Erleben Sie Eingabeaufforderungen, und wir sind sicher: Es ist wichtig, dass die Schulungsaufgaben nahezu real sind. EinschlieĂźlich in Bezug auf das Arbeitsumfeld. Daher musste sichergestellt werden, dass innerhalb der Lektion der gesamte Code direkt im Notizbuch geschrieben, ausgefĂĽhrt und ĂĽberprĂĽft werden konnte.

Bei der grundlegenden Umsetzung der Schwierigkeiten traten keine auf. Das Notizbuch selbst wurde in einem separaten Iframe abgelegt, die Logik zur ĂśberprĂĽfung wurde im Backend vorgeschrieben.


Das SchĂĽler-Notizbuch selbst (rechts) ist nur ein Iframe, dessen URL zu einem bestimmten Notizbuch in JupyterHub fĂĽhrt.

In erster Näherung funktionierte alles reibungslos und reibungslos. Während des Testens kamen jedoch Absurditäten heraus. Beispielsweise wird garantiert, dass Sie die richtige Version des Codes in ein Notizbuch eingeben. Nachdem Sie jedoch auf die Schaltfläche "Testaufgabe" geklickt haben, antwortet der Server, dass die Antwort angeblich falsch ist. Und warum - ein Rätsel.

Nun, was passiert, haben wir am selben Tag festgestellt, als wir einen Fehler gefunden haben: Es stellte sich heraus, dass die Lösung, die nicht flog, die aktuelle war. Die Lösung wurde nur in das Jupyter-Notizbuch-Formular geschrieben, aber die vorherige wurde bereits gelöscht. Das Notebook selbst hatte keine Zeit zum Überleben, und wir haben das Backend verlangsamt, damit es die darin enthaltene Aufgabe überprüft. Was er natürlich nicht konnte.

Wir mussten den Rassinhron zwischen dem Speichern des Notebooks und dem Senden einer Anfrage an den Server zur Überprüfung loswerden. Der Haken war, dass es notwendig war, den Iframe des Notizbuchs mit dem übergeordneten Fenster zu kommunizieren, dh mit dem Frontend, auf dem sich die gesamte Lektion drehte. Natürlich war es unmöglich, ein Ereignis direkt zwischen ihnen weiterzuleiten: Sie leben auf verschiedenen Domänen.

Auf der Suche nach einer Lösung fand ich heraus, dass Jupyter Notebook die Verbindung seiner Plugins ermöglicht. Es gibt ein Jupiter-Objekt - ein Notizbuch - mit dem Sie arbeiten können. Die Arbeit damit beinhaltet Ereignisse, einschließlich der Aufbewahrung des Notizbuchs sowie den Aufruf der entsprechenden Aktion. Nachdem wir das Innere von Jupyter herausgefunden hatten (ich musste: es gibt keine normale Dokumentation dafür), haben die Jungs und ich es getan - wir haben unser eigenes Plug-In dafür erstellt und mithilfe des postMessage-Mechanismus eine koordinierte Arbeit der Elemente erreicht, aus denen die Workshop-Lektion zusammengestellt wurde.

Wir haben eine Problemumgehung ausgearbeitet, die die Tatsache berücksichtigt, dass unser Stack zunächst die bereits erwähnte Redux-Saga enthält - um es einfach auszudrücken: Middleware über Redux, die es ermöglicht, flexibler mit Nebenwirkungen zu arbeiten. Das Speichern eines Notebooks ist beispielsweise nur ein Nebeneffekt. Wir schicken etwas an das Backend, warten auf etwas, holen etwas. All diese Bewegungen werden in Redux-Saga verarbeitet: Sie werfen Ereignisse in das Frontend und diktieren ihm, wie was in der Benutzeroberfläche angezeigt werden soll.

Was ist das Ergebnis? PostMessage wird erstellt und mit einem Notizbuch an den Iframe gesendet. Wenn ein Iframe sieht, dass etwas von auĂźen gekommen ist, analysiert er die empfangene Zeichenfolge. Als er erkennt, dass er das Notizbuch behalten muss, fĂĽhrt er diese Aktion aus und sendet seinerseits eine Antwort postMessage ĂĽber die AusfĂĽhrung der Anforderung.

Wenn wir auf die Schaltfläche "Testaufgabe" klicken, wird das entsprechende Ereignis an den Redux Store gesendet: "So und so, wir wurden überprüft." Redux-Saga sieht die Aktion eintreffen und postMessage in einem Iframe ausführen. Jetzt wartet sie darauf, dass der Iframe eine Antwort gibt. In der Zwischenzeit sieht unser Schüler die Download-Anzeige auf der Schaltfläche "Aufgabe überprüfen" und versteht, dass der Simulator nicht hängt, sondern "denkt". Und erst wenn postMessage zurückkommt und mitteilt, dass das Speichern abgeschlossen ist, arbeitet Redux-Saga weiter und sendet eine Anfrage an das Backend. Die Aufgabe wird auf dem Server überprüft - die richtige Lösung oder nicht, wenn Fehler gemacht werden, welche usw., und diese Informationen werden ordentlich im Redux Store gespeichert. Und von dort zieht das Front-End-Skript es in die Unterrichtsoberfläche.

Hier ist das Diagramm, das am Ende herauskam:



(1) Wir drücken die Taste "Aufgabe prüfen" (Prüfung) → (2) Wir senden die Aktion CHECK_NOTEBOOK_REQUEST → (3) Wir senden die Aktion der Prüfung → (2) Wir senden die Aktion SAVE_NOTEBOOK_REQUEST → (3) Wir fangen die Aktion ab und senden postMessage im Ereignis iframe → save (4) Nachricht empfangen → (5) Notizbuch wird gespeichert → (4) Empfängt das Ereignis von der Jupyter-API, dass das Notizbuch gespeichert wurde, und sendet postMessage-Notizbuch gespeichert → (1) Empfangen des Ereignisses → (2) Senden der Aktion SAVE_NOTEBOOK_SUCCESS → (3) Wir fangen die Aktion ab und senden Sie eine Anfrage zum Überprüfen des Notizbuchs → (6) → (7) Überprüfen Sie, ob sich dieses Notizbuch in der Datenbank befindet → (8) → (7) Suchen Sie den Notizbuchcode → (5) Geben Sie den Code zurück → (7) Führen Sie die Codeprüfung aus → (9) ) → (7) Wir bekommen einen Schnitt tat prüfen → (6) → (3) wir Aktion CHECK_NOTEBOOK_SUCCESS → senden (2) nach unten Antwort überprüfen → sided (1) Zeichnen Ergebnis

Mal sehen, wie das alles im Kontext des Codes funktioniert.

Wir haben am Frontend manager_type_jupyter.jsx - das Skript der Seite, auf der unser Notizbuch gezeichnet ist.

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

Nach dem Klicken auf die Schaltfläche "Job prüfen" wird die handleCheckTasks-Methode aufgerufen.

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

Tatsächlich dient handleCheckTasks dazu, die Redux-Aktion mit den übergebenen Parametern aufzurufen.

 export const checkNotebook = getAsyncActionsFactory(CHECK_NOTEBOOK).request; 

Dies ist eine häufige Aktion für Redux-Saga und asynchrone Methoden. Hier generiert getAsyncActionsFactory drei Aktionen:

// 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}), } } 

Dementsprechend generiert getAsyncConstants drei Konstanten der Form * _REQUEST, * _SUCCESS und * _ERROR.

Nun wollen wir sehen, wie unsere Redux-Saga mit all dieser Wirtschaft umgehen wird:

// 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(); } 

Die Magie? Nichts außergewöhnliches. Wie Sie sehen können, erstellt createAsyncActionSagaWatcher einfach ein Wasserzeichen, mit dem Daten, die in die Aktion eingehen, vorverarbeitet, eine Anforderung unter einer bestimmten URL gestellt, die Aktion * _REQUEST gesendet und * _SUCCESS und * _ERROR nach einer erfolgreichen Antwort vom Server gesendet werden können. Zusätzlich sind für jede Option natürlich Handler in der Uhr vorgesehen.

Sie haben wahrscheinlich bemerkt, dass wir im Datenpräprozessor eine andere Redux-Saga aufrufen, warten, bis sie mit SUCCESS endet, und erst dann weiterarbeiten. Und natürlich müssen iframes nicht an den Server gesendet werden, daher geben wir nur die ID an.

Schauen Sie sich die Funktion saveNotebook genauer an:

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

Wir haben den wichtigsten Mechanismus in der Interaktion von iframes mit dem Frontend erreicht - postMessage. Das angegebene Codefragment sendet eine Aktion mit dem Typ "Notizbuch speichern", die im Iframe verarbeitet wird.

Ich habe bereits erwähnt, dass wir ein Plug-In für das Jupyter-Notebook schreiben müssen, das in das Notebook geladen wird. Diese Plugins sehen ungefähr so ​​aus:

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

Um solche Erweiterungen zu erstellen, mĂĽssen Sie sich mit der Jupyter Notebook-API selbst befassen. Leider gibt es keine eindeutige Dokumentation dazu. Aber Quellcodes sind verfĂĽgbar, und ich habe mich damit befasst. Es ist gut, dass der Code dort lesbar ist.

Dem Plugin muss beigebracht werden, mit dem übergeordneten Fenster im Frontend der Lektion zu kommunizieren: Schließlich ist die Desynchronisierung zwischen ihnen die Ursache für den Fehler bei der Aufgabenüberprüfung. Zunächst abonnieren wir alle Nachrichten, die wir erhalten:

 window.addEventListener('message', actionListener); 

Jetzt werden wir ihre Verarbeitung bereitstellen:

 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; } } 

Alle Ereignisse, die nicht zu unserem Format passen, werden mutig ignoriert.

Wir sehen, dass das Ereignis zum Speichern des Notizbuchs bei uns eintrifft, und rufen die Aktion zum Speichern des Notizbuchs auf. Es bleibt nur eine Nachricht zurĂĽckzusenden, dass das Notizbuch erhalten geblieben ist:

 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), '*' ); } 

Mit anderen Worten, senden Sie einfach {type: 'notebook_saved'} nach oben. Dies bedeutet, dass das Notizbuch erhalten geblieben ist.

Kehren wir zu unserer Komponente zurĂĽck:

//trainer_type_jupyter.jsx

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

Beim Mounten der Komponente bitten wir den Server um einen Link zum Notebook und abonnieren alle Aktionen, die zu uns fliegen können:

 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; } }; 

Hier wird der Aktionsversand setNotebookSaved aufgerufen, mit dem Redux-Saga weiterarbeiten und das Notizbuch speichern kann.

Pannen der Wahl


Wir haben den Fehler bei der Aufbewahrung von Notebooks behoben. Und sofort auf ein neues Problem umgestellt. Es musste gelernt werden, Aufgaben zu blockieren, zu denen der Schüler noch nicht gelangt war. Mit anderen Worten, es war notwendig, die Navigation zwischen unserem interaktiven Simulator und dem Jupyter-Notizbuch zu synchronisieren: In einer Lektion befanden sich ein Notizbuch mit mehreren Aufgaben im Iframe, deren Übergänge mit den Änderungen in der gesamten Benutzeroberfläche der Lektion koordiniert werden mussten. Wenn Sie beispielsweise auf die zweite Aufgabe in der Lektionsoberfläche des Notizbuchs klicken, wird in die Zelle gewechselt, die der zweiten Aufgabe entspricht. Und umgekehrt: Wenn Sie im Jupyter Notebook-Frame eine Zelle auswählen, die mit der dritten Aufgabe verknüpft ist, sollte sich die URL in der Adressleiste des Browsers sofort ändern, und dementsprechend sollte der Begleittext mit der Theorie für die dritte Aufgabe in der Benutzeroberfläche der Lektion angezeigt werden.

Es gab eine schwierigere Aufgabe. Tatsache ist, dass unser Schulungsprogramm auf die konsequente Weitergabe von Lektionen und Aufgaben ausgelegt ist. Währenddessen hindert standardmäßig im Jupiter-Notizbuch nichts den Benutzer daran, eine Zelle zu öffnen. In unserem Fall ist jede Zelle eine separate Aufgabe. Es stellte sich heraus, dass Sie die erste und dritte Aufgabe lösen und die zweite überspringen können. Das Risiko eines nichtlinearen Durchgangs der Lektion musste beseitigt werden.

Die Lösung basierte auf derselben postMessage. Nur mussten wir uns weiter mit der Jupyter Notebook-API befassen, insbesondere mit den Möglichkeiten des Jupiter-Objekts. Und entwickeln Sie einen Mechanismus, mit dem Sie überprüfen können, an welche Aufgabe die Zelle angehängt ist. In seiner allgemeinsten Form ist es wie folgt. In der Struktur des Notizbuchs gehen die Zellen nacheinander nacheinander. Sie können Metadaten haben. Das Feld "Tags" wird in den Metadaten bereitgestellt, und Tags sind nur Bezeichner von Aufgaben innerhalb der Lektion. Darüber hinaus können Sie mithilfe von Tagging-Zellen festlegen, ob sie vom Schüler bisher blockiert werden sollen. Infolgedessen senden wir gemäß dem aktuellen Modell des Simulators durch Klicken auf die Zelle postMessage vom iframe an unser Frontend, das wiederum zum Redux Store geht und anhand der Eigenschaften der Aufgabe prüft, ob es uns jetzt zur Verfügung steht. Wenn nicht verfügbar, wechseln wir zur vorherigen aktiven Zelle.

Wir haben also festgestellt, dass es unmöglich ist, eine Zelle in einem Notizbuch auszuwählen, auf die über die Trainingszeitleiste nicht zugegriffen werden kann. Dies führte zwar zu einem unkritischen, aber fehlerhaften Fehler: Sie versuchen, auf eine Zelle mit einer unzugänglichen Aufgabe zu klicken, und sie „blinkt“ schnell: Es ist klar, dass sie für einen Moment aktiviert, aber sofort blockiert wurde. Obwohl wir diese Rauheit nicht beseitigt haben, stört sie den Unterricht nicht, aber im Hintergrund überlegen wir weiterhin, wie wir damit umgehen sollen (gibt es übrigens irgendwelche Gedanken?).

Ein bisschen darüber, wie wir unser Frontend modifiziert haben, um das Problem zu lösen. Wenden wir uns noch einmal Trainer_type_jupyter.jsx zu - wir konzentrieren uns auf app_initialized und wählen aus.

Mit app_initialized ist alles elementar: Das Notebook wurde geladen, und wir möchten etwas tun. Wählen Sie beispielsweise die aktuelle Zelle abhängig von der ausgewählten Aufgabe aus. Das Plugin wird beschrieben, damit Sie die taskId übergeben und zur ersten Zelle wechseln können, die dieser taskId entspricht.

Nämlich:

// Trainer_Typ_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; 

Jetzt können Sie die Zellen wechseln und aus dem Iframe lernen, dass die Zelle gewechselt wurde.

Beim Wechseln der Zelle ändern wir die URL und fallen in eine andere Aufgabe. Es bleibt nur das Gegenteil zu tun - wenn Sie eine andere Aufgabe in der Schnittstelle auswählen, wechseln Sie die Zelle. Einfach:

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

Separater Kessel fĂĽr Perfektionisten


Es wäre cool, nur damit zu prahlen, wie gut wir gemacht sind. Die Lösung im Endeffekt ist effektiv, obwohl sie etwas chaotisch aussieht: Zusammenfassend haben wir eine Methode, die jede Nachricht verarbeitet, die von außen kommt (in unserem Fall von einem Iframe). Aber in dem System, das wir selbst aufgebaut haben, gibt es Dinge, die ich und meine Kollegen nicht wirklich mögen.

• Es gibt keine Flexibilität bei der Interaktion von Elementen: Wenn wir neue Funktionen hinzufügen möchten, müssen wir das Plugin so ändern, dass es sowohl das alte als auch das neue Kommunikationsformat unterstützt. Es gibt keinen einzigen isolierten Mechanismus für die Arbeit zwischen dem Iframe und unserer Front-End-Komponente, der das Jupyter-Notizbuch in der Lektionsoberfläche rendert und mit unseren Aufgaben arbeitet. Global - es besteht der Wunsch, ein flexibleres System zu schaffen, damit es in Zukunft einfach ist, neue Aktionen, Ereignisse hinzuzufügen und zu verarbeiten. Und das nicht nur beim Jupiter-Notebook, sondern auch bei jedem Iframe in den Simulatoren. Wir versuchen also, den Plug-In-Code über postMessage zu übergeben und ihn (eval) im Plug-In zu rendern.

• Codefragmente, die Probleme lösen, sind im gesamten Projekt verteilt. Die Kommunikation mit iframes erfolgt sowohl über Redux-Saga als auch über die Komponente, was sicherlich nicht optimal ist.

• Iframe selbst mit Jupyter Notebook-Rendering befindet sich auf einem anderen Dienst. Die Bearbeitung ist etwas problematisch, insbesondere in Übereinstimmung mit dem Prinzip der Abwärtskompatibilität. Wenn wir zum Beispiel eine Art Logik am Frontend und im Notebook selbst ändern wollen, müssen wir doppelte Arbeit leisten.

• Viele möchten einfacher implementieren. Nehmen Sie mindestens Reagieren. Er hat eine Menge Lebenszyklusmethoden, von denen jede verarbeitet werden muss. Außerdem verwirrt mich die Bindung an React selbst. Im Idealfall möchte ich mit unseren iframes arbeiten können, unabhängig davon, um welches Front-End-Framework es sich handelt. Im Allgemeinen unterliegt die Überschneidung der von uns ausgewählten Technologien Einschränkungen: Dieselbe Redux-Saga erwartet von uns Redux-Aktionen, nicht von postMessage.

Wir werden also definitiv nicht aufhören, was erreicht wurde. Ein Lehrbuch-Dilemma: Sie können auf die Seite der Schönheit gehen, aber die Optimalität der Leistung opfern oder umgekehrt. Wir haben noch nicht die beste Lösung gefunden.

Vielleicht kommen Ihnen Ideen?

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


All Articles