如何使用用户脚本加载共享网络工作者?

How can I load a shared web worker with a user-script?

我想用用户脚本加载一个共享工作者。问题是用户脚本是免费的,并且没有用于托管文件的商业模式——我也不想使用服务器,即使是免费服务器来托管一个小文件。无论如何,I tried it 和我(当然)得到同源策略错误:

Uncaught SecurityError: Failed to construct 'SharedWorker': Script at
'https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js'
cannot be accessed from origin 'http://whosebug.com'.

There's another way 通过将 worker 函数转换为字符串然后转换为 Blob 并将其作为 worker 加载来加载 web worker,但我也试过了:

var sharedWorkers = {};
var startSharedWorker = function(workerFunc){
    var funcString = workerFunc.toString();
    var index = funcString.indexOf('{');
    var funcStringClean = funcString.substring(index + 1, funcString.length - 1);
    var blob = new Blob([funcStringClean], { type: "text/javascript" });
    sharedWorkers.google = new SharedWorker(window.URL.createObjectURL(blob));
    sharedWorkers.google.port.start();
};

这也不管用。为什么?因为共享 worker 是 shared 基于他们的 worker 文件加载的位置。自 createObjectURL generates a unique file name for each use 以来,工人将永远不会拥有相同的 URL,因此永远不会被共享。

我该如何解决这个问题?


Note: I tried asking about specific solutions, but at this point I think the best I can do is ask in a more broad manner for any solution to the problem, since all of my attempted solutions seem fundamentally impossible due to same origin policies or the way URL.createObjectURL works (from the specs, it seems impossible to alter the resulting file URL).

That being said, if my question can somehow be improved or clarified, please leave a comment.

我很确定你想要一个不同的答案,但遗憾的是,归结起来就是这样。

浏览器实施 同源策略 来保护互联网用户,尽管您的意图很明确,但没有合法的浏览器允许您更改 sharedWorker 的来源。

sharedWorker 中的所有浏览上下文必须共享完全相同的来源

  • 主机
  • 协议
  • 端口

你无法绕过这个问题,除了你的方法之外,我还尝试使用 iframe,但都行不通。

也许你可以把你的 javascript 文件放在 github 上,然后使用他们的 raw. 服务来获取文件,这样你就可以 运行 不用太多努力。

更新

我正在阅读 chrome 更新,我记得你问过这个问题。 跨源服务人员到达 chrome!

为此,请将以下内容添加到 SW 的安装事件中:

self.addEventListener('install', event => {
  event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
  });
});

还需要注意一些其他事项,请查看:

完整 link: https://developers.google.com/web/updates/2016/09/foreign-fetch?hl=en?utm_campaign=devshow_series_crossoriginserviceworkers_092316&utm_source=gdev&utm_medium=yt-desc

您可以使用 fetch()response.blob() 从返回的 Blob 创建 application/javascript 类型的 Blob URL;将 SharedWorker() 参数设置为 URL.createObjectURL() 创建的 Blob URL;利用新打开的 windowwindow.open()load 事件定义先前在原始 window 定义的相同 SharedWorker,将 message 事件附加到原始 SharedWorker 在新开业的 windows.

javascriptconsole 进行了尝试,其中当前问题 URL 应该在新的 tab 加载 message 来自通过在 console 记录的 worker.port.postMessage() 事件处理程序打开 window

打开 window 也应该记录 message 事件,当使用 worker.postMessage(/* message */) 从新打开的 window 发布时,类似地在打开 window

window.worker = void 0, window.so = void 0;

fetch("https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js")
  .then(response => response.blob())
  .then(script => {
    console.log(script);
    var url = URL.createObjectURL(script);
    window.worker = new SharedWorker(url);
    console.log(worker);
    worker.port.addEventListener("message", (e) => console.log(e.data));
    worker.port.start();

    window.so = window.open("https://whosebug.com/questions/" 
                            + "38810002/" 
                            + "how-can-i-load-a-shared-web-worker-" 
                            + "with-a-user-script", "_blank");

    so.addEventListener("load", () => {
      so.worker = worker;
      so.console.log(so.worker);
      so.worker.port.addEventListener("message", (e) => so.console.log(e.data));
      so.worker.port.start();
      so.worker.port.postMessage("hi from " + so.location.href);
    });

    so.addEventListener("load", () => {
      worker.port.postMessage("hello from " + location.href)
    })

  });

consoletab 你可以使用,例如;在 worker.postMessage("hello, again") at new window of current URL worker.port.postMessage("hi, again");,其中每个 window 附加了 message 个事件,两个 window 之间的通信可以使用在 SharedWorker 创建的原始文件来实现初始 URL.

