【问题标题】:How can I load a shared web worker with a user-script?如何使用用户脚本加载共享网络工作者?
【发布时间】:2016-12-13 02:16:58
【问题描述】:

我想使用用户脚本加载共享工作者。问题是用户脚本是免费的,并且没有托管文件的商业模式——我也不想使用服务器,即使是免费的服务器来托管一个小文件。无论如何,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://*.com'.

There's another way 通过将工作函数转换为字符串然后转换为 Blob 并将其作为工作程序加载来加载 Web 工作程序,但我也尝试过:

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();
};

这也不起作用。为什么?因为共享工作人员是根据加载工作人员文件的位置共享的。由于createObjectURL generates a unique file name for each use,worker 将永远不会拥有相同的 URL,因此永远不会被共享。

我该如何解决这个问题?


注意:我尝试询问具体的解决方案,但此时我认为 我能做的最好的就是以更广泛的方式询问任何 问题的解决方案,因为我所有尝试的解决方案似乎 由于同源政策或方式,根本不可能 URL.createObjectURL 有效(from the specs,似乎不可能 更改生成的文件 URL)。

话虽如此,如果我的问题可以以某种方式改进或澄清,请发表评论。

【问题讨论】:

  • 如果可以从多个浏览上下文访问 SharedWorker,则所有这些浏览上下文必须共享完全相同的来源(相同的协议、主机和端口)。看:developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/…
  • @HaHoang 我知道共享网络工作者必须共享同一个来源。我已经彻底研究了规格。
  • 我很确定@HaHoang 的观点是你不能这样做。如果你能做你想做的事,那么你首先就有效地击败了他们为共享工作人员设置的所有安全措施。
  • @Pekka웃 哦,好吧,我想我选择了,因为它是领带哈哈。非常感谢您的赏金 :) 我们讨论的脚本将在周一下午发布在 meta 上 :)

标签: javascript greasemonkey userscripts


【解决方案1】:

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

javascript 已在 console How to clear the contents of an iFrame from another iFrame 尝试,其中当前问题 URL 应在新的 tab 加载,message 从打开 windowworker.port.postMessage() 事件处理程序记录在 console

当使用worker.postMessage(/* message */)从新打开的window发布时,打开window也应该记录message事件,同样在打开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://*.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 然后你可以使用,例如;在How to clear the contents of an iFrame from another iFrame worker.postMessage("hello, again") 在新的window 的当前URL How can I load a shared web worker with a user-script?, worker.port.postMessage("hi, again"); 其中message 事件附加在每个window,两个windows 之间的通信可以使用原始SharedWorker 来实现在初始 URL 处创建。

【讨论】:

  • 这个示例代码应该在哪个页面上运行?我正在尝试对其进行测试,但在 Chrome 中出现错误:Uncaught (in promise) TypeError: Cannot read property 'addEventListener' of undefined(…)fetch.then.then.script @ VM1910:18
  • @Viziionary javascript 已在 console *.com/questions/33645685/… 尝试过。您尝试了哪个版本的 chrome?
  • 我可以确认这是可行的。考虑到其他人(在 250 个问题视图之后)声称这是不可能的,这令人印象深刻。我应该指出有一个限制,但这仍然满足大多数用例。限制是:如果您需要打开来自domain A 的页面,则出于某种原因打开一个 new domain A 页面 from domain B 或用户打开一个页面,不可能在domain A的两个页面之间建立一个共享的web worker。第二个domain A 页面必须 第一个domain A 页面打开。限制不是很大,但值得指出。
  • @Viziionary 可以使用window.postMessage()MessageChannel 或其他方法将现有Tab A SharedWorker 的引用传递给手动打开的Tab B。不同来源之间的交流可能超出了当前问题的范围?不过,应该也是可以的。
  • 此解决方案所做的只是在两个页面之间共享一个对象,该对象已经拥有对彼此对象的完整 javascript 访问权限。在这里使用共享工作者没有任何优势,您也可以使用普通工作者。这两个窗口甚至没有单独的消息端口来往/来自工作人员。因此,对于几乎所有可能需要共享工作器的用例,此解决方案都不够用,抱歉。
