Showing posts with label Service Workers. Show all posts
Showing posts with label Service Workers. Show all posts

Monday, November 11, 2024

Cache in JavaScript: Service Workers

 

Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. In this article, I will elucidate the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses.


1. Service Workers API

This technique is used especially to cache static resources (HTML, CSS, etc.) or API responses. It helps enhance the responsiveness and performance of the web application. Please note that ‘service worker’ is a complex topic and it is impossible to cover all its features in this article. That’s why we focus only on the basic features to show how they cache resources and API responses.


The Service Worker API is a script that runs asynchronously and independently of the main browser thread.

- It is a kind of interface between the browser and the network.

- It is executed in the background.

- It handles tasks such as intercepting network requests and caching assets to allow the application to continue working even offline.


2. Tasks of Service Worker API

Let’s summarize the main tasks that service workers can perform when are ready to operate.


Interception of Network Requests: Service workers can intercept network requests and decide to handle them. Service workers can respond with cached resources or search data from the network according to some conditions like the network connection. If it is a poor connection, it responds to the client from cached resources, allowing fast loading and enhancing offline working. 


Background Sync: Service workers can synchronize data in the background, sending data to the server even if the app is not open. For example, they save user input while the connection is unavailable and send the data when the connection is back.


Caching: Service workers cache resources like HTML, CSS, JavaScript, and images. Thus the web app can be loaded faster and even continue working offline.


Push Notifications: Service workers handle push notifications so the web app sends messages to users even if the corresponding web page is closed.


3. Security Considerations

- Since service workers intercept network requests to make decisions, they must operate in a secure environment, which means they can only operate over HTTPS.

- Service workers run in a thread independent of the main thread, so they do not block the normal execution of the user interface.


4. Benefits of Service Workers

- Service workers improve the performance of web applications. Since they cache resources, they significantly reduce server calls, which reduces load times and strain on the server.

- Worker services allow offline access. In other words, users can interact with the application without an internet connection.

- Through push notifications, service workers keep users informed with regular updates.


5. Service Worker Lifecycle

The Service Worker lifecycle is event-driven and consists of several phases that describe how a Service Worker is installed, activated, and managed. The following table summarizes the key phases of a Service Worker's lifecycle. Note that a step can only be executed if the previous step has been completed successfully.

Service Worker Lifecycle

Phase

Definition

Code

Registration

A service worker must be registered by a page before it starts to work.

Method: ServiceWorkerContainer.register()

Download

A service worker is downloaded to the client.


Installation

- The browser installs the Service Worker.

- The install event is triggered.

- You can handle the install event to cache resources or complete an initial setup.

Event: install

Activation

- Once installed, the service worker is activated.

- Old caches are cleaned.

- The service worker gets ready to take control of the application.

Event: activate

Fetch

The service worker can intercept network requests using the fetch event and respond to them from the network or the cache.

Event: fetch


6. Example of Registering a Service Worker

Let’s take an example of how you can cache resources and API responses using the service worker API. For this, we need some back-end code to return data. A straightforward way to do this is using Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use service workers to cache resources and intercept network requests. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/serviceworker-app-blog


Python Code

In the back-end code, we first define ‘employees_datas’ as a list containing a collection of employees, where each dictionary represents data about an employee and includes details such as its ID, first name, last name, and email.


We define some routes to enable the user to access web pages. For instance, to access the root URL of the application, the index function is called, and it renders the ‘index.html’ template file to display the content on the web page. 


We have also a route for ‘employee.html’ file and another one for ‘about.html’ file. Note that in the ‘employee’ function, the ‘render_template’ function renders the specified template file and passes the ‘employees_data’ variable to the template.


@app.route('/')

def index():

    # Render the index.html file

    return render_template('index.html')


@app.route('/employee')

def employee():

    return render_template('employee.html', employees_data=employees_data)


HTML Code

In the front-end code, we define a few HTML files, ‘index.html’ as the main web page, ‘employee.html’ to display some content sent from the back-end, and ‘about.html’. An additional file called ‘layout.html’ will be defined as a common template that will be extended by the other files.


