I'm back with another post about Chrome extensions! This time I wanted to explore how to store data locally using the chrome.storage
API.
In this post, we're going to add yet another feature to our original extension (Acho, where are we?). This new feature will store the Title and URL of the page each time we call Acho to tell us where we are. We will then list all of the pages and allow the user to navigate to one of them or clear the list.
Here's a quick demo:
So let's get started!
1. Add the storage permission to the manifest.json
As usual, the first thing we need to update is our manifest.json
. This time we're going to add the storage
permission:
{
"manifest_version": 2,
"name": "Acho, where are we?",
...
"permissions": [
"tabs",
"storage" // 👈
]
}
This will allow our extension to use the storage
API.
2. Create the Page Service
Since we already know how to reuse code in chrome extensions, we will create the data access logic in a separate class called PageService
. Here we will add the following methods:
-
getPages
: Will return the list of stored pages. -
savePage
: Will receive the page data and store it. -
clearPages
: Will remove all the pages from the storage.
About the storage API
The chrome.storage
API allows us to store objects using a key that we will later use to retrieve said objects. This API is a bit more robust than the localStorage
API, but it's not as powerful as an actual database, so we will need to manage some things ourselves.
To save an object we will define a key-value pair and use the set
method. Here's an example:
const key = 'myKey';
const value = { name: 'my value' };
chrome.storage.local.set({key: value}, () => {
console.log('Stored name: ' + value.name);
});
And to retrieve our value we will use the get
method and the key:
const key = 'myKey';
chrome.storage.local.get([key], (result) => {
console.log('Retrieved name: ' + result.myKey.name);
});
Finally, to clear the storage we have two options:
// Completely clear the storage. All items are removed.
chrome.storage.local.clear(() => {
console.log('Everything was removed');
});
// Remove items under a certain key
const key = 'myKey';
chrome.storage.local.remove([key], (result) => {
console.log('Removed items for the key: ' + key);
});
Another thing to have in mind when working with this API is error handling. When an error occurs using the get
or set
methods, the property chrome.runtime.lastError
will be set. So we need to check for that value after calling the get/set methods. A few examples:
const key = 'myKey';
const value = { name: 'my value' };
chrome.storage.local.set({key: value}, () => {
if (chrome.runtime.lastError)
console.log('Error setting');
console.log('Stored name: ' + value.name);
});
chrome.storage.local.get([key], (result) => {
if (chrome.runtime.lastError)
console.log('Error getting');
console.log('Retrieved name: ' + result.myKey.name);
});
Don't worry, I promise the actual implementation will be better than a
console.log
.
And, before we move on to the real implementation, I wanted to show you something else. I like to work with async/await
instead of callbacks
. So I created a simple function to promisify the callbacks and still handle errors properly. Here it is:
const toPromise = (callback) => {
const promise = new Promise((resolve, reject) => {
try {
callback(resolve, reject);
}
catch (err) {
reject(err);
}
});
return promise;
}
// Usage example:
const saveData = () => {
const key = 'myKey';
const value = { name: 'my value' };
const promise = toPromise((resolve, reject) => {
chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError)
reject(chrome.runtime.lastError);
resolve(value);
});
});
}
// Now we can await it:
await saveData();
You can replace
chrome.storage.local
withchrome.storage.sync
to sync the data automatically to any Chrome browser where the user is logged into (if they have the sync feature enabled). But keep in mind that there are limits, as specified in the official documentation.
chrome.storage.local
also has limits in the amount of data that can be stored, but that limit can be ignored if we include theunlimitedStorage
permission in themanifest.json
(check the docs).
Let's move on to our actual implementation!
PageService class
As I said before, our PageService will have 3 methods to store, retrieve and remove our pages
. So here they are:
const PAGES_KEY = 'pages';
class PageService {
static getPages = () => {
return toPromise((resolve, reject) => {
chrome.storage.local.get([PAGES_KEY], (result) => {
if (chrome.runtime.lastError)
reject(chrome.runtime.lastError);
const researches = result.pages ?? [];
resolve(researches);
});
});
}
static savePage = async (title, url) => {
const pages = await this.getPages();
const updatedPages = [...pages, { title, url }];
return toPromise((resolve, reject) => {
chrome.storage.local.set({ [PAGES_KEY]: updatedPages }, () => {
if (chrome.runtime.lastError)
reject(chrome.runtime.lastError);
resolve(updatedPages);
});
});
}
static clearPages = () => {
return toPromise((resolve, reject) => {
chrome.storage.local.remove([PAGES_KEY], () => {
if (chrome.runtime.lastError)
reject(chrome.runtime.lastError);
resolve();
});
});
}
}
A few things to notice about this class:
- We are using the
toPromise
function we talked about earlier. - We are storing an array of
pages
, so every time we add a new page to the storage, we need to retrieve the entire array, add our new element at the end and replace the original array in storage. This is one of a few options I came up with to work with arrays and thechrome.storage
API since it doesn't allow me to directly push a new element to the array.
3. Make our PageService available to our components
As we saw in the previous posts of this series, we need to make some changes to allow our new class to be used by our extension's different components.
First, we will add it as a script to our popup.html
so we can later use it in popup.js
:
<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
...
<script src='popup.js'></script>
<script src='acho.js'></script>
<script src='page.service.js'></script> <!-- 👈 -->
</body>
</html>
This will allow us to save pages, retrieve them and clear them from the browser action.
And finally, we'll add it as a background script
in our manifest.json
so we can also call the savePage
method from our background script when the user uses the shortcut:
{
"manifest_version": 2,
"name": "Acho, where are we?",
...
"background": {
"scripts": [
"background.js",
"acho.js",
"page.service.js" // 👈
],
"persistent": false
},
...
}
4. Update our popup.js
Now let's update our popup.js to add the new features.
document.addEventListener('DOMContentLoaded', async () => {
const dialogBox = document.getElementById('dialog-box');
const acho = new Acho();
const tab = await acho.getActiveTab();
const bark = acho.getBarkedTitle(tab.title);
dialogBox.innerHTML = bark;
// Store page.
await PageService.savePage(tab.title, tab.url);
// Display history.
await displayPages();
// Clear history.
const clearHistoryBtn = document.getElementById('clear-history');
clearHistoryBtn.onclick = async () => {
await PageService.clearPages();
await displayPages();
};
});
const displayPages = async () => {
const visitedPages = await PageService.getPages();
const pageList = document.getElementById('page-list');
pageList.innerHTML = '';
visitedPages.forEach(page => {
const pageItem = document.createElement('li');
pageList.appendChild(pageItem);
const pageLink = document.createElement('a');
pageLink.title = page.title;
pageLink.innerHTML = page.title;
pageLink.href = page.url;
pageLink.onclick = (ev) => {
ev.preventDefault();
chrome.tabs.create({ url: ev.srcElement.href, active: false });
};
pageItem.appendChild(pageLink);
});
}
So in the previous code, we are using our three methods from PageService
to add the current page to the storage, list the pages on the screen and allow the user to navigate them, and clear the list.
We use the displayPages
method to display the pages: To do that we retrieve the list of pages and generate a <li>
element and an <a>
element for each page. It's important to notice that we need to override the onclick
event on our <a>
element because if we leave the default functionality, the extension will try to load the page inside our popup, which it's not what we want and it will cause an error. Instead, we create a new tab and navigate to the link using chrome.tabs.create
.
That's all we need to do to add the new feature to our popup.
5. Saving the page from the background script
Now let's make sure the pages are also stored when we use the command shortcut. To achieve that all we need to do is call the savePage
method when the user executes the command:
//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`);
}
});
const barkTitle = async () => {
const acho = new Acho();
const tab = await acho.getActiveTab();
chrome.tabs.sendMessage(tab.id, {
tabTitle: tab.title
});
await PageService.savePage(tab.title, tab.url); // 👈
}
That's it!
The repo
You can find this and all of the previous examples of this series in my repo:
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?
How do you manage data storage?
Top comments (17)
It was quite easy to follow along and understand as a beginner. But I have a doubt regarding the "page.service.js" file. What is that file actually? If I rename it to say just "page.js" at appropriate places, the code breaks and suddenly it cannot find the PageService.savePage function in "background.js" script. Can anyone explain? Links to further resources on the same will be much appreciated. Thank you.
Hi Dilbwag! The
page.service.js
is a regular file containing a class. I just got into the habit of naming my data access files using the suffix.service
, but it could've been calledpage-service.js
too, for example.The reason your code breaks when you change the file's name is that you must also change the references to the file in the following places:
manifest.json
: Where you declare the background scripts, search for thepage.service.js
entry at the end of thescripts
array and replace it with the new filename (e.g.page.js
)popup.html
: At the end of thebody
tag, we added a script tag referencingpage.service.js
. It should be replaced to reference the new filename.Updating the filename in those 2 places should do it. Hope it helps! :)
It does work now. Thank you so much. Also, are you planning on continuing this series for updating it to manifest V3?
Yes! You read my mind 😂 I'm actually working on an article about Manifest V3 right now. My plan is to do an overview and then go through the steps to migrate this sample extension to v3 :)
Cool😁 Will be waiting for it!!!
This post is really creative, I have never thought about doing so👍👍
Thank you! 😊
For example:
X extension is installed in Google Profile 1
X extension is installed in Google Profile 2
Is the data
chrome.storage.local
accessible to other google profiles where the same extension is installed under the same device?Or is it only local to one google profile?
I use local storage for chrome extensions. For me I use this as send state like redux. Local storage is good to save not sensible data on a browser. But I think the point is local storages are 3 for a tab(contents.js), background, and popup. It's nice information!
Hi,
Thank you for making this guide I trying to make an extension for the first time and this is really helpful. I've followed your steps here and created a service class called PageService that contains my storage methods(static methods) and added the file to my popup.html as you've shown but when I try to call any of the static methods from my popup.js file I keep getting an error saying PageService is not defined. I've tried numerous attempts to fix it, but do you have any suggestions?
I can see you are able to call them without any trouble so not sure what I'm missing.
Hi! Could it be that the
<script src='page.service.js'></script>
is missing in your popup.html? Or maybe the filename/path is different than mine?This is the error I get
popup.js:64 Uncaught (in promise) ReferenceError: PageService is not defined at HTMLButtonElement.
This has been a great help as I was planning on building a simple chrome extension, but have no background in javascript. Helped me get through a bunch of the pieces. I could not find a license declared in your repo. What license do you intend the code to be shared under?
Super useful. How can you do the same for a Firefox Addon?
Thanks! I'm planning to cover how to adapt a chrome extension so it works on firefox in a future post :)
Pretty nice and useful guide, thanks a lot
Glad you found it useful 😊