Firefox WebExtension:如何在禁用/卸载之前运行代码?

时间:2016-07-28 19:34:49

标签: javascript firefox firefox-addon firefox-webextensions

我最近将我的GreaseMonkey脚本转换为WebExtension,只是为了获得该过程的第一印象。现在我已经达到了这样的程度,即在禁用/卸载所述扩展时,进行一些清理或简单地撤消所有更改。

从我在Mozilla的页面上看到的内容,runtime.onSuspend应该可以解决问题。不幸的是,它看起来还没有实现(我在常规的Firefox发布渠道上)。

换句话说,我想要做的是运行代码,因为用户删除/禁用我的扩展程序,以便我可以清理监听器等,并通常将选项卡恢复到他们的状态,即。例如,撤消扩展程序所做的所有更改。

2 个答案:

答案 0 :(得分:5)

The other answer不正确。第一部分(关于onSuspend事件)事实上是不正确的。关于setUninstallURL的部分是相关的,但不回答问题,因为它不允许您将标签恢复到其原始状态(正如您在问题中所述)。

在这个答案中,我将首先清除对runtime.onSuspend的误解,然后解释当禁用扩展时如何为内容脚本运行代码。

关于runtime.onSuspend

chrome.runtime.onSuspendchrome.runtime.onSuspendCanceled事件与已禁用/已卸载的扩展程序无关。事件是为event pages定义的,这些事件基本上是在一段时间不活动后暂停(卸载)的后台页面。当事件页面由于暂停而即将卸载时,将调用runtime.onSuspend。如果在此活动期间调用了扩展程序API(例如发送扩展程序消息),则会取消暂停并触发onSuspendCanceled事件。

当由于浏览器关闭或卸载而卸载扩展时,无法扩展扩展的生命周期。因此,您不能依赖这些事件来运行异步任务(例如从后台页面清除选项卡)。

此外,这些事件在内容脚本中不可用(仅扩展页面,例如后台页面),因此这些事件不能用于同步清理内容脚本逻辑。

从上面可以明显看出,runtime.onSuspend 与禁用时的清理目标远程相关。不在Chrome中,更不用说Firefox(Firefox不支持事件页面,这些事件毫无意义)。

在扩展禁用/卸载时在选项卡/内容脚本中运行代码

Chrome扩展程序中的一种常见模式是使用port.onDisconnect事件来检测后台页面是否已卸载,并使用它来推断扩展程序可能已卸载(与option 1 of this method结合使用更高版本准确性)。禁用扩展程序后,Chrome的内容脚本会保留,因此可用于运行异步清理代码。
这在Firefox中是不可能的,因为在port.onDisconnect事件有可能触发之前(至少在bugzil.la/1223425被修复之前)禁用Firefox扩展时会破坏内容脚本的执行上下文

尽管存在这些限制,但在禁用加载项时仍可以为内容脚本运行清理逻辑。此方法基于以下事实:在Firefox中,当禁用加载项时,将删除使用tabs.insertCSS插入的样式表。
我将讨论利用这一特征的两种方法。第一种方法允许执行任意代码。第二种方法不提供任意代码的执行,但如果你只想隐藏一些扩展插入的DOM元素,它就更简单和充分。

方法1:禁用扩展时在页面中运行代码

观察样式更改的方法之一是声明CSS transitionsusing transition events to detect CSS property changes。 为了使其有用,您需要构造一个样式表,使其仅影响HTML元素。因此,您需要生成一个唯一的选择器(类名,ID,...),并将其用于HTML元素和样式表。

这是您必须在后台脚本中添加的代码:

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message !== 'getStyleCanary') return;

    // Generate a random class name, insert a style sheet and send
    // the class back to the caller if successful.
    var CANARY_CLASS = '_' + crypto.getRandomValues(new Uint32Array(2)).join('');
    var code = '.' + CANARY_CLASS + ' { opacity: 0 !important; }';
    chrome.tabs.insertCSS(sender.tab.id, {
        code,
        frameId: sender.frameId,
        runAt: 'document_start',
    }, function() {
        if (chrome.runtime.lastError) {
            // Failed to inject. Frame unloaded?
            sendResponse();
        } else {
            sendResponse(CANARY_CLASS);
        }
    });
    return true; // We will asynchronously call sendResponse.
});

