DEV Community

loading...
Cover image for Lessons learned from creating a Chrome extension 📗

Lessons learned from creating a Chrome extension 📗

Benjamin Rancourt
A passionate Technology Analyst from Sherbrooke (Quebec) 🍁, who love almost everything about web development 🌐.
Originally published at benjaminrancourt.ca on ・6 min read

I recently had a side project where I had to gather information on some websites. It was a repetitive task that I had to do daily and it was quite boring. Since I thought it could be automated, I chose to give it a try by creating my first Chrome extension. 🧩

This post will serve me as a reflection article on some learning that I realized during this project. 🤔

Note : this extension was built using the Manifest V2, which will be replaced in some time by the Manifest V3. Thus, some information in this post may be out of date or needs to be adapted for the next version, which should be released in January 2021.

Callback hell 😈

While building this extension, I experienced something that I think had ceased to exist since the async/await functions and promises : callback hell. Every external function that I needed to call does not return a promise and takes a callback function... Oh my god, it was really a challenge to work with asynchronous code asynchronously!

Fortunately for us, the Manifest V3 should add promises to its APIs and, eventually, all methods will support promises. If I had known this information earlier, I would have tried to begin directly with the next version! I should have read the Chrome extensions guide before starting to create my extension! 😝

Let's see what new functions I used for my extension.

browserAction

Execute a function after clicking on the extension icon

For my extension, the listener function was my entry point , where the main logic is. I have not used its tab parameter, but after looking at it, it looks like it is the information about the current opened tab. I also add an async tag as my code is asynchronous. 🤘

const listener = async (tab) => {};

// Fired when a browser action icon is clicked. Does not fire if the browser action has a popup.
chrome.browserAction.onClicked.addListener(listener);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.browserAction.onClicked.addListener function

For an example of the tab details, take a look at the chrome.tabs.get function below.

tabs

Create a tab

As one of the goals of my extension is to navigate to a list of URLs, I quickly use the function to create a new tab. In its simplest form, I only provide the absolute URL I want to visit, with the url parameter.

I recently added the windowId parameter to make sure the tabs are created in the same window, instead of the active window. It will alloy me to do other things in a separate window while my script is running. 🧭

const createProperties = { url, windowId };
const callback = async (createdTab) => {};

// Creates a new tab
chrome.tabs.create(createProperties, callback);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.tabs.create function

Execute a script inside a tab

Once the tab was created and fully loaded (after a minimal sleep), I could inject any JavaScript file into the page and retrieve with a couple of document.querySelector the information I was looking for.

const details = {
  file: fileToExecute,
};
const callback = (result) => {};

// Injects JavaScript code into a page
chrome.tabs.executeScript(tabId, details, callback);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.tabs.get function

Unfortunately, I had no way to know if my script had finished running or not, as it was wrapped into an async IIFE to have asynchronous functionalities. So I found a not very clean solution by renaming the tab title to a known value as the last line of my script.

Get information about a tab

The chrome.tabs.get function gives a lot of information about a tab, but I found the most interesting are the three following properties:

  • status : the tab's loading status ("unloaded", "loading", or "complete")
  • title : the title of the tab
  • url : the URL of the main frame of the tab
const callback = async (specifiedTab) => {};

// Retrieves details about the specified tab
chrome.tabs.get(tabId, callback);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.tabs.get function
{
  "active": true,
  "audible": false,
  "autoDiscardable": true,
  "discarded": false,
  "favIconUrl":"",
  "height": 767,
  "highlighted": true,
  "id": 188,
  "incognito": false,
  "index": 1,
  "mutedInfo": {
    "muted": false
  },
  "pinned": false,
  "selected": true,
  "status": "complete",
  "title": "Extensions",
  "url": "chrome://extensions/",
  "width": 1440,
  "windowId": 1
}
Enter fullscreen mode Exit fullscreen mode
Tab information from the chrome://extensions/ page

Delete a tab

Once my scripts were injected and executed inside the new tabs, I would manually close them all at first, but it got tedious as I added more and more URLs to check. So with the previous function, I decided to just get rid of the tab once I got all the info I need.

// Closes one or more tabs
const tabIds = 10 || [10, 12];
chrome.tabs.remove(tabIds);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.tabs.remove function

webNavigation

Get all frames of a tab

One of the pages I was interested in had information inside an iframe, so my initial script was not working as I did not have access to it. Fortunately, we could specify the frameId to target a specific frame of the tab. So, I use the getAllFrames function to find the frame I want with its hostname.

const details = { tabId };
const callback = (frames) => {}

// Retrieves information about all frames of a given tab
chrome.webNavigation.getAllFrames(details, callback);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.webNavigation.getAllFrames function

I initially tried to use the allFrames parameter of the executeScript function to inject the script into all frames of the selected tab, but I was not working for me. I now believe it was because the frame had not finished loading. Anyway, I still prefer to inject my script only where it is needed, rather than injecting it on every iframes on the page.

[
  {
    "errorOccurred": false,
    "frameId": 0,
    "parentFrameId": -1,
    "processId": 612,
    "url": "chrome://extensions/"
  }
]
Enter fullscreen mode Exit fullscreen mode
Frames information of chrome://extensions/ page

runtime

Send a message

While I was looking for a way to know when I could close a tab that had an iframe, I came across the sendMessage function. It allows us to send a message to our extension. So I end up sending a message with the current URL to my extension to let it know that the script has been executed successfully.

 // Sends a single message to event listeners within your extension
chrome.runtime.sendMessage(messageObject);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.runtime.sendMessage function

Listen for messages

To listen to messages, I just add the function below at the start of my extension and now I receive messages from my injected scripts. As it seems much better to depend on messages rather than updating the tab title, I plan to refactor that part someday.

const callback = (message, sender) => {};

// Fired when a message is sent from an extension process
chrome.runtime.onMessage.addListener(callback);
Enter fullscreen mode Exit fullscreen mode
Using the chrome.runtime.onMessage.addListener function

While writing this post, I also learned that the function has a sender argument which contains information about the frame AND the tab. I plan to use that information because it seems more reliable than my document.URL message. 😉

Here's an example of a sender argument below:

{
  "id": "ngjdjkfidkkafkjkdadmdfndkmlbffjf",
  "url": "https://FRAME_HOSTNAME.com/FRAME_URI",
  "origin": "https://FRAME_HOSTNAME",
  "frameId": 1233,
  "tab":{
    "active": true,
    "audible": false,
    "autoDiscardable": true,
    "discarded": false,
    "favIconUrl": "https://TAB_HOSTNAME.com/favicon.ico",
    "height": 767,
    "highlighted": true,
    "id": 226,
    "incognito": false,
    "index": 4,
    "mutedInfo": {
      "muted": false
    },
    "pinned": false,
    "selected": true,
    "status": "complete",
    "title": "TAB_TITLE",
    "url":"https://TAB_HOSTNAME.com/TAB_URI",
    "width": 1440,
    "windowId": 1
  }
}
Enter fullscreen mode Exit fullscreen mode
Example of the sender argument

Conclusion

In retrospect, it was really fun learning to code a Chrome extension. I had never had the opportunity to try it before, both at work and in my personal projects. I hope to have another chance to build a more complex extension! 🤓

Note : as my extension is intended entirely for private use and has not been made reusable, so I do not plan on deploying it on the Chrome Web Store or releasing its source code. Sorry! 🔒

Discussion (0)