Extension messaging (iframe controls the logic)
Use chrome.tabs.sendMessage to communicate with the owner tab of the iframe, which can be retrieved using chrome.tabs.getCurrent inside the iframe.
content.js:
var FRAME_URL = chrome.runtime.getURL('frame.html');
var iframe = document.createElement('iframe');
iframe.src = FRAME_URL;
document.body.appendChild(iframe);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.cmd) {
case 'close':
iframe.remove();
iframe = null;
break;
case 'getData':
sendResponse([
['fname', document.querySelector('.web.page.selector.for.fname').textContent],
['lname', document.querySelector('.web.page.selector.for.lname').textContent],
]);
break;
}
});
iframe.js:
tellParent({cmd: 'getData'}, data => {
for (const [id, val] of data) {
document.getElementById(id).textContent = val;
}
});
document.querySelector('.close-btn').onclick = () => {
tellParent({cmd: 'close'});
};
function tellParent(msg, callback) {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, msg, {frameId: 0}, callback);
});
}
Extension messaging (two-way port)
Initiate the port using chrome.tabs.connect in the iframe, then use it in the content script.
content script:
let framePort;
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'frame') {
// global framePort can be used by code that will run in the future
framePort = port;
port.postMessage({foo: 'bar'});
}
});
// add iframe element and point it to chrome.runtime.getURL('iframe.html')
//...........
iframe script:
chrome.tabs.getCurrent(tab => {
const port = chrome.tabs.connect(tab.id, {name: 'frame', frameId: 0});
port.onMessage.addListener(msg => {
if (msg.foo === 'bar') {
console.log(msg);
}
});
});
Web messaging (two-way MessagePort)
It's super fast and supports binary data types like Blob or ArrayBuffer but requires certain care to avoid interception by the web page:
- Create the iframe inside a closed ShadowDOM to avoid exposing
window[0]
- Don't set iframe's
src
, instead navigate its inner location
using a random secret in the url parameters so that its URL won't be spoofed by the web page or other extensions which used chrome.dom.openOrClosedShadowRoot.
- pass the safe MessagePort into the iframe via postMessage
- use this safe MessagePort for two-way communication
// content.js
(async () => {
const port = await makeExtensionFramePort('/iframe.html');
port.onmessage = e => {
console.log('from iframe:', e.data);
};
port.postMessage(123);
port.postMessage({ foo: bar });
port.postMessage(new Blob(['foo']));
})();
async function makeExtensionFramePort(path) {
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
url.searchParams.set('secret', secret);
const el = document.createElement('div');
const root = el.attachShadow({mode: 'closed'});
const iframe = document.createElement('iframe');
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow.location.href = url;
});
const mc = new MessageChannel();
iframe.contentWindow.postMessage(1, '*', [mc.port2]);
return mc.port1;
}
// iframe.html:
<script src="iframe.js"></script>
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
port = e.ports[0];
port.onmessage = onContentMessage;
}
};
function onContentMessage(e) {
console.log('from content:', e.data);
port.postMessage('ok');
}
Modification: a direct two-way port between the content script and the extension's service worker by using navigator.serviceWorker
messaging in the iframe:
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// background.js
self.onmessage = e => {
if (e.data === 'port') {
e.ports[0].onmessage = onContentMessage;
}
}
function onContentMessage(e) {
// prints both in the background console and in the iframe's console
console.log('from content:', e.data);
port.postMessage('ok');
}