Web Worker einfacher als Sie dachten

Web Worker einfacher als Sie dachten


In diesem Artikel wird eine schmutzige, unsichere, instabile und beängstigende <em>eval</em> -Methode beschrieben. Wenn Ihnen das unangenehm ist, hören Sie sofort auf zu lesen.


Zunächst einmal blieben einige Probleme mit der Bequemlichkeit ungelöst: In Code, der an Web-Web-Worker gesendet wird, kann das Schließen nicht verwendet werden.




Wir alle mögen neue Technologien, und wir alle mögen neue Technologien, die bequem zu bedienen sind. Bei Web-Workern ist dies jedoch nicht genau der Fall. Web-Worker akzeptieren Dateien oder Links zu Dateien, was nicht bequem ist. Es wäre gut, wenn Sie Web-Workern jede Aufgabe übertragen könnten, nicht nur speziell geplanten Code.


Was brauchen wir, um die Bedienung von Web-Mitarbeitern zu vereinfachen? Ich glaube, es ist das Folgende:


  • Eine Möglichkeit, in Web Workern jeden Code jederzeit zu starten
  • Eine Möglichkeit, komplizierte Daten (Klasseninstanzen, Funktionen) an Web-Worker zu senden
  • Eine Möglichkeit, ein Versprechen mit einer Antwort von einem Web-Worker zu erhalten.

Versuchen wir es zu schreiben. Für den Anfang benötigen wir ein Kommunikationsprotokoll zwischen einem Web-Worker und dem Hauptfenster. Im Allgemeinen ist ein Protokoll nur eine Struktur und ein Datentyp, die für die Kommunikation zwischen einem Browserfenster und einem Web Worker verwendet werden. Es ist ziemlich einfach. Sie können dies verwenden oder Ihre eigene Version schreiben. Jede Nachricht hat eine ID und Daten, die für einen bestimmten Nachrichtentyp typisch sind. Zunächst haben wir zwei Arten von Nachrichten für Web-Worker:


  • Hinzufügen von Bibliotheken / Dateien zu einem Web Worker
  • Starten.

Eine Datei, die sich in einem Web Worker befindet


Bevor wir einen Web-Worker schreiben, müssen wir eine Datei beschreiben, die sich darin befindet und das oben beschriebene Protokoll unterstützt. Ich mag objektorientierte Programmierung (OOP), daher wird dies eine Klasse namens workerBody sein. Diese Klasse muss ein Ereignis aus dem übergeordneten 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: solche, die eine Antwort implizieren, und alles andere. Lassen Sie uns Ereignisse durchführen: \
Bibliotheken und Dateien werden einem Webworker mithilfe der importScripts- API hinzugefügt .


Und jetzt der gruseligste Teil: Zum Starten einer Zufallsfunktion verwenden wir eval .


 ... 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 Methode onMessage ist für den Empfang einer Nachricht und die Auswahl eines Handlers verantwortlich. DoWork startet eine gesendete Funktion und send sendet eine Antwort an das übergeordnete Fenster.


Parser und Serializer


Nachdem wir den Inhalt des Web-Workers haben, müssen wir lernen, alle Daten zu serialisieren und zu analysieren, damit sie an den Web-Worker gesendet werden können. Beginnen wir mit einem Serializer. Wir möchten in der Lage sein, alle Daten, einschließlich Klasseninstanzen, Klassen und Funktionen, an den Web Worker zu senden, während die native Kapazität des Web Workers das Senden nur von JSON-ähnlichen Daten ermöglicht. Um das zu umgehen, brauchen wir _ eval _. Wir werden alle Daten, die JSON nicht akzeptieren kann, in entsprechende Sting-Strukturen verpacken und auf der anderen Seite starten. Um die Unveränderlichkeit zu erhalten, werden empfangene Daten im laufenden Betrieb geklont und alles, was mit normalen Methoden nicht serialisiert werden kann, durch Serviceobjekte ersetzt, die auf der anderen Seite durch einen Parser ersetzt werden. Auf den ersten Blick ist diese Aufgabe nicht schwierig, aber es gibt viele Fallstricke. Die gruseligste Einschränkung dieses Ansatzes ist die Unfähigkeit, Closure zu verwenden, was zu einem etwas anderen Code-Schreibstil führt. Beginnen wir mit dem einfachsten Teil, der Funktion. Zuerst müssen wir lernen, eine Funktion von einem Klassenkonstruktor zu unterscheiden. Lass uns das machen:


 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 prüfen wir, ob die Funktion einen Prototyp hat. Wenn nicht, ist dies sicherlich eine Funktion. Dann schauen wir uns die Anzahl der Merkmale des Prototyps an. Wenn es nur einen Konstruktor hat und die Funktion kein Nachfolger einer anderen Klasse ist, betrachten wir sie als Funktion.


