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

IT技术 javascript greasemonkey userscripts
2021-02-21 18:56:03

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

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

还有另一种方法可以通过将工作函数转换为字符串然后转换为 Blob 并将其作为工作器加载来加载网络工作器,但我也尝试过:

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 是基于它们的 worker 文件加载的位置共享的。由于createObjectURL为每次使用生成唯一的文件名,工作人员将永远不会拥有相同的 URL,因此永远不会被共享。

我怎么解决这个问题?


注意:我尝试询问具体的解决方案,但在这一点上,我认为我能做的最好的事情就是以更广泛的方式询问 问题的任何解决方案,因为由于同源政策或方式 URL.createObjectURL有效(从规范来看,似乎不可能更改生成的文件 URL)。

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

4个回答

您可以使用fetch(),从返回的response.blob()创建Blob URL类型参数设置创建者利用新打开的事件定义与原先定义的相同,将事件附加新打开原点application/javascriptBlobSharedWorker()Blob URLURL.createObjectURL()window.open()loadwindowSharedWorkerwindowmessageSharedWorkerwindow

javascript在被尝试console如何从其他的iFrame清除iFrame的内容,在当前问题的URL应该在新的装载tabmessage从开window通过worker.port.postMessage()事件处理记录在console

当从新打开的using发布时,打开window也应该记录message事件,同样在打开时windowworker.postMessage(/* 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://stackoverflow.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)
    })

  });

console任何一个tab你都可以使用,例如; 如何 worker.postMessage("hello, again")window当前 URL 的位置从另一个 iFrame清除 iFrame 的内容如何使用用户脚本加载共享的 Web Worker?worker.port.postMessage("hi, again");其中message事件附加在每个window,两个windows之间的通信可以使用SharedWorker在初始 URL 处创建的原始实现

我可以确认这是有效的。考虑到其他人(在 250 个问题查看之后)声称这是不可能的,这令人印象深刻。我应该指出有一个限制,但是这仍然满足大多数用例。该限制是:如果你需要从有一个页面domain A打开,然后由于某种原因打开一个 domain A 页面,从 domain B或用户打开一个页面,这是不可能建立的两个页面之间共享的网络工作者domain A第二domain A页必须第一domain A打开不是一个巨大的限制,但值得指出。
2021-04-27 18:56:03
这个解决方案所做的就是在两个页面之间共享一个对象,这两个页面已经对彼此的对象具有完整的 javascript 访问权限。在这里使用共享工作者没有任何好处,您也可以使用普通工作者。这两个窗口甚至没有单独的消息端口到/从工人。因此,对于几乎所有可能需要共享工作者的用例,此解决方案都不够用,抱歉。
2021-04-29 18:56:03
@Viziionarystackoverflow.com/questions/33645685/...javascript尝试过您尝试使用哪个版本的 chrome?console
2021-05-05 18:56:03
@Viziionary它可能会进行通信,以对现有的参考Tab A SharedWorker到手动打开Tab B使用window.postMessage()MessageChannel或其他方法。不同来源之间的交流可能超出了当前问题的范围?虽然,也应该是可能的。
2021-05-07 18:56:03
这个示例代码应该在哪个页面上运行?我正在尝试对其进行测试,但在 Chrome 中出现错误:Uncaught (in promise) TypeError: Cannot read property 'addEventListener' of undefined(…)fetch.then.then.script @ VM1910:18
2021-05-09 18:56:03

前提

  • 正如您所研究的以及评论中提到的, SharedWorker的 URL 受同源政策的约束。
  • 根据这个问题, 的WorkerURL没有 CORS 支持
  • 根据这个问题, GM_worker支持现在是 WONT_FIX,由于 Firefox 的变化,它 似乎已经接近到不可能实现。还有一个说明是沙盒化Worker(与 相对 unsafeWindow.Worker)也不起作用。

设计

我想您想要实现的是一个用户@include *脚本,它将收集一些统计信息或创建一些将随处可见的全局 UI。因此,您希望有一个工作人员在运行时维护一些状态或统计聚合(这将很容易从用户脚本的每个实例访问),和/或您想要做一些计算量大的例程(因为否则它会减慢目标站点的速度)。

以任何解决方案的方式

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

  • 如果您只想在共享工作者中维护一个状态,只需使用 Greasemonkey 存储(GM_setValue和朋友)。它在所有用户脚本实例之间共享(隐藏在场景中的 SQLite)。
  • 如果您想做一些计算量大的任务,请将其unsafeWindow.Worker放入并将结果放回 Greasemonkey 存储中。
  • 如果你想做一些后台计算并且它必须只由单个实例运行,那么有许多“窗口间”同步库(大多数它们使用localStorage但Greasemomkey的具有相同的API,因此编写一个不难适配器)。因此,您可以在一个用户脚本实例中获取锁并在其中运行您的例程。例如,IWCByTheWay可能在 Stack Exchange 上使用;发布有关它)。

另一种方式

我不知道,但可能会有一些巧妙的回应欺骗,从制作ServiceWorker,使SharedWorker工作,你想。起点在此答案的编辑中

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

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

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

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

  • 主持人
  • 协议
  • 港口

您无法解决此问题,除了您的方法之外,我还尝试使用 iframe,但不会起作用。

也许你可以把你的javascript文件放在github上并使用他们的raw.服务来获取文件,这样你就可以毫不费力地运行它。

更新

我正在阅读 chrome 更新,我记得你问过这个。 跨域服务工作者抵达 chrome!

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

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

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

是的你可以!(就是这样):

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

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

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

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

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

下面是在(也经过测试,确认工作)将这两皱纹上述例子的修改版本,即上运行两个 stackoverflow.comen.wikipedia.org(只是让你可以验证不同的域确实使用独立的共享工作者实例):

// ==UserScript==
// @name stackoverflow & wikipedia userscript shared worker example
// @namespace stackoverflow test code
// @version      2.0
// @description Demonstrate the use of shared workers created in userscript, with code injection after creation
// @icon https://stackoverflow.com/favicon.ico
// @include http*://stackoverflow.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://stackoverflow.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;
    }
  }
})();
在 Firefox 中工作很好,但在 Chrome 中不起作用,所以不是真的可用。data: 和 blob: 都应该可以工作,但 Chrome 只是不遵循规范。
2021-04-17 18:56:03
@Glenn,顺便说一句,这两个脚本都是我在上面提供的经过测试的脚本。另外顺便说一句,你不能使用blob:这个,因为每个窗口都会创建自己独特的 blob URL,不管内容如何,​​并且你不能跨窗口共享它们,所以你会得到每个窗口都有自己独立的结果,孤立的SharedWorker实例,完全违背了共享Worker的目的
2021-04-19 18:56:03
@Gerold 是的,这就是重点:所有代码都必须在用户脚本本身中,因为用户脚本不与其作者可以在其上提供脚本文件的任何服务器相关联。...;base64,' + btoa(...)是必要的,因为您将工作代码包含在数据 URL 中,而 URL 只能包含一组有限的字符:如果您没有将代码转换为 base64,它将在 URL 中无效。
2021-04-27 18:56:03
@Glenn,我可以确认它也可以在 Chrome 中运行 - 我刚刚在 Chrome v91 + Tampermonkey 4.13 中对其进行了测试,并且它完全符合预期。当你尝试的时候到底发生了什么?
2021-05-12 18:56:03
我尝试了第一个示例代码,它可以工作,但您不是从某个地方加载工作人员的代码,它包含在脚本本身中。我不明白为什么这...;base64,' + btoa(...)是必要的。
2021-05-14 18:56:03