Arbeiten Sie mit Worker "wie Sie möchten", nicht "so viel wie möglich".

In diesem Artikel wird die Methode DIRTY, unsicher, "Krücke", beängstigend usw. verwendet. Die Schwachen lesen nicht!


Ich muss sofort sagen, dass es nicht möglich war, einige Usability-Probleme zu lösen: Sie können Closure nicht in dem Code verwenden, der an den Worker übergeben wird.
Arbeiten Sie mit Worker "wie Sie möchten", aber nicht "so viel wie möglich".


Wir alle mögen neue Technologien und wir mögen es, wenn es bequem ist, diese Technologien zu verwenden. Im Falle eines Arbeitnehmers ist dies jedoch nicht ganz richtig. Worker arbeitet mit einer Datei oder einem Dateilink, dies ist jedoch unpraktisch. Ich möchte in der Lage sein, jede Aufgabe in den Arbeiter zu stecken, und nicht nur speziell geplanten Code.


Was ist erforderlich, um die Arbeit mit dem Arbeitnehmer bequemer zu gestalten? Meiner Meinung nach Folgendes:


  • Möglichkeit, jederzeit beliebigen Code im Worker auszuführen
  • Fähigkeit, komplexe Daten an Worker zu übergeben (Klasseninstanzen, Funktionen)
  • Fähigkeit, Versprechen mit einer Antwort des Arbeitnehmers zu erhalten.

Zunächst benötigen wir ein Kommunikationsprotokoll zwischen dem Arbeiter und dem Hauptfenster. Im Allgemeinen ist ein Protokoll einfach die Struktur und die Datentypen, mit denen das Browserfenster und der Worker kommunizieren. Es ist nichts kompliziert. Sie können so etwas verwenden oder Ihre eigene Version schreiben. In jeder Nachricht haben wir eine ID und Daten, die für einen bestimmten Nachrichtentyp spezifisch sind. Zunächst haben wir zwei Arten von Nachrichten für den Arbeiter:


  • Hinzufügen von Bibliotheken / Dateien zum Worker
  • Arbeitsbeginn

Datei innerhalb des Arbeiters


Bevor Sie mit dem Erstellen eines Workers beginnen, müssen Sie eine Datei beschreiben, die im Worker funktioniert, und das von uns beschriebene Protokoll unterstützen. Ich liebe OOP , also wird es eine Klasse namens WorkerBody sein. Diese Klasse muss das Ereignis über das übergeordnete Fenster abonnieren.


 self.onmessage = (message) => { this.onMessage(message.data); }; 

Jetzt können wir Ereignisse aus dem übergeordneten Fenster abhören. Wir haben zwei Arten von Ereignissen: diejenigen, auf die die Antwort impliziert ist, und alle anderen. Wir verarbeiten Ereignisse.
Das Hinzufügen von Bibliotheken und Dateien zum Worker erfolgt über die importScripts- API.


Und das Schlimmste: Wir werden eval verwenden , um eine beliebige Funktion auszuführen.


 ... onMessage(message) { switch (message.type) { case MESSAGE_TYPE.ADD_LIBS: this.addLibs(message.libs); break; case MESSAGE_TYPE.WORK: this.doWork(message); break; } } doWork(message) { try { const processor = eval(message.job); const params = this._parser.parse(message.params); const result = processor(params); if (result && result.then && typeof result.then === 'function') { result.then((data) => { this.send({ id: message.id, state: true, body: data }); }, (error) => { if (error instanceof Error) { error = String(error); } this.send({ id: message.id, state: false, body: error }); }); } else { this.send({ id: message.id, state: true, body: result }); } } catch (e) { this.send({ id: message.id, state: false, body: String(e) }); } } send(data) { data.body = this._serializer.serialize(data.body); try { self.postMessage(data); } catch (e) { const toSet = { id: data.id, state: false, body: String(e) }; self.postMessage(toSet); } } 