This HTML template provides the structure for a basic web page that uses the Bootstrap library for styling, jQuery for JavaScript functionality, and placeholders for dynamic content through a template engine ‘Jinja’ of the Flask framework.


The {% block swjs %}{% endblock %} block is a placeholder for a service worker script that can be inserted by the template engine only in the ‘index.html’ template.


Inside the ‘navbar’, there are different navigation links to different parts of the website:

- ‘Home’ links to ‘/’, which consists of loading the ‘index.html’ template.

- ‘Employee’ links to ‘/employee’, which consists of loading the ‘employee.html’ template.

- ‘About’ links to ‘/about’, which consists of loading the ‘about.html’ template.


<div class="container"> holds the page’s main content and uses Bootstrap’s container class for responsive layout.

{% block content %}{% endblock %} serves as a placeholder for page-specific content that other templates can insert.


<head>

    ...

    {% block swjs %}{% endblock %}

</head>

<body>

    <nav class="navbar navbar-expand-lg navbar-light bg-primary">

        <a class="navbar-brand" href="/">SW Demo</a>

        <div class="collapse navbar-collapse">

            <ul class="navbar-nav mr-auto">

                <li class="nav-item">

                    <a class="nav-link" href="/">Home</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/employee">Employee</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/about">About</a>

                </li>

            </ul>

        </div>

    </nav>


    <div class="container">

        {% block content %}{% endblock %}

    </div>

</body>

</html>


Within the ‘index.html’ file, we have the following code:


{% extends "layout.html" %}


{% block swjs %}

    <script type="text/javascript" src="/static/js/main.js"></script>

{% endblock %}


{% block content %}

    <h1>Welcome to the Home Page</h1>

    <p>This is the home page of the Service Worker API Demo</p>

{% endblock %}


Let’s explain the main parts of this code:


{% extends "layout.html" %} indicates that this template file extends the ‘layout.html’ file,  meaning it inherits the structure and styling defined in ‘layout.html’.


{% block swjs %} and {% endblock %} define a block named ‘swjs’ where you can insert content specific to JavaScript files. In this case, it includes a script tag that links to the ‘main.js’ file located in the /static/js/ directory.


{% block content %} and {% endblock %}: This defines a block named content where you can insert the main content of the page. In this case, there is an <h1> heading and a <p> paragraph welcoming users to the Home Page of the Service Worker API Demo.


The content of the employee template in the file ‘employee.html’ is suggested as follows:


{% extends "layout.html" %}


{% block content %}

    <h1>Employee List</h1>

    <p>This is the employee List</p>

    <table>

        <thead>

            <tr>

                <th>Firstname</th>

                <th>Lastname</th>

                <th>Email</th>

            </tr>

        </thead>

        <tbody id="myTable">

            {% for item in employees_data %}

                <tr>

                    <td>{{ item.first_name }}</td>

                    <td>{{ item.last_name }}</td>

                    <td>{{ item.email }}</td>

                </tr>

            {% endfor %}

        </tbody>

    </table>

{% endblock %}


The above code is a Jinja template that extends a layout defined in ‘layout.html’. Within the {% block content %}, it creates an Employee List page with a table displaying employee data. The table has columns for Firstname, Lastname, and Email. It then loops through the ‘employees_data’ list and populates the table rows with the corresponding data for each employee’s first name, last name, and email address.


JavaScript Code

In the JavaScript code, we show how to register and use a service worker to cache data. Two JavaScript files are defined. The ‘main.js’ file is defined to register a service worker and the ‘sw.js’ file is used to encapsulate the service worker's behavior.


In the ‘main.js’ file, the code waits for the document to be fully loaded. Once the document is ready, it calls the ‘registerServiceWorker()’ function.


$(document).ready(function(){

    registerServiceWorker();

});


function registerServiceWorker() {

    console.log('Register Service Worker ...')

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register(href = 'sw.js')

        .then(function(registration) {

            console.log('Service Worker registered with scope:', registration.scope);

        })

        .catch(function(error) {

            console.log('Service Worker registration failed:', error);

        });

    }

}


