Getting Started with NDNts Web Application using webpack

This article shows how to get started with NDNts, Named Data Networking (NDN) libraries for the modern web. In particular, it demonstrates how to write a consumer-only web application that connects to the NDN testbed, transmits a few Interests, and gets responses. This application uses JavaScript programming language and webpack module bundler.

Prepare the System

To use NDNts, you must have Node.js. As of this writing, NDNts works best with Node.js 14.x, and you should install that version. The easiest way to install Node.js is through Node Version Manager (nvm) or Node Version Manager (nvm) for Windows.

On Ubuntu 18.04, you can install nvm and Node.js with the following commands:

$ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
$ nvm install 14
Now using node v14.2.0 (npm v6.14.4)

For this guide, it is not necessary to install ndn-cxx or NFD on your computer.

Now create a directory for our new project, and then open a code editor such as Visual Studio Code at this directory. If you are on Windows, configure VS Code to use Git Bash terminal for better experience.

Install NDNts

NDNts is a group of Node.js packages. Although they have been published to NPM registry, NPM releases happens rather infrequently. Thus, it's best to install nightly builds to obtain the latest features.

To install NDNts nightly builds, create a file package.json in the project directory, and then paste the following content:

{
  "private": true,
  "dependencies": {
    "@ndn/autoconfig": "https://ndnts-nightly.ndn.today/autoconfig.tgz",
    "@ndn/endpoint": "https://ndnts-nightly.ndn.today/endpoint.tgz",
    "@ndn/packet": "https://ndnts-nightly.ndn.today/packet.tgz"
  }
}

Then, type npm install at the terminal. A minute later, all packages and their dependencies are installed automatically.

Install webpack Module Bundler

ndn-js, the predecessor of NDNts, used to publish a pre-compiled ndn.min.js file. Developer can include this file on their webpage with a <script> tag, and then access the exported global objects.

While it is convenient for a quick prototype, this approach has a performance implication: the ndn.min.js file weighs 134KB (after gzip compression) and contains every functionality of ndn-js; browsers have to download the entire file even if the web application only needs a small subset of the functionality. 134KB does not look like a big number, but in web development terms, it is 79% of the 170KB JavaScript budget on a webpage. This increases Time-to-Interactive and negatively affects user experience. As an example, an NDN video player implemented with ndn.min.js and Shaka Player downloads 287KB of JavaScript (gzipped), and received a "65" score from PageSpeed Insights:

Time to Interactive 5.1s

NDNts does not publish NDNts.min.js. Instead, we encourage the use of a static module bundler, to generate a JavaScript bundle that includes only the functionality needed by your application, while stripping away the parts not used by your application.

As a comparison, a similar NDN video player implemented with NDNts and Shaka Player downloads 131KB of JavaScript, and received a "96" score from PageSpeed Insights:

Time to Interactive 2.9s

There are dozens of static module bundlers to choose from, and I have made NDNts work with at least three of them. I found webpack easiest to use.

To install webpack, type the following in the terminal:

$ npm install -D webpack webpack-cli webpack-dev-server
+ webpack-cli@3.3.12
+ webpack-dev-server@3.11.0
+ webpack@4.44.1

package.json will be automatically updated to record the webpack version number.

The HTML Page

The web application we are building today is a simple "ndnping" demo. It allows the user to enter a name prefix, sends four Interests under that prefix, and displays the responses.

Create a directory public, create a file public/index.html inside, and paste the following markup:

<!DOCTYPE html>
<meta charset="utf-8">
<title>NDNts ndnping demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/light.min.css">
<form id="app_form">
  <caption>NDNts ndnping demo</caption>
  <fieldset>
    <label>
      name prefix
      <input id="app_prefix" type="text">
    </label>
    <input id="app_button" type="submit" value="ping" disabled>
  </fieldset>
</form>
<pre id="app_log"></pre>
<script src="bundle.js"></script>

This webpage renders a <form> for the user to enter the name prefix. The submit button is initially disabled, to prevent the user from activating the form before JavaScript is ready. The page also uses Water.css for effortless CSS styling.

At the end of this webpage, a JavaScript file bundle.js is imported. We do not need to write this file ourselves; instead, it will be generated by webpack.

The JavaScript Source Code

Create a directory src, create a file src/main.js inside, and paste the following code:

import { connectToTestbed } from "@ndn/autoconfig";
import { Endpoint } from "@ndn/endpoint";
import { AltUri, Interest, Name } from "@ndn/packet";