Die onMessage Methode onMessage für den Empfang der Nachricht und die Auswahl des doWork onMessage verantwortlich. doWork die übergebene Funktion und sendet die Antwort an das übergeordnete Fenster.


Parser und Serializer


Nachdem wir den Inhalt des Workers haben, müssen wir lernen, wie Daten serialisiert und analysiert werden, um sie an den Worker weiterzugeben. Beginnen wir mit dem Serializer. Wir möchten in der Lage sein, alle Daten an den Worker zu übergeben, einschließlich Klasseninstanzen, Klassen und Funktionen. Mit den nativen Funktionen von Worker können wir jedoch nur JSON-ähnliche Daten übertragen. Um dieses Verbot zu umgehen , brauchen wir eine Bewertung . Alles, was JSON nicht akzeptieren kann, werden wir in die entsprechenden String-Konstruktionen einschließen und auf der anderen Seite ausführen. Um die Unveränderlichkeit zu erhalten, werden die erhaltenen Daten im laufenden Betrieb geklont, und diejenigen, die nicht auf die übliche Weise serialisiert werden können, werden durch Serviceobjekte ersetzt, und sie werden wiederum durch den Parser auf der anderen Seite ersetzt. Auf den ersten Blick scheint diese Aufgabe nicht schwierig zu sein, aber es gibt viele Fallstricke. Die schlimmste Einschränkung dieses Ansatzes ist die Unfähigkeit, einen Verschluss zu verwenden, der einen etwas anderen Schreibstil für Code enthält. Beginnen wir mit dem Einfachsten, mit der Funktion. Zuerst müssen Sie lernen, wie Sie eine Funktion von einem Klassenkonstruktor unterscheiden.


