Building Chrome Extensions: Communicating Between Scripts
Tags: javascript, webdev, tutorial, programming
Published: 2020-10-29 10:44:11

A while ago, I set out to build my first Chrome extension. Having recently gotten into the world of web development and getting my hands dirty by building a React project, I felt I had the tools necessary to take on this new challenge.
While I wasn't completely wrong to think this, it wouldn't be the whole truth to say I didn't have to rethink the way I approached designing my project.
I realized this pretty early on into development. You see, when developing any sort of app, as our projects get bigger, we'll tend to inevitably break it up into separate classes, functions, and eventually scripts. Nothing forces us to do this, but unless you want to end up with a remake of 1958's [The Blob](https://sourcemaking.com/antipatterns/the-blob), it would be smart to do so.

---

## The Naive Approach to Script Interoperability
In my case, the extension needed to do the following: Whenever a user makes any changes in the text field, its content needs to be parsed and displayed accordingly as entries in the table to its right. Then, as soon as the "Log entries!" button is clicked, these parsed entries are to be used to invoke changes on the web page.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hqgixduu2ohefrhdrsdc.gif)
<figcaption>Demo of the extension pop-up (web page manipulation not shown)</figcaption>

To that end, I broke up my code's functionality into the following scripts:
- `popup.js`: Contains the behavior of the pop-up and its components. For example, what happens when text is inserted into the text field or when a button is pressed.
- `parser.js`: Contains functionality to parse text following certain rules and returns the parsed result in a specific format.
- `crawler.js`: Contains functionality that utilizes data to crawl a web page in search of specific elements and make certain modifications.

There's an obvious interdependence here. `crawler.js` needs data presented to it in a certain format in order to be able to successfully crawl and modify the web page. This data is provided by `parser.js`, which in turn receives its input from the pop-up's text field, managed by `popup.js`.

<br>![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5pjaca0k4f3ofqktayha.png)<figcaption>Interaction diagram - Ant assets by [freepik](freepik.com)</figcaption>

