How to build a chrome extension?

Goutham Jagannatha
Goutham Jagannatha
August 29, 2019
#frontendengineering

This is a beginners guide to build chrome extensions. I'll explain how to build an extension with an example. The extensions are simple to make and are written in plain old HTML, CSS, and JavaScript.

But why build a chrome extension?

With a chrome browser extension, you can customize your browsing experience. Some of my favorite extensions are StayFocused (the one that blocks out distracting websites) or AdBlock Plus (the one that blocks display ads).

Needless to say, chrome extensions work only on Chrome browsers. Check out the chrome web store. It boasts of hosting 200,000+ extensions as of mid-2019.

Let's get started!

What are we building today?

We are making an extension that scans the URLs in the page and shows the number of outgoing links to different domains from the page.

To make things complicated let's open the domain that has the maximum links in a new tab. The end result will look something like this.

How to build a chrome extension
How to build a chrome extension

The above extension shows that the current page has 423 outgoing links to developer.chrome.com, 6 outgoing links to stackoverflow.com, etc.

What is the use of such an extension?

Nothing. Really. I just want to show you how various pieces work together so that you can build more meaningful extensions yourself.

Extension architecture and scripts

Before we start typing the code, let me explain the various pieces. With this understanding, you'll be able to get a sense of how any extension would work.

Here is the full code in case you want to skip the blog and cut to the chase. Go to the end of this blog to know how to load and run the extension.

Chrome extension typically has 3 scripts and they live in its own world.

The scripts are optional.

Background script (background.js): Acts a listener of browser events. It has access to chrome extension APIs to do things like creating a new tab, and it can listen to browser events like closing a tab.

Content script (content.js): This has access to the page rendered in the browser. If you want to manipulate the DOM of the page like highlighting the anchor tags with a different color, you will use this script.

UI script(popup.js): An extension can have its own UI, and the UI script has access to this UI.

Chrome extension architecture
Chrome extension architecture

How do the scripts talk to each other, you ask?

Answer: Message Passing. 

A message can contain any valid JSON object.

Let's apply the theory to our extension.

  1. Our extension has a UI that shows all the outgoing links and the number of such outgoing links per domain. So, we'll have an HTML file,  say popup.html.
  2. popup.html needs a popup.js companion to add the domain names to the popup.html DOM dynamically.
  3. content.js is required to read the currently active page and fetch all the hrefs in the page. content.js will do so after getting a message from popup.js to fetch URLs.
  4. Once content.js fetches all the URLs, it will pass the data to popup.js through a message.
  5. popup.js sends the domain with the maximum number of links to background.js through, you guessed it, a message.
  6. background.js uses a chrome API to open a new tab with the domain name passed form popup.js.

File structure

The extension will have following file structure. Follow the steps below to create them one by one or download the code here.

URLCount
|- manifest.json
|- icon16.png
|- icon48.png
|- icon128.png
|- popup.html
|- popup.js
|- jquery-3.4.1.min.js
|- background.js
|- content.js

Create a folder called URLCount. Let's put all our files in this folder.

Step-1: manifest.json

Create a file called manifest.json and put the following code in it.

//manifest.json

{
"manifest_version": 2,
"name": "URLCount",
"description": "Sample extension to show URL count in a page",
"version": "1.0"
}

manifest.json file holds information about the extension like the name of the extension, version number, permissions required by the extension, etc.

Step-2: Add icons

Our extension has an icon associated with it. Let's add an icon to the URLCount folder.

Extension icon next to URL bar. This acts as a trigger.
Extension icon next to URL bar acts as a trigger.

Chrome developer guide recommends adding the same icon in 16px, 48px and 128px resolutions. Go ahead and grab an icon that is available in these 3 resolutions.

Update the manifest.json file about these icons.

//manifest.json

{
"manifest_version": 2,
"name": "URLCount",
"description": "Sample extension to show URL count in a page",
"version": "1.0",
"icons": {
 "128": "icon128.png",
 "48": "icon48.png",
 "16": "icon16.png"
 }
}

Step-3: Extension UI (the popup)

Now, it’s time to create the UI of the extension - popup.html.

Extension UI
Extension UI

Of course, we have to put this file in manifest.json. We want to show this UI upon clicking the icon. This type of extension is called browser action. There is another type of extension called page action extension. We'll discuss it some other time.

//manifest.json

{
"manifest_version": 2,
"name": "URLCount",
"description": "Sample extension to show URL count in a page",
"version": "1.0",
"icons": {
 "128": "icon128.png",
 "48": "icon48.png",
 "16": "icon16.png"
},

"browser_action": {
 "default_icon": "icon16.png",
 "default_popup": "popup.html"
 }
}

From the UI, you can see that you'll just need a table in the HTML. Let’s also add an id to table called tabs_table. popup.js will use this id to populate the table. Create popup.html and put the code below.

<!DOCTYPE html>
<html>
<head>
   <title>URLCount</title>
</head>
<body>
   <table id=”tabs_table”>
       <tbody>
           <tr></tr>
       </tbody>
   </table>
</body>
</html>

