如何使用用户脚本加载共享Web worker?

时间:2016-08-07 01:06:27

标签: javascript greasemonkey userscripts

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

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

我该如何解决这个问题?

  

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

     

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

4 个答案:

答案 0 :(得分:9)

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

<{> 1}}在window How to clear the contents of an iFrame from another iFrame尝试了javascript,其中当前问题网址应该从consoletab开始加载message通过window记录的worker.port.postMessage()事件处理程序。

打开console也应该使用window从新打开的message发布window事件,同样在开启worker.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,您可以使用,例如;在How to clear the contents of an iFrame from another iFrame tabworker.postMessage("hello, again")当前网址How can I load a shared web worker with a user-script?window其中worker.port.postMessage("hi, again");个事件附加在每个message,之间的通信使用在初始网址创建的原始window可以实现两个window

答案 1 :(得分:4)

前提条件

  • 正如您所研究的那样,正如评论中提到的那样, SharedWorker的网址受同源政策约束。
  • 根据this questionWorker的网址没有CORS支持。
  • 根据this issue GM_worker支持现在是WONT_FIX,并且 由于Firefox的变化,似乎已经足够接近了。 还有一个沙箱Worker的注释(相对于 unsafeWindow.Worker)也不起作用。

设计

我想你要实现的是一个@include *用户脚本,它将收集一些统计信息或创建一些全局用户界面,这些用户界面随处可见。因此,您希望让一个worker在运行时维护一些状态或统计聚合(这将很容易从每个用户脚本实例访问),和/或您想要做一些计算繁重的例程(因为否则它会缓慢的目标网站。)

以任何解决方案的方式

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

  • 如果您只想在共享工作者中维护状态,只需使用Greasemonkey存储(GM_setValue和朋友)。它在所有用户脚本实例之间共享(SQLite隐藏场景)。
  • 如果你想在unsafeWindow.Worker中执行计算繁重的任务,并将结果放回Greasemonkey存储中。
  • 如果你想进行一些后台计算并且它必须只由单个实例运行,那么有很多&#34; inter-window&#34;同步库(大多数都使用localStorage但Greasemomkey具有相同的API,因此编写适配器并不困难)。因此,您可以在一个用户脚本实例中获取锁定并在其中运行您的例程。例如,堆叠交换上的IWCByTheWaylikely used here; post about it)。

其他方式

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

答案 2 :(得分:2)

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

浏览器实施同源政策来保护互联网用户,虽然您的意图很干净,但没有合法的浏览器允许您更改共享工作者的来源。

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

  • 宿主
  • 协议
  • 端口

你不能解决这个问题,除了你的方法之外我一直在尝试使用iframe,但是non会工作。

也许你可以把你的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

答案 3 :(得分:0)

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

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

这可能是最容易通过示例演示的示例,因此这是 stackoverflow.com 的小用户脚本,该脚本使用共享工作程序为每个stackoverflow窗口分配一个唯一的ID号,该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(请参见Disable same origin policy in Chrome)。

第二,可以将用户脚本定义为在多个域上运行,例如您可以在https://amazon.comhttps://amazon.co.uk上运行同一用户脚本。但是,即使是由单个用户脚本创建的,共享工作人员也要遵循相同的政策,因此应该为所有 .com 窗口和所有< em> .co.uk 窗口。注意这一点!

最后,某些浏览器可能会限制数据URL的大小,从而限制了共享工作程序的最大代码长度。即使没有限制,长时间复杂的共享工作程序的所有代码在每次窗口加载时都转换为base64并返回的效率很低。共享工作者使用极长的URL进行索引也是如此(因为您是基于匹配其确切URL来连接到现有共享工作者的)。因此,您可以做的是(a)首先从一个非常小的共享工作程序开始,然后使用eval()向其添加真实的(可能更长的时间)代码,以响应传递给“ InitWorkerRequired”消息的响应第一个打开工作程序的窗口,以及(b)为了提高效率,请预先计算包含初始最小共享工作程序引导程序代码的base-64字符串。

这是上述示例的修改版,其中添加了这两个皱纹(也已测试并确认可以正常工作),它们同时在 stackoverflow.com en.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;
    }
  }
})();