如何在Firefox WebExtensions插件中使用chrome.storage和runtime.connect

时间:2016-08-19 15:10:03

标签: firefox-addon firefox-webextensions

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

  • 在单击页面主体时向后台脚本发送消息
  • 发送的消息由后台脚本
  • 存储
  • 后台脚本
  • 检索存储的消息
  • 单击加载项浏览器按钮时,存储的消息将从后台脚本发送到内容脚本
  • 内容脚本显示收到的消息

我使用的是Firefox 48版的Windows,我无法使用它。有人可以指出我做错了吗。

这是我的内容脚本:

// 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"
        }
    }
}

1 个答案:

答案 0 :(得分:3)

虽然您的代码中可能存在其他问题,但有一个问题是您没有考虑到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 storage.local 1}}来自您的内容脚本。

功能齐全的代码:

还有其他一些问题。大多数问题都是由于用户需要使用 的一些复杂的操作序列才能使其工作。所需要的一些原因是由于WebExtensions / Firefox的一些特点。:

  1. 完全删除以前安装的任何旧版本的加载项
    1. 来自about:addons Ctrl - Shift - A Cmd - Shift - OSX上的 A ,&#34;删除&#34;附加组件。
    2. 刷新所有内容页面(或至少您正在测试的任何内容页面)。这将从先前的安装中删除任何注入的内容脚本。当删除/禁用加载项时,Firefox会自动删除内容脚本
  2. about:debugging重新加载临时加载项。
  3. 刷新您正在测试的页面。这是必需的,因为重新加载加载项时,首先注入内容脚本。在内容脚本中,您执行runtime.connect()。如果没有第一个 runtime.onConnect侦听器,则无法连接。因此,内容脚本已经在后台脚本之后加载。
  4. 点击内容页面
  5. 点击browser_action按钮
  6. 如果不知道你需要完成这个序列,就很难让它做你想做的事。

    在弄清楚发生了什么的困难中,很大一部分就是你有大量的状态信息只包含在代码中。没有迹象表明用户关于内容脚本或后台脚本所处的状态。为了帮助可视化每个脚本中发生的事情,我已经向{{1}添加了大量调用。更好地说明脚本中发生的事情。

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

    1. 尝试发送消息时(由于页面被点击)识别出尚未建立连接,并再次尝试获取连接(结果:不再需要在上面的用户序列中执行#3)。
    2. 如果检测到连接已丢失,则自行禁用。导致这种情况的最可能原因是受影响的内容脚本是由重新加载的加载项孤立的。 (结果:不再需要在上面的序列中执行#1;可以在不删除旧版本/重新加载内容页面的情况下重新加载加载项。)
    3. 代码中还有其他问题。我相信我在代码中添加了足以解释它们的注释。
    4. 现在在浏览器控制台中生成的输出是:

      console.log

      的manifest.json

      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
      

      background.js

      {
          "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"
              }
          }
      }
      

      content.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');
      

      注意:
      这适用于演示或学习,但在生产扩展中使用效果不佳。代码中没有任何内容可以说明有多个标签。

      您正在点击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按钮。在这种情况下,应将消息发送到当前活动的选项卡,而不是已切换到的选项卡(进行连接)。

      虽然您可以跟踪在收到连接或单击消息时处于活动状态的选项卡(这可能因为实际建立了连接,或者基于点击事件(假设是用户输入)发送了消息) ),使用tabs.connect()可能更好,它允许您仅与特定选项卡建立连接。了解哪个runtime.Port与每个标签对应,您可以确保仅向点击browser_action按钮时处于活动状态的标签发送消息。您需要保留一个包含由选项卡ID索引的连接端口的数组或对象。