Web Worker plus facile que vous ne le pensiez

Web Worker plus facile que vous ne le pensiez


Dans cet article, une méthode <em>eval</em> sale, dangereuse, instable et effrayante sera décrite. Donc, si vous n'êtes pas à l'aise avec cela, arrêtez de lire maintenant.


Tout d'abord, certains problèmes de commodité n'ont pas été résolus: dans le code envoyé aux travailleurs Web, la fermeture ne peut pas être utilisée.




Nous aimons tous les nouvelles technologies et nous aimons tous que les nouvelles technologies soient pratiques à utiliser. Cependant, ce n'est pas exactement le cas avec les travailleurs du Web. les travailleurs Web acceptent des fichiers ou des liens vers des fichiers, ce qui n'est pas pratique. Il serait bon de pouvoir confier n'importe quelle tâche aux travailleurs Web, pas seulement du code spécifiquement planifié.


De quoi avons-nous besoin pour rendre les travailleurs du Web plus pratiques à utiliser? Je crois que c'est la suivante:


  • Une possibilité de lancer dans les travailleurs Web n'importe quel code à tout moment
  • Une possibilité d'envoyer aux travailleurs Web des données compliquées (instances de classe, fonctions)
  • Une possibilité de recevoir une promesse avec une réponse d'un web travailleur.

Essayons de l'écrire. Pour commencer, nous aurons besoin d'un protocole de communication entre un travailleur Web et la fenêtre principale. En général, un protocole n'est qu'une structure et des types de données utilisés pour la communication entre une fenêtre de navigateur et un travailleur Web. C'est assez simple. Vous pouvez l'utiliser ou écrire votre propre version. Chaque message aura un ID et des données typiques d'un type de message spécifique. Au départ, nous aurons deux types de messages pour les travailleurs Web:


  • Ajout de bibliothèques / fichiers à un travailleur Web
  • Lancer.

Un fichier qui sera à l'intérieur d'un travailleur Web


Avant d'écrire un travailleur Web, nous devons décrire un fichier qui s'y trouvera, prenant en charge le protocole décrit ci-dessus. J'aime la programmation orientée objet (POO), donc ce sera une classe nommée workerBody. Cette classe doit s'abonner à un événement depuis la fenêtre parent.


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

Nous pouvons maintenant écouter les événements de la fenêtre parent. Nous avons deux types d'événements: ceux qui impliquent une réponse et tout le reste. Faisons des événements: \
Les bibliothèques et les fichiers sont ajoutés à un travailleur Web à l'aide de l'API importScripts .


Et maintenant la partie la plus effrayante: pour lancer une fonction aléatoire, nous utiliserons 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); } } 

La méthode onMessage est chargée de recevoir un message et de choisir un gestionnaire, doWork lance une fonction envoyée et envoie envoie une réponse à la fenêtre parent.


Analyseur et sérialiseur


Maintenant que nous avons le contenu du travailleur Web, nous devons apprendre à sérialiser et analyser toutes les données, afin qu'elles puissent être envoyées au travailleur Web. Commençons par un sérialiseur. Nous voulons pouvoir envoyer au travailleur Web toutes les données, y compris les instances de classe, les classes et les fonctions, tandis que la capacité native du travailleur Web permet d'envoyer uniquement des données de type JSON. Pour contourner cela, nous aurons besoin de _ eval _. Nous encapsulerons toutes les données que JSON ne peut pas accepter dans les structures de piqûres correspondantes et les lancerons de l'autre côté. Pour préserver l'immuabilité, les données reçues seront clonées à la volée, remplaçant tout ce qui ne peut pas être sérialisé par des méthodes ordinaires par des objets de service, qui seront remplacés de l'autre côté par un analyseur. À première vue, cette tâche n'est pas difficile, mais il existe de nombreux pièges. La limitation la plus effrayante de cette approche est l'incapacité à utiliser la fermeture, ce qui conduit à un style d'écriture de code légèrement différent. Commençons par la partie la plus simple, la fonction. Tout d'abord, nous devons apprendre à distinguer une fonction d'un constructeur de classe. Faisons ça:


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

