Remember the use case from the previous post? For brevity's sake, we are going to simplify the requirements to:
Create a new, 2 state button on the browser’s toolbar. When the user turns the button ON, inject <p>Hello world</p> into every webpage that is open. Additionally, if the button is ON, and the user opens a new webpage, inject the <p>Hello world</p> tag into that too. If the user turns the button OFF, hide the tag from every webpage that we have injected it into.
We are starting the series with Chrome, because that is the easiest one of the three. There are two sites that you need get familiar with, the Chrome extension development docs, and the Chrome web store.
Show me the code
Create a new folder, place 3 files in it: manifest.json, background.js, content.js. The manifest.json file is the extension's description file, that tells the name, version, requested permissions, etc. of our extension. Background.js will contain our background code, and content.js the content script. Pop the following into manifest.json:
{ "manifest_version": 2, "name": "HelloWorld", "description": "This extension injects hello world into every webpage you visit", "version": "1.0", "permissions": [ "storage", "tabs" ], "background": { "scripts": [ "background.js" ] }, "content_scripts": [ { "matches": [ "http://*/*", "https://*/*" ], "js": [ "content.js" ], "run_at": "document_end" } ], "browser_action": { "default_title": "OFF" } }
You can check the full list of properties on the manifest docs page, here is a brief explanation of the fields that aren't so self-explanatory:
- Manifest_version is the version of the manifest file that you are using (version 1 is deprecated), and version is the current version of your addon (important for autoupdating)
- In permissions, you list what capabilities you need for your addon (see the list of available permission), we request storage because we will store settings in the LocalStorage (the state of the button), and we need tabs because we will need to query the currently open tabs (when the user turns the button ON).
- Background specifies the name of the file that runs as a background script
- Content_scripts define the content scripts that you want to inject into pages
- Browser_action is a weird name, but that specifies that you want a toolbar button too.
Our background.js file:
var isEnabled; function refreshIcon() { var badge = "OFF"; if (isEnabled) { badge = "ON"; } chrome.browserAction.setBadgeText({ text: badge }); } function notifyTab(tabId) { chrome.tabs.sendMessage(tabId, { event: 'stateChange', state: isEnabled }); } function notifyAllTabs() { var query = { windowType: "normal", windowId: chrome.windows.WINDOW_ID_CURRENT }; chrome.tabs.query(query, function(tabs) { tabs.filter(function(tab) { return tab.url.match(/^http/); }).map(function(tab) { return tab.id; }).forEach(notifyTab); }); } chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.hasOwnProperty('csReady')) { if (isEnabled) { notifyTab(sender.tab.id); } } }); chrome.browserAction.onClicked.addListener(function() { isEnabled = !isEnabled; refreshIcon(); notifyAllTabs(); chrome.storage.sync.set({ state: isEnabled }); }); chrome.storage.local.get({ state: true }, function(result) { isEnabled = result.state; refreshIcon(); if (isEnabled) { notifyAllTabs(); } });
The code is full of bad practices, I know, bear with it for the sake of the example. We are using a global variable isEnabled, at the top of our file, to keep track of the state of the button. The first interesting line is 42, this is where our extension's background code starts doing something, everything else before that is event listener assignments, and function declarations. Chrome.storage.local.get simple get's a value from the localStorage, named "state". If state is not in the storage, it defaults to "true". We store this value in the localStorage because we want to keep the button state between browser sessions. The storage implementation is async, so we need to pass a callback to execute when the value gets retrieved.
Our callback function sets the global isEnabled variable according to what was in the localStorage, refreshes our toolbar button to reflect the new state, then calls notifyAllTabs(). NotifyAllTabs queries all the open tabs in the current window, drops the ones where the url does not begin with http, and calls notifyTab() for each one, passing the tab’s id. NotifyTab uses sendMessage(), to send a message to the content script, telling it that our state changed, which either injects the <p>Hello World</p> tag, or hides it, depending on the state of the button.
Line 36 is our toolbar button handler, if the user clicks on the button we flip the global isEnabled variable, refresh the toolbar button, then notify all tabs that the state has changed, and put the new state into the localStorage.
I'll explain line 29 after the content script:
var elementId = 'hello-world'; var injected = false; var shown = false; function hide() { document.getElementById(elementId).style.display = 'none'; shown = false; } function show() { document.getElementById(elementId).style.display = 'block'; shown = true; } function inject(url) { var p = document.createElement('p'); p.innerHTML = 'Hello world'; p.id = elementId; var styles = { position: 'fixed', background: 'black', color: 'white', left: 0, top: 0 }; for (var s in styles) { p.style[s] = styles[s]; } document.body.appendChild(p); injected = shown = true; } chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.event !== 'stateChange') { return; } if (request.state && !injected) { inject(); } else if (request.state && !shown) { show(); } else { hide(); } }); chrome.runtime.sendMessage({ csReady: true });
We keep track of 2 things at the top, whether we have injected the <p> tag, and if it is shown currently or not. The 3 helper functions are self-explanatory. Line 23 assigns a listener that will receive the messages from the background script (look at background.js line 13). We check if the event is "stateChange" (we don't fire any other event though), and check if the state of the button is on or off, and depending on that:
- If the button is turned on, but the tag is not yet injected (the user turned it on the first time), we inject it
- If the button is turned on, the tag is injected, but not shown (it is hidden, because the user did something like on->off->on), we show it
- If the button is turned off, we hide the tag
Line 35 sends a message to the background script, that the content script is ready and assigned the necessary listeners. The reason we do this, is because we might have to inject the <p> tag on page load, instead of the toolbar button click, but we cannot access the background script's variables to see the state of the button, in the content script (nor we can use the chrome.* APIs). This is the message that line 29 in the background.js handles. "Notify the background script, so it can notify us back", sounds retarded, I know, there is probably a better way to handle this.
Autoupdating, hosting, and distributing the extension
This is the biggest misinformation you will see in regards to Chrome extension development: You cannot host your extension, on your own page (unless you just want to share it with a small group, then you can open the extension settings in Chrome, and drag your extension file into the window). There are several places that claim that you can do it, some of them are even Chrome's own docs, but it's all BS. Chrome offered a way to do it in the past, but it is no longer available, anything you try will result in Chrome blocking the install. There was probably a lot of abuse, and now the only way that remains is through the Chrome webstore:
- Go to the webstore
- Register an account
- Pay a one time fee of $5
- Upload your extension, fill out the necessary information, like name, description, screenshots
- Register your webpage with Google webmaster tools
- Wait for the approval (I think this is automated, ours was done in a few hours)
After the extension is approved, you have two options, you either redirect users to your extension's webstore page, or you can use the webstore API built into Chrome, to initiate the install from your own site. This latter process is called inline install, just so you know what to search for, if you get stuck, but the docs are clear on how to implement it. Only the site that you specified in step 5, can initiate an inline install. I'm not going to go into much detail about that here, since the article is already growing too long, but here is the snippet that we are using to initiate the install (this is YUI3 code):
var linkTag = Y.Node.create('<link/>'); var url = "your webstore url"; linkTag.setAttrs({ rel: 'chrome-webstore-item', href: url }); linkTag.appendTo(document.head); var that = this; chrome.webstore.install(url, function(e) { // installation success window.location = '...'; }, function(e) { // installation failed</pre> if (e !== 'User cancelled install') { window.open(url); // open the webstore, maybe it succeeds there } });
Bind this code with an event listener to a button or something, and it should pop up Chrome's inbuilt extension install dialog, when you click on it.
Updating the extension is done by modifying the version in your manifest.json, and uploading the new zip on the webstore, which is a pain in the ass, since you can't automate the deployment of this. After you have uploaded the new zip, it should get rolled out to everyone who has it installed.
So there you have it, your very first Chrome extension. Coming next is Safari, and lastly, Firefox.