如何在 Firefox WebExtensions 附加组件中使用 chrome.storage 和 runtime.connect

How to use chrome.storage and runtime.connect in a Firefox WebExtensions add-on

我正在尝试创建一个执行以下操作的 Firefox 附加组件:

我正在为 Windows 使用 Firefox 48 版,但无法正常工作。请有人指出我做错了什么。

这是我的内容脚本:

// setup message channel between this script and background script
var msgPort = chrome.runtime.connect({name:"msgPort"});

// fires when background script sends a message
msgPort.onMessage.addListener(function(msg) {
    console.log(msg.txt);
});

// sends a message to background script when page is clicked
document.body.addEventListener("click", function() {
    msgPort.postMessage({txt: "page clicked"});
});

这是我的背景脚本:

var msgPort;
var tmp;

// fires when message port connects to this background script
function connected(prt)
{
    msgPort = prt;
    msgPort.postMessage({txt: "message channel established"});
    msgPort.onMessage.addListener(gotMessage);
}

// fires when content script sends a message
frunction gotMessage(msg)
{
    // store the message
    chrome.storage.local.set({message : msg.txt});

    // read the stored message back again
    chrome.storage.local.get("message", function(item){
        tmp = item;
    });
}

// send the saved message to the content script when the add-on button is clicked
chrome.browserAction.onClicked.addListener(function() {
    msgPort.postMessage({txt: "saved message: "+tmp});
});

chrome.runtime.onConnect.addListener(connected);

这是我的清单:

{
    "name": "test",

    "manifest_version": 2,

    "version": "1.0",

    "permissions": ["activeTab","storage"],

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

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

    "browser_action": {
        "default_title": "Go"
    },

    "applications": {
        "gecko": {
        "id": "test@example.com",
        "strict_min_version": "48.0a1"
        }
    }
}

虽然您的代码中其他地方可能存在其他问题,但一个问题是您没有考虑 chrome.storage.local.set 是异步的这一事实。

正如您的代码一样,在您通过 chrome.storage.local.set 请求存储数据后立即执行对 chrome.storage.local.get 的调用,而无需等待数据实际存储。因此,数据可能尚不可用。

您的 gotMessage(msg) 函数应该更像:

function gotMessage(msg)
{
    // store the message
    chrome.storage.local.set({message : msg.txt}, function(){
        // read the stored message back again
        chrome.storage.local.get("message", function(item){
            tmp = item;
            //Indicate that the data is available
            //Only do this if you desire.
            //chrome.browserAction.setTitle({title:'Send data to content script'});
        });
    });
}

注意:您的问题中还有一个语法错误,其中 frunction gotMessage(msg) 应该是 function gotMessage(msg)

storage.local 可用于内容脚本:
您目前正在内容脚本和后台脚本之间来回传递消息,以便 set()get() 后台脚本中 storage.local 的内容。我假设您这样做是为了测试 runtime.connectbrowser_action 等。另一种可能性是您不知道可以使用 [=] getset 21=] 来自您的内容脚本。

全功能代码:

还有其他多个问题。大多数问题是由于用户 需要的一些复杂的操作序列导致的。一些需要的是由于 WebExtensions/Firefox.:

