Dojo and its AMD loader provide outstanding tools for structuring a Web application on the client-side. However, the notion of “writing a JavaScript application” has widened in definition over the past few years with the increased popularity of Node.js. Though Dojo can be used in a Node.js environment with the AMD module pattern, other key frameworks have gained prominence in the server-side JavaScript space, including Express, Flatiron, Sails.js and the Dojo Foundation’s very own Persevere. These frameworks give structure and handle common tasks such as routing, template rendering, and content negotiation. Still, since most operations on a Node.js server are asynchronous, server-side JavaScript can be a complex, treacherous mess of callbacks. Enter Koa, a Node.js framework that attempts to save us from this callback hell by using ECMAScript 2015 Generators. Using Dojo on the client-side and Koa on the server-side makes for a robust, clean, and expressive application. In this post, we’ll explain what generators are and how to use Koa with Dojo for ultimate code cleanliness.
Gena-what?
Generators are an exciting part ES2015. In fact, some browsers don’t support them yet. Despite their currently-lacking browser support, generators are exciting because of their power and ability to aid in how asynchronous code is written. Before understanding what a generator is and why they are useful, let’s first understand how callbacks become unmanageable quickly.
Callback hell
Let’s define a simple async function for example purposes:
function async(delay, done) {
setTimeout(done, delay);
}
This code asynchronously waits for a specified amount of time then executes a callback function. Calling this function is just like calling any other asynchronous function, such as making a request – you provide a callback function:
// ...
async(1000, function () {
console.log('done!');
});
So far so good. But say we want to call this function ten consecutive times; quickly, the code gets complex and ugly, as we’d end up with ten nested callbacks:
// ...
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
async(1000, function () {
console.log('done!');
});
});
});
});
});
});
});
});
});
});
Generators to the rescue!
A generator is a fancy type of iterator: it has a next
method that accepts one argument and returns {done, value}
tuples and it also has a throw
method. Generators allow functions to be “exited” and then “re-entered” at a later time, with state persisted across each re-entrance:
function* oneToThree() {
yield 1;
yield 2;
yield 3;
}
var iterator = oneToThree();
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
// '1'
// '2'
// '3'
The yield
keyword is like a “pause and return” flow control statement. When the next
method is repeatedly called, the generator is partially executed, advancing through the code until a yield
keyword is hit. This simple yet powerful API can be used to provide an alternative to traditional nested callbacks. Let’s revisit our async example from earlier with ten nested callbacks and write it using generators instead:
function run(generatorFunction) {
var iterator = generatorFunction(resume);
function resume(val) { iterator.next(val); }
iterator.next()
}
run(function* asyncExample(resume) {
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield async(1000), resume);
console.log(yield 'done!');
});
This code will execute exactly the same as our nested callback code from hell. Note that we wrapped our async function calls in a generator function called asyncExample
. We also wrote a special function, run
, that executes a generator function in an iterative manner. This wrapper generator function and runner function together allow us to ditch the callback ugliness.
Koa, front and center
Because generators help us avoid callback hell in an asynchronous environment, they make our server-side JavaScript much cleaner. Koa is a Node.js framework designed specifically for this purpose: to clean up server-side JavaScript code by eliminating callbacks. This also makes error handling much easier as an added bonus.
A Koa server is an Application object containing an array of middleware, each of which is a generator function. Let’s look at a server that responds with “Hello World” for every request, adapted from the Koa website:
var koa = require('koa');
var app = koa();
// response
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
Each middleware has a Context via this
that allows you to access the Request and Response objects as well as get/set other information. In this example, we set the response body to ‘Hello World’.
Koa middleware functions cascade in a very expressive, elegant manner, something that was hard to accomplish with Express and non-generator frameworks. Based on the order in which middleware functions are registered, control is yielded “downstream” then flows back “upstream” before returning to the client. For example, let’s add a x-response-time middleware to our hello world server:
var koa = require('koa'),
app = koa();
// x-response-time
app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
// response
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3000);
Before responding with the expected ‘Hello World’, the request flows through our x-response-time middleware: the start time is saved, control is yielded to the response middleware, then control flows back to the response time function to pick up where it left off, setting the X-Response-Time header using the stored start time.
Dojo + Koa = clean code
Let’s walk through a simple example application that uses Dojo on the client-side and Koa on the server-side. We’ll create a form that allows a user to enter his or her name and submit it; the name is then persisted in a MongoDB instance. Here is what it will look like when we are done:
First, let’s start with what we know: Dojo on the client-side. We’ll create a index.html
HTML page that includes Dojo and references our application JavaScript file (via dojoConfig
.)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Dojo, Koa, and Mono
<link rel="stylesheet" href="app.css?1">
</head>
<body>
<div class="content">
<form action="/users/add" method="post">
<input type="text" name="name" placeholder="Enter your name..." autocomplete="off">
<button type="submit" id="addButton">Add
</form>
<div id="users"></div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/dojo/1.10.4/dojo/dojo.js" data-dojo-config="deps:['./app.js']">
</body>
</html>
Next, let’s create an app.js
file alongside our index page. This file needs to do one thing: request all users so we can list them on the index page. We’ll use dojo/request
to make the XHR request and dojo/dom-construct
to create an element for each persisted user.
define([
'dojo/request',
'dojo/dom-construct'
], function (request, domConstruct) {
document.querySelector('[name="name"]').focus();
request('/users', {handleAs: 'json'}).then(function (users) {
var userList = document.querySelector('#users');
users.forEach(function (user) {
domConstruct.create('div', {
innerHTML: user.name
}, userList, 'first');
});
});
});
At this point, we have a complete client: we have an HTML page with a form with a method
set to “/users/add” and some JavaScript that requests users from “/users”. All that’s left is to create a server to service these two requests. And of course, let’s use Koa. Let’s create a server.js
alongside the index page and application JavaScript:
var serve = require('koa-static'),
route = require('koa-route'),
parse = require('co-body'),
monk = require('monk'),
wrap = require('co-monk'),
db = monk('localhost/koa-dojo'),
users = wrap(db.get('users')),
koa = require('koa'),
app = koa();
Except for the MongoDB dependency, these are all different koa components, as it is architected extremely modularly. We are requiring middleware for routing, body parsing, etc. The usage of these modules will be clear once we fill in the rest of the file. Next, we need to serve our application and set up our routes:
// ...
// Serve the application code statically
app.use(serve('.'));
// Allow users to be added or filtered via an API
app.use(route.post('/users/add', addUser));
app.use(route.get('/users', filterUsers));
Above, we’re using Koa’s provided static middleware to serve our application. Next, we use Koa’s provided route middleware to set up two routes: a POST to ‘users/add’ and a GET to ‘/users’, the two routes our client will make requests to. Lastly, we need to define these two routes and start the server:
//...
function *addUser() {
var user = yield parse(this);
yield users.insert(user);
this.redirect('/');
}
function *filterUsers() {
this.body = yield users.find({});
}
app.listen(3000);
console.log('listening on port 3000');
Here is where the magic happens. First, note how the addUser
and filterUsers
functions are actually generator functions, meaning we can use yield
within them. Additionally, note how we are using monk, a MongoDB wrapper that supports generators. Inserting a user, which normally would involve a database operation with nested callback, is a one line operation; the same is true for finding a user and sending these results to the client.
With that, our demo application is complete. We have a simple Dojo client that lists users and allows users to be added, and we have a simple generator-powered Koa server that handles these requests.
Conclusion
As Web developers, we inherently enjoy writing JavaScript, so Node.js’s increased popularity is a good thing. While Express and other libraries have served a great purpose to date, libraries such as Koa that make use of new language features as they become available will allow more elegant and clear coding. Dojo works well with such a library and can easily serve as a robust client in this client-server architecture.
Learning more
Want to learn how to use modern ES2015 or TypeScript features in your application? Looking for advice on how to architect an application that leverages JavaScript or TypeScript on both the client-side and server-side, based on your specific functional and technical requirements? We’re happy to help. Contact us for a free 30 minute consultation to discuss your application.