【解决方案2】:

前提条件

  • 正如你所研究的,正如你在 cmets 中提到的那样, SharedWorker 的 URL 受同源政策约束。
  • 根据this questionWorker 的 URL 不支持 CORS。
  • 根据this issue GM_worker 支持现在是 WONT_FIX,并且 由于 Firefox 的变化,似乎几乎不可能实现。 还有一个注释是沙盒化Worker(而不是 unsafeWindow.Worker) 也不起作用。

设计

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

以任何解决方案的方式

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

  • 如果您只想在共享工作器中保持状态,只需使用 Greasemonkey 存储(GM_setValue 和朋友)。它在所有用户脚本实例之间共享(SQLite 在幕后)。
  • 如果您想做一些计算量很大的任务,请将其放入 unsafeWindow.Worker 并将结果放回 Greasemonkey 存储中。
  • 如果你想做一些后台计算并且它必须只由单个实例运行,有许多“窗口间”同步库(大多数他们使用localStorage,但 Greasemomkey 有相同的 API,所以它应该很难为它写一个适配器)。因此,您可以在一个用户脚本实例中获得锁并在其中运行您的例程。比如IWCByTheWay(堆栈交换上的likely used herepost about it)。

其他方式

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

【讨论】:

  • 您对 guest271314 的回答有何看法。它没有实现你认为不可能的事情吗?
  • @Viziionary,您是否得到了客人在用户脚本中工作的答案? (或者完全基于您的评论?)尤其是@grant 设置为有用的值(不是none)?
  • @BrockAdams 它从控制台工作。我不认为它是从控制台而不是从用户脚本工作的。我会尽快测试并做出回应。
  • @BrockAdams 我没有尝试过@grant / GM 功能。我刚刚验证它有效。也许这是解决方案的另一个问题,我得看看。 GM_setValue() 也是一个我还没有研究过的函数。如果你认为你可以用它来提出一个更好的解决方案,请随意。如果我认为新答案更好,我会更改所选答案。
  • @Viziionary 好吧,我无法理解答案中的文字。我试着读了好几遍。 sn-p 所展示的想法并没有让我改变我的信念。当您有一个用户导航的新窗口实例时,这是一个非常狭窄的案例,因此您可以将一个不可序列化的对象(在我们的案例中为共享工作器)附加到它。
【解决方案3】:

我很确定你想要一个不同的答案,但遗憾的是这就是它归结为的原因。

浏览器实施 same-origin-policies 来保护互联网用户,尽管您的意图是干净的,但没有合法的浏览器允许您更改 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']
  });
});

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

完整链接: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

【讨论】:

  • 用户脚本可以访问称为 GM 函数的特殊 JS 函数。其中一个函数是GM_xmlhttpRequest 也许我可以修改共享工作者加载脚本的方式,用这个忽略同源策略的函数替换它的加载机制。我们可以修改共享网络工作者的那部分吗?加载工作脚本的函数是否暴露在任何地方?它是否使用页面的原生 xmlhttpRequest
  • @Viziionary GM 函数是greasemonkey 和其他浏览器JS 注入器的自定义函数,不能在生产或其他人的机器上使用。遗憾的是,您无法更改本机代码或使用它,您可以将其放在代理前面,但这不会实现您所追求的。我会尽力为你找到方法,但我非常怀疑..
  • 哥们,这个问题的前提你一定没看懂。我正在编写用户脚本。用户下载 Greasemonkey,将我的脚本添加到他的客户端,当他进入我的脚本所针对的站点时,我的脚本(问题中询问的用户脚本)在客户端浏览器上运行,插入到目标页面中。
  • 也许我误会了你。我不太确定。对不起,如果是这样的话。
  • 您对 guest271314 的回答有何看法。它没有实现你认为不可能的事情吗?
