Building a Simple Chrome Extension
Tags: chrome, extensions, javascript, vue
Published: 2019-02-16 19:59:00

---
title: Building a Simple Chrome Extension
published: true
description: Building a Simple Chrome Extension with Webpack, Vue, & Bootstrap.
tags: chrome, extensions, javascript, vue
---

I decided to make my first experimental `Chrome Extension`. My colleague came up with a really simple idea to implement, so I decided to give it a try.

# The Functional Requirement

Create a `Chrome Extension` that will output a small colored square in the top left corner of a page, alerting you of which type of `domain (i.e. .dev, .stage)` you're on. These `domains` and `colors` will be managed on an `Options Page`.

## Options Page

The `environments` and their corresponding `color` should be managed on a `Options Page`, allowing you to add / remove any number of entries.

## Active Tab

The small square should only appear on `domains` that match entries the user has added on the `Options Page`.

The square's background color will reflect the current entry.

# Getting Started

_I originally followed [this tutorial][extensions-getting-started] to get started._

Every extension needs to have a `manifest.json`. For a full list of options, visit their [official documentation][docs-manifest-json].

Below is a bare bones example of a `manifest.json`.

```json
{
  "name": "Environment Flag Example",
  "version": "1.0",
  "description": "Environment Flag Example Extension",
  "manifest_version": 2,
  "background": {},
  "permissions": [],
  "options_page": "",
  "content_scripts": []
}
```

## Notable Settings

### Background Scripts

[Documentation][background-scripts]

> _Extensions are event based programs used to modify or enhance the Chrome browsing experience. Events are browser triggers, such as navigating to a new page, removing a bookmark, or closing a tab. Extensions monitor these events in their background script, then react with specified instructions._

We'll be using `background scripts` to add an `event listener` to the `onInstalled` event.

This will allow us to run code when the `extension` is installed. We'll use this `event` to add some default entries for the `Options Page`.

```json
{
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  }
}
```

_Why is `persistent` marked as `false?` As the documentation states:_

> _The only occasion to keep a background script persistently active is if the extension uses chrome.webRequest API to block or modify network requests. The webRequest API is incompatible with non-persistent background pages._

### Permissions

[Documentation][permissions]

> _To use most chrome.\* APIs, your extension or app must declare its intent in the "permissions" field of the manifest._

For example, if you would like to use Chrome's [Storage API][storage-api], you'll have to request permission for `storage`.

```json
{
  "permissions": ["storage"]
}
```

### Options Page

[Documention][options-page]

This entry will tell Chrome which `html` file you would like to use for the `Options Page` for your `Extension`.

```json
{
  "options_page": "options/options.html"
}
```

You would access this page by clicking on `Options` in the menu dropdown for your `Extension`.

![Options Menu Item][img-options-menu-item]

### Content Scripts

[Documention][content-scripts-api]

> _Content scripts are files that run in the context of web pages. By using the standard Document Object Model (DOM), they are able to read details of the web pages the browser visits, make changes to them and pass information to their parent extension._

Essentially, any script you would like to actually run on a given page, needs to leverage this `api`. In our example, we'll be injecting a colored square in the top left corner of the `active tab`.

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

I also recommend watching the video on [Content Scripts and Isolated Worlds][content-scripts-and-isolated-worlds-video] for a better understanding of what's going on behind the scenes.

We'll also need to update our permissions to use the `activeTab`:

```json
{
  "permissions": ["storage", "activeTab"]
}
```

## Complete `manifest.json`

```json
{
  "name": "Environment Flag Example",
  "version": "1.0",
  "description": "Environment Flag Example Extension",
  "manifest_version": 2,
  "permissions": ["storage", "activeTab"],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "options_page": "options/options.html",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"]
    }
  ]
}
```

# Let's Get Coding

The entire codebase is available in my [github repo][github-environment-flag].

_Bonus - for the purists out there, I made a [branch with no dependencies][github-environment-flag-no-dependencies]._