async function ping(evt) {
  evt.preventDefault();
  // Disable the submit button during function execution.
  const $button = document.querySelector("#app_button");
  $button.disabled = true;

  try {
    // Construct the name prefix <user-input>+/ping .
    const prefix = new Name(document.querySelector("#app_prefix").value).append("ping");
    const $log = document.querySelector("#app_log");
    $log.textContent = `ping ${AltUri.ofName(prefix)}\n`;

    const endpoint = new Endpoint();
    // Generate a random number as initial sequence number.
    let seqNum = Math.floor(Math.random() * 99999999);
    for (let i = 0; i < 4; ++i) {
      ++seqNum;
      // Construct an Interest with prefix + seqNum.
      const interest = new Interest(prefix.append(`${seqNum}`),
        Interest.MustBeFresh, Interest.Lifetime(1000));
      const t0 = Date.now();
      try {
        // Retrieve Data and compute round-trip time.
        const data = await endpoint.consume(interest);
        const rtt = Date.now() - t0;
        $log.textContent += `\n${AltUri.ofName(data.name)} rtt=${rtt}ms`;
      } catch {
        // Report Data retrieval error.
        $log.textContent += `\n${AltUri.ofName(interest.name)} timeout`;
      }

      // Delay 500ms before sending the next Interest.
      await new Promise((r) => setTimeout(r, 500));
    }
  } finally {
    // Re-enable the submit button.
    $button.disabled = false;
  }
}

async function main() {
  // Connect to the NDN testbed in one line.
  // This function queries the NDN-FCH service, and connects to the nearest testbed router.
  await connectToTestbed();

  // Enable the form after connection was successful.
  document.querySelector("#app_button").disabled = false;
  document.querySelector("#app_form").addEventListener("submit", ping);
}

window.addEventListener("load", main);

The main function is triggered when the webpage finishes loading. It initiates a connection to the NDN testbed, and then enables the submit button on the webpage.

The ping function is triggered when the user clicks the submit button. The submit button is disabled during function execution to prevent conflicts. It constructs four Interests whose names start with the user-specified name prefix, transmits them, and measures round-trip time to display in the on-page log section.

Interests are sent through the Endpoint class. The article Getting Started with NDNts in Node.js introduced a few features of endpoint.consume() function, and they can be used here as well.

Configure and Run webpack-dev-server

Create a file webpack.config.js in the top-level directory, and paste the following:

const path = require("path");

module.exports = {
  entry: "./src/main.js",
  devtool: "source-map",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "public"),
  },
  devServer: {
    contentBase: path.join(__dirname, "public"),
    disableHostCheck: true,
    port: 3333,
  },
};

This configuration instructs webpack to use src/main.js as input, compile and combine its dependencies, and write to public/bundle.js output file.

Then, open package.json and insert this section:

{
  "scripts": {
    "build": "webpack",
    "serve": "webpack-dev-server"
  },
}

After that, start a development web server with this command:

$ npm run serve
i 「wds」: Project is running at http://localhost:3333/
Version: webpack 4.44.1
Time: 1690ms
Built at: 08/29/2020 3:41:38 PM
        Asset     Size  Chunks                   Chunk Names
    bundle.js  736 KiB    main  [emitted]        main
bundle.js.map  763 KiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
i 「wdm」: Compiled successfully.

Now you can open a browser and access the web application at http://localhost:3333/.

NDNts ndnping demo screenshot on a desktop browser

To stop the development web server, press CTRL+C on the terminal.

Production Build

To generate a production build suitable for deployment on a web server, type this command:

$ npm run build
Version: webpack 4.44.1
Time: 1376ms
Built at: 08/29/2020 3:43:05 PM
        Asset      Size  Chunks                   Chunk Names
    bundle.js  89.9 KiB       0  [emitted]        main
bundle.js.map   375 KiB       0  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map

We can see that a public/bundle.js file is generated. The size "89.9 KiB" is before gzip compression; it would be 26KB gzipped.

The public/bundle.js.map file contains debug information. Despite its large size, it is not normally downloaded by the browser and would not affect webpage performance. It is only used when you (or a visitor) opens the browser's developer tools. You may turn off the generation of this file by deleting devtool line in webpack.config.js file.

Deployment

After generating a production build with webpack (npm run build command), the public folder is a static website that can be deployed to any web server. The fastest way to deploy a static website is to use Netlify.

  1. Visit www.netlify.com.
  2. Login with GitHub or another supported account.
  3. Drag the public folder and drop it into Netlify. (if you are in VS Code, right click on the public folder and select "Reveal in File Explorer", then drag from the file explorer)
  4. The website is deployed instantly.

Manual deploys on Netlify

Now you can access the webpage from anywhere, including on mobile devices:

NDNts ndnping demo screenshot on a mobile browser

PageSpeed Insights gave this page a "100" score:

Time to Interactive 1.6s

What's Inside bundle.js

If you are curious enough to open bundle.js, you'll see some completely unreadable mess like this:

!function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]=
{i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.
exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,
{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.
toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),

This is to be expected: JavaScript bundles are optimized for smaller download size and not meant to be human readable.

To gain an insight into what went into bundle.js, you can use source-map-explorer:

$ npx source-map-explorer public/bundle.js
public/bundle.js
  Unable to map 13/92088 bytes (0.01%)

It opens a browser window showing a diagram of the JavaScript modules included in the bundle. We can see that NDNts takes up 48.15KB in this bundle, while the rest belongs to other dependencies.

source-map-explorer diagram of bundle.js

Conclusion

This article is a getting started guide of developing a web application using NDNts libraries and webpack module bundler. If you followed along, you have created a webpage that connects to the NDN testbed and sends "ping" Interests under a user-specified name prefix.

Source code in this article can be downloaded from NDNts-starter repository.

Tags: