Question

I am developing a library which I want to host on a CDN. The library is going to be used on many different domains across multiple servers. The library itself contains one script (let's call it script.js for now) which loads a web worker (worker.js).

Loading the library itself is quite easy: just add the <script type="text/javascript" src="http://cdn.mydomain.com/script.js"></script> tag to the domain on which I want to use the library (www.myotherdomain.com). However since the library is loading a worker from http://cdn.mydomain.com/worker.js new Worker('http://cdn.mydomain.com/worker.js'), I get a SecurityException. CORS is enabled on cdn.mydomain.com.

For web workers it is not allowed to use a web worker on a remote domain. Using CORS will not help: browsers seem to ignore it and don't even execute the preflight check.

A way around this would be to perform an XMLHttpRequest to get the source of the worker and then create a BLOB url and create a worker using this url. This works for Firefox and Chrome. However, this does not seem to work for Internet Explorer or Opera.

A solution would be to place the worker on www.myotherdomain.com or place a proxy file (which simply loads the worker from the cdn using XHR or importScripts). I do not however like this solution: it requires me to place additional files on the server and since the library is used on multiple servers, updating would be difficult.

My question consists of two parsts:

  1. Is it possible to have a worker on a remote origin for IE 10+?
  2. If 1 is the case, how is it handled best to be working cross-browser?
Était-ce utile?

La solution 2

For those who find this question:

YES.

It is absolutely possible: the trick is leveraging an iframe on the remote domain and communicating with it through postMessage. The remote iframe (hosted on cdn.mydomain.com) will be able to load the webworker (located at cdn.mydomain.com/worker.js) since they both have the same origin. The iframe can then act as a proxy between the postMessage calls. The script.js will however be responsible from filtering the messages so only valid worker messages are handled.

The downside is that communication speeds (and data transfer speeds) do take a performance hit.

In short:

  • script.js appends iframe with src="//cdn.mydomain.com/iframe.html"
  • iframe.html on cdn.mydomain.com/iframe.html, executes new Worker("worker.js") and acts as a proxy for message events from window and worker.postMessage (and the other way around).
  • script.js communicates with the worker using iframe.contentWindow.postMessage and the message event from window. (with the proper checks for the correct origin and worker identification for multiple workers)

Autres conseils

The best is probably to generate a simple worker-script dynamically, which will internally call importScripts(), which is not limited by this cross-origin restriction.

To understand why you can't use a cross-domain script as a Worker init-script, see this answer. Basically, the Worker context will have its own origin set to the one of that script.

// The script there simply posts back an "Hello" message
// Obviously cross-origin here
const cross_origin_script_url = "https://greggman.github.io/doodles/test/ping-worker.js";

const worker_url = getWorkerURL( cross_origin_script_url );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => console.log( evt.data );
URL.revokeObjectURL( worker_url );

// Returns a blob:// URL which points
// to a javascript file which will call
// importScripts with the given URL
function getWorkerURL( url ) {
  const content = `importScripts( "${ url }" );`;
  return URL.createObjectURL( new Blob( [ content ], { type: "text/javascript" } ) );
}

It's not possible to load a web worker from a different domain.

Similar to your suggestion, you could make a fetch call, then take that JS and base64 it. Doing so allows you to do:

const worker = new Worker(`data:text/javascript;base64,${btoa(workerJs)}`)

You can find out more info about data URIs here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.

This is the workaround I prefer because it doesn't require anything crazy like an iframe with a message proxy and is very simple to get working provided you setup CORS correctly from your CDN.

Since @KevinGhadyani answer (or blob techniques) require to lessen your CSPs (by adding a worker-src data: or blob: directive, for example), there is a little example of how you can take advantage of importScripts inside a worker to load another worker script hosted on another domain, without lessening your CSPs.

It may help you to load a worker from any CDN allowed by your CSPs.

As far as I know, it works on Opera, Firefox, Chrome, Edge and all browsers that support workers.


/**
 * This worker allow us to import a script from our CDN as a worker
 * avoiding to have to reduce security policy.
 */

/**
 * Send a formated response to the main thread. Can handle regular errors.
 * @param {('imported'|'error')} resp
 * @param {*} data
 */
function respond(resp, data = undefined){
    const msg = { resp };

    if(data !== undefined){
        if(data && typeof data === 'object'){
            msg.data = {};
            if(data instanceof Error){
                msg.error = true;
                msg.data.code = data.code;
                msg.data.name = data.name;
                msg.data.stack = data.stack.toString();
                msg.data.message = data.message;
            } else {
                Object.assign(msg.data, data);
            }
        } else msg.data = data;
    }

    self.postMessage(msg);
}

function handleMessage(event){
    if(typeof event.data === 'string' && event.data.match(/^@worker-importer/)){
        const [ 
            action = null, 
            data = null 
        ] = event.data.replace('@worker-importer.','').split('|');

        switch(action){
            case 'import' :
                if(data){
                    try{
                        importScripts(data);
                        respond('imported', { url : data });

                        //The work is done, we can just unregister the handler
                        //and let the imported worker do it's work without us.
                        self.removeEventListener('message', handleMessage);
                    }catch(e){
                        respond('error', e);
                    }
                } else respond('error', new Error(`No url specified.`));
                break;
            default : respond('error', new Error(`Unknown action ${action}`));
        }
    }
}

self.addEventListener('message', handleMessage);

How to use it ?

Obviously, your CSPs must allow the CDN domain, but you don't need more CSP rule.

Let's say that you domain is my-domain.com, and your cdn is statics.your-cdn.com.

The worker we want to import is hosted at https://statics.your-cdn.com/super-worker.js and will contain :


self.addEventListener('message', event => {
    if(event.data === 'who are you ?') {
        self.postMessage("It's me ! I'm useless, but I'm alive !");
    } else self.postMessage("I don't understand.");
});

Assuming that you host a file with the code of the worker importer on your domain (NOT your CDN) under the path https://my-domain.com/worker-importer.js, and that you try to start your worker inside a script tag at https://my-domain.com/, this is how it works :


<script>

window.addEventListener('load', async () => {
    
    function importWorker(url){     
        return new Promise((resolve, reject) => {
            //The worker importer
            const workerImporter = new Worker('/worker-importer.js');

            //Will only be used to import our worker
            function handleImporterMessage(event){
                const { resp = null, data = null } = event.data;

                if(resp === 'imported') {
                    console.log(`Worker at ${data.url} successfully imported !`);
                    workerImporter.removeEventListener('message', handleImporterMessage);

                    // Now, we can work with our worker. It's ready !
                    resolve(workerImporter);
                } else if(resp === 'error'){
                    reject(data);
                }
            }

            workerImporter.addEventListener('message', handleImporterMessage);
            workerImporter.postMessage(`@worker-importer.import|${url}`);
        });
    }

    const worker = await importWorker("https://statics.your-cdn.com/super-worker.js");
    worker.addEventListener('message', event => {
        console.log('worker message : ', event.data);
    });
    worker.postMessage('who are you ?');

});

</script>

This will print :


Worker at https://statics.your-cdn.com/super-worker.js successfully imported !
worker message : It's me ! I'm useless, but I'm alive !

Note that the code above can even work if it's written in a file hosted on the CDN too.

This is especially usefull when you have several worker scripts on your CDN, or if you build a library that must be hosted on a CDN and you want your users to be able to call your workers without having to host all workers on their domain.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top