Wenn wir eine Funktion entdecken, ersetzen wir sie einfach durch ein Serviceobjekt mit den Feldern __type = "serialized-function" und die Vorlage entspricht der Vorlage dieser Funktion (func.toString ()).


Im Moment überspringen wir die Klasse und schauen uns die Klasseninstanz an. Später müssen wir zwischen regulären Objekten und 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 glauben, dass ein Objekt regulär ist, wenn es keinen Konstruktor hat oder sein Konstruktor eine native Funktion ist. Sobald wir eine Klasseninstanz entdeckt haben, ersetzen wir sie durch ein Serviceobjekt mit den folgenden Feldern:


  • __type: 'serialisierte Instanz'
  • Daten sind Daten, die in der Instanz enthalten sind
  • index ist der Klassenindex dieser Instanz in der Serviceklassenliste.

Um Daten zu senden, müssen wir ein zusätzliches Feld erstellen, in dem wir eine Liste der von uns gesendeten eindeutigen Klassen speichern. Es gibt jedoch eine Herausforderung: Um eine Klasse zu entdecken, müssen wir nicht nur ihre Vorlage, sondern auch die Vorlagen aller übergeordneten Klassen als unabhängige Klassen speichern, sodass jede übergeordnete Klasse nur einmal gesendet wird und auch eine Beweisinstanz gespeichert wird. Das Erkennen einer Klasse ist einfach: Dies ist eine Funktion, bei der unser Serializer.isFunction-Beweis fehlgeschlagen ist. Beim Hinzufügen einer Klasse überprüfen wir das Vorhandensein dieser Klasse in der Liste der serialisierten Daten und fügen nur eindeutige Klassen hinzu. Code, der eine Klasse zu einer Vorlage zusammenfügt, ist ziemlich groß und hier verfügbar.


Im Parser überprüfen wir zunächst alle an uns gesendeten Klassen und kompilieren sie, wenn sie nicht gesendet wurden. Anschließend überprüfen wir rekursiv jedes Datenfeld und ersetzen Serviceobjekte durch kompilierte Daten. Der interessanteste Teil sind Klasseninstanzen. Wir haben eine Klasse und Daten, die in ihrer Instanz waren, aber wir können nicht einfach eine Instanz erstellen, da eine Konstruktoranforderung möglicherweise Parameter erfordert, die wir nicht haben. Wir erhalten das von der fast vergessenen Object.create- Methode, die ein Objekt mit einem festgelegten Prototyp erstellt. Auf diese Weise vermeiden wir, einen Konstruktor anzufordern, eine Klasseninstanz abzurufen und nur Eigenschaften in die Instanz zu kopieren.


Erstellen eines Web-Workers


Damit ein Web-Worker erfolgreich arbeiten kann, benötigen wir einen Parser und einen Serializer innerhalb und außerhalb des Web-Workers. Also nehmen wir einen Serializer und verwandeln ihn, Parser und Web Worker Body in eine Vorlage. Aus der Vorlage erstellen wir einen Blob und erstellen einen Download-Link über URL.createObjectURL (diese Methode funktioniert möglicherweise nicht für einige "Content-Security-Policy"). Diese Methode eignet sich auch zum Starten von Zufallscode 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


Wir haben also eine einfach zu verwendende Bibliothek, die jeden Code an den Web Worker senden kann. Es unterstützt TypeScript-Klassen, 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 

Zukünftige Entwicklung


Leider ist diese Bibliothek alles andere als ideal. Wir müssen die Unterstützung von Setzern und Gettern für Klassen, Objekte, Prototypen und statische Merkmale hinzufügen. Außerdem müssen wir Caching hinzufügen, einen alternativen URL.createObjectURL ohne URL.createObjectURL stattdessen URL.createObjectURL verwendet. Schließlich muss der Assembly eine Datei mit dem Web-Worker-Inhalt hinzugefügt werden (falls keine On-the-Fly-Erstellung verfügbar ist) usw. Besuchen Sie das Repository !

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


All Articles