在内容脚本中:

chrome.runtime.sendMessage('getStyleCanary', function(CANARY_CLASS) {
    if (!CANARY_CLASS) {
        // Background was unable to insert a style sheet.
        // NOTE: Consider retry sending the message in case
        // the background page was not ready yet.
        return;
    }

    var s = document.createElement('script');
    s.src = chrome.runtime.getURL('canaryscript.js');
    s.onload = s.remove;
    s.dataset.canaryClass = CANARY_CLASS;

    // This function will become available to the page and be used
    // by canaryscript.js. NOTE: exportFunction is Firefox-only.
    exportFunction(function() {}, s, {defineAs: 'checkCanary'}); 

    (document.body || document.documentElement).appendChild(s);
});

我在上面使用了一个脚本标记,因为它是在页面中运行脚本而不被页面内容安全策略阻止的唯一方法。请务必将canaryscript.js添加到web_accessible_resources in manifest.json,否则脚本将无法加载。

如果运行清理代码并不重要(例如因为你也使用我稍后解释的方法2),那么你最好使用内联脚本而不是外部脚本(即使用s.textContent = '<content of canaryscript.js>'而不是{{1} })。这是因为将s.src = ...与扩展资源一起使用会引入fingerprinting vulnerability to Firefox (bug 1372288)

这是.src

的内容
canaryscript.js

注意:仅当选项卡处于活动状态时才会触发CSS转换事件。如果选项卡处于非活动状态,则在显示选项卡之前不会触发转换事件。

注意:exportFunction是一个仅限Firefox的扩展方法,用于在不同的执行上下文中定义一个函数(在上面的示例中,该函数是在页面的上下文中定义的,可用于运行的脚本该页)。

