All you’ll ever need to know about Chrome Extensions

Aggelos Arvanitakis
ITNEXT
Published in
16 min readJun 20, 2019

--

Sounds promising? Hopefully it will be! In this article I’ll attempt to demystify all you’ll need to know when developing a Chrome Extension. I’m positive that after reading this article, you’ll be able to easily develop your own extension without too much trouble. There are a lot of things to cover, so let’s get dig straight in.

Basic Concepts

When we think of an extension (also named plugin), we sometimes think about an icon in the top right corner of Google Chrome’s toolbar. Most of you might remember that when you click this icon, a nice popup usually appears. In reality that’s only one part of an extension. An extension is an ecosystem of three (3) individual pieces:

  1. The popup that you see when you click the icon in the toolbar.
  2. The content scripts that run on top of an existing website.
  3. The background scripts that run in the background of Google Chrome.

The names are not random, that’s how they are called. These three (3) pieces are totally independent of one another and serve entirely different purposes. I know that doesn’t say much yet, but hang tight. I can guarantee you that by the end of this article, you’ll have the full picture. Let’s go over them one-by-one:

The Popup

The popup, as I previously mentioned, is simply the UI that you see when you interact with the plugin by clicking on its icon.

In reality, it’s just a typical website

Yup, that’s right, it’s just a normal website with the only difference that it gets limited resources from Chrome. You are able to do anything you would do in a website; write CSS, use React, perform AJAX requests, etc. The only difference between a website and a Chrome Extension popup, is that it needs to be registered as an extension popup. To do that, all you need is to add an extra file named manifest.json. By adding it and configuring it, your website is ready to be ran as a plugin.

A popup is your website code + manifest.json

The manifest.json is what Google reads in order to configure your extension (among others). To demonstrate, select any directory and create a simple HTML file with the name index.html:

<html>
<body>
<h1>Hello world</h1>
</body>
</html>

Next, in the same directory, create a manifest.json file and fill it with the following basic content:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2
}

We haven’t done anything related to the extension yet. All we have done is created an HTML and a manifest that just states some info about our project. Now is the time to mark our project as a Chrome extension. Edit the manifest.json and add the following lines:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2,
"browser_action": {
"default_popup": "./index.html",
"default_title": "Open the popup",
"default_icon":"./image.png"
}

}

The browser_action key essentially translates to “give me the info that I need in order to create an extension extension popup”. We are telling Google that “when the users click on the extension icon (default_icon) , show them a popup whose content can be found in the default_popup. In addition, when they hover over the extension’s icon, show them the text found under default_title”. Now all we have left to do is actually load the plugin into the browser. We have 2 ways of doing that:

1. Automatically through the Chrome Store

Navigate to the plugin’s page within the store and simply click “Install Plugin” (similar to Google Play)

Useful for Production deployment

2. Manually through the chrome://extensions page

Load the folder containing our work by selecting “Load unpacked”

Useful for local development

or alternatively, pack (compress) the files and then select “Load packed” by selecting the single compressed file

Useful for sharing work with others

We don’t have it in the store yet, so we are going to do this the manual way. We will follow the first approach where we will load the “unpacked” version of our work ,by indicating to google where the folder with our files is.

Navigate to chrome://extension on your Google Chrome.

From there, turn on developer mode (switch in the top right corner).

Click on the “Load unpacked” button in the top right corner.

Select the folder containing your 2 files (plus an image for the icon).

If all goes well, you should be seeing your plugin both in the main page and on the toolbar.

Now, if you click on the toolbar icon you should be seeing the HTML we have added:

Congrats! You have successfully created a small plugin! Now you might think that it’s just a “Hello world”, but don’t be fooled. This is a fully functional website. You can do everything you would normally do in a typical website. You can make the index.html load javascript files, css files, fonts, etc. You can create a React application inside it if you wanted. Literally, anything! One thing to know though, is that each time you close it, it’s as if you have closed the tab of a website. Everything that was stored in memory gets lost, just like a website would do after a refresh. After all, it is a website!

So why would we want to have a popup in the first place? Well it’s mainly used as a UI for your users. It provides a nice way for a plugin to interact with its users so they can give input/feedback on certain things. Interestingly enough, the core functionality of the plugin is not normally stored there, since that’s something for the content scripts or the background scripts to handle.

Content scripts

These are the CSS and JS files that get added on top of a website. That means that you can add extra files to a website as if they were added by the original developers themselves. Have you ever seen how some extensions manipulate the things you see in a page? That’s how they do it! They simply add additional Javascript and/or CSS to it. You can add as many extra files as you want, as long as you specify them in the manifest.

