How to Make a Chrome Extension - The Ultimate Guide

This is the ultimate guide to chrome extension development. I describe all the best practices learned and tested over several years, and teach you how to create a secure, robust and maintainable chrome extension.

Think of chrome extensions as containers for web apps

Yes, chrome extensions can be more powerful and have features that websites can't have ( like closing/opening tabs, accessing content from other websites, changing how a webpage looks ). But most of the time, the most complex and time-consuming part of creating a chrome extension is designing a user interface in a form of a popup or an overlay. That is all done with familiar to you technologies like JavaScript, HTML and CSS.

Learning by example

To teach you the basics of chrome extension development, in this guide we will develop a plugin from start to end and then publish it in Chrome Web Store.

Here is the idea for the addon: a quick reference for the chrome extension APIs. Right now it can find it here. We will be moving that page and all it's child pages into a smaller and simpler UI for faster reference.

Installing the extension

Enough of the theory already, let's get our hands dirty :) In the process of this tutorial, we will be improving this extension, building upon it, until we have a finished product. You can download the starting here: https://github.com/bashvlas/chrome-extension-api-reference. After downloading the zip file, please follow steps in this article to install the chrome extension in developer mode. "Developer mode" is meant for programmers who are working on chrome extensions, it allows us to test plugins locally. After following the steps - you should see a new icon in the top right of your browser window:

When you click on the icon - you will see a popup:

Congratulations, installation is complete!

Understanding file structure

Here is the full directory structure of the folder that you downloaded and installed:

|-- scr |-- manifest.json |-- lib |-- vue.min.js |-- img |-- logo_32.png |-- logo_128.png |-- pages |-- popup |-- index.html |-- main.js |-- style.css

/manifest.json

{ "manifest_version": 2, "name": "Chrome Extension API Reference", "version": "1.0.0", "description": "This extension provides a simple and quick reference for chrome extension APIs, methods and properties.", "icons": { "128": "/img/logo.png" }, "browser_action": { "default_icon": "/img/logo_32.png", "default_popup": "/pages/popup/index.html" } }

/manifest.json is the main file of our extension. It declares the name of the plugin, description, logos, icons, which permissions are required, what scripts will be executed and on which websites.

/lib

/lib is the folder that contains external libraries used by our extension. In this example it has only one file - vue.min.js ( a UI framework ). But you could potentially keep other library files there like jQuery or moment.js.

/img

/img is the folder that contains images used by our extension.

/img/logo_32.png

/img/logo_32.png is the icon used by chrome on the chrome://extensions/ page:

/img/logo_128.png

/img/logo_128.png is the icon used by chrome on this page:

/img is the folder that contains images used by our extension.

/pages

/pages is the directory for specific pages in our extension. In this example it has only one page - popup, this will be a page that is displayed when a user clicks on the browser action icon in the top right of his window. If your chrome extension needs other pages, like settings, or an admin dashboard - you can put them here.

/pages/popup

This folder contains HTML, CSS and JavaScript files for our popup.

/pages/popup/index.html

<!DOCTYPE html> <html lang="en" > <head> <meta charset="utf-8"> <link rel = "stylesheet" href = "style.css" > </head> <body> <div> example text </div> <div id = "script" hidden > <script src = "main.js" ></script> </div> </body> </html>

This is the page that is displayed in our popup. As you can see, this is a very basic HTML page, nothing complicated.

/pages/popup/style.css

html, body { width: 420px; height: 420px; margin: 0px; } div { padding: 24px; }

This is the file with styles for our popup. On line 7 of index.html you can see a link to this file.

/pages/popup/main.js

This file is currently empty, but if you wanted to add event listeners for buttons, or make AJAX requests - you would do that here.

It's simple, right ?

|-- scr |-- manifest.json |-- lib |-- vue.min.js |-- img |-- logo_32.png |-- logo_128.png |-- pages |-- popup |-- index.html |-- main.js |-- style.css

Developing the browser action popup

Browser action popup is a small webpage that is opened when you click on the extension icon. Here is an example:

That is the most important part of the extension and it is very versatile. You could put anything there:

A timer / productivity app:

A UI that shows promo codes for the current website:

You can even put inside an iframe that shows a page from your website. This is a little known hack that can save you a lot of time and money :)

Here is an example with an iframe from example.com

But we are building a plugin that will contain the chrome extension API reference. So we are putting this small interface inside:

Here is the extension's file structure after I created the application inside the popup:

|-- scr |-- manifest.json |-- lib |-- vue.min.js |-- img |-- logo_32.png |-- logo_128.png |-- pages |-- popup |-- index.html |-- main.js |-- style.css |-- main_item_arr.json

A new file was created - main_item_arr.json - it contains information about chrome APIs. This file was generated by a different chrome extension that scraped this page: api_index ( I show how to build it later );

