Iframes y Web Workers

Resulta que por cuestiones de trabajo me vengo a enterar que los Web Workers tienen acceso a los recursos de localStorage cuando no se les ejecuta con un origen opaco (opaque origin - referencia).
La propuesta sería entonces restringir ese origen para poder limitar el acceso, pero cuando el Web Worker va a recibir un script grande (más de 2 megas grande), esto se nota - osea, tarda la ejecución y pues no es la mejor experiencia para el usuario.
Se vieron e intentaron varias propuestas (acá están si a alguien le intersan) pero ninguna fue final. Tirando ideas llegó otra que proponía usar los Web Workers dentro de un iframe limitado por sandbox (sanboxed iframes), así que me puse a hackear para ver si lo podía hacer funcionar.
El tema es que el código que se va a ejectuar en el Web Worker es “dinámico”, osea, no le vamos simplemente dar una URL donde está el script que va a ejecutar, sino que hay que darle un Blob, por lo cual va a haber problemas. Resumiendo:
- el iframe va a tener
sandboxy sólo e le va a dar el permiso deallow-scripts - hay que usar
JavaScriptpara generar el código que se va a ejecutar en eliframe - el
Web Wokerva a mandar mensajes aliframey el mismo los tiene que responder al contexto que lo ejecutó
Solución propuesta:
class IframeWorker extends EventTarget {
constructor(scriptUrl, id) {
super()
this.id = id
this.iframe = document.createElement('iframe')
this.iframe.sandbox = 'allow-scripts'
const source = `
<script>
const init = async () => {
const res = await fetch(url, { mode: 'cors' })
const blob = await res.blob()
const workerURL = URL.createObjectURL(blob)
const worker = new Worker(workerUrl, { name: '${id}' })
worker.addEventListener('error', error => window.parent.postMessage({ from: '${id}', error }, '*'), false)
worker.addEventListener('message', event => window.parent.postMessage({ from: '${id}', msg: event.data }, '*'), false)
window.addEventListener('message', ({ data }) => worker.postMessage(data))
URL.revokeObjectURL(workerUrl)
}
init()
</script>
`
this.iframe.srcdoc = source
document.body.appendChild(this.iframe)
window.addEventListener('message', this.handleIframeMessage, false)
}
postMessage(msg) {
this.iframe.contentWindow.postMessage(msg, '*')
}
destroy() {
window.removeListener('message', this.handleIframeMessage)
document.removeChild(this.iframe)
this.iframe = null
}
handleIframeMessage = ({ source, data: { from, error, msg } }) => {
if (source === this.iframe.contentWindow && from === this.id) {
this.dispatchEvent(
new MessageEvent(error ? 'error' : 'message', {
data: error || msg,
})
)
}
}
}
export default iframeWorker
Lo que se hizo cumple con lo que queríamos:
- Inyectar el código directamente en la clase, para lo cual se recivbe
scriptUrl, que se transforma en unblob, así se puede usar directamente en elWorkeren eliframe. - Para que el
iframeejecute el código inyectado hay que settear la propiedadsrcdocen lugar desrco de modificar el cuerpo dinámicamente (esos métodos tiraríanDOM Exceptionpor que eliframeestá en unsandbox). - Simular la interfaz de un
Workerpara hacer comunicación transparente, en este caso está el métodopostMessagey la emisión de eventos del tipoMessageEvent - Nota la implementación del método
handleIframeMessagenecesita de un transpilador para mantener contexto, de lo contrario tendría que hacerse unbinden el constructor (this.handleIframeMessage = this.handleIframeMessage.bind(this))
Y así quedó la fiesta.
Bueno, con esta clase se pueden tener Workers ejectuandose un contexto JS sin que tengan acceso a los stores de IndexedDB del mismo. No es algo normal pero es algo que le va a venir bien a quien lo esté buscando.
Saludos,
Gorka