If you’re new to this series and don’t want to read the previous posts, here’s a quick recap:

  • I started this series building a very simple chrome extension that I’ve been updating and improving in every post.
  • The chrome extension we’re working with is called “Acho, where are we?”
  • Acho is the name of my dog 🐶, and in this extension, he will bark and tell you the Title of the page you’re currently navigating:
    • Through a browser action (a popup that appears at the right of the navigation bar in Chrome)
    • Or through a keyboard shortcut that shows a notification at the bottom-right of the screen.

Table of contents

Introduction

So far, our extension has the following features:

  • Display a browser action (popup) with the title of the active tab
  • A command that duplicates the current tab
  • A command that shows a notification at the bottom-right of the screen with the active Tab title.

And these are the components we built to manage the logic of these features:

Three components (popup.js, background.js, and content.js) with their primary functions. The popup.js has the following functions: Listen OnLoad, Get active tab, and Show tab title. The background.js has the following functions: Listen OnCommand, Duplicate tab, Send a message to the content script, Get active tab. The content.js has the following functions: Build notification and Show tab title.

The functions “Get active tab” and “Show tab title” are used by multiple components, but right now, their logic is duplicated inside each of the components. As you may have imagined, we need to find a way to write that logic a single time and share it across our project.

ℹ️ About reusability: Reusing code allows us to save time and reduce redundancy in our project. By avoiding writing the same code multiple times, we’re also making our project easier to maintain since our code gets cleaner and updates to the shared logic can be done in a single place.

So, a better version of our app would look something like this:

The functions Get active tab and Show tab title appear a single time inside a new file called acho.js and are shared with popup.js, background.js, and content.js

In this version, our components are only responsible for their particular logic, and the shared logic is separated in the acho.js file, where it can be easily maintained and shared. There’s also no duplicated logic.

Let’s see how to achieve that in our sample chrome extension.

Centralize the shared logic in a separate file

For starters, we need our reusable logic to be centralized in a separate file. So we are going to create a new file called acho.js. Here we will create a class named Acho and add the methods that will later be called from each component.

In a real example, you’d probably use more than one file for your shared logic. We are using just one to keep the example simple.

Here’s how the acho.js file looks like:

/** Shared logic */
class Acho {

    /**
     * Gets the active Tab
     * @returns {Promise<*>} Active tab
     */
    getActiveTab = async () => {
        const query = { active: true, currentWindow: true };
        const getTabTitlePromise = new Promise((resolve, reject) => {
            chrome.tabs.query(query, (tabs) => {
                resolve(tabs[0]);
            });
        });
        return getTabTitlePromise;
    }

    /**
     * Concatenates the tab title with Acho's barks.
     * @param {String} tabTitle Current tab title
     * @returns {String} 
     */
    getBarkedTitle = (tabTitle) => {
        const barkTitle = `${this.getRandomBark()} Ahem.. I mean, we are at: <br><b>${tabTitle}</b>`
        return barkTitle;
    }
    
    /**
     * Array of available bark sounds
     * @private
     * @returns {String[]}
     */
    getBarks = () => {
        return [
            'Barf barf!',
            'Birf birf!',
            'Woof woof!',
            'Arf arf!',
            'Yip yip!',
            'Biiiirf!'
        ];
    }

    /**
     * Returns a random bark from the list of possible barks.
     * @private
     * @returns {String}
     */
    getRandomBark = () => {
        const barks = this.getBarks();
        const bark = barks[Math.floor(Math.random() * barks.length)];
        return bark;
    }
}

We have two public methods:

  • getActiveTab returns the active tab.
  • getBarkedTitle generates a string concatenated with a random bark sound and the tab title. We’ll use this both in the browser action (the popup) and the notification.

Then we have a few private methods just to simplify the logic in our public methods.

Accessing the reusable code

Great. Now our reusable logic is ready to be used by many components, but that’s not all. We need to figure out how to access this logic from each component:

  • Background script (background.js)
  • Content script (content.js)
  • Browser action script (popup.js)

