Lighthouse Scanner: Frontend Development with PlainJS
Tags: javascript, tutorial
Published: 2020-06-15 16:41:51

With my Lighthouse-as-a-Service website scanner you can quickly check a webpage about its performance, SEO and best practices. You can use the scanner here: **[https://lighthouse.admantium.com/](https://lighthouse.admantium.com/)**.

In this article, I describe the frontend part, specifically the scanner page. It uses the [Lighthouse](https://developers.google.com/web/tools/lighthouse) tool for which I provide a HTTP API using the nodejs framework [Hapi](https://hapi.dev/). I will discuss the requirements, and then explain why I don't need a framework, but will use PlainJS[^1] that runs in the browser. Afterwards, I will explain the necessary features of PlainJS and show how they are applied for the lighthouse scanner.

_This article originally appeared at [my blog](https://admantium.com/blog/light03_frontend_with_plain_js/)_.

## Lighthouse Webpage Requirements

Considering the requirements, it seems that the amount of interactivity is minimal.

You enter the webpage in the scanner URL field, then click the scan button. 

![Lighthouse Scanner Homepage](https://admantium.com/images/blog/light/light03_scanner_homepage.png)

The frontend calls the backend with `/scan?url=URL`, and as outlined in the previous article, receives these status codes:

- `202` Job is starting
- `400` No URL or invalid URL is given
- `429` No workers available

From here on, the frontend uses simple long polling to periodically request the `/jobs?id=ID` API endpoint until the job is complete or stopped with an error.

- `200` job is finished and successful
- `202` job is running
- `404` job id is unknown
- `409` job is finished but failed

In essence, the frontend requires these features:

- Request different backend endpoints
- Show messages (success, errors)
- Periodically request job status
- During scanning, disable scan button and scan bar
- When the job is finished: enable scan button and scan bar
- When the job is successful: render download link

So, should I use a frontend framework like [React](https://reactjs.org/) or [Vue](https://vuejs.org/)? The number of features does not seem to be too complex, and I don’t see much additional features in future iterations of the webpage. A framework adds considerable load and CPU processing time to your page[^2]. Therefore, I decided to implement these features with PlainJS.

## PlainJS Basics

PlainJS is client-side JS that runs in your browser. Its inherently different from NodeJS because it exposes APIs for interacting with the client, such as manipulating the DOM, controlling the browser behavior with the `window` object, or device APIs to display notifications. For a good introduction to Web APIs, refer to the [Mozilla MDN documentation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Introduction).

In our cases, we are interested in the APIs that allow us to manipulate the DOM, how to define event listeners, and how to load data from a service.

## DOM Manipulation

The DOM is a tree-like structure, starting with the root `document` at the root, continuing with nodes like `body`, `h1` or `p`, to finally arrive at the leaves, which is the text value of HTML elements. DOM manipulation techniques inherit this tree-like structure and expose methods for selecting, adding and deleting nodes.

So, lets discuss this in the context of showing a message.

The scanner box is a `<form>` consisting of a `<label>`, the `<input>` field for the url, an `<input>` button to start a scan, and an empty `<div>` to show a message.

```html
<form class="form" id="search-form">
  <label for="URL">URL</label>
  <input type="text" id="search-form-input" placeholder="http://example.com">
  <input type="button" value="scan" id="search-form-button"></div>
  <div id="search-form-msg-box"></div>
</form>
```

Now, when we want to display a message, we select this node, and then set its inner HTML to display a message. Selecting a node is done with the `document.getElementById()` function[^3]. The value of a node is an attribute, so we can set any HTML with `innerHTML`.

```js
function setMsg(msg) {
  const msg_box = document.getElementById('search-form-msg-box');
  msg_box.innerHTML = `<span class="text-green-500 font-medium">${msg}</span>`
}
```

Let’s see this in action:

![light03_show_message.png](https://admantium.com/images/blog/light/light03_show_message.png)

To remove the message, we simple set the inner html to an empty string:

```js
function removeMsg() {
  msg_box.innerHTML = '';
}
```

## Event Listeners

When we want to show a message conditionally, for example when clicking a button, we need to work with event listeners attached to DOM nodes. The methods `addEventListener` receives two arguments:

- Event: The name of the event for which the trigger is defined, see this [list of event triggers](https://developer.mozilla.org/en-US/docs/Web/Events)
- Function: The functions that gets executed

As an example, to show the message `Scan started` when the search button is clicked, we define the event listeners as follows:

```js
document
  .getElementById('search-form-button')
  .addEventListener('click', setMsg('Scan started'));
```

Similarly, when the form is submitted, we call the same function

```js
document
  .getElementById('search-form')
  .onsubmit = setMsg('Scan started'));
```

## API Requests with Fetch

When the scan button is pressed, the value of the search bar will be determined and we send a request to the backend API. PlainJS provides the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make asynchronous calls to other URLs.

If you have worked with [Axios](https://www.npmjs.com/package/axios) or [Request](https://www.npmjs.com/package/request), you will see similarities: A `fetch()` call consists of the target `url` and an optional object with the `method`, `headers` and the `body`.

Here is an example to post a JSON payload to the `/api` endpoint:

```js
fetch('/api', {
  method: 'POST',
  headers: {
    "Content-type": "application/json"
  },
  body: '{"url":"http://example.org"}'
})
```

The fetch request returns a promise. There are two patterns to work with the return values of promises:

- Using `.then` chains to process the results, and `.catch` for error cases.
- Using `await` to get the results, and wrap the function call in a `try ... catch` block

I prefer the second option because it increases code readability. In the following example you can see this style applied. In line 5, we start the fetch call, and in line 6, we `await` the result. In the next lines, we check the response code, and display a corresponding message.

```js
async function requestScan(event) {
  try {
    const targetUrl = input.value;

    const response = await fetch(`${backend_server}/scan?url=${targetUrl}`);
    const body = await response.json();

    if ([400, 429].includes(response.status)) {
      setMsg(default_error_message);
    }

    if ([202].includes(response.status)) {
      setMsg(default_success_message);
    }
  } catch (e) {
    setMsg(default_error_message);
    enableSearchBar()
  }
}
```

Those are all the necessary features we use. Now let’s discuss how to structure this code.

## Code Structure

The code structure is, well, very basic: One big file, global DOM selectors at the top, global variables, and then functions for activating or deactivating the visual effects. Event listeners are attached with an `init()` method. Here is a rough outline.

```js
const form = document.getElementById(...)
const input = document.getElementById(...)
const button = document.getElementById(...)
const msg_box = document.getElementById(...)

//...

function setMsg(msg, success = false) { ... }
function removeMsg() { ... }
function enableSearchBar() { ... }
function disableSearchBar() { ... }

//...

function pollJobStatus { ... }
async function pollUntilReportDone { ... }

//...

function init() { ... }
window.onload = init;
```

I honestly don’t have much finesse in structuring, and seeing the code caps at 100 lines, it feels ok.

## Conclusion

This article showed how to use PlainJS for developing the frontend webpage of the lighthouse scanner. The runtime environment of PlainJS is your browser. It provides a set of WebAPIs, from which I showed (a) DOM API for selecting, adding and removing nodes, (b)  Fetch API for asynchrony calls to other URLs, and (c) Event API to execute events based on frontend interaction.

PlainJS feels basic, especially in how close to DOM you are and how to structure the code. It fulfills the requirements of the frontend features. I'm surprised that the logic for displaying success and error messages, shown a loading bar, and making periodic requests to an API in about 100 lines of code.

## Footnotes

[^1]: Read this [great article](https://code-boxx.com/check-javascript-version/) to understand why it’s not possible to pinpoint the JavaScript version that you are using in your browser.

[^2]: See how different JavaScript frameworks impact the loading and CPU processing time in this [report](https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/).

[^3]: You can use other selectors, including tag names or CSS as well.