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

JC2*_*2k8 3 javascript firefox firefox-addon firefox-addon-webextensions

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

从我在Mozilla的页面上看到的内容来看,runtime.onSuspend应该可以解决问题。不幸的是,它似乎尚未实现(我在Firefox常规发行版中)。

换句话说,我要执行的操作是运行代码,因为用户删除/禁用了我的扩展程序,这样我就可以清理监听器等,并通常将选项卡还原为它们的状态,即撤消所有的更改。扩展名。

Rob*_*b W 5

另一个答案不正确。第一部分(关于onSuspend事件)实际上是不正确的。关于的部分setUninstallURL是相关的,但由于它不允许您将选项卡还原到其原始状态(如您在问题中所问的那样),因此未回答问题。

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

关于 runtime.onSuspend

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

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

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

从上面应该是显而易见的runtime.onSuspend没有对清理后关闭的目标远程相关。不是在Chrome中,更不用说Firefox(Firefox不支持事件页面,这些事件将毫无意义)。

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

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

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

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

观察样式更改的一种方法是声明CSS过渡使用过渡事件来检测CSS属性更改。为此,您需要以仅影响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.
});
Run Code Online (Sandbox Code Playgroud)

在内容脚本中:

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);
});
Run Code Online (Sandbox Code Playgroud)

我在上面使用了脚本标记,因为这是在页面中运行脚本而不被页面的内容安全策略阻止的唯一方法。确保您添加canaryscript.jsweb_accessible_resourcesmanifest.json中,否则脚本不会加载。

如果运行清理代码不是很关键(例如,因为您还使用了方法2(我将在后面说明)),那么最好使用内联脚本而不是外部脚本(即,使用s.textContent = '<content of canaryscript.js>'代替s.src = ...)。这是因为.src与扩展资源一起使用为Firefox引入了指纹漏洞(错误1372288)

这是内容canaryscript.js

(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);
})();
Run Code Online (Sandbox Code Playgroud)

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

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

其他所有API也可以在其他浏览器(Chrome / Opera / Edge)中使用,但是该代码不能用于检测禁用的扩展名,因为tabs.insertCSS在卸载时不会删除样式表(我仅使用Chrome进行了测试;它可能在Edge中工作) 。

方法2:卸载后的视觉还原

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

当内容脚本创建要在DOM中插入的元素时,可以使用内联样式将其隐藏:

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.
Run Code Online (Sandbox Code Playgroud)

在添加的样式表中tabs.insertCSS,然后display使用!important标志定义所需的值,以便覆盖内联样式:

// Put this snippet after "var code = '.' + CANARY_CLASS, above.
code += '.' + CANARY_CLASS + 'block {display: block !important;}';
Run Code Online (Sandbox Code Playgroud)

上面的示例是有意的通用。如果您有多个具有不同CSS display值的UI元素(例如blockinline...),则可以添加多行以重复使用我提供的框架。

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

// 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';
});
Run Code Online (Sandbox Code Playgroud)

如果扩展中包含多个元素,请考虑CANARY_CLASS将局部变量的值缓存在本地变量中,以便每个执行上下文仅插入一个新样式表。