This tutorial supposes you have experience or basic knowledge for developing Chrome extension or you can start from here.
A Chrome extension basically made of HTML and JavaScript files even though they have different roles and responsibilities. You can easily create it through original HTML, JavaScript, but there is a problem we want to solve — support modern JavaScript such as ES2015+ or SCSS, etc.
To finish our goal, the core concept is that we have to transfer new syntax of JavaScript or others that browsers do NOT support yet. I know there are many solutions, for example, you can use Babel directly, Webpack, rollup, etc.
Most of those ways need configuration I can only say not very simple for a Chrome extension project. in the next section, We will start our simple solution with Parcel which adapts for a Chrome extension project.
Getting Started
$ mkdir demo && cd demo
$ npm init
$ mkdir src
$ mkdir static
We got a structure look like:
.
├── package.json
├── src
└── static
We will place files such as manifest.json
, _locales
, images
in static
which is no need to transfer. Then no matter .html
or .js
if it needs transfer we should place in src
folder.
Manifest
An extension start with manifest.json
It used for configuring permissions and features. Because we don’t need to transfer manifest.json
so we place it into static
$ touch static/manifest.json
The JSON as follows
{
"name": "Demo",
"version": "1.0.0",
"description": "",
"manifest_version": 2
}
An extension is made of files that save in a folder and zip. The first step we’ll install Parcel and let it help us copy those files which no need to be compiled into dist
$ npm i parcel-bundler parcel-plugin-static-files-copy -D# Parcel need a entry so we create background first
# We'll discuss later$ touch src/background.js
Modify package.json
to add scripts command
{
...
"scripts": {
"build": "parcel build src/background.js"
},
...
}
After change the package.json
we can run the command:
$ npm run build
You should see a dist
folder including manifest.json
and background.js
.
In this step, you already get our core concept, base on your extension experience you can organize the structure you want. The core concept again — files need to be compiled should save in src
, the rest of the files which no need to be compiled save in static
. Finally, all results will stay in dist
then zip it to become an extension.
Right now you can test your extension by open chrome://extensions
in your Chrome
- Turn on the Developer Mode
- Click Load unpacked button then select our
dist
folder
Background
You already know that an extension is an event-based program. What does it mean? It means we can support some operations, instructions when the browser executes an action such as closing a tab, removing a bookmark, open a new page, etc. To support do something when the event happens, we have to register event listeners and background
is the place we need.
First, we need to register a background in manifest.json
to enable it.
{
"name": "Demo",
"version": "1.0.0",
"description": "",
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
Change the src/background.js
chrome.runtime.onInstalled.addListener(() => {
console.log('[background] chrome.runtime.onInstalled initialize');
});
In this post, we only focus on organizing and support modern JavaScript. If you want more please check docs.
Once again, you run npm run build
to check the result in dist
Uncaught ReferenceError: regeneratorRuntime is not defined
If you use such as async/await
syntax, you may get errors above.
To fix it, add browserslist
in package.json
.
"browserslist": [
"last 1 Chrome versions"
]
User Interface
The most important of reasons I recommend you should try Parcel for Chrome extension is that it can easily add an additional entry to compile. For example, in this section, we like to add a popup for browser_action
. We can add src/popup.html
<!DOCTYPE html>
<html>
<head>
<style>
button {
height: 30px;
width: 30px;
outline: none;
}
</style>
</head>
<body>
<button id="change-color"></button>
</body>
</html>
Modify manifest.json
"browser_action": {
"default_popup": "popup.html"
}
Finally, the most important step is to add our source to Parcel command in package.json
"scripts": {
"build": "parcel build src/background.js src/popup.html"
}
Even more, you can use a modern JavaScript frontend framework such as React, Vue, Angular in this popup.html. Next section I will show you how to integrate with React.
Content Script with a React application
content-script
is the way to help our extension inject content to the target URL. In this section, we will add an iframe with our React application. I think this ability is very important which is helpful to implement your idea of extension.
Install React and library
$ npm i react react-dom
Create files in src
$ touch src/content-script.js
$ touch src/app.html
$ touch src/app.js
Again register our feature in manifest.json
"content_scripts": [
{
"matches": ["\\u003Call\\u005Furls\\u003E"],
"js": ["content-script.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": ["images/*", "app.html"]
src/app.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#app {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="app.js" type="text/javascript"></script>
</body>
</html>
app.js
import React from 'react';
import { render } from 'react-dom';const App = () => (
<div>Hello, Chrome extension with React</div>
);render(
<App />,
document.getElementById('app'),
);
content-script.js
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts allow-same-origin allow-forms';
iframe.setAttribute('allowFullScreen', '');
iframe.scrolling = 'no';
iframe.style.cssText = `
width: 100%;
height: 100%;
border: 0;
margin: 0;
top: 0;
left: 0;
z-index: 2147483647;
background-color: white;
filter: none;
display: block;
position: fixed;
overflow: auto;
`;// NOTE: Add React application.
iframe.src = chrome.runtime.getURL('app.html');
document.body.appendChild(iframe);
Again add two entries in package.json
"scripts": {
"build": "parcel build src/background.js src/popup.html src/app.html src/content-script.js"
}
npm run build
again and you should check the result. To make commands more convenient we can improve our scripts in package.json
"scripts": {
"start": "parcel watch src/background.js src/popup.html src/app.html src/content-script.js --no-hmr",
"build": "parcel build src/background.js src/popup.html src/app.html src/content-script.js && zip -9 -r extension.zip dist"
}