## Installation

Installing a development extension is pretty well documented already, so I won't be going over it here.

Go ahead and follow [their official documentation][extensions-getting-started].

## Background Script

The first thing we should do is set some default data using chrome's [storage api][storage-api].

The two methods you need to know about for this tutorial are:

```javascript
chrome.storage.sync.set({ key: value }, function() {
  console.log('Value is set to ' + value);
});

chrome.storage.sync.get(['key'], function(result) {
  console.log('Value currently is ' + result.key);
});
```

The `second parameter` for each method is a `callback function` once the `storage` operation is complete. We'll be leveraging this in `Vue` to update internal state.

Let's open up `background.js` and add an event for when an extension is `installed`:

```javascript
// background.js

chrome.runtime.onInstalled.addListener(function() {
  /**
   * lets add a default domain
   * for our options page
  */
  chrome.storage.sync.set(
    {
        config: [
            {
                domain: 'docker',
                color: '#2496ed',
            },
        ],
    },
    null
  );
}
```

In the code above, we are doing the following:

1. add a new key to the `storage object` called `config`
2. add one `entry` into `config` for a domain ending with `docker`

## Options Page

For my tech stack, I decided to go with [Bootstrap 4][bootstrap-4], [Vue JS][vue-js], [Webpack][webpack], & native [ES6 Javascript][javascript]. I chose these because I'm comfortable with them, but feel free to choose your own.

> _For the purposes of this tutorial, I won't be explaining much about Vue, since it's an implementation detail, and not necessary to build an extension._
>
> _For a dependency free implementation, [checkout this branch][github-environment-flag-no-dependencies]._

The `options.html` page is very simple:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta content="width=device-width, initial-scale=1.0" name="viewport" />
    <meta content="ie=edge" http-equiv="X-UA-Compatible" />
    <title>Environment Flag Options</title>
    <link
      crossorigin="anonymous"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
      integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
      rel="stylesheet"
    />
  </head>
  <body>
    <main>
      <div class="container  py-5">
        <div class="col-sm-8  offset-sm-2">
          <div id="app"></div>
        </div>
      </div>
    </main>
    <script src="../dist/options.bundle.js"></script>
  </body>
</html>
```

Go ahead and review the [options folder][github-options-folder] before we continue. It's a pretty standard `Vue` application.

Let's review some of the notable `Vue` code. `Options.vue` is where most of the magic happens with leveraging the `chrome api`.

```javascript
// options/Options.vue

{
    data() {
        return {
            /**
             * empty array to be used to store
             * the chrome storage result
             */
            config: [],
        };
    },
    mounted() {
        /**
         * once the component mounts
         * lets call the storage api
         * and request our `config` key
         * 
         * on our callback, lets call a method
         * to set our internal state
         */
        chrome.storage.sync.get(['config'], this.setConfig);
    },
    methods: {
        setConfig(storage) {
            /**
             * set our internal state
             * with the result from the
             * chrome api call
             */
            this.config = storage.config;
        },
    },
}
```

In the code above, we are doing the following:

1. setting internal state for a key called `config`, and assigning it to an empty `array`
2. on the `mounted()` method, we are requesting the key `config` from the `storage api`
3. on the `callback function`, we call a method called `this.setConfig`
4. `setConfig()` assigns our internal state to what is returned from the `chrome api`

We then have two methods for altering the `chrome storage state`:

```javascript
{
    deleteEntry(index) {
        /**
         * remove the entry at a specific index
         * from our internal state
         */
        this.config.splice(index, 1);

        /**
         * update the chrome storage api
         * with the new state
         */
        chrome.storage.sync.set(
            {
                config: this.config,
            },
            null
        );
    },
    addEntry(entry) {
        /**
         * add an entry to our internal state
         */
        this.config.push(entry);

        /**
         * update the chrome storage api
         * with the new state
         */
        chrome.storage.sync.set(
            {
                config: this.config,
            },
            null
        );
    },
}
```

After implementing these methods, the final `Options Page` looks like this:

![Options Page Complete][img-options-page-final]

I know, it's nothing fancy&#8230; but that's not the point. Get out there and have some fun! You'll notice I added a `edu` domain, go ahead and add that now if you would like.

## Content Script

Now that we have an `Options Page` with a way to `add / delete entries`, let's now implement the small square that will appear in the top left corner of valid domains.

To do this, we need to use the `content script` we discussed before. Let's go ahead and open up the `content/content.js` file.

```javascript
// content/content.js

