Port communication between background script and popup script

Hello, people of Mozilla Discourse!

I am trying to make an add-on and I am currently stuck on passing data from the background script to the popup(?) script.

My directory structure is very simple right now.
├── background.js
├── manifest.json
├── popup.js
└── tabs.html

My manifest.json looks like this.

{
  "manifest_version": 2,
  "name": "Borderify",
  "version": "1.0",
  "description": "Adds a red border to all webpages matching mozilla.org.",
  "icons": {
    "48": "icons/border-48.png"
  },
  "permissions": [
    "<all_urls>",
    "tabs"
  ],
  "background": {
    "scripts": ["background.js"]
  },
  "browser_action": {},
  "browser_specific_settings": {
    "gecko": {
      "id": "borderify@example.com"
    }
  }
}

Inside tabs.html, it has a script tag that references popup.js.

What I would like is to pass some data (an array, to be more specific) from background.js when the user clicks on the add-on icon. This has been set up like such:

browser.runtime.onConnect.addListener((port) => {
  console.log('background.js connected to general port!', port);
  port.postMessage({ msg: 'Hi from background.js again!' });
  port.onMessage.addListener((msg) => {
    console.log('Received in background.js through general port:', msg);
  });
});

browser.browserAction.onClicked.addListener(async () => {
  // ...
  browser.tabs.create({ url })
    .then((tab) => {
      const port = browser.tabs.connect(tab.id);
      console.log('background.js connected to tab port.');
      // the data would be an array but this is a decent example!
      port.postMessage('Hello from background.js, through tab port!');
    }, onError);
});

And inside popup.js:

console.log('popup.js is running!');

browser.runtime.onConnect.addListener((port) => {
  console.log('popup.js connected to general port', port);
  port.onMessage.addListener((msg) => {
    console.log('Listened to a message in popup.js:', msg);
  });
  port.postMessage({ msg: 'Hi from popup.js again, but from the general port' });
});

browser.tabs.query({ active: true, currentWindow: true })
  .then(tabs => {
    const { id } = tabs[0];
    const port = browser.tabs.connect(id);
    port.postMessage({ msg: 'Hi from popup.js again' });
  });

This is the output:
“background.js connected to tab port.”
“popup.js is running!”
“popup.js connected to general port
Object { name: “”, sender: {…}, error: null, onMessage: {…}, onDisconnect: {…}, postMessage: (), disconnect: () }”
“Listened to a message in popup.js:
Object { msg: “Hi from popup.js again” }”

If background.js and popup.js are connected to the same tab port, why is it that the former is not receiving anything? In fact, it looks like popup.js is just talking and listening to itself. The messages that were supposed to be sent were never sent to each other. :frowning_face:

I would appreciate any help.

So what you call “popup” is actually a normal tab, right?
Also the browser.tabs.query({ active: true, currentWindow: true }) can be replaced with: browser.tabs.getCurrent()

Now regarding ports - I’m not very good with these, so instead I would use messaging.

  1. when your popup script runs, just send message to your background script with:
const arrayData = await browser.runtime.sendMessage({type: 'get_array'});
  1. in your background script:
browser.runtime.onMessage.addListener(data => {
  if (data.type === 'get_array') return Promise.resolve([1, 2, 3, 4]);
});

And that’s it. When you return promise from the message handler, the value will be send back to the sender.
See the docs for more info:

Hi Juraj,

Yep! What I call a “popup” is just a normal tab as that gets created from the background script.

I’ve tried your method and it looks like it works! Just a weird observation though – when I do browser.runtime.sendMessage(...) within browser.browserAction.onClicked.addListener(async () => ...), it doesn’t work. It works when I send a response within browser.runtime.onMessage.addListener((msg, sender) => ...). I tried debugging for a bit but to no avail. But that’s okay, because I have the data sent over to popup.js now.

Thank you very much for your help!

1 Like

But you are calling browser.browserAction.onClicked.addListener in the background script, right? So sending message from there won’t be received by background script, only all other parts of the extension.

Yeah, the browser.browserAction.onClicked handler is located in background.js.

What I was hoping for was when the user clicked on the addon icon (which triggers the onClicked handler) was the data would be sent over to popup.js. That’s why I had a sendMessage inside the onClicked handler.

However, popup.js did not retrieve the data through its onMessage handler, which was slightly confusing.

The problem is probably that you have to wait for the “popup.js” to be loaded, else you can’t send it anything. But so far I couldn’t find any stable and easy to use solution for that. More info here:

EDIT:
Actually, I think I’ve just found a super handy workaround!
You can use dummy “executeScript” call (right after creating the tab) to wait for tab to be ready to receive messages!

await browser.tabs.executeScript(tabId, {code: ''});

(it won’t work in Chrome though because you can’t inject scripts to own extension pages, but I’m not sure you actually need a workaround there)

Anyway, I’m so happy!!! :slight_smile:

1 Like