内容脚本和后台脚本之间的消息传递问题(WebExtensions)

时间:2017-01-12 23:24:24

标签: javascript google-chrome-extension promise firefox-webextensions

我目前正在尝试编写一个浏览器扩展程序来捕获所有mailto链接,而不是让它们被默认邮件应用程序打开。可以使用简单的切换按钮启用和禁用该机制 当我的扩展程序加载时,一切似乎都有效,但我得到Error: Could not establish connection. Receiving end does not exist.。然后,当我使用切换按钮禁用并再次启用它时,我得到以下内容:

23:52:15.247 function sendMsgToTabs(msg)  background.js:16:9
23:52:15.256 this.sendMsgToTabs(...) is undefined  background.js:17
23:52:15.302 Error: Could not establish connection. Receiving end does not exist.  (unknown)
23:53:29.347 TypeError: this._recipeManager is null[Learn More]  LoginManagerParent.jsm:77:9

修改:禁用似乎无法在内容脚本中正常运行。由于某种原因,单击处理程序未正确删除...

enter image description here

那么可能导致所有这些错误的原因是什么?我在调试器中检查了this.sendMsgToTabs,但它实际上并未定义。我也没有打开任何不寻常的网站,所以我不明白为什么会出现连接问题。

我目前只在Firefox中测试过我的代码。但人们说Chrome的API基本相同。这是我的代码:

background.js

'use strict'
class MyWebExtensionBackend {
    constructor() {
        this.icons = {
            enabled: '/icons/on.png',
            disabled: '/icons/off.png'
        }
        this.isEnabled = true
        browser.browserAction.onClicked.addListener(this.toggle.bind(this)) //toolbar button
        browser.runtime.onMessage.addListener(this.msgListener.bind(this))
        this.enable()
    }
    enable() {
        browser.browserAction.setIcon({ path: this.icons.enabled })
        this.isEnabled = true
        console.log(this.sendMsgToTabs)
        this.sendMsgToTabs({isEnabled: this.isEnabled}).catch(console.error)
    }
    disable() {
        browser.browserAction.setIcon({ path: this.icons.disabled })
        this.isEnabled = false
        this.sendMsgToTabs({isEnabled: this.isEnabled}).catch(console.error)
    }
    toggle() {
        if (this.isEnabled)
            this.disable()
        else
            this.enable()
    }
    sendMsgToTabs(msg) {
        return browser.tabs.query({}, tabs => {
            let msgPromises = []
            for (let tab of tabs) {
                let msgPromise = browser.tabs.sendMessage(tab.id, msg)
                msgPromises.push(msgPromise)
            }
            return Promise.all(msgPromises)
        })
    }
    msgListener(msg, sender, sendResponse) {
        console.log(msg.link)
        /* browser.notifications.create({ // doesn't work
            "type": "basic",
            "iconUrl": browser.extension.getURL("icons/on.png"),
            "title": 'url opened',
            "message": msg.link
        }); */
    }
}

let myWebExtensionBackend = new MyWebExtensionBackend()

内容-的script.js

'use strict'
class MyWebExtensionFrontend {
    constructor() {
        this.isEnabled = false
        browser.runtime.onMessage.addListener(this.msgListener.bind(this))
    }
    linkHandler(event) {
        if (event.target.tagName !== 'A')
            return
        let link = event.target.href
        if (link.startsWith('mailto:')) {
            event.preventDefault() // doesn't appear to have an effect
            console.log(link)
            browser.runtime.sendMessage({'link': link}).catch(console.error)
            //using a promise here felt kind of wrong 
            //because event handler functions can't really deal with that 
            //from what I can tell
            return false  // doesn't appear to have an effect
        }
    }
    enable() {
        console.log('enable frontend')
        window.addEventListener('click', this.linkHandler.bind(this))
        this.isEnabled = true
    }
    disable() {
        console.log('disable frontend')
        window.removeEventListener('click', this.linkHandler.bind(this))
        this.isEnabled = false
    }
    msgListener(req) {
        if (req.isEnabled)
            this.enable()
        else 
            this.disable()
        return Promise.resolve({res: ''})
    }
}

let myWebExtensionFrontend = new MyWebExtensionFrontend()

的manifest.json

{

  "description": "A basic toggle button",
  "manifest_version": 2,
  "name": "toggle-button",
  "version": "1.0",
  "homepage_url": "https://github.com/TODO",
  "icons": {
    "48": "icons/on.png"
  },

  "background": {
    "scripts": ["background.js"]
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"]
    }
  ],

  "browser_action": {
    "default_icon": "icons/on.png"
  }

}

