Chrome Extension Communication


Have you been debugging your code and realized that the native dev-tools just don’t seem to be effective enough? Our team had the same thought! We wanted a better way to inspect our data tree. We settled that the best solution was to create a custom chrome extension. There’s a lot of varieties of extensions and applications. There is also quite a bit of documentation, but I found most of it confusing and went through much trial and error while working on the extension.
First, I want to go over a few different types of extensions:
Dev-tool panel
- This is usually what you’d do if you are inspecting something in your system. React has an extension that inspects components, their props, etc. That falls into this category.
Workflows
- 1Password is an example of this, it has a UI that pops up when an email or password field is detected.
Background/Scripting
- Similar to service workers these have scripts that run on the page and can listen to events that the user is triggering. This gets into the CDP (Chrome DevTools Protocol), which I’ll get into more later.
Basic Setup
We chose to do a dev-tool panel. Here’s how we can add a dev-tool panel to our extension:
<!-- devtools.html -->
<html>
<head></head>
<body>
<script>
chrome.devtools.panels.create(“Extension Name”,
“path/to/icon.png”,
“panel.html",
function (panel) {}
)
</script>
</body>
</html>
// manifest.json
{
"manifest_version": 3,
"name": "Extension Name",
"version": "1.0",
"devtools_page": "devtools.html",
"icons": {
"16": "images/logo-16.png",
"32": "images/logo-32.png",
"48": "images/logo-48.png",
"128": "images/logo-128.png"
},
"host_permissions": [
...
],
...
}
Regardless of what the extension is doing there are a set of permissions that you can enable in the manifest.json. The primary permissions we used were scripting
and debugger
. Scripting enables us to run JavaScript on web pages and interact with objects. This enables us to inspect our smart domain objects that live in the browser. For all of our synchronous scripts, we switch to use the CDP since it allows us to continue using the extension even when the page is stopped in a debugger.
// manifest.json
{
...
"permissions": [
"scripting",
"debugger"
...
],
...
}
Here is an example of executing javascript using the CDP:
const tabId = chrome.devtools.inspectedWindow.tabId
const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
'function myFunction() { return "value" }; myFunction()',
awaitPromise: false
})
return result.result.value
And also using the scripting permission:
return new Promise((resolve, _reject) => {
const tabId = chrome.devtools.inspectedWindow.tabId
chrome.scripting.executeScript(
{
target: { tabId },
world: 'MAIN',
func: (arg1, arg2, ...) => { ... },
args: [‘foo’, true]
},
injectionResult => resolve(injectionResult[0].result)
)
})
Both of these allow for returning simple objects so that we can display data in the extension.
The next problem we needed to solve was detecting when something on the page changes so we can properly update the extension panel. First, we need to add several content scripts. These also require the scripting permission.
// manifest.json
{
...
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["message_forwarding.js"]
},
{
"matches": ["<all_urls>"],
"world": "MAIN",
"js": ["event_emitter.js"]
}
],
...
}
It is important to note that the world is different. The default is ISOLATED
. This will run the javascript in a unique environment to the extension. MAIN
runs in the main page thread which enables us to inspect things in the browser. However from the main thread you can’t directly talk to the extension since you can’t access chrome.runtime
// message_forwarding.js
const port = chrome.runtime.connect({ name: ‘extension-name-browser-port’ })
window.addEventListener('ExtensionNameMessage', event => port.postMessage(event.detail), false)
// event_emitter.js
// monitor your objects and fire this event when something changes
const rebroadcastEvent = new CustomEvent('ExtensionNameMessage', { detail: ... })
window.dispatchEvent(rebroadcastEvent)
Next, we need to add a background script that forwards the message from the browser to the extension. This step is necessary since the browser may not be setup and the extension might not be set either. This allows one side to send messages without throwing errors since the other side isn’t connected.
// manifest.json
{
...
"background": {
"service_worker": "background.js",
"type": "module"
}
...
}
// background.js
let extensionPort, browserPort
chrome.runtime.onConnect.addListener(function(port) {
if (port.name === 'extension-name-browser-port') {
browserPort = port
browserPort.onMessage.addListener(function (request) {
extensionPort?.postMessage(request)
})
}
if (port.name === 'extension-name-devtool-port') {
extensionPort = port
extensionPort.onMessage.addListener(function (request) {
browserPort?.postMessage(request)
})
}
})
Lastly, we listen in the extension panel.html for those messages:
// panel.js
// This needs to be loaded in panel.html
let port = chrome.runtime.connect({ name: 'extension-name-devtool-port' })
port.onMessage.addListener(message => { ... })
port.onDisconnect.addListener(() => { port = undefined })
We also added some additional tracking to detect when context is lost and these ports are closed so that we can reconnect them when we come back:
chrome.idle.onStateChanged.addListener(newState => {
if (newState === 'active') {
// re-establish the connection to the background worker
port ??= chrome.runtime.connect({ name: 'extension-name-devtool-port' })
}
})
chrome.webNavigation.onCommitted.addListener(details => {
if (['link', 'typed', 'generated', 'reload'].includes(details.transitionType)) {
chrome.webNavigation.onCompleted.addListener(async function onComplete() {
port ??= chrome.runtime.connect({ name: 'extension-name-devtool-port' }) // ensure the port is still defined
// refresh your extension state and display
chrome.webNavigation.onCompleted.removeListener(onComplete)
})
}
})
Loading Extensions
Finally we can install our extension:
visit
chrome://extensions
Turn on
Developer mode
click
Load unpacked
and select the root directory of your extension
Conclusion
Extensions can be powerful to inspect your application and correct issues that arise with data. We’ve continued to add new features and make it more valuable to our team. At this point it is the first tool to debug instead of reaching for something else.
Subscribe to my newsletter
Read articles from Malachi Irwin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
