Cet article utilisera la méthode eval
DIRTY, unsafe, "béquille", effrayant, etc. Les faibles de cœur ne lisent pas!
Je dois dire tout de suite que certains problèmes d'utilisabilité n'ont pas pu être résolus: dans le code qui sera transmis au travailleur, vous ne pouvez pas utiliser la fermeture.

Nous aimons tous les nouvelles technologies, et nous l'aimons quand il est commode d'utiliser ces technologies. Mais dans le cas des travailleurs, ce n'est pas entièrement vrai. Le travailleur travaille avec un fichier ou un lien de fichier, mais cela n'est pas pratique. Je voudrais pouvoir mettre n'importe quelle tâche dans le travailleur, et pas seulement du code spécialement conçu.
De quoi a-t-on besoin pour rendre le travail avec le travailleur plus pratique? À mon avis, ce qui suit:
- Possibilité d'exécuter du code arbitraire dans le travailleur à tout moment
- Capacité à transmettre des données complexes au travailleur (instances de classe, fonctions)
- Capacité d'obtenir Promise avec une réponse du travailleur.
Tout d'abord, nous avons besoin d'un protocole de communication entre le travailleur et la fenêtre principale. En général, un protocole est simplement la structure et les types de données avec lesquels la fenêtre du navigateur et le travailleur communiqueront. Il n'y a rien de compliqué. Vous pouvez utiliser quelque chose comme ça ou écrire votre propre version. Dans chaque message, nous aurons un identifiant et des données spécifiques à un type de message particulier. Pour commencer, nous aurons deux types de messages pour le travailleur:
- ajout de bibliothèques / fichiers au travailleur
- lancement des travaux
Fichier à l'intérieur du travailleur
Avant de commencer à créer un travailleur, vous devez décrire un fichier qui fonctionnera dans travailleur et prendra en charge le protocole décrit par nous. J'adore la POO , ce sera donc une classe appelée WorkerBody. Cette classe doit s'abonner à l'é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 auxquels la réponse est implicite, et tous les autres. Nous traitons les événements.
L'ajout de bibliothèques et de fichiers au travailleur se fait à l'aide de l'API importScripts .
Et le pire: nous utiliserons eval pour exécuter une fonction arbitraire.
... 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
responsable de la réception du message et de la sélection du gestionnaire, doWork
- démarre la fonction passée, et send
envoie la réponse à la fenêtre parente.
Analyseur et sérialiseur
Maintenant que nous avons le contenu de travailleur, nous devons apprendre à sérialiser et analyser toutes les données afin de les transmettre à travailleur. Commençons par le sérialiseur. Nous voulons pouvoir transmettre toutes les données au travailleur, y compris les instances de classe, les classes et les fonctions. Mais avec les fonctionnalités natives de Worker, nous ne pouvons transmettre que des données de type JSON. Pour contourner cette interdiction, nous avons besoin d' eval . Tout ce qui ne peut pas accepter JSON, nous encapsulerons dans les constructions de chaînes correspondantes et exécuterons de l'autre côté. Pour préserver l'immuabilité, les données obtenues sont clonées à la volée, et celles qui ne peuvent pas être sérialisées de la manière habituelle sont remplacées par des objets de service, et à leur tour, ils sont remplacés par l'analyseur de l'autre côté. À première vue, il peut sembler que cette tâche n'est pas difficile, mais il existe de nombreux pièges. La pire limitation de cette approche est l'incapacité à utiliser la fermeture, qui porte un style d'écriture de code légèrement différent. Commençons par le plus simple, avec la fonction. Vous devez d'abord apprendre à distinguer une fonction d'un constructeur de classe.
Essayons de distinguer:
static isFunction(Factory){ if (!Factory.prototype) {
Tout d'abord, nous verrons si la fonction a un prototype. Si ce n'est pas le cas, c'est certainement une fonction. Ensuite, nous regardons le nombre de propriétés dans le prototype, et si dans le prototype seuls le constructeur et la fonction ne sont pas les héritiers d'une autre classe, nous considérons que c'est une fonction.
Après avoir trouvé une fonction, nous la remplaçons simplement par un objet de service avec les champs __type = "serialized-function"
et template
, qui est égal au template de cette fonction ( func.toString()
).
Pour l'instant, ignorez la classe et analysez l'instance de classe. Plus loin dans les données, nous devons distinguer les objets ordinaires des 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 considérons un objet comme ordinaire s'il n'a pas de constructeur ou si son constructeur est une fonction native. Après avoir identifié l'instance de classe, nous la remplaçons par un objet de service avec des champs:
__type
- 'instance sérialisée'data
- données qui étaient dans l'instanceindex
- l'index de classe de cette instance dans la liste des services de classe.
Pour transférer des données, nous devons créer un champ supplémentaire: nous y stockons une liste de toutes les classes uniques que nous passons. La partie la plus difficile est que lorsqu'une classe est détectée, prenez non seulement son modèle, mais aussi le modèle de toutes les classes parentes et enregistrez-les en tant que classes distinctes - de sorte que chaque "parent" ne soit transmis qu'une seule fois - et enregistrez la vérification sur instanceof. La définition d'une classe est simple: il s'agit d'une fonction qui n'a pas réussi notre test Serializer.isFunction. Lors de l'ajout d'une classe, nous vérifions la présence d'une telle classe dans la liste des données sérialisées et n'ajoutons que des données uniques. Le code qui collecte la classe dans un modèle est assez volumineux et se trouve ici .
Dans l'analyseur, nous parcourons d'abord toutes les classes qui nous sont passées et les compilons si elles n'ont pas été passées auparavant. Ensuite, nous parcourons récursivement chaque champ de données et remplaçons les objets de service par des données compilées. La chose la plus intéressante est dans l'instance de classe. Nous avons une classe et il y a des données qui étaient dans son instance, mais nous ne pouvons pas simplement les instancier, car l'appel au constructeur peut avoir des paramètres que nous n'avons pas. Une méthode Object.create presque oubliée vient à notre aide, qui renvoie un objet avec un prototype donné. Nous évitons donc d'appeler le constructeur et d'obtenir une instance de la classe, puis de simplement réécrire les propriétés dans l'instance.
Travailleur de création
Pour que le travailleur fonctionne correctement, nous devons avoir un analyseur et un sérialiseur à l'intérieur et à l'extérieur du travailleur.Nous prenons donc le sérialiseur et transformons le sérialiseur, l'analyseur et le corps du travailleur en modèle. Nous créons un blob à partir du modèle et créons un lien de téléchargement via URL.createObjectURL (cette méthode peut ne pas fonctionner avec certaines "Content-Security-Policy"). Cette méthode convient également pour exécuter du code arbitraire à 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
Ainsi, nous avons obtenu une bibliothèque facile à utiliser qui peut exécuter du code arbitraire dans worker. Il prend en charge les classes de TypeScript. Par exemple:
const wrapper = workerWrapper.create(); wrapper.process((params) => {
Plans supplémentaires
Cette bibliothèque, malheureusement, est loin d'être idéale. Il est nécessaire d'ajouter un support pour les setters et les getters sur les classes, les objets, les prototypes, les propriétés statiques. Nous aimerions également ajouter la mise en cache, faire exécuter un script alternatif sans eval
via URL.createObjectURL et ajouter un fichier avec le contenu de travailleur à l'assembly (si la création n'est pas disponible à la volée). Venez au référentiel !