Single-Page Apps

Single-page apps are websites that extensively use JavaScript to construct the user interface of a website. The HTML file for such an application is typically a bare-bones skeleton, really just a place to link in JavaScript and CSS files.

Creating a Node Project

Let's consider this format by building an application for helping manage a SCRUM-like software development process. We'll start by creating a new Node application, Scrumtastic, using npm init at the terminal:

> mkdir scrumtastic > cd scrumtastic > npm init

We create the directory for our project, navigate to that directory, and then run npm init. The init command will launch a brief wizard that asks us questions about our project - what it is named, what the version should be, where the directory is located, what license it is released under, etc. Feel free to use the defaults. It creates a package.json file in our directory, which should look something like:

{ "name": "scrumtastic", "version": "1.0.0", "description": "A web app for SCRUM software development", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/zombiepaladin/scrumtastic.git" }, "keywords": [ "SCRUM", "software", "development", "task", "board" ], "author": "Nathan H. Bean", "license": "MIT", "bugs": { "url": "https://github.com/zombiepaladin/scrumtastic/issues" }, "homepage": "https://github.com/zombiepaladin/scrumtastic#readme", "dependencies": {} }

Installing Dependencies

One of the more important roles the package.json file plays is keeping track of dependencies of our project. These are node packages created by third parties (i.e. other developers) that we wish to use. SQLite3, which we used in the database lesson is one of these. Let's install it now:

> npm install sqlite3 --save --save-exact

This will place the sqlite3 library in the _nodemodules directory, and additionally, it will add a line in our package.json dependencies list:

{ ... "dependencies": { "sqlite3": "3.1.8" } }

By including the library in our dependency list, we can install the dependencies on any future machines we want to deploy our software to, simply by running the command npm install. This is because we typically won't include dependencies in our repository. We can make sure of this by adding the line:

node_modules

to our .gitignore file. We don't include dependencies for several reasons: 1) they make our project unnecessarily large, 2) we may need to install different versions of dependencies for different operating systems, and 3) sometimes license terms of a dependency preclude them from being bundled in other software.

To ensure that every platform we are deploying on uses the same version of a dependency library, we use the --save-exact flag when we install it.

The Index Page

Next we want to create our "single" page, index.html. For a single-page app, this is typically a static page that links to JavaScript and CSS files. So our Scrumtastic index page might look like:

<!doctype html> <html> <head> <title>Scrumtastic</title> <link href="/scrumtastic.css" type="text/css" rel="stylesheet"/> </head> <body> <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> <script src="/scrumtastic.js" type="text/javascript"></script> </body> </html>

As you can see, it's little more than a bare-bones webpage linking to app.css, app.js, and JQuery hosted on a Content Delivery Network.

We'll want to serve index.html, scrumtastic.js, and scrumtastic.css from our web server. Since they're all static files, we can cache them and serve them much like we have before. In fact, we can place them in a public directory and utilize our static fileserver middleware to serve everything in that directory:

/** @module fileserver * loads and serves static files */ module.exports = { loadDir: loadDir, isCached: isCached, serveFile: serveFile } var files = {}; var fs = require('fs'); function loadDir(directory){ var items = fs.readdirSync(directory); items.forEach(function(item) { var path = directory + '/' + item; var stats = fs.statSync(path); if(stats.isFile()) { var parts = path.split('.'); var extension = parts[parts.length-1]; var type = 'application/octet-stream'; switch(extension) { case 'html': case 'htm': type = 'text/html'; break; case 'css': type = 'text/css'; break; case 'js': type = 'text/javascript'; break; case 'jpeg': case 'jpg': type = 'image/jpeg'; break; case 'gif': case 'png': case 'bmp': case 'tiff': case 'svg': type = 'image/' + extension; break; } files[path] = { contentType: type, data: fs.readFileSync(path) }; } if(stats.isDirectory()){ loadDir(path); } }); } function isCached(path) { return files[path] != undefined; } function serveFile(path, req, res) { res.statusCode = 200; res.setHeader('Content-Type', files[path].contentType); res.end(files[path].data); }

This middleware needs to be initialized and run from our server code, which typically goes in a file named server.js:

"use strict"; var PORT = 3000; var http = require('http'); var fileserver = require('./lib/fileserver'); // Cache static directory in the fileserver fileserver.loadDir('public'); var server = new http.Server(function(req, res) { // Remove the leading '/' from the resource url var resource = req.url.slice(1); // If no resource is requested, serve the cached index page. if(resource == '') fileserver.serveFile('public/index.html', req, res); // If the resource is cached in the fileserver, serve it else if(fileserver.isCached(resource)) fileserver.serveFile(resource, req, res); // Otherwise, serve a 404 error else { res.statusCode = 404; res.statusMessage = "Resource not found"; res.end("Resource not found"); } }); // Launch the server server.listen(PORT, function(){ console.log("listening on port " + PORT); });

We can now serve our single page, and the associated JavaScript and CSS files (both of which are currently empty). This may not seem like much, but it's an important first step in creating our single-page app.