/**
 * lets first request the `config` key from
 * the chrome api storage
 */
chrome.storage.sync.get(['config'], ({ config }) => {
  /**
   * lets see if the `window.location.origin`
   * matches any entry from our
   * options page
   */
  let match = config.find((entry) => {
    let regex = RegExp(`${entry.domain}\/?$`);

    return regex.test(window.location.origin);
  });

  /**
   * if no match, don't do anything
   */
  if (!match) return;

  /**
   * lets create the style attribute
   * by building up an object
   * then using join to combine it
   */
  let node = document.createElement('div');
  let nodeStyleProperties = {
    'background-color': match.color,
    height: '25px',
    left: '5px',
    opacity: 0.5,
    'pointer-events': 'none',
    position: 'fixed',
    top: '5px',
    width: '25px',
    'z-index': '999999',
  };
  let nodeStyle = Object.entries(nodeStyleProperties)
    .map(([key, value]) => {
      return `${key}: ${value}`;
    })
    .join('; ');

  /**
   * apply the style to the node
   * and a class flag (doesn't do anything)
   */
  node.setAttribute('style', nodeStyle);
  node.setAttribute('class', 'chrome-extension-environment-flag');

  /**
   * append the node to the document
   */
  document.body.appendChild(node);
});
```

## Conclusion

Now, when I go to an `edu` domain, I see the following in the top left corner:

![UMBC Homepage][img-umbc-homepage]

I hope this tutorial at least got you interested in Chrome Extensions. We only scratched the surface. Feel free to use any of the code in my repo for any purpose.

[docs-manifest-json]: https://developer.chrome.com/extensions/manifest
[extensions-getting-started]: https://developer.chrome.com/extensions/getstarted
[background-scripts]: https://developer.chrome.com/extensions/background_pages
[permissions]: https://developer.chrome.com/extensions/declare_permissions
[storage-api]: https://developer.chrome.com/apps/storage
[options-page]: https://developer.chrome.com/extensions/options
[content-scripts-api]: https://developer.chrome.com/content_scripts.html
[content-scripts-and-isolated-worlds-video]: https://www.youtube.com/watch?v=laLudeUmXHM
[vue-js]: https://vuejs.org/
[bootstrap-4]: https://getbootstrap.com/docs/4.0/getting-started/introduction/
[webpack]: https://webpack.js.org/
[javascript]: https://www.javascript.com/
[github-environment-flag]: https://github.com/michaeldscherr/chrome-extension-environment-flag
[github-environment-flag-no-dependencies]: https://github.com/michaeldscherr/chrome-extension-environment-flag/tree/no-dependencies
[github-options-html]: https://github.com/michaeldscherr/chrome-extension-environment-flag/blob/master/options/options.html
[github-options-folder]: https://github.com/michaeldscherr/chrome-extension-environment-flag/tree/master/options
[img-options-menu-item]: https://res.cloudinary.com/michaeldscherr/image/upload/v1549736115/Dev.to/Posts/Simple%20Chrome%20Extension/options-menu-item.png
[img-options-page-final]: https://res.cloudinary.com/michaeldscherr/image/upload/v1550340324/Dev.to/Posts/Simple%20Chrome%20Extension/options-page-final.png
[img-umbc-homepage]: https://res.cloudinary.com/michaeldscherr/image/upload/v1550340821/Dev.to/Posts/Simple%20Chrome%20Extension/umbc-homepage.png