-
Notifications
You must be signed in to change notification settings - Fork 0
Developing Browser Extensions
The purpose of this page is to collect all useful and helpful information for the development process of a browser extension. Either cross browser or only for one. The information is in no specific order, however can be searched easily and is seperated into smaller chunks or paragraphs. Also often code examples will be shown or further readings for specific themes. Feel free to enhance this page, but keep the style!!
doomsayer
A general introduction to the process of creating a Web Extension with Angluar is given in the following article, which highlights the key aspects of the development process. The most important fact here is, that we can use the WebExtensionsAPI to develop browser extensions that are cross browser compatible: https://cito.github.io/blog/web-ext-with-angular/
It seems to be difficult to create a browser extension with Angular, however, it compiles anyway to plain js. The advantage is that we gain the modularity of Angular and the object-oriented programming experience with strong typechecks of Typescript. The use of Angular our life easier because the project acquires a well-defined structure. It can can be more easily maintained and enhanced due to the modularity of Angular. Also, TypeScript, brings the object-oriented programming experience with strict typing to the development.
A Chrome Extension can have three distinct front-end components:
- Extension icon – this is an icon that is displayed next to the browser’s Omnibox (the adress bar)
- Popup – This is a popup HTML page that is displayed when the icon is clicked. It can reference JavaScript and CSS files.
- Extension pages – these are HTML pages hosted by the extension. Each page can reference both JavaScript and CSS files.
While this frontend part is important for the user, the backend part is more important for our development. The following important parts need to be considered (taken from developers api):
-
Content script – a JavaScript file that runs in the context of a page displayed in the browser tab. It has limited access to Chrome extension API (eg. it cannot influence other tabs), but it can do a lot of things in context of the page, like:
- Explore DOM elements
- Inject new objects
- Read the page’s local storage and even expand it with permissions to unlimited storage
- Event page (background script) – a page that runs in the background, that is developed either as and HTML page or as a single JavaScript file. It has full access to the Chrome extension API. It is typically used to receive the requests and send replies to other extension elements. External requests (eg. to external APIs or servers like HTTP) should be executed there.
- Extension/popup page script – a JavaScript file referenced by the HTML page hosted by the extension. It has full access to Chrome extension API.
The configuration and all permissions needed, of our extension, needs to be defined in a special manifest.json file. In this file, we can define the different parts of our extension. It also allows us to specify what kind of premissions our extension requests. Below an example manifest structure:
{
"manifest_version": 1,
"name": "Simple Extension",
"version": "1.0.0",
"permissions": [ "tabs", "activeTab" ],
"content_scripts": [
{
"matches": [ "http*://*/*" ],
"js": [ "content-script.js" ]
}
],
"background": {
"page": "index.html#/event-page",
"persistent": false
},
"browser_action": {
"default_title": "Show Popup",
"default_popup": "index.html#/popup"
},
"default_icon": {
"19": "button/geo-19.png",
"38": "button/geo-38.png"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}Of course there are numerous options for the configuration of such a manifest file and further attributes. All available keys and another example can be viewed here: Manifest.Json
However, some options in this example need further clarification maybe, which should be given here now:
- Most of the extensions need to have some permissions for access. In the example above we want to be able to open new browser tabs and access currently-opened ones. That’s why we have to define a
tabsand anactiveTabpermission request. - It is important to define the
matchesparameter for content script node, which will allow us to limit the number of pages the content script get's added. - The
persistentparameter of background script determines if we need our background script or page continuously. By setting this parameter tofalsethe script will be deactivated when not needed (eg. when the extension is not in use at the moment). - The
content_security_policykey is necessary in order to run compiled JavaScript files from Angular.
Normally, extensions are made available in the Extensions Store of either Chrome or Firefox for example. As we don't want to publish an unfished extension, we have to upload it locally. Once we have our manifest file, we can test how it is working locally by turning the developer-mode on in the Chrome://extensions browser page. Having done that, the browser allows us to install the extension by pointing to the location on our computer.
Because we point the path to the location where the manifest file is stored, our extension gets installed and an ID assigned. We can use it to navigate to HTML pages that are included with the extensi. The browsers or at least chrome is hosting its files. As an example, to open the popup.html page we need to go to the following address:
Chrome-extension://[extension-id]/popup.html
JavaScript and CSS files can be referenced in HTML files by relative paths, like this:
<link rel="stylesheet" type="text/CSS" href="style.css">
<script type="text/javascript" src="event-page.js"></script>As there is no out of the box support for Angular and browser extensions, we have to transcode them from typescript to javascript. Which is anway done by Angular, so we can use it. First of all we create our project normally as usual with ng create [projectname].
After building the project, the resulting files can be found normally in the dist folder, where also our manifest file should be located. However, it's not a good idea to place it there from start, as the CLI replaces the whole folder everytime we build it. So we move the manifest file to our src folder and enter it in the angular-cli.json at the assets key in order to bundle it with the other files:
// ...
"assets": [
"assets",
"favicon.ico",
"manifest.json"
],
// ...As Angular CLI is focused on building single-page applications, it produces a single HTML file, which is our start page in the extension. We can make use of routing in order to dynamically change the content on the page or popup as it's called, depending on the user interactions. First we need to setup some components, so we type the following commands in order to generate them:
ng g c homepage
ng g c event-page
ng g c popupIn our app-routing.module.ts we need to include the useHash setting if we want to route. The setting for the router module enables routing in old-fashioned way, using the # sign. So we can open our popup html the following way:
chrome-extension://[extension-id]/index.html#/popup
This is necessary as we have ot use the same method in the manifest file we include in the src folder.
Now we have the basic setup for the project and need to install the Chrome extensions API in order to use the typings, by performing the following command: npm install @types/chrome --save-dev
A good idea, though not needed directly will be to add the configuration to the types property of the tsconfig.app.json.
In order to let particular pieces of the extension we develop, communicate with each other, we need a kind of messaging system. Luckily Chrome Extensions API offers three messaging methods:
-
Chrome.tabs.connect - Opens a communication path to the selected browser tab and allow it to send the message:
- Communicate with single extension elements
- Declare Method to handle incoming Response Events
-
Chrome.tabs.sendMessage - Sends the message to the selected browser tab, e.g. the currently active one:
- Send message within currently executed extension (default behavior).
- Define an ID of the extension
- Define a method to handle an incoming response event
-
Chrome.runtime.sendMessage - Sends a message that can be received by any part of the extension, e.g. content script, event page. The characteristics are similar to the
tabsmethod above.
Once a message is sent it can be retrieved of course by another part of the extension. The API provides the following two methods for receiving messages: * **Chrome.runtime.onConnect** - Here we can define a listener for messages, that were sent, through the `tabs` method. Typically used in a content script, that is used in some tabs. * **Chrome.runtime.onMessage** - Here we can define a listener, that receives messages sent through `tabs.sendMessage` and `runtime.sendMessage` methods. It can be implemented in any part of the extension.
The problem of a content script is, that it must be developed as a JavaScript file, whereas the Angular app assets need to be bootstrapped by an HTML tag. Unfortunately, Angular CLI doesn’t provide a way to easily build selected TypeScript files to separate out the JavaScript assets. The other options however are:
- We can develop a content script as a JavaScript source file and add it to assets node in
.angular-cli.json - We can develop it as single TypeScript source file without internal modules dependencies and add it to scripts node in
.angular-cli.json. It will be bundeled to the later bundle. - Develop it normally and compile it using a third party tool, like gulp.
The options 1. and 2. are fine if the content script, doesn't include any complex logic, otherwise the 3. options is better. We will perform the third option in the next step.
First you need to install Gulp locally in the project:
npm install gulp --save-devFurthermore, we need the following packages:
- gulp-typescript -
npm i gulp-typescript - gulp-sourcemaps -
npm i gulp-sourcemaps - gulp-uglify -
npm i gulp-uglify - browserify -
npm i browserify - vinyl-source-stream -
npm i vinyl-source-stream - vinyl-buffer -
npm i vinyl-buffer
We should install them only as Dev Dependencies. The next step is to define a gulpfile.js where we write all the tasks we want to perform. For hot reloading another method needs to be found at the moment.
The default tasks consist of 2 sub tasks that are chained:
- One performs the
ng buildcommand; - The other builds the minified content script to
dist/content-script.jsfile.
Now we can add the manifest.json file to the src folder of our project. We just create it there and pouplate it with the important parameters. Example given here:
{
"manifest_version": 2,
"name": "Example Expansion Chrome",
"version": "1.0.0",
"permissions": [ "tabs", "activeTab" ],
"background": {
"page": "index.html#/event-page",
"persistent": false
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content-script.js"]
}
],
"browser_action": {
"default_title": "Open Popup!",
"default_popup": "index.html#/popup"
},
"icons": {
"19": "assets/Icon-19.png",
"38": "assets/Icon-38.png"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}Of course we have to register the manifest.json in the angular-cli.json in order to pack it with the dist folder. Therefore, we add it to the assets option:
"assets": [
"assets",
"favicon.ico",
"manifest.json"
],(C) University of applied sciences St.Pölten Austria