的一些特性
  1. 完全删除以前安装的任何旧版本的插件
    1. 来自about:addons (Ctrl-Shift-A,Cmd-Shift-A on OSX), "remove" 附加组件.
    2. 刷新所有内容页面(或至少刷新您正在测试的页面)。这将从先前的安装中删除任何注入的内容脚本。当附加组件为 removed/disabled.
    3. 时,内容脚本 不会被 Firefox 自动删除
  2. about:debugging 重新加载临时加载项。
  3. 刷新您正在测试的页面。这是必需的,因为当加载项重新加载时,内容脚本首先被注入。在内容脚本中,你做 runtime.connect(). This will not connect if there is not first a runtime.onConnect 听众。因此,由于内容脚本 必须 在后台脚本之后加载。
  4. 点击内容页
  5. 单击 browser_action 按钮`

如果不知道您需要准确执行此序列,就很难让它执行您希望它执行的操作。

弄清楚发生了什么困难的一个重要部分是您有大量仅包含在代码中的状态信息。没有向用户说明内容脚本或后台脚本处于什么状态。为了帮助可视化每个脚本中发生的事情,我添加了大量调用 console.log 以更好地说明脚本中发生的事情。

我对代码进行了重大修改:

  1. 尝试发送消息时(由于页面被点击)发现连接未建立,并再次尝试获取连接(结果:不再需要执行上述用户序列中的#3 ).
  2. 如果它检测到它有一个连接并且丢失了它,则会禁用自己。这种情况最可能的原因是受影响的内容脚本被重新加载的附加组件孤立了。 (结果:不再需要按照上面的顺序执行#1;无需先删除旧的 version/reloading 内容页面即可重新加载插件。)
  3. 代码中还有其他问题。我相信我在代码中添加的注释足以解释它们。

现在在浏览器控制台中生成的输出是:

1471884197092 addons.xpi WARN Addon with ID demo-runtime.connect-and-storage.local@example.com already installed, older version will be disabled
        content: Content script injected.
        content: Making message port available for connection
alert() is not supported in background windows; please use console.log instead.
Open the Browser Console.
background: In background script.
background: listening for a connection
        content: page clicked
        content: Sending message failed: not yet connected
        content: Retrying connection for message: Object { type: "page clicked" }
        content: Making message port available for connection
background: Port connected: sending confirmation message: Object { type: "message channel established" }
        content: Received message: type: message channel established  
        content: Sending pending message Object { type: "page clicked" }
        content: Sending message Object { type: "page clicked" }
background: Received message: Object { type: "page clicked" }
background: Got data from storage.local.get: Object { message: "page clicked" }
background: Button clicked sending message: Object { type: "saved message", data: "page clicked" }
        content: Received message: type: saved message 
                          message: data: page clicked

manifest.json:

{
    "name": "Demo runtime.connect and storage.local",
    "manifest_version": 2,
    "version": "0.1",
    "permissions": ["activeTab","storage"],
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "js": ["content.js"]
        }
    ],

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

    "browser_action": {
        "default_title": "Data not yet available",
        "browser_style": true
    },

    "applications": {
        "gecko": {
        "id": "demo-runtime.connect-and-storage.local@example.com",
        "strict_min_version": "48.0a1"
        }
    }
}

background.js:

//* For testing, open the Browser Console
try{
    //Alert is not supported in Firefox. This forces the Browser Console open.
    //This abuse of a misfeature works in FF49.0b+, not in FF48
    alert('Open the Browser Console.');
}catch(e){
    //alert throws an error in Firefox versions below 49
    console.log('Alert threw an error. Probably Firefox version below 49.');
}
//*
console.log('background: In background script.');

var msgPort;
var dataFromStorage;

// Fires when message port connects to this background script
function connected(prt) {
    msgPort = prt;
    msgPort.onMessage.addListener(gotMessage); //This should be done first
    let message = {type: "message channel established"};
    console.log('background: Port connected: sending confirmation message:', message);
    msgPort.postMessage(message);
}

//Fires when content script sends a message
//Syntax error this line (misspelled function)
function gotMessage(msg) {
    console.log('background: Received message:', msg);
    // store the message
    chrome.storage.local.set({message : msg.type}, function(){
        // read the stored message back again
        chrome.storage.local.get("message", function(item){
            console.log('background: Got data from storage.local.get:',item);
            //You were setting tmp (now dataFromStorage) to the item object, not the
            //  message key which you requested.
            /*
            for(x in item){
                console.log('background: property of item:',x);
            }
            //*/
            dataFromStorage = item.message;
            //Indicate to the user that the data is available
            chrome.browserAction.setTitle({title:'Send data to content script'});
        });
    });
}

// send the saved message to the content script when the add-on button is clicked
chrome.browserAction.onClicked.addListener(function() {
    //msgPort not defined unless user has clicked on page
    if(msgPort) {
        let message = {type: "saved message",data:dataFromStorage};
        console.log('background: Button clicked sending message:', message);
        msgPort.postMessage(message);
    } else {
        console.log('background: No message port available (yet).');
    }
});

//Be open to establishing a connection.  This must be done prior to the
//  chrome.runtime.connect elsewhere in your code. 
chrome.runtime.onConnect.addListener(connected);
console.log('background: Listening for a connection');

content.js:

console.log('\tcontent: Content script injected.');

var isConnected=false;
var retryConnectionTimerId=-1; //In case we want to cancel it
var retryConnectionCount=0;
var messageBeingRetried=null;

//setup message channel between this script and background script
var msgPort;

function messageListener(msg){
    //Using a closure for this function is a bad idea.  This should be a named
    //  function defined at the global scope so we can remove it as a
    //  listener if the background script sends a message to disconnect.
    //  You need to be able to disable any active content scripts if the 
    //  add-on is disabled/removed.  This is a policy from Mozilla. However,
    //  for WebExtensions it is not yet possible due to the current lack of the 
    //  runtime.onSuspend event.
    if(typeof msg === 'object' && msg.hasOwnProperty('type')){
        //Should look specifically for the message indicating connection.
        console.log('\tcontent: Received message: type:', msg.type
            ,(msg.hasOwnProperty('data') ? '\n\t\t\t  Message: data:':'')
            ,(msg.hasOwnProperty('data') ? msg.data : '')
        );
        if(msg.type === 'disableAddon'){
            //Allow for the background script to disable the add-on.
            disableThisScript('Received disableAddon message');
        }
        if(isConnected && msg.type === 'message channel established'){
            //We are in a content script that is left over from a previous load
            //  of this add-on. Or, at least that is the most likely thing 
            //  while testing.  This probably needs to change for a real add-on.
            //  This is here because reloading the temporary add-on does not
            //  auto-disable any content scripts.
            disableThisScript('Received second channel established message');
            return;
        }//else
        isConnected=true; //Any correctly formatted message received indicates connection
        //Immediately send a message that was pending (being retried).
        //  Note: This only immediately sends the message which was most recently attempted
        //  to send via sendMessage, not all messages which might be waiting in timers.
        //  Any others will be sent when their timers expire.
        sendPendingMessageIfPending();
    }else{
        console.log('\tcontent: Received message without a "type":', msg);
    }
}

function receiveDisconnect(){
    //The port was disconnected
    disableThisScript('Port disconnected');
    isConnected=false;
}

function makePortAvailableForConnection(){
    console.log('\tcontent: Making message port available for connection');
    if(msgPort && typeof msgPort.disconnect === 'function'){
        //Call disconnect(), if we have already tried to have a connection
        msgPort.disconnect();
    }
    //Try to make a connection. Only works if ocConnect listener
    //  is already established.
    msgPort = chrome.runtime.connect({name:"msgPort"});
    //Fires when background script sends a message
    msgPort.onMessage.addListener(messageListener);
    msgPort.onDisconnect.addListener(receiveDisconnect);
    //Can not use runtime.onConnect to detect that we are connected.
    //  It only fires if some other script is trying to connect
    //  (using chrome.runtime.connect or chrome.tabs.connect)
    //  to this script (or generally). It does not fire when the connection
    //  is initiated by this script.
    chrome.runtime.onConnect.addListener(portConnected); //Does not fire
}

function portConnected(){
    //This event does not fire when the connection is initiated,
    //  chrome.runtime.connect() from this script.
    //  It is left in this code as an example and to demonstrate that the event does
    //  not fire.
    console.log('\tcontent: Received onConnect event');
    isConnected=true;
}

// sends a message to background script when page is clicked
function sendClickMessage() {
    console.log('\tcontent: Page clicked');
    sendMessage({type: "page clicked"});
            chrome.storage.local.get("message", function(item){
            console.log('content: Got data from storage.local.get:',item);
            });

}

function clearPendingMessage(){
    window.clearTimeout(retryConnectionTimerId);
    messageBeingRetried=null;
}

function sendPendingMessageIfPending() {
    //Pending messages should really be implemented as a queue with each message
    //  being retried X times and then sent once a connection is made. Right now
    //  this works for a single message.  Any other messages which were pending
    //  are only pending for the retry period and then they are forgotten.
    if(messageBeingRetried !== null && retryConnectionTimerId){
        let message = messageBeingRetried;
        clearPendingMessage();
        console.log('\tcontent: Going to send pending message', message);
        sendMessage(message);
    }
}

function retryingMessage(message) {
    retryConnectionTimerId=-1;
    messageBeingRetried=null;
    sendMessage(message);
}

function sendMessage(message) {
    if(isConnected){
        try{
            console.log('\tcontent: Sending message', message);
            msgPort.postMessage(message);
            retryConnectionCount=0;
        }catch(e){
            if(e.message.indexOf('disconnected port') > -1){
                console.log('\tcontent: Sending message failed: disconnected port');
                if(isConnected){
                    console.log('\tcontent: Had connection, but lost it.'
                                + ' Likely add-on reloaded. So, disable.');
                    disableThisScript('Add-on likely reloaded.');
                }else{
                    retryConnection(message);
                }
            }else{
                console.log('\tcontent: Sending message failed: Unknown error', e);
            }
        }
    }else{
        console.log('\tcontent: Sending message failed: not yet connected');
        retryConnection(message);
    }
}

function retryConnection(message){
    if(retryConnectionCount>=5){
        //Limit the number of times we retry the connection.
        //  If the connection is not made by now, it probably won't be
        //  made at all.  Don't fill up the console with a lot of
        //  messages that might make it harder to see what is happening.
        retryConnectionCount=0; //Allow more retries upon another click event.
        //The current message is forgotten. It is now just discarded.
        return;
    }
    console.log('\tcontent: Retrying connection for message:', message);
    makePortAvailableForConnection();
    //Try sending the message after a timeout.
    //  This will result in the repeated attempts to
    //  connect and send the message.
    messageBeingRetried=message;
    retryConnectionTimerId = window.setTimeout(retryingMessage,500,message);
    retryConnectionCount++;
}

function disableThisScript(reason){
    console.log('\tcontent: Disable the content script:', reason);
    //Gracefully disable everything previously set up.
    msgPort.onMessage.removeListener(messageListener);
    msgPort.onDisconnect.removeListener(receiveDisconnect);
    try{
        msgPort.disconnect();
    }catch(e){
        //most likely the port was already disconnected
    }
    chrome.runtime.onConnect.removeListener(portConnected);
    document.body.removeEventListener("click", sendClickMessage);
    isConnected=false;
}

//Making the connection available will silently fail if there is not already a
//  onConnect listener in the background script.  In the case of a "temporary"
//  add-on upon load or reload, the content script is run first and
//  no connection is made.
makePortAvailableForConnection();

document.body.addEventListener("click", sendClickMessage);

注意:
这适用于演示或学习,但不适用于生产扩展。代码中没有任何内容说明有多于一个选项卡。

您正在向基于单击 browser_action 按钮的内容脚本发送消息。这意味着您需要能够在用户单击 browser_action 按钮时,仅将消息从后台脚本显式发送到活动 window 中显示的选项卡中的内容脚本.例如,用户可能在一个页面中单击内容,然后切换到另一个选项卡,并单击 browser_action 按钮。在这种情况下,消息应该发送到当前活动的选项卡,而不是被切换的选项卡(建立连接)。

虽然您可以跟踪收到连接或点击消息时处于活动状态的选项卡(这会起作用,因为实际建立连接或发送消息,基于点击事件(假定为用户input)), 最好使用 tabs.connect() which allows you to establish a connection only with a specific tab. Knowing which runtime.Port 对应于每个选项卡,这样可以确保您只向单击 browser_action 按钮时处于活动状态的选项卡发送消息。您需要保留一个数组或对象,其中包含由选项卡 ID 索引的连接端口。