DEV Community

Cover image for Chrome extensions: Reusing code
Paula Santamaría
Paula Santamaría

Posted on • Updated on

Chrome extensions: Reusing code

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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"]
        }
    ],
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    });
}
Enter fullscreen mode Exit fullscreen mode

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:

GitHub logo pawap90 / acho-where-are-we

Acho (a cute pup) tells you the title of the current page on your browser. A sample chrome extension.

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?

Top comments (13)

Collapse
 
mightycoderx profile image
MightyCoderX

But you can't use this technique if you have the popup stuff in an inner folder, which is what I do to keep the main folder clean. So what would be a clean way to do this in my case?

Collapse
 
paulasantamaria profile image
Paula Santamaría

I think you should be able to make it work using relative paths to reference the shared logic including the folder. Did you try something like this?:

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

    <script src='your-dir/popup.js'></script> 
    <script src='your-other-dir/acho.js'></script>
</body>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mightycoderx profile image
MightyCoderX

In which file?

Thread Thread
 
paulasantamaria profile image
Paula Santamaría

In your popup file. In my example it would be in popup.html

Thread Thread
 
mightycoderx profile image
MightyCoderX

Not really, because I would have liked to put the file in the main folder, and in the src you can't put files which are in a parent directory, I tried suspecting it wouldn't work, and in fact I was right. Just like a normal web app you can only access files in the same directory as the index.html

Thread Thread
 
paulasantamaria profile image
Paula Santamaría • Edited

So, I was curious and decided to make some tests in another branch. I changed the structure of the project to organize files in different directories, like this:

├───📁images/ 
├───📁src/
│   ├───📁content/
│   │   ├───content.css
│   │   └───content.js
│   ├───📁logic/
│   │   ├───acho.js
│   │   └───page.service.js
│   ├───📁popup/
│   │   ├───popup.css
│   │   ├───popup.html
│   │   └───popup.js
│   └───service-worker.js
└───manifest.json
Enter fullscreen mode Exit fullscreen mode

Then updated every reference to each file to make it point to the correct relative path. It works!

You can see how I did it here on this branch.

If you want to see how I set up the relative paths, I did it all here in a single commit for clarity

Thread Thread
 
mightycoderx profile image
MightyCoderX

Thanks a lot! I really didn't expect you to do all of this! I'm happy that I was wrong because this is gonna make the dev process for extensions easier. In a perfect world we could also be able to use ES6 modules and the import syntax for more clarity while using classes from other files! I'll try to come up with some way to use ES6 modules (if it's possible) and keep you informed!
Thanks again for the time you took to make this awesome example!

Thread Thread
 
paulasantamaria profile image
Paula Santamaría

That's a good idea. It's supposed to be supported! But I don't know if you can get away with it without declaring each file in the manifest.json, even if it's imported as a module. Let me know if you figure it out!

Collapse
 
khangnd profile image
Khang

Thank you for the great series, really enjoy seeing the progress from a dead simple extension for beginners to a more structural yet still simple one 😃

I also made a post to share my experience for building a cross-browser extension with Svelte: dev.to/khangnd/build-a-browser-ext...

An unrelated question, which tool did you use to generate those awesome header images for the posts?

Collapse
 
paulasantamaria profile image
Paula Santamaría

I'm glad you like the series! I'm working on a new post for it. Hopefully it'll be done for tomorrow.

Love Svelte! I'll check your post :)

I use Figma for the banner!

Collapse
 
qarunqb profile image
Bearded JavaScripter

Would anything have to be done differently for manifest v3?

Collapse
 
mightycoderx profile image
MightyCoderX

Yes, because manifest v3 allows only one background script now, and it's now called serviceWorker. See the docs for more info!

Collapse
 
paulasantamaria profile image
Paula Santamaría