DEV Community

Cover image for Simple communication between page and browser extension
Przemyslaw Jan Beigert
Przemyslaw Jan Beigert

Posted on • Edited on

5 3

Simple communication between page and browser extension

Introduction

This will not be a great pattern for a public browser extension. However there is many cases when this will be good enough way. For example some internal debugger tool, or something similar to redux dev tools.

Requirements

In manifest.json:

{
  "action": {
    "default_popup": "index.html"
  },
  "permissions": ["tabs", "activeTab", "scripting"],
  "manifest_version": 3
}
Enter fullscreen mode Exit fullscreen mode

Prepare

We have to have some popup to display received data somewhere and permissions to inject script in active tab.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link href="index.css" rel="stylesheet">
    <title>Internal Dev Tools</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
// popups index.js
chrome.tabs.query(
  { currentWindow: true, active: true },
  ([tab]) => {
    chrome.scripting.executeScript(
      {
        target: { tabId: tab.id },
        args: [42],
        func: function (arg) {
          // executed in the page
          return arg
        },
      },
      ([{ result }]) => {
        // executed in the extension
        console.log('received data: ', result); // 42
      }
    );
  }
);
Enter fullscreen mode Exit fullscreen mode

In this script, the extension is executing func on active page tab with one argument "42". Return from it is catch in the second argument callback.

Send data from page

Now we have to pass data from the page:

// page
const stateReport = generateDataToSendIntoExtension();
const message = { type: 'DEV_TOOLS_REPORT_MESSAGE_TYPE', payload: stateReport };
window.postMessage(message);
Enter fullscreen mode Exit fullscreen mode

The easiest way to keep it up to date is wrap it with setInterval:

// page
setInterval(() => {
  // postMessage
}, 1_000)
Enter fullscreen mode Exit fullscreen mode

Get data in extension

To receive data from window.postMessage( we have to use window.addEventListener('message', callback);. Unfortunately callback will be called asynchronously so a simple return from previous part wouldn't work, we have to wrap it into a Promise. Also page is calling window.postMessage( every 1 second so in the extension we have to use setInterval as well.

// popups index.js
const intervalTimeout = setInterval(() => {
  chrome.tabs.query(
    { currentWindow: true, active: true },
    ([tab]: [{ id: number }]) => {
      // @ts-expect-error
      chrome.scripting.executeScript(
        {
          target: { tabId: tab.id },
          args: ['DEV_TOOLS_REPORT_MESSAGE_TYPE'],
          func: function (messageType) {
            return new Promise((resolve) => {
              const callback = ({ data }) => {
                window.removeEventListener("message", callback);
                if (data?.type === messageType) {
                  resolve(data.payload);
                }
              };
              window.addEventListener("message", callback);
            });
          },
        },
        ([{ result }]) => {
          setState(result);
        }
      );
    }
  );
}, 1_000);
Enter fullscreen mode Exit fullscreen mode

Because we're using setInterval we have to remember about remove listener after his callback is called. Also to avoid conflicts with another window messages we should add an if statement to filter out another messages. Last thing is to display data from the page in the extensions's popup.html. In my case popup contains a simple react application, so only thing to do is call setState with data from the page.

Summary

For sure this is not a production solution. There's too much permissions, weak reactivity and potential data leaks. However for internal purposes (as e.g. dev tools) this works.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (9)

Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge

"Unfortunately callback will be called asynchronously so simple return from previous part wouldn't work" --> "Unfortunately the callback will be called asynchronously so a simple return from the previous part wouldn't work"

Collapse
 
przemyslawjanbeigert profile image
Przemyslaw Jan Beigert

Thanks a lot :)

Collapse
 
triptych profile image
Andrew Wooldridge • Edited
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author
Collapse
 
triptych profile image
Andrew Wooldridge
Comment hidden by post author

Some comments have been hidden by the post's author - find out more

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay