执行来自不同来源的网络工作者

IT技术 javascript cross-browser cross-domain web-worker
2021-02-15 17:57:12

我正在开发一个我想在 CDN 上托管的库。该库将用于跨多个服务器的许多不同域。该库本身包含一个脚本(我们现在称其为 script.js),它加载一个 web worker (worker.js)。

加载库本身非常简单:只需将<script type="text/javascript" src="http://cdn.mydomain.com/script.js"></script>标记添加到我要使用该库的域 (www.myotherdomain.com)。但是,由于该库正在从http://cdn.mydomain.com/worker.js 加载工作人员new Worker('http://cdn.mydomain.com/worker.js'),因此我收到了 SecurityException。在 cdn.mydomain.com 上启用 CORS。

对于网络工作者,不允许在远程域上使用网络工作者。使用 CORS 无济于事:浏览器似乎忽略了它,甚至不执行预检。

解决此问题的一种方法是执行 XMLHttpRequest 以获取工作程序的来源,然后创建一个 BLOB url 并使用此 url 创建一个工作程序。这适用于 Firefox 和 Chrome。但是,这似乎不适用于 Internet Explorer 或 Opera。

一个解决方案是将 worker 放在 www.myotherdomain.com 或放置一个代理文件(它只是使用 XHR 或 importScripts 从 CDN 加载 worker)。然而,我不喜欢这个解决方案:它需要我在服务器上放置额外的文件,并且由于该库在多台服务器上使用,更新会很困难。

我的问题由两部分组成:

  1. 是否可以在 IE 10+ 的远程源上设置工作人员?
  2. 如果是 1,跨浏览器工作的最佳处理方式是什么?
4个回答

最好的可能是动态生成一个简单的工作脚本,它会在内部调用importScripts(),不受这种跨域限制的限制。

要了解为什么不能将跨域脚本用作 Worker init-script,请参阅此答案基本上,Worker 上下文将其自己的原点设置为该脚本之一。

// 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" } ) );
}

这应该是这里的答案。
2021-04-17 17:57:12

对于那些发现这个问题的人:

是的。

这绝对是可能的:诀窍是利用远程域上的 iframe 并通过 postMessage 与其通信。远程 iframe(托管在 cdn.mydomain.com 上)将能够加载 webworker(位于 cdn.mydomain.com/worker.js),因为它们都具有相同的来源。然后,iframe 可以充当 postMessage 调用之间的代理。然而,script.js 将负责过滤消息,因此只处理有效的工作消息。

缺点是通信速度(和数据传输速度)确实会影响性能。

简而言之:

  • script.js 附加 iframe src="//cdn.mydomain.com/iframe.html"
  • cdn.mydomain.com/iframe.html 上的 iframe.html 执行new Worker("worker.js")并充当message来自 window 和worker.postMessage(以及其他方式)事件的代理
  • script.js 使用来自窗口iframe.contentWindow.postMessagemessage事件与工作人员进行通信(正确检查多个工人的正确来源和工人身份)
这是一个准确的解决方案,但它超级hacky。我认为这个答案最好先说不可能,然后详细说明可能的解决方法,例如使用 iframe。
2021-05-01 17:57:12
当然,但是如果脚本被加载为提供给其他用户网站的库的工作器,这是不可能的,这是这个问题的用例。
2021-05-06 17:57:12

不可能从不同的域加载网络工作者。

与您的建议类似,您可以进行 fetch 调用,然后使用该 JS 和 base64。这样做可以让您:

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

您可以在此处找到有关数据 URI 的更多信息:https : //developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs

这是我更喜欢的解决方法,因为它不需要像带有消息代理的 iframe 那样疯狂的东西,而且只要您从 CDN 正确设置 CORS,就可以很容易地工作。

由于@KevinGhadyani 的回答(或 blob 技术)需要减少您的 CSP(例如,通过添加一个worker-src data:orblob:指令),有一个小例子说明您可以如何利用importScripts工作人员内部来加载另一个域上托管的另一个工作人员脚本,不会减少您的 CSP。

它可以帮助您从您的 CSP 允许的任何 CDN 加载工作人员。

据我所知,它适用于 Opera、Firefox、Chrome、Edge 和所有支持 worker 的浏览器。


/**
 * 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);

如何使用它 ?

显然,您的 CSP 必须允许 CDN 域,但您不需要更多的 CSP 规则。

假设您的域是my-domain.com,而您的 CDN 是statics.your-cdn.com.

我们要导入的工作人员托管在https://statics.your-cdn.com/super-worker.js并将包含:


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

假设您在 path 下的域(不是您的 CDN)上托管了一个包含工作程序导入程序代码的文件https://my-domain.com/worker-importer.js,并且您尝试在 脚本标签内启动您的工作程序https://my-domain.com/,这就是它的工作原理:


<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>

这将打印:


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

请注意,如果上面的代码也编写在 CDN 上托管的文件中,它甚至可以工作。

当您的 CDN 上有多个工作程序脚本时,或者如果您构建一个必须托管在 CDN 上的库并且您希望您的用户能够调用您的工作人员而无需在其域上托管所有工作人员,这将特别有用。