Trabaje con el trabajador "como desee", no "tanto como sea posible"

Este artículo utilizará el método de eval SUCIO, inseguro, "muleta", aterrador, etc. Los débiles de corazón no leen!


Debo decir de inmediato que no fue posible resolver algunos problemas de usabilidad: no se puede usar el cierre en el código que se pasará al trabajador.
Trabaje con el trabajador "como desee", pero no "tanto como sea posible"


A todos nos gustan las nuevas tecnologías, y nos gusta cuando es conveniente usar estas tecnologías. Pero en el caso del trabajador, esto no es del todo cierto. El trabajador trabaja con un archivo o enlace de archivo, pero esto es inconveniente. Me gustaría poder poner cualquier tarea en el trabajador, y no solo código especialmente planificado.


¿Qué se necesita para que trabajar con el trabajador sea más conveniente? En mi opinión, lo siguiente:


  • Capacidad para ejecutar código arbitrario en el trabajador en cualquier momento
  • Capacidad para pasar datos complejos al trabajador (instancias de clase, funciones)
  • Capacidad de obtener promesa con una respuesta del trabajador.

Primero, necesitamos un protocolo de comunicación entre el trabajador y la ventana principal. En general, un protocolo es simplemente la estructura y los tipos de datos con los que la ventana del navegador y el trabajador se comunicarán. No hay nada complicado Puedes usar algo como esto o escribir tu propia versión. En cada mensaje tendremos una identificación y datos específicos para un tipo particular de mensaje. Para empezar, tendremos dos tipos de mensajes para el trabajador:


  • Agregar bibliotecas / archivos al trabajador
  • lanzamiento de trabajo

Archivo dentro del trabajador


Antes de comenzar a crear un trabajador, debe describir un archivo que funcionará en el trabajador y admitirá el protocolo descrito por nosotros. Me encanta OOP , por lo que será una clase llamada WorkerBody. Esta clase debe suscribirse al evento desde la ventana principal.


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

Ahora podemos escuchar eventos desde la ventana principal. Tenemos dos tipos de eventos: aquellos para los que está implícita la respuesta, y todos los demás. Procesamos eventos.
La adición de bibliotecas y archivos al trabajador se realiza mediante la API importScripts .


Y lo peor: utilizaremos eval para ejecutar una función arbitraria.


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

El método onMessage responsable de recibir el mensaje y seleccionar el controlador, doWork : inicia la función pasada y envía la respuesta a la ventana principal.


Analizador y serializador


Ahora que tenemos el contenido de trabajador, necesitamos aprender a serializar y analizar cualquier dato para pasarlo al trabajador. Comencemos con el serializador. Queremos poder pasar cualquier información al trabajador, incluyendo instancias de clase, clases y funciones. Pero con las características nativas de trabajador, solo podemos transmitir datos similares a JSON. Para evitar esta prohibición, necesitamos evaluar . Todo lo que no pueda aceptar JSON, lo envolveremos en las construcciones de cadena correspondientes y lo ejecutaremos en el otro lado. Para preservar la inmutabilidad, los datos obtenidos se clonan sobre la marcha, y lo que no se puede serializar de la manera habitual se reemplaza por objetos de servicio, y estos, a su vez, se reemplazan por el analizador en el otro lado. A primera vista, puede parecer que esta tarea no es difícil, pero hay muchas dificultades. La peor limitación de este enfoque es la imposibilidad de usar el cierre, que conlleva un estilo de escritura de código ligeramente diferente. Comencemos con el más simple, con la función. Primero debe aprender a distinguir una función de un constructor de clases.


Tratemos de distinguir:


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

Primero, descubriremos si la función tiene un prototipo. Si no es así, esta es definitivamente una función. Luego observamos el número de propiedades en el prototipo, y si en el prototipo solo el constructor y la función no son los herederos de otra clase, consideramos que esto es una función.


Habiendo encontrado una función, simplemente la reemplazamos con un objeto de servicio con los campos __type = "serialized-function" y template , que es igual a la plantilla de esta función ( func.toString() ).


Por ahora, omita la clase y analice la instancia de clase. Además en los datos, necesitamos distinguir los objetos ordinarios de las instancias de clase.


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

Consideramos que un objeto es ordinario si no tiene un constructor o si su constructor es una función nativa. Una vez identificada la instancia de clase, la reemplazamos con un objeto de servicio con campos:


  • __type - 'instancia serializada'
  • data : datos que estaban en la instancia
  • index : el índice de clase de esta instancia en la lista de servicios de clase.

Para transferir datos, necesitamos hacer un campo adicional: en él almacenaremos una lista de todas las clases únicas que pasamos. La parte más difícil es tomar no solo su plantilla al detectar una clase, sino también la plantilla de todas las clases principales y guardarlas como clases separadas, para que cada "padre" se pase no más de una vez, y guardar el cheque en instancia de. Definir una clase es fácil: esta es una función que no pasó nuestra prueba Serializer.isFunction. Al agregar una clase, verificamos la presencia de dicha clase en la lista de datos serializados y agregamos solo los únicos. El código que recopila la clase en una plantilla es bastante grande y se encuentra aquí .


En el analizador, primero revisamos todas las clases que nos pasaron y las compilamos si no se han pasado antes. Luego recorremos recursivamente cada campo de datos y reemplazamos los objetos de servicio con datos compilados. Lo más interesante está en la instancia de clase. Tenemos una clase y hay datos que estaban en su instancia, pero no podemos simplemente instanciarla, porque la llamada al constructor puede tener parámetros que no tenemos. Un método Object.create casi olvidado viene en nuestra ayuda, que devuelve un objeto con un prototipo dado. Así que evitamos llamar al constructor y obtener una instancia de la clase, y luego simplemente reescribimos las propiedades en la instancia.


Trabajador de la creación


Para que el trabajador funcione con éxito, necesitamos tener un analizador y un serializador dentro y fuera del trabajador, por lo que tomamos el serializador y convertimos el serializador, analizador y cuerpo del trabajador en una plantilla. Creamos un blob de la plantilla y creamos un enlace de descarga a través de URL.createObjectURL (este método puede no funcionar con alguna "Política de seguridad de contenido"). Este método también es adecuado para ejecutar código arbitrario desde una cadena.


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

Resultado


Por lo tanto, hemos obtenido una biblioteca fácil de usar que puede ejecutar código arbitrario en trabajador. Es compatible con las clases de TypeScript. Por ejemplo:


 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 

Planes adicionales


Esta biblioteca, desafortunadamente, está lejos de ser ideal. Es necesario agregar soporte para setters y getters en clases, objetos, prototipos, propiedades estáticas. También nos gustaría agregar almacenamiento en caché, hacer que se ejecute un script alternativo sin evaluar a través de URL.createObjectURL y agregar un archivo con el contenido del trabajador al ensamblado (si la creación no está disponible sobre la marcha). ¡Ven al repositorio !

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


All Articles