我正在开发一个库,我想将其托管在 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。
解决方案是将工作程序放置在 www.myotherdomain.com 上或放置代理文件(只需使用 XHR 或 importScripts 从 CDN 加载工作程序)。然而,我不喜欢这个解决方案:它要求我在服务器上放置额外的文件,并且由于该库在多个服务器上使用,因此更新会很困难。
我的问题由两部分组成:
importScripts()
,不受此跨域限制的限制。
要了解为什么不能使用跨域脚本作为 Worker 初始化脚本,请参阅此答案。基本上,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" } ) );
}
对于那些发现这个问题的人:
是的。
这绝对是可能的:技巧是利用远程域上的 iframe 并通过 postMessage 与其进行通信。远程 iframe(托管在 cdn.mydomain.com 上)将能够加载 webworker(位于 cdn.mydomain.com/worker.js),因为它们具有相同的来源。 然后,iframe 可以充当 postMessage 调用之间的代理。然而,script.js 将负责过滤消息,以便仅处理有效的工作消息。
缺点是通信速度(和数据传输速度)确实会影响性能。
简而言之:
src="//cdn.mydomain.com/iframe.html"
new Worker("worker.js")
并充当来自窗口和 message
(反之亦然)的 worker.postMessage
事件的代理。iframe.contentWindow.postMessage
和窗口中的 message
事件与工作人员通信。 (对多名工人的正确来源和工人身份进行适当检查)无法从不同的域加载 Web Worker。
与您的建议类似,您可以进行 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:
或 blob:
指令),这里有一个小例子说明如何在工作人员内部利用 importScripts
加载另一个域上托管的另一个工作脚本,而不减少您的 CSP。
它可以帮助您从 CSP 允许的任何 CDN 加载工作人员。
据我所知,它适用于 Opera、Firefox、Chrome、Edge 和所有支持工作人员的浏览器。
/**
* 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.");
});
假设您在您的域(不是您的 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 上并且您希望用户能够调用您的工作程序而不必托管其域上的所有工作程序时,这尤其有用。