【问题标题】:Firefox WebExtension: How Do I Run Code Prior to Disable/Uninstall?Firefox WebExtension:如何在禁用/卸载之前运行代码?
【发布时间】:2016-07-28 19:34:48
【问题描述】:

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

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

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

【问题讨论】:

    标签: javascript firefox firefox-addon firefox-addon-webextensions


    【解决方案1】:

    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 中是不可能的,因为当 Firefox 扩展被禁用时,内容脚本的执行上下文会在port.onDisconnect 事件有机会触发之前被破坏(至少在bugzil.la/1223425 被修复之前)。

    尽管有这些限制,当插件被禁用时,仍然可以为内容脚本运行清理逻辑。此方法基于这样一个事实,即在 Firefox 中,使用 tabs.insertCSS 插入的样式表会在插件被禁用时被删除。
    我将讨论利用这一特性的两种方法。第一种方法允许执行任意代码。第二种方法不提供任意代码的执行,但如果你只想隐藏一些扩展插入的 DOM 元素,它更简单也足够。

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

    观察样式变化的方法之一是声明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>' 而不是 s.src = ...)。这是因为将.src 与扩展资源一起使用会引入fingerprinting vulnerability to Firefox (bug 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);
    })();
    

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

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

    所有其他 API 在其他浏览器中也可用(Chrome/Opera/Edge),但代码不能用于检测禁用的扩展,因为来自tabs.insertCSS 的样式表在卸载时不会被删除(我只用 Chrome 测试过;它可能在 Edge 中工作)。

    方法二:卸载后视觉恢复

    方法 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.
    

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

    // Put this snippet after "var code = '.' + CANARY_CLASS, above.
    code += '.' + CANARY_CLASS + 'block {display: block !important;}';
    

    上面的例子是故意通用的。如果您有多个具有不同 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';
    });
    

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

    【讨论】:

      【解决方案2】:

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

      在卸载/禁用之前在您的 WebExtension 插件中运行代码:
      不,即使它被支持,runtime.onSuspend 事件也不会做你想做的事。它用于向事件页面发出即将被卸载的信号。当他们正在侦听的事件的处理完成时,即使是页面也会定期卸载。这并不表示正在卸载扩展程序。

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

      不幸的是,您不能在您的 WebExtension 中使用检测此 URL 被访问作为指示您的 WebExtension 正在被卸载/禁用。根据测试,在 WebExtension 完全卸载后访问此 URL。此外,WebExtension 被禁用时不会访问,也不会在禁用后卸载时访问。只有在启用加载项的情况下卸载 WebExtension 时才会访问它。从这是一个仅在启用扩展程序时运行的 JavaScript 调用这一事实来看,人们会期望该页面仅在离开启用状态时才会打开。

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

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

      考虑到它的实际功能(仅在启用并直接卸载 WebExtension 时访问),将其用作“确定 WebExtension 是否被禁用/卸载的方法”只会部分有效。应该清楚的是,如果加载项在卸载之前被禁用,访问此 URL 将不会通知您。

      【讨论】:

      • 抱歉措辞不清楚,我有点累了。但是,是的,我想运行代码作为用户删除/禁用我的扩展的结果,以便我可以清理侦听器等,并且通常将选项卡恢复到它们的状态,即。例如,撤消扩展所做的所有更改。
      • @JC2k8,没问题。我理解您延期后清理的愿望。 Mozilla 通过引导扩展非常强烈地强调这一点。而且,坦率地说,我非常同意。从用户的角度来看,如果我删除/禁用扩展程序,它应该撤消所做的任何更改,除非用户另有指示。谷歌浏览器似乎不同意这种观点,即使在禁用/卸载加载项时不删除加载项的内容脚本也是如此。谷歌搜索显示至少有一位 Chrome 扩展程序开发人员依赖此错误功能的帖子。
      猜你喜欢
      • 1970-01-01
      • 2017-12-31
      • 1970-01-01
      • 2011-10-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-29
      • 2015-09-29
      相关资源
      最近更新 更多