所有其他API也可以在其他浏览器中使用(Chrome / Opera / Edge),但代码不能用于检测禁用的扩展,因为(function() { // Thes two properties are set in the content script. var checkCanary = document.currentScript.checkCanary; var CANARY_CLASS = document.currentScript.dataset.canaryClass; var canary = document.createElement('span'); canary.className = CANARY_CLASS; // The inserted style sheet has opacity:0. Upon removal a transition occurs. canary.style.opacity = '1'; canary.style.transitionProperty = 'opacity'; // Wait a short while to make sure that the content script destruction // finishes before the style sheet is removed. canary.style.transitionDelay = '100ms'; canary.style.transitionDuration = '1ms'; canary.addEventListener('transitionstart', function() { // To avoid inadvertently running clean-up logic when the event // is triggered by other means, check whether the content script // was really destroyed. try { // checkCanary will throw if the content script was destroyed. checkCanary(); // If we got here, the content script is still valid. return; } catch (e) { } canary.remove(); // TODO: Put the rest of your clean up code here. }); (document.body || document.documentElement).appendChild(canary); })(); 中的样式表在卸载时不会被删除(我只测试过Chrome;它可能适用于Edge)。

方法2:卸载时的视觉恢复

方法1允许您运行任意代码,例如删除您在页面中插入的所有元素。作为从DOM中删除元素的替代方法,您还可以选择通过CSS隐藏元素 下面我将展示如何修改方法1以隐藏元素而不运行其他代码(例如tabs.insertCSS)。

当您的内容脚本创建一个元素以便在DOM中插入时,您可以使用内联样式隐藏它:

canaryscript.js

在使用var someUI = document.createElement('div'); someUI.style.display = 'none'; // <-- Hidden // CANARY_CLASS is the random class (prefix) from the background page. someUI.classList.add(CANARY_CLASS + 'block'); // ... other custom logic, and add to document. 添加的样式表中,然后使用tabs.insertCSS标志定义所需的display值,以便覆盖内联样式:

!important

以上示例是故意通用的。如果您有多个具有不同CSS // Put this snippet after "var code = '.' + CANARY_CLASS, above. code += '.' + CANARY_CLASS + 'block {display: block !important;}'; 值的UI元素(例如displayblock,...),那么您可以添加多个这些行来重用我使用的框架提供。

要显示方法2相对于方法1的简单性:您可以使用相同的后台脚本(使用上述修改),并在内容脚本中使用以下内容:

inline

如果您的扩展程序包含多个元素,请考虑在局部变量中缓存// Example: Some UI in the content script that you want to clean up. var someUI = document.createElement('div'); someUI.textContent = 'Example: This is a test'; document.body.appendChild(someUI); // Clean-up is optional and a best-effort attempt. chrome.runtime.sendMessage('getStyleCanary', function(CANARY_CLASS) { if (!CANARY_CLASS) { // Background was unable to insert a style sheet. // Do not add clean-up classes. return; } someUI.classList.add(CANARY_CLASS + 'block'); someUI.style.display = 'none'; }); 的值,以便每个执行上下文只插入一个新样式表。

答案 1 :(得分:1)

你最初的措辞有点不清楚你究竟想要什么。因此,在某些情况下,此答案还包含有关您可以收到卸载通知的一种方式的信息。

在卸载/禁用之前在WebExtension插件中运行代码:
是的,runtime.onSuspend事件似乎是用于此的事件。不幸的是,正如您从"Browser Compatibility"部分确定的那样,此功能尚未实现。 source code没有任何迹象表明存在时间框架或框架,这可能暗示何时实施。

网站arewewebextensionsyet.com跟踪在Firefox中实施的WebExtension API,并为尚未实现的API提供“排名”。对于runtime.onSuspend the "rank" is 131。但是,该页面并未明确“排名”与实施时的关系。另外值得注意的是,已经实现了具有更高和更低“等级”数量的API。

注意:如果您要收听runtime.onSuspend,您还应该收听runtime.onSuspendCanceled事件。此事件被描述为“在onSuspend之后发送,表示该应用程序毕竟不会被卸载”。因此,如果您在runtime.onSuspend之后收到此事件,则需要将您的加载项返回到功能状态(即撤消您在runtime.onSuspend上所做的更改,以清理期望您的加载项为禁用)。 "rank" for runtime.onSuspendCanceled is 255.

“确定”您的“WebExtension已被禁用/卸载”:
如果您的问题确实是您在问题的最后一行中所说的那样:“......有没有办法确定WebExtension是否已被禁用/卸载?”然后,看起来您可以使用runtime.setUninstallURL(),这是从Firefox 47开始实现的。这将允许您设置在卸载加载项时访问的URL。这可以在您的服务器上使用,以注意卸载了该加载项。它不会通知您的WebExtension它已被卸载,也不允许您在WebExtension中运行代码。

不幸的是,您无法在WebExtension中使用检测到此URL被访问,因为表明您的WebExtension正在被卸载/禁用。根据测试,在完全卸载WebExtension之后,将访问此URL 。此外,在禁用WebExtension时以及禁用后卸载时不会访问它。仅在启用加载项时卸载WebExtension时才会访问它。由于这是一个只在启用扩展时运行的JavaScript调用,因此可以预期只有在离开启用状态时才会打开页面。

通过将以下行添加到WebExtension并查看页面打开时间来完成测试:

chrome.runtime.setUninstallURL("http://www.google.com");

鉴于它实际上是如何起作用的(仅在启用并直接卸载WebExtension时才访问),使用它作为“确定WebExtension是否被禁用/卸载的方法”只会部分有效。应该清楚的是,如果在卸载之前禁用了加载项,则不会通过访问此URL来通知您。

关于开发WebExtensions的注意事项(要使用的Firefox版本):
您声明“我正在使用常规的Firefox发布渠道。”对于开发WebExtensions,这应该。 WebExtensions API仍处于开发阶段。目前,您最好使用Firefox Developer EditionFirefox Nightly开发和测试您的WebExtension插件。正如您似乎已经在做的那样,您还应该仔细记录您希望使用的功能所需的Firefox版本。如您所知,此信息包含在MDN文档页面的“浏览器兼容性”部分中。

请注意当前位于official launch of WebExtensions is planned for Firefox 48Firefox Beta。 Firefox 48是scheduled to become the release version on 2016-08-02。虽然该版本中将提供大部分WebExtensions API,但仍远未完成。