To showcase their powers, we’ll tweak our previous example and give it some additional functionality. What doesn’t seem like fun, is to change the background colour of all of google’s websites to red, so let’s do just that. Create a content.css file and add the following code to it:

body {
background-color: red !important;
}

In addition, why not write some javascript as well? Let’s create a content.js file and let’s make sure that each time the user clicks anywhere in the page, an alert shows up:

document.addEventListener('click', () => alert('Click occurred!'));

Finally, let’s register both of these files as content scripts. To do that, we need to specify to which websites will these scripts be “added to”. This is done through the “matches” key, which is a regex pattern and can allow you to target any website, from every single one of them to only particular ones. In our example, we want to register our content scripts to all websites that end with google.com. To do that, let’s change our manifest.json and add the following code:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2,
"browser_action": {
"default_popup": "./index.html",
"default_title": "Open the popup",
"default_icon":"./image.png"
},
"content_scripts": [
{
"js": ["content.js"],
"css": ["content.css"],
"matches": ["https://*.google.com/*"]
}
]

}

Reload the plugin by clicking on the refresh icon in the plugin card, found under chrome://extensions:

Refreshing the plugin’s data after an update on the manifest

Now, try opening google.com. You should be seeing something like this:

modifications of CSS content script

If you attempt to click anywhere, you should see a horrible alert popping up:

modiciations of the JS content script

It’s stupid, I know, but see the big picture here. You can add anything you wish, to any website you wish. You can mount a React app on a DOM element of a website and run your own React application alongside the existing website! These scripts are loaded as soon as the websites are opened and remain active up until their corresponding tabs close. Think of them exactly like normal scripts that the website would load by itself.

Background Scripts

Let’s forget for a minute the dummy extension that we are working on and let’s say we wanted to sync all the files that the user has downloaded to a remote repo. We would need a script that continuously monitors Chrome’s downloads, but where would we put it? We wouldn’t put it in the content scripts since it would only load when a particular page loaded. We also wouldn’t put it in the popup since its scripts only run when you open the popup and stop running as soon as you close the popup. Well, that’s a job for the background scripts.

These are JS scripts that run outside the context of a page but within the context of a browser. They are activated once when you install the plugin and — as long as the plugin remains installed — remain active as long as they have at least one (1) listener registered. The only way a background script would stop running is if it literally doesn’t have anything to do. If you instruct it to monitor or listen for something, it will always be up & running in the background, patiently waiting... Ugh, that’s kinda creepy.

The background scripts are the most stable pieces of the Chrome Extension ecosystem when it comes to logging stuff & communicating with your server or API, because they are not prone to “sudden kills”. Content scripts and scripts inside the popup have a lifecycle that depends upon the website and popup accordingly. On the contrary, background scripts depend upon your extension, so as long as it remains installed, then the scripts will be running in the background in a daemon fashion.

To dip our toes in the water, let’s go ahead and create a file background.js and add a simple alert to it:

alert('Hello from background script!');

Now let’s make sure we register it in our extension as a background script by tweaking the manifest.json as seen below:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2,
"browser_action": {
"default_popup": "./index.html",
"default_title": "Open the popup",
"default_icon":"./image.png"
},
"content_scripts": [
{
"js": ["content.js"],
"css": ["content.css"],
"matches": ["https://*.google.com/*"]
}
],
"background": {
"scripts": ["background.js"]
}

}

Now, if we refresh the plugin (like in the previous example), you should be instantly greeted by an alert.

Activation of the background script

As we mentioned before, the background script is activated once, as soon as you install your extension. If you deactivate & reactivate the plugin (through the switch that is visible in the screenshot above), you will see this alert again.

Putting it all together

As mentioned before we have three (3) pieces to our ecosystem; the popup, the content scripts and the background scripts. Let’s make sure we got it right:

  • Popup: The custom UI for the plugin. You can define HTML, CSS, JS, Images related to the popup. It behaves exactly like a typical website. Whenever you toggle the popup, it’s as if you were refreshing a website! (i.e. data gets reseted)
  • Background Scripts: A collection of JS scripts that run in the background once per extension installation. Useful for configuration & setup that we only declare once & don’t want to ever close or reset (global listeners, etc.).
  • Content Scripts: A collection of scripts that get added to a website’s code, as if they were shipped with the website itself. Has access to everything in the DOM. Useful for interacting with the DOM of a page in any sort of way.

