May 16, 2017

Building a Web Browser Using Electron

by Jscrambler

Building a Web Browser Using Electron

In this tutorial, we will show you how to build a simple web browser using Electron HTML5 techniques.

We will be using 'vanilla' JavaScript in this tutorial. If you are looking for a deeper dive into Electron using React check out our previous blog post: Building an Expense Application with Electron and React.

What Is Electron?

Electron is a framework that you can use to build desktop applications for Windows, MacOS, and Linux.

It was developed by Github to power their code editor Atom, and later used by Microsoft to build their increasingly popular editor VSCode. On its backend, Electron uses Node.js and, on its frontend, Chromium is used to render applications. Writing code in Electron is similar to writing code in a browser since you can use all the DOM API’s but with all the Node.js API's available as well.

Why Should You Use Electron?

One Size Fits All

Since Electron uses the same Chromium version on each platform to render your app, you won’t have to worry about cross-browser compatibility when writing CSS. The same goes for JavaScript, if a new spec is available in the version of Chromium which Electron uses, you can use it without having to transpile it back to ES5. As of writing, most of the new ES6 features are available to use in Electron. In desktop development terms, this is even better than writing native applications for each platform as you don't have to write different code for every operating system.

Faster Development

Development using Electron is faster, easier and less expensive than writing normal desktop apps. Electron uses JavaScript, which is currently the most popular programming language in the world according to Stack Overflow. JavaScript is fairly easy to learn and there are a lot of developers who know the language already from using it on the web. Using JavaScript also means you can reuse a whole bunch of code that you've used for your website and, on top of that, you have also the JavaScript community on your side, including npm, the largest package manager in the world.

Getting Started

Before you can start, you need to make sure that Electron works on your OS. Electron is available for Windows, Mac, and, Linux but it can be a pain to install. Since this tutorial is about coding and not styling, we've already made a template that you can use for this tutorial. It is essentially a copy of Electron-quickstart with an added template. You can find the code along with instructions on how to install it on Github.

We are going to write code in the renderer.js file. The code in main.js is part of Electron-quickstart and creates our window and adds a few best practices, but nothing more. As you can see index.html has already been setup with the structure of the web browser we'll be building. Our renderer.js file is also being required at the bottom of the page.

Like we said before, in our renderer.js file we can access both the DOM and Node.js API's. It might seem a little confusing at first, but utilizing the power of both together is pretty awesome. Hence we can require this JavaScript file instead of using a script tag.


Webview

In order to render pages we are going to use a webview. A web view is similar to an iframe but it has a range of APIs that you can access and it runs in a completely separate process than our page. Its APIs can be accessed like any DOM elements methods and it even emits events.


The first thing we need to do is require the Node.js libraries we're going to use, some of these are from npm and others are internal. If you followed the instructions correctly, they should have been automatically installed so you have nothing to worry about. We also want to add a little function to save some time selecting elements from the DOM, and then put those elements into variables using this function.

var ById = function (id) {  
    return document.getElementById(id);
}
var jsonfile = require('jsonfile');  
var favicon = require('favicon-getter').default;  
var path = require('path');  
var uuid = require('uuid');  
var bookmarks = path.join(__dirname, 'bookmarks.json');

var back = ById('back'),  
    forward = ById('forward'),
    refresh = ById('refresh'),
    omni = ById('url'),
    dev = ById('console'),
    fave = ById('fave'),
    list = ById('list'),
    popup = ById('fave-popup'),
    view = ById('view');

This code doesn't do anything for our applications UI. By the way, you run it by either typing npm start or electron . into the terminal.
We’ve also declared a variable with a path to a JSON file. We'll be using this file to store bookmarks although we could also have used HTML5 storage, but for this example, it’s a lot more interesting to use some Node.js features.

Right now, the UI is totally unresponsive, you can't event reload the page. We're going to change that by writing some functions that use the webview APIs to manipulate the page.

function reloadView () {  
    view.reload();
}

function backView () {  
    view.goBack();
}

function forwardView () {  
    view.goForward();
}

These are just some simple functions that will allow the user to reload the page and go backward and forward. We'll hook this up to the UI later on.

Next is a function that handles the input from the URL bar. This isn't as easy as it sounds since we have to format the URL first to make sure that the user won't be getting any errors. A page request in a webview needs to have the correct protocol in front of it, so if the user doesn't add it himself we have to make sure it is added. We've written this function below to deal with that.

function updateURL (event) {  
    if (event.keyCode === 13) {
        omni.blur();
        let val = omni.value;
        let https = val.slice(0, 8).toLowerCase();
        let http = val.slice(0, 7).toLowerCase();
        if (https === 'https://') {
            view.loadURL(val);
        } else if (http === 'http://') {
            view.loadURL(val);
        } else {
        view.loadURL('http://'+ val);
        }
    }
}

Just like the function we wrote above, this one will also be added to an event listener later on. We're basically checking to see if the user has added a protocol in front of their URL and, if not, adding HTTP in front of it. Yes, HTTPS is a better protocol but we don't know if the accessed website supports HTTPS, and the webview will be redirected to the HTTPS version anyway if the site does support HTTPS.

Bookmarks

To make this tutorial a little more interesting we are also going to build a very simple bookmarking system. The user interface for this has already been created, although some of it is hidden by default. First, we're going to create a constructor for a bookmark and add a method to it that returns an element that we can easily add to the DOM.

var Bookmark = function (id, url, faviconUrl, title) {  
    this.id = id;
    this.url = url;
    this.icon = faviconUrl;
    this.title = title;
}

