是的,你可以! (方法如下):
我不知道是不是因为自从提出这个问题以来的四年里发生了一些变化,但是完全有可能完全按照问题的要求去做。这甚至不是特别困难。诀窍是从包含其代码的 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.com 和https://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;
}
}
})();