Simple communication between page and browser extension
Przemyslaw Jan Beigert
Posted on May 6, 2022
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
}
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>
// 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
}
);
}
);
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);
The easiest way to keep it up to date is wrap it with setInterval:
// page
setInterval(() => {
// postMessage
}, 1_000)
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);
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.
Posted on May 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024