Bookmark.prototype.ELEMENT = function () {  
    var a_tag = document.createElement('a');
    a_tag.href = this.url;
    a_tag.className = 'link';
    a_tag.textContent = this.title;
    var favimage = document.createElement('img');
    favimage.src = this.icon;
    favimage.className = 'favicon';
    a_tag.insertBefore(favimage, a_tag.childNodes[0]);
    return a_tag;
}

Apart from the obvious title and URL, our bookmarks will also get a unique ID and a link to the favicon for the site. We will be using Node.js libraries to get the ID and the link to the favicon. The code for this is located below, this function is triggered when a user clicks on the star next to the URL bar.

function addBookmark () {  
    let url = view.src;
    let title = view.getTitle();
    favicon(url).then(function(fav) {
        let book = new Bookmark(uuid.v1(), url, fav, title);
        jsonfile.readFile(bookmarks, function(err, curr) {
            curr.push(book);
            jsonfile.writeFile(bookmarks, curr, function (err) {
            })
        })
    })
}

First, we get the URL and the title of the current page from the webview. Then we fetch the favicon using a simple library that we added at the start of the code. This then returns a link to the favicon for the current page. After we've gotten the favicon we'll create a new bookmark using our previously created constructor, also passing in a unique time-based ID which is generated using the uuid library for Node.js.

After all this is done, we're getting our current JSON file first, then adding our new bookmark and saving the updated version again. This is all done using the jsonfile Node.js library.

Being able to save bookmarks is useless if you can't view them. Next, we're adding a function that will open up a popup when the user clicks on the list icon next to the star icon in the URL bar.

function openPopUp (event) {  
    let state = popup.getAttribute('data-state');
    if (state === 'closed') {
        popup.innerHTML = '';
        jsonfile.readFile(bookmarks, function(err, obj) {
            if(obj.length !== 0) {
                for (var i = 0; i < obj.length; i++) {
                    let url = obj[i].url;
                    let icon = obj[i].icon;
                    let id = obj[i].id;
                    let title = obj[i].title;
                    let bookmark = new Bookmark(id, url, icon, title);
                    let el = bookmark.ELEMENT();
                    popup.appendChild(el);
                }
            }
                popup.style.display = 'block';
                popup.setAttribute('data-state', 'open');
        });
    } else {
        popup.style.display = 'none';
        popup.setAttribute('data-state', 'closed');
    }
}

The state of the popup is controlled through a HTML5 data attribute on the element. If the popup is already open the popup will simply be closed by clicking on the icon again. However, if the popup is closed we'll first clear the HTML inside of it, in case a user already opened it before and added new bookmarks at that time. Then we read our JSON file and loop through the bookmarks, turning them back into Bookmark objects and then getting the premade HTML before adding them to the popup. Before we can hook everything up with event listeners there are three more functions required.

function handleUrl (event) {  
    if (event.target.className === 'link') {
        event.preventDefault();
        view.loadURL(event.target.href);
    } else if (event.target.className === 'favicon') {
        event.preventDefault();
        view.loadURL(event.target.parentElement.href);
    }
}

function handleDevtools () {  
    if (view.isDevToolsOpened()) {
        view.closeDevTools();
    } else {
        view.openDevTools();
    }
}

function updateNav (event) {  
    omni.value = view.src;
}

The first function takes care of quite a large issue. When a user clicks on a link in the bookmarks popup he is taken to that page, but not in the webview. The entire app will navigate to the link since the popup is part of the app and not the webview. To fix this, we have created this function which deals with this, canceling the event and then loading the URL in question in our webview. The second function is just a simple function that will open or close the dev tools for the webview depending on their current state. Last is the update of the nav function, which adds the URL that is actually in a webview to the input, since this usually changes quite a bit after a page has finished loading.

All Together

We can now add these functions to the correct event listeners. Everything should be working properly. You can refresh the page, navigate through history, go to a new site and even save a bookmark. Note that newly added bookmarks might take a few seconds to show up since the favicon has to be fetched first.

refresh.addEventListener('click', reloadView);  
back.addEventListener('click', backView);  
forward.addEventListener('click', forwardView);  
omni.addEventListener('keydown', updateURL);  
fave.addEventListener('click', addBookmark);  
list.addEventListener('click', openPopUp);  
popup.addEventListener('click', handleUrl);  
dev.addEventListener('click', handleDevtools);  
view.addEventListener('did-finish-load', updateNav);  

As you can see, the webview also emits events, one of which we are using to know when we can update the URL bar. All of the other events are just clicks, except the one for the URL bar in which we only want to load the page if the user hits enter. We check if the user pressed enter and not another key within the function that handles the event.

Were to go from here?

Although our browser now has some basic features, it is still missing a lot. Here is a list of things we could consider to add:
* Feedback when adding a bookmark. (alert) * Feedback when loading a page. (a spinner) * Functionality to remove a bookmark. * Google Search from the URL bar.

And if you really want to challenge yourself, try these:
* Tabs * A Download Manager

Conclusion

If you want to learn more about Electron you can find the full documentation here. Looking for a finished version of this tutorial? You can find that here. Remember that such as any other JavaScript code, you can protect this browser or specific functionalities you design with Jscrambler, making sure that your browser can’t be tampered by using Self Defending, and obfuscating each functionality with transformations such as Control Flow Flattening, Function Reordering, Identifiers Renaming, and many others. You can test these transformations in Jscrambler’s playground application.