Versuchen wir zu unterscheiden:


 static isFunction(Factory){ if (!Factory.prototype) { // Arrow function has no prototype return true; } const prototypePropsLength = Object.getOwnPropertyNames(Factory.prototype) .filter(item => item !== 'constructor') .length; return prototypePropsLength === 0 && Serializer.getClassParents(Factory).length === 1; } static getClassParents(Factory) { const result = [Factory]; let tmp = Factory; let item = Object.getPrototypeOf(tmp); while (item.prototype) { result.push(item); tmp = item; item = Object.getPrototypeOf(tmp); } return result.reverse(); } 

Zuerst werden wir herausfinden, ob die Funktion einen Prototyp hat. Wenn dies nicht der Fall ist, ist dies definitiv eine Funktion. Dann betrachten wir die Anzahl der Eigenschaften im Prototyp, und wenn im Prototyp nur der Konstruktor und die Funktion nicht die Erben einer anderen Klasse sind, betrachten wir dies als eine Funktion.


Nachdem wir eine Funktion gefunden haben, ersetzen wir sie einfach durch ein Serviceobjekt mit den Feldern __type = "serialized-function" und template , die der Vorlage dieser Funktion ( func.toString() ) entsprechen.


Überspringen Sie vorerst die Klasse und analysieren Sie die Klasseninstanz. Weiter in den Daten müssen wir gewöhnliche Objekte von Klasseninstanzen unterscheiden.


 static isInstance(some) { const constructor = some.constructor; if (!constructor) { return false; } return !Serializer.isNative(constructor); } static isNative(data) { return /function .*?\(\) \{ \[native code\] \}/.test(data.toString()); } 

Wir betrachten ein Objekt als gewöhnlich, wenn es keinen Konstruktor hat oder sein Konstruktor eine native Funktion ist. Nachdem wir die Klasseninstanz identifiziert haben, ersetzen wir sie durch ein Serviceobjekt mit Feldern:


  • __type - 'serialisierte Instanz'
  • data - Daten, die in der Instanz waren
  • index - Der Klassenindex dieser Instanz in der Klassendienstliste.

Um Daten zu übertragen, müssen wir ein zusätzliches Feld erstellen: Darin speichern wir eine Liste aller eindeutigen Klassen, die wir übergeben. Der schwierigste Teil ist, dass wenn eine Klasse erkannt wird, nicht nur ihre Vorlage, sondern auch die Vorlage aller übergeordneten Klassen genommen und als separate Klassen gespeichert wird - so dass jedes "übergeordnete" nicht mehr als einmal übergeben wird - und die Prüfung auf Instanz von gespeichert wird. Das Definieren einer Klasse ist einfach: Dies ist eine Funktion, die unseren Serializer.isFunction-Test nicht bestanden hat. Beim Hinzufügen einer Klasse prüfen wir, ob eine solche Klasse in der Liste der serialisierten Daten vorhanden ist, und fügen nur eindeutige hinzu. Der Code, der die Klasse in einer Vorlage sammelt, ist ziemlich groß und liegt hier .


Im Parser gehen wir zuerst alle an uns übergebenen Klassen durch und kompilieren sie, wenn sie noch nicht übergeben wurden. Dann durchlaufen wir rekursiv jedes Datenfeld und ersetzen die Serviceobjekte durch kompilierte Daten. Das Interessanteste ist in der Klasseninstanz. Wir haben eine Klasse und es gibt Daten, die in ihrer Instanz waren, aber wir können sie nicht einfach instanziieren, da der Aufruf des Konstruktors möglicherweise Parameter enthält, die wir nicht haben. Eine fast vergessene Object.create- Methode hilft uns dabei, ein Objekt mit einem bestimmten Prototyp zurückzugeben. Wir vermeiden es also, den Konstruktor aufzurufen und eine Instanz der Klasse abzurufen, und schreiben dann einfach die Eigenschaften in die Instanz um.


Schöpfungsarbeiter


Damit der Worker erfolgreich arbeiten kann, benötigen wir einen Parser und einen Serializer innerhalb und außerhalb des Workers. Daher nehmen wir den Serializer und verwandeln den Serializer, Parser und den Body des Workers in eine Vorlage. Wir machen einen Blob aus der Vorlage und erstellen einen Download-Link über URL.createObjectURL (diese Methode funktioniert möglicherweise nicht mit einigen "Content-Security-Policy"). Diese Methode eignet sich auch zum Ausführen von beliebigem Code aus einer Zeichenfolge.


 _createWorker(customWorker) { const template = `var MyWorker = ${this._createTemplate(customWorker)};`; const blob = new Blob([template], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } _createTemplate(WorkerBody) { const Name = Serializer.getFnName(WorkerBody); if (!Name) { throw new Error('Unnamed Worker Body class! Please add name to Worker Body class!'); } return [ '(function () {', this._getFullClassTemplate(Serializer, 'Serializer'), this._getFullClassTemplate(Parser, 'Parser'), this._getFullClassTemplate(WorkerBody, 'WorkerBody'), `return new WorkerBody(Serializer, Parser)})();` ].join('\n'); } 

Ergebnis


So haben wir eine benutzerfreundliche Bibliothek erhalten, die beliebigen Code in Worker ausführen kann. Es unterstützt Klassen aus TypeScript. Zum Beispiel:


 const wrapper = workerWrapper.create(); wrapper.process((params) => { // This code in worker. Cannot use closure! // do some hard work return 100; // or return Promise.resolve(100) }, params).then((result) => { // result = 100; }); wrapper.terminate() // terminate for kill worker process 

Weitere Pläne


Diese Bibliothek ist leider alles andere als ideal. Es ist erforderlich, Unterstützung für Setter und Getter für Klassen, Objekte, Prototypen und statische Eigenschaften hinzuzufügen. Wir möchten auch Caching hinzufügen, ein alternatives Skript ohne Auswertung über URL.createObjectURL ausführen lassen und der Assembly eine Datei mit dem Inhalt des Workers hinzufügen (falls die Erstellung nicht im laufenden Betrieb verfügbar ist). Komm ins Repository !

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


All Articles