If, like me, you were spoiled by the simplicity of using [ES6 modules](https://exploringjs.com/es6/ch_overviews.html#overview-entry-modules) in React, your first notion might be to say, "Well, no problem. I'll just export the relevant functions in `parser.js` and `crawler.js` and import them in `popup.js`."

However, my then-vanilla ES5 JavaScript codebase had other ideas, and by the time I emerged bruised and bloodied from my attempt to integrate ES6 features into my project, I had already discovered the proper way of getting my extension's scripts to talk to each other.

*Fun fact: On the road to ES6 integration, I did eventually make the leap to [Parcel](https://parceljs.org/) (which I can highly recommend to anyone getting started using bundlers after a brief incident with [Webpack](https://webpack.js.org/) left me questioning my life choices). The use of a bundler was motivated partly by the need to [easily reference external libraries](https://forum.freecodecamp.org/t/referencing-external-libraries-and-ui-components-in-vanilla-js-project/415730/7).*

*Since Parcel comes preconfigured with [Babel](https://babeljs.io/), I was then also able to use ES6 features such as import/export, which did enable that more familiar way of working with different files. Nevertheless, that isn't the way communication is intended in Chrome extensions, as we'll see shortly.*


---

## Content and Background Scripts
A Chrome extension will typically consist of various cohesive parts or [components](https://developer.chrome.com/extensions/overview#arch), each with a different set of responsibilities. In order for all these components to work together, they communicate via [messaging](https://developer.chrome.com/extensions/messaging).

In our example, `crawler.js` needs to interact with the web page and is thus declared as a so-called [content script](https://developer.chrome.com/extensions/content_scripts). Content scripts are those that need to be able to perform actions on web pages, such as DOM manipulations.

On the other hand, `parser.js` doesn't need this, but it still needs to receive data from `popup.js` and send it back. Thus, we'll declare it as a [background script](https://developer.chrome.com/extensions/background_pages).

A background script, as the name implies, runs in the background. Its roles include listening and reacting to browser events (e.g. closing a tab, perform actions when the extension is (un-)installed), as well as sending and receiving messages.

The declaration of content and background scripts is done in the extension's manifest.json.

{% gist https://gist.github.com/osharaki/b947eccae38601b69d61d5149aea1dfd %}<figcaption>Part of the extension's [manifest.json](https://developer.chrome.com/extensions/manifest) showing background and content script declaration</figcaption>

---

## Message Passing 101
Now we know enough to finally get to the nitty-gritty.

`popup.js`, being the communication initiator here, will need to send out two messages. One whenever the text field is changed and another when the button is clicked. Depending on who the recipient is, it does this using one of two ways. If the recipient is a content script, [chrome.tabs.sendMessage()](https://developer.chrome.com/extensions/tabs#method-sendMessage) is used. Otherwise, it's [chrome.runtime.sendMessage()](https://developer.chrome.com/extensions/runtime#method-sendMessage).

### Non-content script communication
Let's start with the second case. Here's an example of what that might look like in `popup.js`:


{% gist https://gist.github.com/osharaki/b1f8b53862115d682dddff5fb3117f07 %}<figcaption>Example of a message sent from `popup.js` to `parser.js`</figcaption>

Here, we're assuming this piece of code gets executed in `popup.js` whenever a change happens in the text field. As you can see, we've passed `runtime.sendMessage()` two parameters: a required object and an optional callback. What the object should contain is entirely up to you, but in my case, I've included two properties. The first, `msg`, contains a string identifier that is checked by the receiving end to determine how to handle the request. The second property, `data`, simply contains the new content of the text field following the change.

The callback function passed as the second argument to `runtime.sendMessage()` must have a single parameter. This function handles the response sent by the recipient of this message.

*Note: The intended recipient of this message is `parser.js`. However, as we'll see shortly, any background script listening for `onMessage` events will receive it. This is another reason why it's useful to have a property such as `msg` in the passed object. It acts as an identifier so that recipients can determine whether a message is intended for them.*

### Content script communication
As mentioned before, when the recipient is a content script, we use `tabs.sendMessage()`. Here's what that could look like in `popup.js`:


{% gist https://gist.github.com/osharaki/e15799afdb6c16d1ef398b9e6fa8371f %}<figcaption>Example of a message sent from `popup.js` to `crawler.js`</figcaption>

You'll notice this time around that we don't send the message straight away. With `tabs.sendMessage()`, we need to know which tab to send the message to. To do that, we first call `tabs.query()`, which retrieves all tabs that match the properties specified in the first argument. Since my extension pop-up only activates when I'm on a specific URL, I can simply get the active tab in the current window and be sure that it's the one I need.

<br>💡 *Hint: To retrieve all tabs, pass an empty object as the first argument.*

<br>The retrieved tabs are passed to the callback specified in the second argument. This is where we send our actual message, which should now look familiar. The only difference is that with `tabs.sendMessage()`, we need to pass the ID of the relevant tab. The rest follows the same structure as before.

### Receiving and responding to messages
On the receiving end, it's quite straightforward. There, we use [chrome.runtime.onMessage.addListener()](https://developer.chrome.com/extensions/runtime#event-onMessage). Essentially, what it does is add a listener to the `onMessage` event, which gets fired whenever a message is sent using either of the `sendMessage()` variations we've seen.

This method takes a callback function as its single argument, which gets called when the event is fired (i.e. a message is received). That callback, in turn, takes three arguments: the content of the message, its sender, and a function that is called if a response is to be sent back. This function takes a single argument of type object. That was verbose. Let's look at some code.

{% gist https://gist.github.com/osharaki/a3b61847eeec86420a61f7b1688339fc %}<figcaption>Example of how to receive a message in `parser.js`. Notice how the msg field in the request is being used as an identifier for the message</figcaption>

---

## Bonus: Communication Between Content Scripts

So far, so good. But what if we had not just one content script, as was the case here with `crawler.js`, but two that wanted to communicate? To continue with our running example, say we broke up `crawler.js` into two separate content scripts: `finder.js` and `filler.js`. As the names imply, the former searches for certain elements on the webpage, while the latter fills those elements with content.

`finder.js` wants to be able to send the elements it finds to `filler.js`. "Well, no big deal," I hear you saying. We'll just use `tabs.sendMessage()` and `onMessage.addListener()` like before. As much as I hate to be the bearer of bad news, not quite. As it turns out, content scripts can't communicate directly. This actually had me scratching my head for a while. Fortunately, the solution is simple.

*Fun fact: In case you're wondering why I even ran into this problem since I only have one content script, at some point, I unnecessarily had `popup.js` registered as a content script too and consequently its messages weren't reaching `crawler.js` using the direct path of communication. I've since removed this error, but the lesson learned remains.*

All we need to do is have a background script act as a middleman in this exchange. This then looks as follows. Don't be intimidated by the size. I've essentially jammed code from three scripts into one gist for display purposes.

{% gist https://gist.github.com/osharaki/3d4b3c87f9ed4b33f3176f3b6f0731c5 %}<figcaption>Example showing what communication between two content scripts could look like. Note that each code snippet would be contained in the respective actor's script</figcaption>

Essentially, there's nothing new here except a slight logistical change. Instead of direct point-to-point communication, we're using a background script to relay messages between the communicating parties (i.e. the content scripts).

One thing to note here is that we're returning `true` in the background script's `addListener()`. Without going too much into detail, this keeps the communication channel at the background script open to allow for `filler.js`'s response to make it through to `finder.js`. For more on that, take a look at the description provided in [Chrome's documentation](https://developer.chrome.com/extensions/runtime#event-onMessage) for the `sendResponse` parameter of `runtime.onMessage.addListener()`.


---

## Conclusion
Thanks for sticking around! Chrome extensions can be quite idiosyncratic and there's often not much to go on on the internet when you're stuck. So I hope you found some of this useful.

I'd be happy to hear your thoughts and answer any questions you may have.


---

## Resources
- [Project repo](https://github.com/osharaki/sparkpeople-logger)
- [Chrome Extensions: great starting point with links to further resources](https://developer.chrome.com/extensions)
- [Great intro to Javascript modules](https://medium.com/@crohacz_86666/basics-of-modular-javascript-2395c82dd93a)
- [Javascript modules in depth](https://exploringjs.com/es6/ch_modules.html)
- [Overview of ES6 features](https://exploringjs.com/es6/ch_overviews.html)
- [The Blob anti-pattern](https://sourcemaking.com/antipatterns/the-blob)

---
Hungry for more? You might also like:

- [From Static to Dynamic CSS Values](https://medium.com/better-programming/from-static-to-dynamic-css-values-23b2d258f60d)
- [The 10 Browser Extensions I Can’t Live Without](https://medium.com/better-programming/the-10-browser-extensions-i-cant-live-without-df651612d602)
- [Detecting Document Similarity With Doc2vec](https://towardsdatascience.com/detecting-document-similarity-with-doc2vec-f8289a9a7db7)

<br>*Originally published on [Medium](https://medium.com/better-programming/building-chrome-extensions-communicating-between-scripts-75e1dbf12bb7)*