
En este artículo, se describirá <em>eval</em>
método <em>eval</em>
SUCIO, inseguro, inestable y aterrador. Entonces, si no te sientes cómodo con eso, deja de leer ahora mismo.
En primer lugar, algunos problemas con la conveniencia permanecieron sin resolver: en el código enviado a los trabajadores web, no se puede usar el cierre.
A todos nos gustan las nuevas tecnologías, y a todos nos gusta que las nuevas tecnologías sean cómodas de usar. Sin embargo, no es exactamente el caso con los trabajadores web. los trabajadores web aceptan archivos o enlaces a archivos, lo cual no es conveniente. Sería bueno poder poner cualquier tarea en los trabajadores web, no solo el código planificado específicamente.
¿Qué necesitamos para que los trabajadores web sean más convenientes para operar? Creo que es lo siguiente:
- Posibilidad de lanzar en los trabajadores web cualquier código en cualquier momento
- Posibilidad de enviar a los trabajadores web datos complicados (instancias de clase, funciones)
- La posibilidad de recibir una promesa con una respuesta de un trabajador web.
Tratemos de escribirlo. Para empezar, necesitaremos un protocolo de comunicación entre un trabajador web y la ventana principal. En general, un protocolo es solo una estructura y tipos de datos utilizados para la comunicación entre una ventana del navegador y un trabajador web. Es bastante sencillo. Puede usar esto o escribir su propia versión. Cada mensaje tendrá una identificación y datos típicos de un tipo de mensaje específico. Inicialmente, tendremos dos tipos de mensajes para los trabajadores web:
- Agregar bibliotecas / archivos a un trabajador web
- Lanzamiento
Un archivo que estará dentro de un trabajador web
Antes de escribir un trabajador web, necesitamos describir un archivo que estará dentro de él, que admita el protocolo descrito anteriormente. Me gusta la programación orientada a objetos (OOP), por lo que esta será una clase llamada workerBody. Esta clase tiene que suscribirse a un 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 que implican una respuesta y todo lo demás. Hagamos eventos: \
Las bibliotecas y los archivos se agregan a un trabajador web mediante la API importScripts .
Y ahora la parte más aterradora: para lanzar una función aleatoria, usaremos 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); } }
El método onMessage es responsable de recibir un mensaje y elegir un controlador, doWork lanza una función enviada y send envía una respuesta a la ventana principal.
Analizador y serializador
Ahora que tenemos el contenido del trabajador web, necesitamos aprender a serializar y analizar cualquier información, para que puedan ser enviados al trabajador web. Comencemos con un serializador. Queremos poder enviar al trabajador web cualquier dato, incluidas instancias de clase, clases y funciones, mientras que la capacidad nativa del trabajador web permite enviar solo datos similares a JSON. Para evitar eso, necesitaremos _ eval _. Empaquetaremos todos los datos que JSON no puede aceptar en las estructuras de picadura correspondientes y los lanzaremos desde el otro lado. Para preservar la inmutabilidad, los datos recibidos serán clonados sobre la marcha, reemplazando lo que no pueda ser serializado por métodos ordinarios con objetos de servicio, que serán reemplazados en el otro lado por un analizador. A primera vista, esta tarea no es difícil, pero hay muchas dificultades. La limitación más aterradora de este enfoque es la incapacidad de usar el cierre, lo que conduce a un estilo de escritura de código ligeramente diferente. Comencemos con la parte más fácil, la función. Primero, debemos aprender a distinguir una función de un constructor de clases. Hagamos eso
static isFunction(Factory){ if (!Factory.prototype) {
Primero, verificaremos si la función tiene un prototipo. Si no es así, esta es ciertamente una función. Luego, observamos el número de características del prototipo. Si solo tiene un constructor y la función no es sucesora de otra clase, la consideramos una función.
Cuando descubrimos una función, simplemente la reemplazamos con un objeto de servicio con los campos __type = "serialized-function" y la plantilla corresponde a la plantilla de esta función (func.toString ()).
Por ahora, omitiremos la clase y veremos la instancia de la clase. Más tarde, tendremos que distinguir entre objetos regulares e 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()); }
Creemos que un objeto es regular si no tiene un constructor o si su constructor es una función nativa. Una vez que hayamos descubierto una instancia de clase, la reemplazaremos con un objeto de servicio con los siguientes campos:
- __type: 'serialized-instance'
- datos son datos contenidos en la instancia
- index es el índice de clase de esta instancia en la lista de clases de servicio.
Para enviar datos, tenemos que crear un campo adicional, en el que almacenaremos una lista de clases únicas que enviamos. Sin embargo, existe un desafío: al descubrir una clase, necesitamos tomar no solo su plantilla, sino también las plantillas de todas las clases principales y guardarlas como clases independientes, por lo que cada clase principal se envía solo una vez, también guardando una instancia de prueba. Descubrir una clase es fácil: esta es una función que falló en nuestra prueba Serializer.isFunction. Al agregar una clase, verificamos la presencia de esa clase en la lista de datos serializados y agregamos solo clases únicas. El código que ensambla una clase en una plantilla es bastante grande y está disponible aquí .
En el analizador, inicialmente verificamos todas las clases que nos envían y las compilamos si no se han enviado. Luego, verificamos recursivamente cada campo de datos y reemplazamos los objetos de servicio con datos compilados. La parte más interesante son las instancias de clase. Tenemos una clase y datos que estaban en su instancia, pero no podemos simplemente crear una instancia ya que una solicitud del constructor puede requerir parámetros que no tenemos. Obtenemos eso del método Object.create casi olvidado, que crea un objeto con un prototipo establecido. De esta forma, evitamos solicitar un constructor, obtener una instancia de clase y simplemente copiar propiedades en la instancia.
Crear un trabajador web
Para que un trabajador web funcione con éxito, necesitamos un analizador y un serializador dentro y fuera del trabajador web. Entonces tomamos un serializador y lo convertimos, analizador y cuerpo del trabajador web en una plantilla. A partir de la plantilla, creamos un blob y creamos un enlace de descarga sobre URL.createObjectURL (este método puede no funcionar para alguna "Política de seguridad de contenido"). Este método también es bueno para lanzar código aleatorio 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
Entonces, tenemos una biblioteca fácil de usar que puede enviar cualquier código al trabajador web. Admite clases de TypeScript, por ejemplo:
const wrapper = workerWrapper.create(); wrapper.process((params) => {
Desarrollo futuro
Desafortunadamente, esta biblioteca está lejos de ser ideal. Necesitamos agregar soporte de setters y getters para clases, objetos, prototipos y características estáticas. Además, necesitamos agregar el almacenamiento en caché, un lanzamiento de secuencia de comandos alternativo sin evaluación , utilizando URL.createObjectURL
en URL.createObjectURL
lugar. Finalmente, un archivo con el contenido del trabajador web debe agregarse al ensamblado (en caso de que la creación sobre la marcha no esté disponible), etc. ¡Visita el repositorio !