A plugin must register at least one of those, else it had no reason to be a plugin to begin with. Even though all of these pieces are optional (for example you might not ever need a popup cause you don’t want to expose any interface), it’s still mandatory to declare at least one (1) of them.

All of these three (3) individual pieces live in their own little isolated worlds. They are fully sandboxed, have no knowledge of one another and, as a result, cannot directly access each other. Instead, they rely on a set of APIs defined by Chrome in order to exchange information. You may have seen that before if you have worked with web workers, where the main & worker thread communicate with each other through messages. There are a ton of APIs (63 to be exact) and we won’t be able to cover all of them, but I’ll make sure I go through the core ones, since I rarely find myself using any other unless I’m building something extremely specific.

Chrome APIs

As mentioned right above, they provide a way for your extension to ask Chrome to do stuff for you. You can ask about the users’ tabs, get access to their downloads, to their browsing history, etc. With some exceptions, each API is coupled with a particular permission. Permissions are registered in the manifest.json and are a way of telling the user “Hey my plugin is going to have access to this, this and that”. It’s similar to how native apps work, where a lot of them will require access to a certain API — like the camera — when you download them. If you don’t specify the permission for a certain API, Google will now allow you to use it.

Messaging (Runtime) API

This is broad spectrum that contains all the methods used for the exchange of messages within the extension ecosystem and doesn’t require any permissions. There are currently two (2) different ways to send a message from one part of an extension to another:

  1. One-time message requests (imagine them like an HTTP request)
  2. Long-lived connections (imagine them like a websocket)

The first one sends a one-time message to any target that you want, similar to an event being fired in the DOM. For each request message there can be an optional response message, so that’s why I’m correlating that to your typical HTTP request.

// send a message
chrome.runtime.sendMessage({ greeting: 'hello '}, response =>
console.log(response.farewell)
});
// respond to a message
chrome.runtime.onMessage.addListener(request,sender, sendResponse =>
if (request.greeting === 'hello') {
sendResponse({ farewell: 'goodbye' })
}
});

Unfortunately, if you are spamming messages, then it’s not very efficient to go this route. Instead, you pick the second option by creating a port that you can freely spam messages to. It can handle lots of traffic, but unfortunately it doesn’t provide a mechanism for responding to a message. Thus if you are dependent upon a response, then the first option would be the way to go.

// connect to a custom channel/port
const port = chrome.runtime.connect({ port: 'foo' });
// push a message to the channel
port.sendMessage({ greeting: 'hello' });
// react to the message (can't respond back!)
port.onMessage.addListener(request => {
if (request.greeting === 'hello') {
console.log('received a hello message');
}
});

When targeting a background script or a popup, then the chrome.runtime will do just fine. You can find all the info you might need in its documentation page.Unfortunately, if you are targeting a content script, then you need to specify which tab do you want to receive this message. In order to find the tab that you want (i.e. the one that contained “google.com” in our previous example), you must utilise the tabs API.

Tabs API

The tabs API allows you to interact with all the tabs that a particular chrome window has opened. You can query the currently active tab, force a tab to go back/forward, open, close, zoom etc. It’s a very powerful API and that’s why, for most of its methods, it requires the "tabs" permission to be set in the manifest.json. You can find all the info you’ll need in its corresponding API documentation page.

Let’s revisit our initial example where we were setting the background colour of google’s pages to red. Imagine that we want to tweak it, in order to allow users to select the background colour. We want to give them the option to specify it through the popup UI and then read their selection from the content script. To do that, we will need to use both the messaging & the tabs APIs. In short, we will allow the users to enter a colour through an input field and when they click an “apply” button, we will send a one-time message containing the selected colour to the content scripts of all the tabs that have “google.com” loaded. Sounds interesting? Again, most likely not…

The first thing we need to do, is request a permission to read the user’s tabs:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2,
"browser_action": {
"default_popup": "./index.html",
"default_title": "Open the popup",
"default_icon":"./image.png"
},
"content_scripts": [
{
"js": ["content.js"],
"css": ["content.css"],
"matches": ["https://*.google.com/*"]
}
],
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs"
]

}

Afterwards, let’s modify our popup’s index.html to accommodate the aforementioned input & button:

<html>
<body>
<input id="colour-input" type="text" />
<button id="colour-submit-btn">Apply</button>
<script src="./popup.js" />
</body>
</html>

Let’s also create a popup.js to handle the button’s clicks:

document
.querySelector('#colour-submit-btn')
.addEventListener('click', () => {
// read the colour that the user has selected
const colour = document.querySelector('#colour-input').value;
// get all the google tabs and send a message to their tabs
chrome.tabs.query({ url: 'https://*.google.com/*' }, tabs => {
tabs.forEach(tab =>
chrome.tabs.sendMessage(tab.id, { colour } )
);
});
});

Finally, let’s handle the message in our content script. Afterwards, let’s modify the content.js to handle our custom one-time messages:

chrome.runtime.onMessage.addListener(request => {
if (request.colour) {
document.body.style.backgroundColor = request.colour;
}
});

We are all set! Before we refresh the plugin, let’s clear all the contents from content.css and background.js, since we won’t be needing them anymore. Now refresh and boom:

User selected “black” as a background color through the popup’s input

It bloody works! Notice through, that if we refresh the page our changes are not persistent. This is because our content script only reacts to messages, but on boot-time we don’t send any message to it from our popup. Ideally we would want to store the selection somewhere, so that when the content script can read it and apply it the moment it’s loaded. To do that we’ll utilise the Storage API.

Storage API

The storage API is the “localstorage” version of Chrome, but implemented in an async way. Whenever you want to store something across sessions in order to be readable by the whole plugin ecosystem, then that should be your go-to option. In case you were wondering, both the content scripts & the popup have their own actual localstorage, but since they are sandboxed, they don’t have access to each other’s instance of it. There are currently two (2) versions of the Storage API: local and sync:

  1. The local stores data to the local instance Chrome (your PC) and is not tied to any user.
  2. The sync stores data in “the cloud” and ties them to the current Gmail account. The data is then available on any PC you’re logged in.

Both have the exact same API and the decision on what to pick lies on your needs. The API is dead simple and gives you: get, set, remove , clear and an onChanged listener. You can read all about it in the docs. To use the storage API you need the “storage” permission added to your manifest.json.

With that in mind, let’s go ahead and use it to store the user’s preference. Each time the user selects a colour, we will persist it in Chrome’s storage (local version). Then, as soon as the content scripts boot, we will attempt to restore the background colour from the previous session.

Let’s make the following modification to our manifest.json:

{
"name": "My plugin",
"version": "0.0.1",
"description": "This is the full description of the plugin",
"manifest_version": 2,
"browser_action": {
"default_popup": "./index.html",
"default_title": "Open the popup",
"default_icon":"./image.png"
},
"content_scripts": [
{
"js": ["content.js"],
"css": ["content.css"],
"matches": ["https://*.google.com/*"]
}
],
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs",
"storage"
]
}

And then save the user’s choice, by adding the following code whenever the user selects a new colour:

document
.querySelector('#colour-submit-btn')
.addEventListener('click', () => {
// read the colour that the user has selected
const colour = document.querySelector('#colour-input').value;
// Store the user's data
chrome.storage.local.set({ colour });
// get all the google tabs and send a message to their tabs
chrome.tabs.query({ url: 'https://*.google.com/*' }, tabs => {
tabs.forEach(tab =>
chrome.tabs.sendMessage(tab.id, { colour } )
);
});
});

Finally, modify the content.js to boot up from the stored value:

chrome.storage.local.get('colour', (response) => {
if (response.colour) {
document.body.style.backgroundColor = response.colour;
}
});



chrome.runtime.onMessage.addListener(request => {
if (request.colour) {
document.body.style.backgroundColor = request.colour;
}
});

And that’s it! You can now persist data.

Closing notes

The purpose of this article was to make you feel comfortable with the underlying infrastructure of a Chrome plugin. What I want you to remember, is that at the end of the day, an extension is like a collection of different & isolated scripts that you can coordinate with one another. The core APIs for doing that is the Tabs, the Messaging and the Storage, but Chrome has a lot of different APIs. I encourage you to give their descriptions a quick 1-min read, since it’s extremely interesting what you can build with them. Hopefully you now feel more confident when it comes to creating your own plugin and I’ll be more than glad to answer any questions that you might have.

Thanks for sticking until the end!

P.S. (1) Make sure to disable or remove the plugin through the chrome://extensions page, cause the background colour change will immediately start pissing you off.

P.S. (2) In case you were wondering, a devtools extension is just a popup, but with a different UI. The same rules that apply to the popup, apply to the devtools as well, with the exception that you register its HTML under a different key in manifest.json.

P.S (3). 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter and sharing the story with your dev friends 😀

--

--