I also added this line to manifest.json:

"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"

This will allow us to use Vue.js

I also updated all the files in the /pages/popup folder. Now it contains a small web app instead of nothing. I will not go into the details of how that app works, because this is not a small web app tutorial :)

Creating a second chrome extension that scrapes information about APIs

Here is a simple script that scrapes the chrome extension documentation. Go to this url: api_index, open developer console, and execute this script:

(async function () {
  function download_string(file_name, str) {
    var blob = new Blob([str]);
    var url = URL.createObjectURL(blob);
    var link = document.createElement("a");
    link.download = file_name;
    link.href = url;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    delete link;
    URL.revokeObjectURL(url);
  }
  function normalize_links(element) {
    var link_arr = element.querySelectorAll("a");
    for (var i = link_arr.length; i--; ) {
      link_arr[i].href = link_arr[i].href;
      link_arr[i].target = "_blank";
    }
  }
  function wait(time) {
    return new Promise(function (resolve) {
      setTimeout(resolve, time);
    });
  }
  function get_description(element) {
    var p_list = element.querySelectorAll(":scope>p, :scope>code");
    if (p_list.length > 0) {
      var html = "";
      for (var i = 0; i < p_list.length; i++) {
        html += p_list[i].outerHTML;
      }
      return `
${html}
`;
    } else {
      return null;
    }
  }
  function table_to_item_data_arr(table) {
    var item_data_arr = [];
    for (var i = 0; i < table.rows.length; i++) {
      if (table.rows[i].cells.length > 1) {
        var item_data = {};
        item_data.type = table.rows[i].cells[0].innerHTML.trim();
        item_data.name = table.rows[i].cells[1].innerText.trim();
        item_data.description = get_description(table.rows[i].cells[2]);
        if (item_data.type === "object" || item_data.type === "function") {
          var child_table = table.rows[i].cells[2].querySelector(":scope>table");
          if (child_table) {
            item_data.item_data_arr = table_to_item_data_arr(child_table);
          }
        }
        item_data_arr.push(item_data);
      }
    }
    return item_data_arr;
  }
  function method_header_to_item_data(header) {
    var item_data = {};
    item_data.name = header.parentElement.querySelector("h3").innerText.trim();
    item_data.description = get_description(header.parentElement.querySelector(".description"));
    console.log(item_data.name);
    var table = header.parentElement.querySelector(".description>table");
    if (table) {
      item_data.item_data_arr = table_to_item_data_arr(table);
    }
    return item_data;
  }
  async function url_to_item_data_arr(url) {
    var response = await fetch(url);
    var html = await response.text();
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, "text/html");
    normalize_links(doc);
    var item_data_arr = [];
    var method_header_list = doc.querySelectorAll("#methods~div>h3[id*=method-]");
    for (var i = 0; i < method_header_list.length; i++) {
      var item_data = method_header_to_item_data(method_header_list[i]);
      item_data_arr.push(item_data);
    }
    return item_data_arr;
  }
  async function get_main_item_data_arr() {
    var rows = document.querySelector("#stable_apis+p+table").rows;
    var item_data_arr = [];
    normalize_links(document);
    for (var i = 1; i < rows.length; i++) {
      if (i > 1) {
        await wait(5000);
      }
      var item_data = {};
      item_data.name = rows[i].cells[0].innerText.trim();
      item_data.description = rows[i].cells[1].innerHTML;
      item_data.item_data_arr = await url_to_item_data_arr(rows[i].cells[0].querySelector("a").href);
      console.log(item_data.name, item_data);
      item_data_arr.push(item_data);
    }
    return item_data_arr;
  }
  var item_data_arr = await get_main_item_data_arr();
  console.log(item_data_arr);
  download_string("main_item_arr.json", JSON.stringify(item_data_arr));
})();

Executing this script will start the scraping process. Every 5 seconds a new url will be scraped and the result will be logged into the console. After everything was scraped - a file will be downloaded. We can use that file in our first extension.

Packaging the scraping script inside of a chrome extension

Let's create a small add-on that will scrape chrome extension APIs. All that it will do is ad a button to the context menu ( the menu that opens when you right click on a page ) and inject the script above into the current page when you click that button.

Here is the file structure of our new extension:

|-- scr |-- manifest.json |-- background.js |-- scraping_script.js

You can find it here: chrome-extension-api-scraper When you install the extension, go to this url: api_index and right click on the page - you will see this context menu pop up. It will have this item that says "Scrape Chrome Extension APIs":

Clicking on that icon will inject the script and scraping will start.

Conclusion

If you want to learn more about chrome extension development, then you should visit chrome's official extension documentation. If you are interested in learning how to publish a chrome extension - check out my blog post about publishing the Pomodoro Chrome Extension: here. Thank you for reading! Feel free to send me any questions you may have :)