【解决方案4】:

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

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

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

// ==UserScript==
// @name * userscript shared worker example
// @namespace * test code
// @version      1.0
// @description Demonstrate the use of shared workers created in userscript
// @icon https://*.com/favicon.ico
// @include http*://*.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 标头,该标头明确限制了脚本或工作脚本(script-src 或 worker-src 策略)的来源。在这种情况下,带有脚本内容的 data-url 可能会被阻止,而且我想不出办法解决这个问题,除非添加一些未来的 GM_ 函数以允许用户脚本覆盖页面的 CSP 或更改其 HTTP标头,或者除非用户使用扩展程序或浏览器设置运行浏览器以禁用 CSP(参见例如 Disable same origin policy in Chrome)。

其次,用户脚本可以定义为在多个域上运行,例如您可以在https://amazon.comhttps://amazon.co.uk 上运行相同的用户脚本。但是即使是由这个单一的用户脚本创建的,共享工作者也遵守同源策略,所以应该有一个不同的共享工作者实例,它为所有 .com 窗口创建,而不是为所有 .co.uk 窗口。请注意这一点!

最后,一些浏览器可能会对 data-url 的长度施加大小限制,从而限制共享工作器的最大代码长度。即使不受限制,将长而复杂的共享工作者的所有代码转换为 base64 并在每个窗口加载时返回也是非常低效的。通过极长的 URL 对共享工作者进行索引也是如此(因为您根据匹配其确切 URL 连接到现有的共享工作者)。所以你可以做的是(a)从一个最初非常小的共享工作者开始,然后使用eval()向它添加真正的(可能更长的)代码,以响应传递给第一个的“InitWorkerRequired”消息之类的东西打开工作器的窗口,以及 (b) 为了提高效率,预先计算包含初始最小共享工作器引导代码的 base-64 字符串。

这是上面示例的修改版本,添加了这两个皱纹(也经过测试并确认可以工作),它在 *.com 上运行>en.wikipedia.org(这样您就可以验证不同的域确实使用单独的共享工作实例):

// ==UserScript==
// @name * & wikipedia userscript shared worker example
// @namespace * test code
// @version      2.0
// @description Demonstrate the use of shared workers created in userscript, with code injection after creation
// @icon https://*.com/favicon.ico
// @include http*://*.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://*.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;
    }
  }
})();

【讨论】:

  • 我尝试了第一个示例代码,它可以工作,但是您没有从某个地方加载工作人员的代码,它包含在脚本本身中。我不明白为什么...;base64,' + btoa(...) 是必要的。
  • @Gerold 是的,这就是重点:所有代码必须在用户脚本本身中,因为用户脚本不与任何服务器相关联,其作者可以使脚本文件可用在。 ...;base64,' + btoa(...) 是必要的,因为您将工作代码包含在数据 URL 中,并且 URL 只能包含一组有限的字符:如果您没有将代码转换为 base64,它在网址。
  • 在 Firefox 中工作很好,但在 Chrome 中却不行,所以不能真正使用。 data: 和 blob: supposed 都可以工作,但 Chrome 只是不遵循规范。
  • @Glenn,我可以确认它也可以在 Chrome 中运行 - 我刚刚在 Chrome v91 + Tampermonkey 4.13 中对其进行了测试,它完全按照它的预期运行。当你尝试它时究竟发生了什么?
  • @Glenn,顺便说一句,这两个脚本都是我在上面提供的我测试过的。另外顺便说一句,您不能为此使用blob:,因为每个窗口都会创建自己独特的blob URL,无论内容如何,​​并且您不能跨窗口共享它们,所以您会得到结果是每个窗口都有自己独立的、隔离的SharedWorker 实例,完全违背了SharedWorker 的目的。
最近更新 更多