先决条件

  • 根据您的研究和评论中提到的, SharedWorker 的 URL 受同源政策约束。
  • 根据 this questionWorker 的 URL 没有 CORS 支持。
  • 根据 this issue GM_worker 支持现在是 WONT_FIX,并且 由于 Firefox 的变化,似乎几乎不可能实施。 还有一个注意事项是沙盒 Worker(相对于 unsafeWindow.Worker) 也不起作用。

设计

我想你想要实现的是一个 @include * 用户脚本,它将收集一些统计数据或创建一些全局 UI 将出现在任何地方的东西。因此你希望有一个工作人员在 运行 时间内维护一些状态或统计聚合(这将很容易从用户脚本的每个实例访问),and/or 你想做一些计算 -繁重的例程(因为否则它会减慢目标网站的速度)。

以任何方式解决

我想提出的解决方案是用替代方案替换SharedWorker设计。

  • 如果您只想在共享工作者中维护一个状态,只需使用 Greasemonkey 存储(GM_setValue 和朋友)。它在所有用户脚本实例之间共享(幕后的 SQLite)。
  • 如果你想做一些计算量大的任务,在 unsafeWindow.Worker 中完成并将结果放回 Greasemonkey 存储中。
  • 如果你想做一些后台计算,并且它必须 运行 只能通过单个实例,有许多 "inter-window" 个同步库(它们大多使用 localStorage 但 Greasemomkey 有相同的 API,因此为其编写适配器应该不难)。因此,您可以获得一个用户脚本实例中的锁,并在其中 运行 您的例程。比如,IWC or ByTheWay (likely used here on Stack Exchange; post about it).

其他方式

我不确定,但可能有一些巧妙的响应欺骗,由 ServiceWorker 制作,使 SharedWorker 可以按照您的意愿工作。起点在 this answer's edit.

是的,你可以! (方法如下):

我不知道是不是因为自问这个问题以来的四年里发生了一些变化,但是完全有可能完全按照问题的要求去做 .这甚至不是特别困难。诀窍是从直接包含其代码的 data-url 初始化共享工作者 ,而不是从 createObjectURL(blob).

这可能最容易通过示例来演示,所以这里有一个 whosebug.com 的小用户脚本,它使用共享工作者为每个 Whosebug window 分配一个唯一 ID 号,显示在选项卡标题中。请注意,shared-worker 代码直接作为模板字符串包含在内(即在反引号之间):

// ==UserScript==
// @name Whosebug userscript shared worker example
// @namespace Whosebug test code
// @version      1.0
// @description Demonstrate the use of shared workers created in userscript
// @icon https://whosebug.com/favicon.ico
// @include http*://whosebug.com/*
// @run-at document-start
// ==/UserScript==

(function() {
  "use strict";

  var port = (new SharedWorker('data:text/javascript;base64,' + btoa(
  // =======================================================================================================================
  // ================================================= shared worker code: =================================================
  // =======================================================================================================================

  // This very simple shared worker merely provides each window with a unique ID number, to be displayed in the title
  `
  var lastID = 0;
  onconnect = function(e)
  {
    var port = e.source;
    port.onmessage = handleMessage;
    port.postMessage(["setID",++lastID]);
  }

  function handleMessage(e) { console.log("Message Recieved by shared worker: ",e.data); }
  `
  // =======================================================================================================================
  // =======================================================================================================================
  ))).port;

  port.onmessage = function(e)
  {
    var data = e.data, msg = data[0];
    switch (msg)
    {
      case "setID": document.title = "#"+data[1]+": "+document.title; break;
    }
  }
})();

我可以确认这适用于 FireFox v79 + Tampermonkey v4.11.6117。

有一些小警告:

首先,您的用户脚本所定位的页面可能带有 Content-Security-Policy header,它明确限制了脚本或工作脚本的来源(script-src 或 worker-src 政策)。在那种情况下,带有脚本内容的 data-url 可能会被阻止,而且 OTOH 我想不出解决这个问题的方法,除非添加一些未来的 GM_ 函数以允许用户脚本覆盖页面的 CSP 或更改它的 HTTP headers,或者除非用户 运行s 他们的浏览器带有扩展程序或浏览器设置以禁用 CSP(参见 Disable same origin policy in Chrome)。

其次,可以在多个域上将用户脚本定义为 运行,例如您可能 运行 https://amazon.com and https://amazon.co.uk 上使用相同的用户脚本 。但即使由这个单一用户脚本创建,共享工作者也遵守 same-origin 政策,因此应该为所有 .com 创建一个不同的共享工作者实例 windows 对比所有 .co.uk windows。请注意这一点!

最后,一些浏览器可能会对 data-urls 的长度施加大小限制,从而限制共享工作者的最大代码长度。即使不受限制,在每次 window 加载时将长而复杂的共享工作程序的所有代码转换为 base64 并返回的效率也非常低。共享工作人员的索引也非常长 URLs(因为您连接到现有的共享工作人员是基于其精确 URL 的匹配)。所以你可以做的是 (a) 从一个最初非常小的共享工作者开始,然后使用 eval() 向它添加真实的(可能更长的)代码,以响应传递给的“InitWorkerRequired”消息之类的东西第一个 window 打开 worker,以及 (b) 为了提高效率,pre-calculate 包含初始最小 shared-worker bootstrap 代码的 base-64 字符串。