Function ‘registerServiceWorker()’: This function checks if the browser supports service workers by checking if ‘serviceWorker’ is in ‘navigator’. If service workers are supported, it attempts to register a service worker by calling ‘navigator.serviceWorker.register('sw.js')’. If the registration is successful, it logs a message to the console with the service worker’s scope. Note that the scope is the URL path that determines which pages the service worker will control. If the registration fails, it logs an error message to the console.


In the ‘sw.js’ file, we define the main behaviors of the service worker. This consists of handling the ‘install’, ‘activate’, and ‘fetch’ events. Thus we can install and activate the service workers. The ‘fetch’ event handler will intercept network requests to respond to the client from the cache.


First, we need a few global variables like the cache name (‘cache_name’) and the URLs we want to cache (‘urls_to_cache’).


// Cache name

const cache_name = 'sw-1';


// Urls to cache

const urls_to_cache = [

    '/',

    '/employee',

    '/about',

    '/static/css/styles.css',

    '/static/js/main.js',

    '/static/js/employee.js',

];


We add an event listener for the install event in the service worker as follows:


self.addEventListener('install', function(event) {

    console.log('Installing ...');

    event.waitUntil(

        caches.open(cache_name).then(function(cache) {

            console.log('Caching data ...');

            return cache.addAll(urls_to_cache);

        })

    );

}); 


When the service worker is installed, it performs some actions.  It uses ‘event.waitUntil()’ to extend the installation process until the provided promise resolves successfully. This means the service worker will only be considered installed if everything within ‘waitUntil’ (in this case, the caching process) completes without errors. 


It opens a cache (or creates one if it doesn’t exist) with the name specified by ‘cache_name’. Once the cache is opened, it adds all the URLs listed in the ‘urls_to_cache’ array to the cache for offline access. Note that the ‘Self’ word inside the service worker script refers to the Service Worker itself.


The activate event listener triggers when the service worker becomes active. This usually happens after installation and when there are no more active clients using the previous version of the service worker. The following code snippet is a listener for the ‘activate’ event:


// Activate event: Clean up old caches

self.addEventListener('activate', event => {

    console.log('Activating ...');

    const cacheList = [cache_name];

    event.waitUntil(

        caches.keys().then(cacheNames => {

            return Promise.all(

                cacheNames.map(cName => {

                    if (!cacheList.includes(cName)) {

                        console.log('Deleting old cache:', cName);

                        return caches.delete(cName);

                    }

                })

            );

        })

    );

});


When a service worker is activated, it means it is ready to take control of the page and handle caching and other tasks. Let’s explain what the code does. This code defines an array ‘cacheList’ containing the value of ‘cache_name’. This array specifies the caches to keep. Only the caches in this array will be kept, the others will be deleted.


It uses ‘event.waitUntil(...)’ to delay the activation until all old caches are deleted. This ensures a clean start with only the necessary caches.  Inside ‘event.waitUntil(...)’, it calls caches.keys() to get a list of all the cache names. It then uses ‘Promise.all(...)’ to map over each cache name and check if it's in the ‘cacheList’. If a cache name is not in the ‘cacheList’, it logs the cache to be deleted to the console and deletes that cache using ‘caches.delete(cName)’.


We now develop a common event listener that intercepts the ‘fetch’ event, which is triggered whenever a network request is made.


self.addEventListener("fetch", (event) => {

    console.log('Fetching data ...');

    // Intercept the network request to handle it

    event.respondWith(

        (async () => {

        // Search the response first in a cache

        let cache = await caches.open(cache_name);

        let cachedResponse = await cache.match(event.request);

        console.log("cachedResponse:", cachedResponse)


        // Return a response if it is found in the cache

        if (cachedResponse) return cachedResponse;


        // If no response is already cached, use the network

        return fetch(event.request);

    })(),

    );

});


After logging 'Fetching data ...' to the console, we call the ‘event.respondWith()’ method to intercept the network request and handle it. Within the ‘respondWith()’ method, we first try to find a response in a cache by opening a cache with the name ‘cache_name’ and attempting to match the request with the cached response. If a cached response is found, we return the cached response. If no cached response is found, we make a network request using fetch (‘event.request’).