Tout d'abord, nous allons vérifier si la fonction a un prototype. Si ce n'est pas le cas, c'est certainement une fonction. Ensuite, nous regardons le nombre de fonctionnalités du prototype. Si elle n'a qu'un constructeur et que la fonction n'est pas le successeur d'une autre classe, nous la considérons comme une fonction.


Lorsque nous découvrons une fonction, nous la remplaçons simplement par un objet de service avec les champs __type = "serialized-function" et le modèle correspond au modèle de cette fonction (func.toString ()).


Pour l'instant, nous allons sauter la classe et regarder l'instance de classe. Plus tard, nous devrons faire la distinction entre les objets normaux et les instances de classe.


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

Nous pensons qu'un objet est régulier s'il n'a pas de constructeur ou si son constructeur est une fonction native. Une fois que nous avons découvert une instance de classe, nous la remplacerons par un objet de service avec les champs suivants:


  • __type: 'instance-sérialisée'
  • les données sont des données contenues dans l'instance
  • index est l'index de classe de cette instance dans la liste des classes de service.

Pour envoyer des données, nous devons créer un champ supplémentaire, dans lequel nous stockons une liste des classes uniques que nous envoyons. Cependant, il y a un défi: découvrir une classe, nous devons prendre non seulement son modèle, mais aussi les modèles de toutes les classes parentes et les enregistrer en tant que classes indépendantes, de sorte que chaque classe parente n'est envoyée qu'une seule fois, ce qui permet également d'économiser instanceof proof. La découverte d'une classe est facile: c'est une fonction qui a échoué à notre preuve Serializer.isFunction. Lors de l'ajout d'une classe, nous vérifions la présence de cette classe dans la liste des données sérialisées et n'ajoutons que des classes uniques. Le code qui assemble une classe dans un modèle est assez volumineux et est disponible ici .


Dans l'analyseur, nous vérifions d'abord toutes les classes qui nous sont envoyées et les compilons si elles n'ont pas été envoyées. Ensuite, nous vérifions récursivement chaque champ de données et remplaçons les objets de service par des données compilées. La partie la plus intéressante est les instances de classe. Nous avons une classe et des données qui étaient dans son instance, mais nous ne pouvons pas simplement créer une instance car une demande de constructeur peut nécessiter des paramètres que nous n'avons pas. Nous obtenons cela de la méthode Object.create presque oubliée, qui crée un objet avec un prototype défini. De cette façon, nous évitons de demander un constructeur, obtenons une instance de classe et copions simplement les propriétés dans l'instance.


Création d'un travailleur Web


Pour qu'un travailleur Web fonctionne correctement, nous avons besoin d'un analyseur et d'un sérialiseur au sein du travailleur Web et à l'extérieur. Nous prenons donc un sérialiseur et le transformons, l'analyseur et le corps du travailleur Web en modèle. À partir du modèle, nous créons un blob et créons un lien de téléchargement sur URL.createObjectURL (cette méthode peut ne pas fonctionner pour certaines «Content-Security-Policy»). Cette méthode est également utile pour lancer du code aléatoire à partir d'une chaîne.


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

Résultat


Nous avons donc obtenu une bibliothèque simple à utiliser qui peut envoyer n'importe quel code au travailleur Web. Il prend en charge les classes TypeScript, par exemple:


 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 

Développement futur


Malheureusement, cette bibliothèque est loin d'être idéale. Nous devons ajouter le support des setters et des getters pour les classes, les objets, les prototypes et les fonctionnalités statiques. En outre, nous devons ajouter la mise en cache, un lancement de script alternatif sans eval , en utilisant URL.createObjectURL place. Enfin, un fichier avec le contenu du travailleur Web doit être ajouté à l'assemblage (au cas où la création à la volée n'est pas disponible), etc. Visitez le référentiel !

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


All Articles