Search code examples
javascriptgoogle-chrome-extensionfirefox-addon

chrome.runtime.sendMessage not working on the 1st click when running normally. it works while debugging though


I have a function in the context.js which loads a panel and sends a message to panel.js at the last. The panel.js function updates the ui on receiving that msg. But it is not working for the first click i.e. it just loads normal ui, not the one that is expected that is updated one after the msg is received. while debugging it works fine.

manifest.json

"background": {
    "scripts": ["background.js"],
    "persistent": false
  },
"content_scripts": [{
    "all_frames": false,
    "matches": ["<all_urls>"],
    "js":["context.js"]
  }],
"permissions": ["activeTab","<all_urls>", "storage","tabs"],
  "web_accessible_resources": 
    "panel.html",
    "panel.js"
  ]

context.js - code


fillUI (){
    var iframeNode = document.createElement('iframe');
    iframeNode.id = "panel"
    iframeNode.style.height = "100%";
    iframeNode.style.width = "400px";
    iframeNode.style.position = "fixed";
    iframeNode.style.top = "0px";
    iframeNode.style.left = "0px";
    iframeNode.style.zIndex = "9000000000000000000";
    iframeNode.frameBorder = "none"; 
    iframeNode.src = chrome.extension.getURL("panel.html")
    document.body.appendChild(iframeNode);
    var dataForUI = "some string data"
    chrome.runtime.sendMessage({action: "update UI", results: dataForUI}, 
        (response)=> {
          console.log(response.message)
        })
     }
}

panel.js - code

var handleRequest = function(request, sender, cb) {
  console.log(request.results)
  if (request.action === 'update Not UI') {
    //do something
  } else if (request.action === 'update UI') {
    document.getElementById("displayContent").value = request.results
  }
};

chrome.runtime.onMessage.addListener(handleRequest);

background.js

chrome.runtime.onMessage.addListener((request,sender,sendResponse) => {
    chrome.tabs.sendMessage(sender.tab.id,request,function(response){
        console.log(response)`
    });
});

panel.html

<!DOCTYPE html>

<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="panel.css" />
</head>

<body>
  <textarea id="displayContent" rows="10" cols="40"></textarea>
</body>
</html>

Any suggestions on what I am doing wrong or what can I do instead?


Solution

  • An iframe with a real URL loads asynchronously so its code runs after the embedding code finishes - hence, your message is sent too early and is lost. The URL in your case points to an extension resource so it's a real URL. For reference, a synchronously loading iframe would have a dummy URL e.g. no src at all (or an empty string) or it would be something like about:blank or javascript:/*some code here*/, possibly srcdoc as well.

    Solution 1: send a message in iframe's onload event

    Possible disadvantage: all extension frames in all tabs will receive it, including the background script and any other open extension pages such the popup, options, if they also have an onMessage listener.

    iframeNode.onload = () => {
      chrome.runtime.sendMessage('foo', res => { console.log(res); });
    };
    document.body.appendChild(iframeNode);
    



    Solution 2: let iframe send a message to its embedder

    Possible disadvantage: wrong data may be sent in case you add several such extension frames in one tab and for example the 2nd one loads earlier than the 1st one due to a bug or an optimization in the browser - in this case you may have to use direct DOM messaging (solution 3).

    iframe script (panel.js):

    chrome.tabs.getCurrent(ownTab => {
      chrome.tabs.sendMessage(ownTab.id, 'getData', data => {
        console.log('frame got data');
        // process data here
      });
    });
    

    content script (context.js):

    document.body.appendChild(iframeNode);
    chrome.runtime.onMessage.addListener(
      function onMessage(msg, sender, sendResponse) {
        if (msg === 'getData') {
          chrome.runtime.onMessage.removeListener(onMessage)
          sendResponse({ action: 'update UI', results: 'foo' });
        }
      });
    



    Solution 3: direct messaging via postMessage

    Use in case of multiple extension frames in one tab.

    Disadvantage: no way to tell if the message was forged by the page or by another extension's content script.

    The iframe script declares a one-time listener for message event:

    window.addEventListener('message', function onMessage(e) {
      if (typeof e.data === 'string' && e.data.startsWith(chrome.runtime.id)) {
        window.removeEventListener('message', onMessage);
        const data = JSON.parse(e.data.slice(chrome.runtime.id.length));
        // process data here
      }
    });
    

    Then, additionally, use one of the following:

    • if content script is the initiator

      iframeNode.onload = () => {
        iframeNode.contentWindow.postMessage(
          chrome.runtime.id + JSON.stringify({foo: 'data'}), '*');
      };
      document.body.appendChild(iframeNode);
      
    • if iframe is the initiator

      iframe script:

      parent.postMessage('getData', '*');
      

      content script:

      document.body.appendChild(iframeNode);
      window.addEventListener('message', function onMessage(e) {
        if (e.source === iframeNode) {
          window.removeEventListener('message', onMessage);
          e.source.postMessage(chrome.runtime.id + JSON.stringify({foo: 'data'}), '*');
        }
      });