2 个答案:

答案 0 :(得分:2)

您的代码有两个问题。一个特定于WebExtensions,另一个特定于DOM和JavaScript。

问题1:WebExtensions API中的异步方法的返回值

在Firefox中,WebExtensions有两个名称空间,chrome.browser.chrome.的设计与Chrome的扩展程序API非常接近,而browser.则有一些增强功能。特别是,异步browser. API可能会返回一个promise。只有在没有使用回调函数调用方法时才会返回promise。如果它确实收到回调作为参数,则browser. API的行为类似于chrome.。以下是一些例子:

// These have a callback and return undefined.
chrome.tabs.query({}, function(tabs) { /* ... */ });
browser.tabs.query({}, function(tabs) { /* ... */ });

chrome.tabs.query({}); // No callback, returns undefined (this is quite useless).
browser.tabs.query({}); // No callback, returns a Promise<Array>.

browser.tabs.query({}).then(function(tabs) { /* ... */});

请注意,您收到的错误消息显示返回值未定义:

this.sendMsgToTabs(...) is undefined

如果方法未定义(如您所愿),Firefox将打印以下错误:

this.sendMsgToTabs is not a function

您的案例的修复方法是使用promises,如下所示:

sendMsgToTabs(msg) {
    // Note: Changed ", tabs =>" to ").then(tabs =>"
    return browser.tabs.query({}).then(tabs => {
        let msgPromises = []
        for (let tab of tabs) {
            let msgPromise = browser.tabs.sendMessage(tab.id, msg)
            msgPromises.push(msgPromise)
        }
        return Promise.all(msgPromises)
    });
}

如果有任何没有内容脚本的标签,则承诺将被拒绝。如果您不关心承诺的返回值,请在let msgPromise之后添加以下内容:

msgPromise = msgPromise.catch(() => {}); // Ignore errors.

问题2:将.bind()与addEventListener / removeEventListener一起使用

您的代码如下所示:

// your enable() function:
window.addEventListener('click', this.linkHandler.bind(this))

// your disable() function:
window.removeEventListener('click', this.linkHandler.bind(this))

问题是bind会返回函数。因此,当调用enable()方法时,会创建一个新函数并将其用作事件侦听器。调用disable()时,会创建一个新函数并将其传递给removeEventListener。由于此新函数与任何其他函数(特别是前一个参数addEventListener)不同,因此结果是不会删除单击处理程序。

有三种方法可以使用所需的this值注册DOM事件:

  1. 在构造类时,用绑定函数替换实例方法:

    // In the constructor:
    this.linkHandler = this.linkHandler.bind(this);
    
    // In your enable() function:
    window.addEventListener('click', this.linkHandler);
    
    // In your disable() function:
    window.removeEventListener('click', this.linkHandler);
    
  2. 在调用enable()时,如果需要,创建一个绑定函数:

    // In your enable() function:
    if (!this.linkHandlerBound) {
        this.linkHandlerBound = this.linkHandler.bind(this);
    }
    window.addEventListener('click', this.linkHandlerBound);
    
    // In your disable() function:
    window.removeEventListener('click', this.linkHandlerBound);
    // If wanted (but not needed), run: this.linkHandlerBound = null;
    
  3. 使用handleEvent method传递对象:

    // A method of your class
    handleEvent(event) {
        if (event.type === 'click') {
            this.linkHandler(event);
        }
    }
    
    // In your enable() function:
    window.addEventListener('click', this);
    
    // In your disable() function:
    window.removeEventListener('click', this);
    

    注意, not 使用handleEvent方法犯下类似错误的错误如下:

    // Do NOT do this! You won't be able to remove the listener because
    // you did not store a reference to the event handler object.
    window.addEventListener('click', {
        handleEvent: this.linkHandler.bind(this)
    });
    

答案 1 :(得分:0)

我刚刚注意到您还没有分享处理连接的方式,但您可能需要检查Long-lived connections以区分不同类型的连接以及如何建立连接。

如给定链接中所述,

  

建立连接时,每个端都有一个runtime.Port对象,用于通过该连接发送和接收消息。

以下是有关如何从内容脚本打开频道以及发送和收听消息的示例:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question == "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

此外,您可能还想查看相关SO post中的建议解决方案,看看哪些适用于您。