7. Execution of the Example

When you run this example on the server, you get the following UI:

Visit all the pages and then stop the server. You will see that the application continues to work offline and the pages are showing normally.

Saturday, October 5, 2024

Cache in JavaScript



Caching is a technique used to store data in a temporary storage area called a cache. Caching aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. Thus, caching helps improve the performance of multiple applications, including web applications. Caching helps decrease the time required to access data. JavaScript offers various possibilities for performing data caching or resources for a short or long time. In this article, I want to present how you can cache your data or resources in JavaScript. I explain how to choose the appropriate caching possibility according to your needs.


1. Common Caching Techniques in JavaScript

JavaScript offers a variety of caching techniques, each suited to specific situations. This may depend on the volume of data you want to cache, the nature of the data, and how long you need to persist your data. Here are the most common techniques to cache data in JavaScript.


- Web Storage API (localStorage and sessionStorage): This technique is the best fit to cache small pieces of data like default or user preferences values for search inputs or selected items for drop-down lists. Web Storage API is a technique to store data using the concept of key-value pair. It includes two types of storage,  localStorage and sessionStorage. LocalStorage is used to cache data permanently. However, sessionStorage allows data to be stored only for a session until the browser is closed.


- In-memory JavaScript Objects: It is used to persist data for a short time, like the result of API calls in a single session. The data will be deleted from the cache after the page refreshes. For example, if you have a dropdown list and every time you select an item (employee), you make an Ajax call to retrieve their data (birth date, email, etc.) from the backend. It will be better to cache this data. Every time you come back in the same single session to select an employee you have already selected, your code will fetch the data from the cache. It is more efficient, faster, and without loading the server.


- Service Workers: This technique is used especially to cache static resources (HTML, CSS, etc.) or API responses. It helps enhance the responsiveness and performance of the web application.


- IndexedDB: It is a low-level API to store large amounts of data, which is good for complex applications that need to cache large JSON responses or images. Thus, it allows working offline.


In this article, we through in detail the first technique and leave the others for other articles.


2. Comparison between Caching Methods

Here is a comparison table between the four above techniques. We consider in our comparison the main criteria such as data persistence time, storage capacity, use cases, the type of data that can be cached, and performance.

Technique

Web Storage API (localStorage/sessionStorag)

In-memory JavaScript Objects

Service Workers Cache API

IndexedDB

Data Persistence

- Persistent for localStorage

- Temporary for sessionStorage (only for a session)

Temporary (only when page is active)

Persistent (even after closing the page)

Persistent (even after closing the page)

Storage Capacity




- Limited and depends on the browser

- Usually 5-10 MB

Limited to available memory

- Depends on the browser

- Commonly larger than Web Storage

- Virtually unlimited

- Limits set by the browser

Data Structure Support


- String only

- Objects need manual serialization to be cached

Any data structure

Requests and responses (for assets like HTML, CSS, JS, etc.)

Complex data structures

Use Case

Storing small amounts of data like user preferences or settings

Caching data like the result of API calls for fast access in a single page load

Caching static elements like HTML, CSS, JS for offline use or to speed up loading

Storing large datasets like a client-side database,

large JSON responses, or images

Performance

Fast read/write for small data

Fastest data reading/writing technique because data is stored in memory

Slower than in-memory

Slower than in-memory or localStorage because it caches a large amount of data


3. Web Storage API (LocalStorage and SessionStorage)

LocalStorage and sessionStorage are used to cache a small amount of data. They store data in the form of key-value pairs. SessionStorage keeps data only for the current session. In other words, it saves data until you close the browser or tab. However, localStorage keeps data for a long time, even if you close the browser.


3.1. Main Properties and Methods

In the table below you will find the principal localStorage properties and methods you need to know to cache data. Note that sessionStorage has the same properties and methods.


LocalStorage Properties

Property

Definition

Syntax

length

Returns the count of the key-value pairs in the localStorage.

localStorage.length

LocalStorage Methods

Method

Definition

Syntax

clear

Removes all key-value pairs in storage.