这是上面示例的修改版本,添加了这两条皱纹(也经过测试并确认有效),运行s 在 both whosebug.comen.wikipedia.org(这样您就可以验证不同的域确实使用了单独的共享工作者实例):

// ==UserScript==
// @name Whosebug & wikipedia userscript shared worker example
// @namespace Whosebug test code
// @version      2.0
// @description Demonstrate the use of shared workers created in userscript, with code injection after creation
// @icon https://whosebug.com/favicon.ico
// @include http*://whosebug.com/*
// @include http*://en.wikipedia.org/*
// @run-at document-end
// ==/UserScript==

(function() {
  "use strict";

  // Minimal bootstrap code used to first create a shared worker (commented out because we actually use a pre-encoded base64 string created from a minified version of this code):
/*
// ==================================================================================================================================
{
  let x = [];
  onconnect = function(e)
  {
    var p = e.source;
    x.push(e);
    p.postMessage(["InitWorkerRequired"]);
    p.onmessage = function(e)  // Expects only 1 kind of message:  the init code.  So we don't actually check for any other sort of message, and page script therefore mustn't send any other sort of message until init has been confirmed.
    {
      (0,eval)(e.data[1]);  // (0,eval) is an indirect call to eval(), which therefore executes in global scope (rather than the scope of this function). See http://perfectionkills.com/global-eval-what-are-the-options/ or https://whosebug.com/questions/19357978/indirect-eval-call-in-strict-mode
      while(e = x.shift()) onconnect(e);  // This calls the NEW onconnect function, that the eval() above just (re-)defined.  Note that unless windows are opened in very quick succession, x should only have one entry.
    }
  }
}
// ==================================================================================================================================
*/

  // Actual code that we want the shared worker to execute.  Can be as long as we like!
  // Note that it must replace the onconnect handler defined by the minimal bootstrap worker code.
  var workerCode =
// ==================================================================================================================================
`
  "use strict";  // NOTE: because this code is evaluated by eval(), the presence of "use strict"; here will cause it to be evaluated in it's own scope just below the global scope, instead of in the global scope directly.  Practically this shouldn't matter, though: it's rather like enclosing the whole code in (function(){...})();
  var lastID = 0;
  onconnect = function(e)  // MUST set onconnect here; bootstrap method relies on this!
  {
    var port = e.source;
    port.onmessage = handleMessage;
    port.postMessage(["WorkerConnected",++lastID]);  // As well as providing a page with it's ID, the "WorkerConnected" message indicates to a page that the worker has been initialized, so it may be posted messages other than "InitializeWorkerCode"
  }

  function handleMessage(e)
  {
    var data = e.data;
    if (data[0]==="InitializeWorkerCode") return;  // If two (or more) windows are opened very quickly, "InitWorkerRequired" may get posted to BOTH, and the second response will then arrive at an already-initialized worker, so must check for and ignore it here.
    // ...
    console.log("Message Received by shared worker: ",e.data);  // For this simple example worker, there's actually nothing to do here
  }
`;
// ==================================================================================================================================

  // Use a base64 string encoding minified version of the minimal bootstrap code in the comments above, i.e.
  // btoa('{let x=[];onconnect=function(e){var p=e.source;x.push(e);p.postMessage(["InitWorkerRequired"]);p.onmessage=function(e){(0,eval)(e.data[1]);while(e=x.shift()) onconnect(e);}}}');
  // NOTE:  If there's any chance the page might be using more than one shared worker based on this "bootstrap" method, insert a comment with some identification or name for the worker into the minified, base64 code, so that different shared workers get unique data-URLs (and hence don't incorrectly share worker instances).

  var port = (new SharedWorker('data:text/javascript;base64,e2xldCB4PVtdO29uY29ubmVjdD1mdW5jdGlvbihlKXt2YXIgcD1lLnNvdXJjZTt4LnB1c2goZSk7cC5wb3N0TWVzc2FnZShbIkluaXRXb3JrZXJSZXF1aXJlZCJdKTtwLm9ubWVzc2FnZT1mdW5jdGlvbihlKXsoMCxldmFsKShlLmRhdGFbMV0pO3doaWxlKGU9eC5zaGlmdCgpKSBvbmNvbm5lY3QoZSk7fX19')).port;

  port.onmessage = function(e)
  {
    var data = e.data, msg = data[0];
    switch (msg)
    {
      case "WorkerConnected": document.title = "#"+data[1]+": "+document.title; break;
      case "InitWorkerRequired": port.postMessage(["InitializeWorkerCode",workerCode]); break;
    }
  }
})();