To approach this issue it’s important to remember that, even though all of these components are part of the same extension, they run in different contexts:

  • The popup.js runs in the context of our Browser Action
  • The content script runs in the context of the web page.
  • The background script handles events triggered by the browser and is only loaded when needed. It works independently from the current web page and the browser action.

So how can we make our reusable code available to all of these different contexts?

From the Browser Action

This one will probably feel familiar to you since the solution we are going to implement it’s what we do in static HTML + JS websites: We are going to add the file acho.js as a script in our browser action HTML file (popup.html) using the <script> tag:

Open the popup.html file and add the script at the bottom of the <body> tag, like so:

<body>
    <!-- the rest of the body -->

    <script src='popup.js'></script> 
    <script src='acho.js'></script> <!-- 👈 -->
</body>

Done! Now we can use the Acho class from popup.js, and our code will be significantly reduced:

document.addEventListener('DOMContentLoaded', async () => {
    
    const dialogBox = document.getElementById('dialog-box');
    const query = { active: true, currentWindow: true };
    
    const acho = new Acho(); // 👈
    const tab = await acho.getActiveTab();
    const bark = acho.getBarkedTitle(tab.title);

    dialogBox.innerHTML = bark;
});

From the content script

The solution here may not be as obvious, but it’s pretty simple: Just add acho.js to the js array inside our current content script object in the manifest.json file:

{
    "manifest_version": 2,
    "name": "Acho, where are we?",
    ... 
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "js": ["content.js", "acho.js"], // 👈
            "css": ["content.css"]
        }
    ],
}

And now we can instantiate and use the Acho class in content.js to generate the “barked title” string:

// Notification body.
const notification = document.createElement("div");
notification.className = 'acho-notification';

// Notification icon.
const icon = document.createElement('img');
icon.src = chrome.runtime.getURL("images/icon32.png");
notification.appendChild(icon);

// Notification text.
const notificationText = document.createElement('p');
notification.appendChild(notificationText);

// Add to current page.
document.body.appendChild(notification);

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {

    const notification = document.getElementsByClassName('acho-notification')[0];
    const notificationText = notification.getElementsByTagName('p')[0];

    // 👇👇👇
    const acho = new Acho();
    notificationText.innerHTML = acho.getBarkedTitle(request.tabTitle); 

    notification.style.display = 'flex';

    setTimeout(function () {
        notification.style.display = 'none';
    }, 5000);
    
    return true;
});

From the background script

Here the solution is similar: We need to add acho.js to the scripts array of our background object in the manifest.json:

{
    "manifest_version": 2,
    "name": "Acho, where are we?",
    ... 
    "background": {
        "scripts": [ "background.js", "acho.js" ], // 👈
        "persistent": false
    }
}

And just like that, we can now access the Acho class from background.js:

chrome.commands.onCommand.addListener(async (command) => {
    switch (command) {
        case 'duplicate-tab':
            await duplicateTab();
            break;
        case 'bark':
            await barkTitle();
            break;
        default:
            console.log(`Command ${command} not found`);
    }
});

/**
 * Gets the current active tab URL and opens a new tab with the same URL.
 */
const duplicateTab = async () => {
    const acho = new Acho(); // 👈 
    const tab = await acho.getActiveTab();

    chrome.tabs.create({ url: tab.url, active: false });
}

/**
 * Sends message to the content script with the currently active tab title.
 */
const barkTitle = async () => {
    const acho = new Acho(); // 👈 
    const tab = await acho.getActiveTab();

    chrome.tabs.sendMessage(tab.id, {
        tabTitle: tab.title
    });
}

I had to make the functions async so I could await the promise from acho.getActiveTab(). You can use acho.getActiveTab().then((tab) => { }) instead if you like.

That’s it! Now all our components are reusing the logic from acho.js.

Conclusion

We managed to remove our duplicated code and apply reusability by creating a separate file containing the shared logic and using different strategies to make that file available in every component.

Now our extension’s code is easier to read and maintain 👌

The repo

You can find all my Chrome Extensions examples in this repo:

Let me know what you think! 💬

Are you working on or have you ever built a Chrome extension?

Do you know any other strategies for code reusability in Chrome extensions?

This post is also available on DEV.