localStorage.clear()

getItem

- Retrieve the value corresponding to the key.

- If the key does not exist, it returns null.

- If the cached value is a serialized object, you can convert it from a string to a JavaScript object using JSON.parse.

localStorage.getItem(‘key’)

setItem

- Stores a key-value pair.

- key and value are stored as strings.

- to store an object, you should convert it to a string using JSON.stringify.

localStorage.setItem(‘key’, value)

removeItem

Removes the key-value pair from the storage.

localStorage.removeItem(‘key’)

key

- Returns the name of the key at a specific index in the storage.

- localStorage saves key-value pairs in the order they were inserted.

localStorage.key(keyindex)



3.2. Example of Caching Data Using LocalStorage

Let’s take an example to show how to use localStorage to cache data. We are developing a small web application with Python's Flask framework for the backend and HTML, CSS, and JavaScript for the frontend.


Note. We only write the relevant part here to explain how to use the Web Storage API to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/webstorage-app-blog


Python Code

In the backend code, we first define ‘countries’ as a list containing a collection of dictionaries, where each dictionary represents a country and includes details such as its ID, name, capital, population, area, and dialing code.


countries = [

    {'Id': 10, 'Name': 'Austria', 'Capital': 'Vienna', 'Population': '9M', 'Area': '83,879 km2', 'Dialing_code': '+43'},

    {'Id': 20, 'Name': 'Germany', 'Capital': 'Berlin', 'Population': '84M', 'Area': '357,592 km2',

     'Dialing_code': '+49'},

    {'Id': 30, 'Name': 'France', 'Capital': 'Paris', 'Population': '67M', 'Area': '551,695 km2', 'Dialing_code': '+33'},

    {'Id': 40, 'Name': 'Italy', 'Capital': 'Rome', 'Population': '58M', 'Area': '301,230 km2', 'Dialing_code': '+39'},

    {'Id': 50, 'Name': 'Spain', 'Capital': 'Madrid', 'Population': '47M', 'Area': '505,990 km2', 'Dialing_code': '+34'},

    {'Id': 60, 'Name': 'England', 'Capital': 'London', 'Population': '56M', 'Area': '130,279 km2',

     'Dialing_code': '+44'},

]


In this Python code, we define two methods that we explain as follows:


Method ‘load_country_detail’: To return these details in response to an AJAX POST request, we define a Flask route in /load_country_detail. It fetches the JSON data from the request, extracts the country ID, and searches for the corresponding country details in the predefined list of countries using the ‘search’ method. Finally, it returns the country details as a JSON response.


Method ‘search’: The search method takes two parameters, ‘ct_id’ and ‘ls’. It searches for a country in the ‘ls’ list whose ‘id’ matches the provided ‘ct_id’. If a match is found, it returns the details of that country as a dictionary. If no match is found, it returns None.


@app.route('/load_country_detail', methods=['POST'])

def load_country_detail():

    # Get JSON data from request

    data = request.get_json()


    country_id = data.get('countryId')

    country_detail = search(int(country_id), countries)

    print("Found country detail: ", country_detail)


    # Send a JSON response back to the client

    return jsonify({

        'status': 'success',

        'country_detail': country_detail

    })


def search(ct_id, ls):

    result = [element for element in ls if element['Id'] == ct_id]

    if len(result) > 0:

        return result[0]

    return None


HTML Code

In the frontend code, we define an HTML file with some elements like a dropdown list containing the list of some European countries and a table to display the details of each country when the user selects it.


<body>

    <div class="container">

        <h1>List of European Countries</h1>

        <select id="selectCountry" class="select-country" onchange="onChangeCountry()" size="1">

            <option value="10">Austria</option>

            <option value="20">Germany</option>

            <option value="30">France</option>

            <option value="40">Italy</option>

            <option value="50">Spain</option>

            <option value="60">England</option>

        </select>


        <br><br><br><br>

        <div id="country_detail">

            <table id="country_table">

                <thead>

                    <tr>

                        <th>Country Name</th>

                        <th>Capital</th>

                        <th>Population</th>

                        <th>Area</th>

                        <th>Dialing Code</th>

                    </tr>

                </thead>

                <tbody>

                </tbody>

            </table>

        </div>

    </div>