Since the domains will be added to the HTML dynamically, we'll need a popup.js to do so. Let's use jquery to make our life easy. Download the latest jquery min file and put it in the same folder. Update the HTML like so.

<head>
   <title>URLCount</title>
   <script src="jquery-3.4.1.min.js"></script>
   <script src="popup.js"></script>
</head>

The point to note here is we will not add popup.js to the manifest.json.

The first task of popup.js is to send a message to the content script to fetch the domains. Create the file popup.js and put the following code in it.

//popup.js

$(function() {
 // Send a message to content.js to fetch all the top domains
 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
   var activeTab = tabs[0];
   chrome.tabs.sendMessage(activeTab.id, {"message": "fetch_top_domains"});
 });
});

  1. chrome.tabs API is used to create, close or rearrange tabs. chrome.tabs.query is an asynchronous API that returns all the tabs that matches the given properties. We are looking for the active tab in the current window.
  2. chrome.tabs.sendMessage sends any data to the selected tab. Let's send "fetch_top_domains" message to the content script to trigger collection of the URLs.

We are not entirely done with the popup.js yet. Let's first see how the fetch_top_domains is handled by the content script.

Step-4: Content Script

Our content script should listen for the fetch_top_domains message. After handling fetch_top_domains, it should send a message to the UI script that it has finished its job. Let's call this second message all_urls_fetched.

Before we write the content.js, let's update the manifest.json.

//manifest.json

"content_scripts": [
 {
 "matches": ["<all_urls>"],
 "js": ["content.js"]
 }
]

The "matches" expression tells chrome to load content.js for all pages. You can make the content.js to run on twitter like so: "matches": ["https://twitter.com/*"]

After updating manifest.json, create content.js in the same folder and put the following code.

//content.js

chrome.runtime.onMessage.addListener(
 function(request, sender, sendResponse) {
   if( request.message === "fetch_top_domains" ) {
// Handle the message
chrome.runtime.sendMessage({"message": "all_urls_fetched"});
    }
  }
);

chrome.runtime.sendMessage sends a single message to event listeners.

Along with all_urls_fetched message, the content script should also send a payload. The payload should contain all domains and the number of references to each domain.

Let's use document.links to fetch all hrefs from the page and parse them. The final code would look like this.

// content.js

chrome.runtime.onMessage.addListener(
 function(request, sender, sendResponse) {
   if( request.message === "fetch_top_domains" ) {
     var urlHash = {}, links = document.links;
     for(var i=0; i<links.length; i++) {
       var domain = links[i].href.split('/')[2]
       if (urlHash[domain]) {
         urlHash[domain] = urlHash[domain] + 1;
       }
       else {
         urlHash[domain] = 1;
       }
     }
     chrome.runtime.sendMessage({"message": "all_urls_fetched", "data": urlHash});
   }
 }
);

Now, it's time to handle all_urls_fetched in popup.js.

//popup.js

chrome.runtime.onMessage.addListener(
 function(request, sender, sendResponse) {
   if( request.message === "all_urls_fetched" ) {
   }
  }
);

Add logic to parse urlHash sent by content.js and find the domain that has maximum references. Also, add the contents of urlHash to tabs_table in popup.html.

//popup.js

if( request.message === "all_urls_fetched" ) {
 var urlWithMaxLinks;
 var maxLinks = 0;

 for ( var key in request.data ) {
   if(request.data.hasOwnProperty(key)) {
     $('#tabs_table tr:last').after('<tr><td>' + key + '</td>' + '<td>' + request.data[key] +'</td></tr>');

     if(request.data[key] > maxLinks) {
       maxLinks = request.data[key];
       urlWithMaxLinks = key;
     }
   }
 }
}

We are almost there. How will we open urlWithMaxLinks in a new tab?

Enter background script. Let's send a message from popup.js to chrome runtime with urlWithMaxLinks as payload.

if ( maxLinks != 0 ) {
 chrome.runtime.sendMessage({"message": "open_max_url", "url": urlWithMaxLinks});
}

Step-5: Background Script

It's time to update the manifest.json with information on background.js. Since we'll be opening a new tab with urlWithMaxLinks, let's give tabs permission.

//manifest.json

"permissions": ["tabs"],
"background": {
 "scripts": ["background.js"]
}

background.js listens to open_max_url and uses chrome.tabs API to create a new tab with the URL in the payload. chrome.tabs.create does not work without http:// or https:// (remember, we removed it in popup.js). Let's add an http:// for simplicity before create is called.

//background.js

chrome.runtime.onMessage.addListener(
 function(request, sender, sendResponse) {
   if( request.message === "open_max_url" ) {
     fullURL = "http://" + request.url;
     chrome.tabs.create({"url": fullURL, "active": false});
   }
 }
);

Test the extension

It's time to load our extension and test.

  1. Go to chrome://extensions.
  2. Enable "Developer mode."
  3. Click on "Load unpacked."
  4. Select your extension folder.

How to load a chrome extension?
How to load a chrome extension?

Please check out the chrome extension developer guide for in-depth information on the complete extension architecture, best coding practices and learn how to distribute your chrome extension on the chrome web store.

Happy coding!