How to Build a Chrome Extension with Vue
In this tutorial, I’m going to show you how to build a simple Vue Chrome extension. Our extension will alter the behavior of the new tab page. For the JavaScript part of the extension, I’ll be using the Vue.js framework, as it will allow us to get up and running quickly and is a lot of fun to work with.
Browser extensions are small programs that can modify and enhance the functionality of a web browser. They can be used for a variety of tasks, such as blocking ads, managing passwords, organizing tabs, altering the look and behavior of web pages, and much more.
The good news is that browser extensions aren’t difficult to write. They can be created using the web technologies you’re already familiar with — HTML, CSS and JavaScript — just like a regular web page. However, unlike regular web pages, extensions have access to a number of browser-specific APIs, and this is where the fun begins. You’re probably already using browser extensions in your web development workflow.
The code for this tutorial can be found on GitHub.
The Basics of a Chrome Extension
The core part of any Chrome extension is a manifest file and a background script. The manifest file is in a JSON format and provides important information about an extension, such as its version, resources, or the permissions it requires. A background script allows the extension to react to specific browser events, such as the creation of a new tab.
To demonstrate these concepts, let’s start by writing a “Hello, World!” Chrome extension.
Make a new folder called hello-world-chrome
and two files: manifest.json
and background.js
:
mkdir hello-world-chrome
cd hello-world-chrome
touch manifest.json background.js
Open up manifest.json
and add the following code:
{
"name": "Hello World Extension",
"version": "0.0.1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
The name
, version
and manifest_version
are all required fields. The name
and version
fields can be whatever you want; the manifest version should be set to 2 (as of Chrome 18).
The background
key allows us to register a background script, listed in an array after the scripts
key. The persistent
key should be set to false
unless the extension uses chrome.webRequest API to block or modify network requests.
Now let’s add the following code to background.js
to make the browser say hello when the extension is installed:
chrome.runtime.onInstalled.addListener(() => {
alert('Hello, World!');
});
Finally, let’s install the extension. Open Chrome and enter chrome://extensions/
in the address bar. You should see a page displaying the extensions you’ve installed.
As we want to install our extension from a file (and not the Chrome Web Store) we need to activate Developer mode using the toggle in the top right-hand corner of the page. This should add an extra menu bar with the option Load unpacked. Click this button and select the hello-world-chrome
folder you created previously. Click Open and you should see the extension installed and a “Hello, World!” popup appear.
Congratulations! You just made a Chrome extension.
Overriding Chrome’s New Tab Page
The next step will to have our extension greet us when we open up a new tab. We can do this by making use of the Override Pages API.
Note: before you progress, please make sure to disable any other extensions which override Chrome’s new tab page. Only one extension at a time may alter this behavior.
We’ll start off by creating a page to display instead of the new tab page. Let’s call it tab.html
. This should reside in the same folder as your manifest file and background script:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My New Tab Page!</title>
</head>
<body>
<h1>My New Tab Page!</h1>
<p>You can put any content here you like</p>
</body>
</html>
Next we need to tell the extension about this page. We can do so by specifying a chrome_url_overrides
key in our manifest file, like so:
"chrome_url_overrides": {
"newtab": "tab.html"
}
Finally, you need to reload the extension for the changes to take effect. You can do this by clicking the reload icon for the Hello World extension on Chrome’s extensions page.
Now, when you open a new tab, you should be greeted by your custom message.
Adding Vue to the Extension
Now we have a very basic implementation of our extension up and running, the time has come to think about what the rest of the desired functionality will look like. When a user opens a new tab, I would like the extension to:
- Fetch a joke from the wonderful icanhazdadjoke.com.
- Display that joke in a nicely formatted manner to the user.
- Display a button for the user to favorite the joke. This will save the joke to
chrome.storage
. - Display a button for the user to list favorited jokes.
You could, of course, do all of this with plain JavaScript, or a library like jQuery — and if that’s your thing, feel free!
For the purposes of this tutorial, however, I’m going to implement this functionality using Vue and the awesome vue-web-extension boilerplate.
Using Vue allows me to write better, more organized code faster. And as we’ll see, the boilerplate provides several scripts that take the pain out of some of the common tasks when building a Vue Chrome extension (such as having to reload the extension whenever you make changes).
vue-web-extension-boilerplate
This section assumes that you have Node and npm installed on your computer. If this isn’t the case, you can either head to the project’s home page and grab the relevant binaries for your system, or you can use a version manager. I would recommend using a version manager.
We’ll also need Vue CLI installed and the @vue/cli-init package:
npm install -g @vue/cli
npm install -g @vue/cli-init
With that done, let’s grab a copy of the boilerplate:
vue init kocal/vue-web-extension new-tab-page
This will open a wizard which asks you a bunch of questions. To keep this tutorial focused, I answered as follows:
? Project name new-tab-page
? Project description A Vue.js web extension
? Author James Hibbard <jim@example.com>
? License MIT
? Use Mozilla's web-extension polyfill? No
? Provide an options page? No
? Install vue-router? No
? Install vuex? No
? Install axios? Yes
? Install ESLint? No
? Install Prettier? No
? Automatically install dependencies? npm
You can adapt your answers to suit your preferences, but the main thing to be certain of is that you choose to install axios. We’ll be using this to fetch the jokes.
Next, change into the project directory and install the dependencies:
cd new-tab-page
npm install
And then we can build our new extension using one of the scripts the boilerplate provides:
npm run watch:dev
This will build the extension into a dist
folder in the project root for development and watch for changes.
To add the extension to Chrome, go through the same process as outlined above, making sure to select the dist
folder as the extension directory. If all goes according to plan, you should see a “Hello world!” message when the extension initializes.
Vue Chrome Extension Project Setup
Let’s take a minute to look around our new project and see what the boilerplate has given us. The current folder structure should look like this:
.
├── dist
│ └── <the built extension>
├── node_modules
│ └── <one or two files and folders>
├── package.json
├── package-lock.json
├── scripts
│ ├── build-zip.js
│ └── remove-evals.js
├── src
│ ├── background.js
│ ├── icons
│ │ ├── icon_128.png
│ │ ├── icon_48.png
│ │ └── icon.xcf
│ ├── manifest.json
│ └── popup
│ ├── App.vue
│ ├── popup.html
│ └── popup.js
└── webpack.config.js
As you can see, from the config file in the project root, the boilerplate is using webpack under the hood. This is awesome, as this gives us Hot Module Reloading for our background script.
The src
folder contains all of the files we’ll be using for the extension. The manifest file and background.js
should be familiar, but also notice a popup
folder containing a Vue component. When the boilerplate builds the extension into the dist
folder, it will pipe any .vue
files through the vue-loader and output a JavaScript bundle which the browser can understand.
Also in the src
folder is an icons
folder. If you look in Chrome’s toolbar, you should see a new icon for our extension (also known as the browser action). This is being pulled from this folder. If you click it, you should see a popup open which displays “Hello world!” This is created by popup/App.vue
.
Finally, note a scripts
folder containing two scripts — one to remove eval
usages to comply with the Content Security Policy of Chrome Web Store and one to package your extension into a .zip file, which is necessary when uploading it to the Chrome Web Store.
There are also various scripts declared in the package.json
file. We’ll be using npm run watch:dev
for developing the extension and later on npm run build-zip
to generate a ZIP file to upload to the Chrome Web Store.
Using a Vue Component for the New Tab Page
Start off by removing the annoying alert
statement from background.js
.
Now let’s make a new tab
folder in the src
folder to house the code for our new tab page. We’ll add three files to this new folder — App.vue
, tab.html
, tab.js
:
mkdir src/tab
touch src/tab/{App.vue,tab.html,tab.js}
Open up tab.html
and add the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>New Tab Page</title>
<link rel="stylesheet" href="tab.css">
</head>
<body>
<div id="app"></div>
<script src="tab.js"></script>
</body>
</html>
Nothing special going on here. This is a simple HTML page which will hold our Vue instance.
Next, in tab.js
add:
import Vue from 'vue';
import App from './App';
new Vue({
el: '#app',
render: h => h(App)
});
Here we import Vue, pass a selector for the element that we want it to replace with our application, then tell it to render our App
component.
Finally, in App.vue
:
<template>
<p>{{ message }}</p>
</template>
<script>
export default {
data () {
return {
message: "My new tab page"
}
}
}
</script>
<style scoped>
p {
font-size: 20px;
}
</style>
Before we can use this new tab page, we need to update the manifest file:
{
"name":"new-tab-page",
...
"chrome_url_overrides": {
"newtab": "tab/tab.html"
}
}
And we also need to have the boilerplate compile our files and copy them over to the dist
folder, so that they’re available to the extension.
Alter webpack.config.js
like so, updating both the entry
and plugins
keys:
entry: {
'background': './background.js',
'popup/popup': './popup/popup.js',
'tab/tab': './tab/tab.js'
},
plugins: [
...
new CopyPlugin([
{ from: 'icons', to: 'icons', ignore: ['icon.xcf'] },
{ from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml },
{ from: 'tab/tab.html', to: 'tab/tab.html', transform: transformHtml },
...
})
You’ll need to restart the npm run watch:dev
task for these changes to take effect. Once you’ve done this, reload the extension and open a new tab. You should see “My new tab page” displayed.
Fetching and Displaying Jokes in Our Vue Chrome Extension
Okay, so we’ve overriden Chrome’s new tab page and we’ve replaced it with a mini Vue app. Now let’s make it do more than display a message.
Alter the template section in src/tab/App.vue
as follows:
<template>
<div>
<div v-if="loading">
<p>Loading...</p>
</div>
<div v-else>
<p class="joke">{{ joke }}</p>
</div>
</div>
</template>
Change the <script>
section to read as follows:
<script>
import axios from 'axios';
export default {
data () {
return {
loading: true,
joke: "",
}
},
mounted() {
axios.get(
"https://icanhazdadjoke.com/",
{ 'headers': { 'Accept': 'application/json' } }
)
.then(res => {
this.joke = res.data.joke;
this.loading = false;
});
}
}
</script>
And finally, change the <style>
section to read as follows:
<style>
body {
height: 98vh;
text-align: center;
color: #353638;
font-size: 22px;
line-height: 30px;
font-family: Merriweather,Georgia,serif;
background-size: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.joke {
max-width: 800px;
}
</style>
If you’re running the npm run watch:dev
task, the extension should automatically reload and you should see a joke displayed whenever you open a new tab page.
Once you’ve verified it’s working, let’s take a minute to understand what we’ve done.
In the template, we’re using a v-if block to either display a loading message or a joke, depending on the state of the loading
property. Initially, this will be set to true
(displaying the loading message), then our script will fire off an Ajax request to retrieve the joke. Once the Ajax request completes, the loading
property will be set to false
, causing the component to be re-rendered and our joke to be displayed.
In the <script>
section, we’re importing axios, then declaring a couple of data properties — the aforementioned loading
property and a joke
property to hold the joke. We’re then making use of the mounted
lifecycle hook, which fires once our Vue instance has been mounted, to make an Ajax request to the joke API. Once the request completes, we update both of our data properties to cause the component to re-render.
So far, so good.
Persisting Jokes to Chrome’s Storage
Next, let’s add some buttons to allow the user to favorite a joke and to list out favorited jokes. As we’ll be using Chrome’s storage API to persist the jokes, it might be worth adding a third button to delete all of the favorited jokes from storage.
First, add the following to the bottom of manifest.json
:
"permissions": [ "storage" ]
This gives the Vue Chrome extension access to the chrome.storage
API.
Reload the extension, then add the buttons to the v-else
block:
<div v-else>
<p class="joke">{{ joke }}</p>
<button @click="likeJoke" :disabled="likeButtonDisabled">Like Joke</button>
<button @click="logJokes" class="btn">Log Jokes</button>
<button @click="clearStorage" class="btn">Clear Storage</button>
</div>
Nothing too exciting here. Note the way that we are binding the like button’s disabled
property to a data property on our Vue instance to determine its state. This is because a user shouldn’t be able to like a joke more than once.
Next, add the click handlers and the likeButtonDisabled
to our script section:
export default {
data () {
return {
loading: true,
joke: "",
likeButtonDisabled: false
}
},
methods: {
likeJoke(){
chrome.storage.local.get("jokes", (res) => {
if(!res.jokes) res.jokes = [];
res.jokes.push(this.joke);
chrome.storage.local.set(res);
this.likeButtonDisabled = true;
});
},
logJokes(){
chrome.storage.local.get("jokes", (res) => {
if(res.jokes) res.jokes.map(joke => console.log(joke));
});
},
clearStorage(){
chrome.storage.local.clear();
}
},
mounted() { ... }
}
Here we’ve declared three new methods to deal with the three new buttons.
The likeJoke
method looks for a jokes
property in Chrome’s storage. If it’s missing (that is, the user has yet to like a joke), it initializes it to an empty array. Then it pushes the current joke onto this array and saves it back to storage. Finally it sets the likeButtonDisabled
data property to true
, disabling the like button.
The logJokes
method also looks for a jokes
property in Chrome’s storage. If it finds one, it loops over all of its entries and logs them to the console.
Hopefully what the clearStorage
method does is clear.
Go ahead and try this new functionality in the extension and satisfy yourself that it works.
Adding Polish to the Vue Chrome Extension
Okay, so that seems to work, but the buttons are ugly and the page is a little plain. Let’s finish off this section by adding some polish to the extension.
As a next step, install the vue-awesome library. This will allow us to use some Font Awesome icons in on our page and make those buttons look a bit nicer:
npm install vue-awesome
Register the library with our Vue app in src/tab/tab.js
:
import Vue from 'vue';
import App from './App';
import "vue-awesome/icons";
import Icon from "vue-awesome/components/Icon";
Vue.component("icon", Icon);
new Vue({
el: '#app',
render: h => h(App)
});
Now alter the template like so:
<template>
<div>
<div v-if="loading" class="centered">
<p>Loading...</p>
</div>
<div v-else>
<p class="joke">{{ joke }}</p>
<div class="button-container">
<button @click="likeJoke" :disabled="likeButtonDisabled" class="btn"><icon name="thumbs-up"></icon></button>
<button @click="logJokes" class="btn"><icon name="list"></icon></button>
<button @click="clearStorage" class="btn"><icon name="trash"></icon></button>
</div>
</div>
</div>
</template>
Finally, let’s add some more styling to the buttons and include a picture of everyone’s favorite dad:
<style>
body {
height: 98vh;
text-align: center;
color: #353638;
font-size: 22px;
line-height: 30px;
font-family: Merriweather,Georgia,serif;
background: url("https://uploads.sitepoint.com/wp-content/uploads/2018/12/1544189726troll-dad.png") no-repeat 1% 99%;
background-size: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.joke {
max-width: 800px;
}
.button-container {
position: absolute;
right: 0px;
top: calc(50% - 74px);
}
.btn {
background-color: #D8D8D8;
border: none;
color: white;
padding: 12px 16px;
font-size: 16px;
cursor: pointer;
display: block;
margin-bottom: 5px;
width: 50px;
}
.btn:hover {
background-color: #C8C8C8;
}
.btn:disabled {
background-color: #909090;
}
</style>
The extension should reload. Try opening a new tab and you should see something like this.
Uploading the Extension to the Chrome Web Store
Should you want to make your extension available for others to download, you’d do this via the Chrome Web Store.
The first thing you’ll need in order to do this is a Google account, which you can use to sign in to the Developer Dashboard. You’ll be prompted for your developer details, and before you publish your first app, you must pay a one-time $5 developer signup fee (via credit card).
Next, you need to create a ZIP file for your app. You can do this locally by running the npm run build-zip
. This will create a dist-zip
folder in your project root, containing a ZIP file ready to upload to the Web Store.
For a minimal extension, this is all you’d really need to do. However, before you upload anything, it’s worth reading the official Publish in the Chrome Web Store guide.
Conclusion
And with that, we’re done. In this tutorial, I’ve highlighted the main parts of a Chrome extension and shown how to use the vue-web-extension boilerplate to build a Vue Chrome extension rapidly. We finished off by looking at how to upload an extension to the Web Store and everything that involves.
I hope you enjoyed this tutorial and can use it to get started building Chrome extensions of your own. Let me know if you make anything cool.