</body>


JavaScript Code

In the JavaScript code, we implement the code of the ‘onChangeCountry()’ and other behaviors to demonstrate the usage of localStorage as a data caching mechanism. In summary, the code handles the loading, storing, and displaying country details. Here's a breakdown of the key functions.


We add a handler to the ‘DOMContentLoaded’ event to ensure that the JavaScript program starts once the HTML page is completely loaded and the elements are created. Within this handler, we call the ‘loadCountry()’ function.


document.addEventListener('DOMContentLoaded', function() {

   loadCountry();

}, false);


Function ‘loadCountry()’: This function retrieves the country name value from the browser’s localStorage, where the data can be stored persistently even after a page reload. If the element does not exist, the ‘storedCountry’ will be null.

If ‘storedCountry’ is not null, the function sets the drop-down value to the stored value. This ensures that when the page reloads, the drop-down will display the previously selected country.


function loadCountry() {

  let storedCountry = localStorage.getItem("country");

  console.log("Value of country in localStorage is:", storedCountry)

  console.log(document.getElementById("selectCountry").value)

  if (storedCountry){

     document.getElementById("selectCountry").value = storedCountry;

  } else {

     localStorage.setItem("country", document.getElementById("selectCountry").value);

  }

  onChangeCountry();

}


Function ‘onChangeCountry()’: This function is called when the selected country is changed. It updates the localStorage with the value of the selected country. Here we use the string ‘country’ as a key to save the country ID in the localStorage.

Then the function fetches the data for the selected country from localStorage using the country ID. If the data is found, we need to convert it to an object using the built-in JavaScript function ‘JSON.parse()’. This function takes a JSON string and converts it into a JavaScript object. If the country data is not found, the ‘getCountryDetail()’ function will be called.


function onChangeCountry(){

    let selValue = document.getElementById("selectCountry").value;

    console.log("Store the value:", selValue, " in localStorage")

    localStorage.setItem("country", document.getElementById("selectCountry").value);


    let id = document.getElementById("selectCountry").value;

    let country_detail = localStorage.getItem(id);

    console.log("country_detail:", country_detail);

    if (country_detail){

        showCountryDetail(JSON.parse(country_detail));

    } else {

        getCountryDetail(document.getElementById("selectCountry").value);

    }

}


Function ‘getCountryDetail(countryId)’: This function sends a POST request to '/load_country_detail' endpoint with the selected country ID, retrieves the country details, stores them in localStorage, and updates the country details table. To cache an object in localStorage, we use the country ID as a key, and then we need to serialize the object using the built-in function ‘JSON.stringify()’ which converts the object to a string.


function getCountryDetail(countryId) {

    const dataToSend = {

        countryId: countryId

    };


    // Sending a POST request using fetch

    fetch('/load_country_detail', {

        method: 'POST',

        headers: {

            'Content-Type': 'application/json'

        },

        body: JSON.stringify(dataToSend)

    })

    .then(response => response.json())

    .then(data => {

        console.log('Success:', data);

        localStorage.setItem(data.country_detail["Id"], JSON.stringify(data.country_detail));

        showCountryDetail(data.country_detail);

    })

    .catch((error) => {

        console.error('Error:', error);

    });

}


Function ‘showCountryDetail(country_detail)’: This function dynamically creates a row in the country details table with information such as country name, capital, population, area, and dialing code. Let’s note that we create only one row every time we insert country data. So, we clear the table’s body before adding the new row.


Function ‘findRow(tBody, country_name)’: This function checks if a row with the given country name already exists in the table to avoid duplicates.


Function ‘addCell(tr, item)’: This function adds a cell with a given content to a table row.


When you execute this code and you opened the browser, you find the following interface:




4. Web Storage API in Chrome DevTools

Open Chrome DevTools and go to the Application tab. Here you can manage localStorage and sessionStorage. You can clear them. You can also edit, delete any key-value pair, or look at what is inside. This is very useful for debugging your web application.








Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...