CAPÍTULO III: RESULTADOS Y DISCUSIÓN
3. IMPORTANCIA DE DISTINTOS ASPECTOS DE LA E.D EN EL DÍA A DÍA DE LOS CENTROS SEGOVIANOS
In Node.js, and in general in JavaScript, there are only a few libraries supporting promises out-of-the-box. Most of the time, in fact, we have to convert a typical callback-based function into one that returns a promise; this is also known as promisification.
Fortunately, the callback conventions used in Node.js allow us to create a reusable function that we can utilize to promisify any Node.js style API. We can do this easily by using the constructor of the Promise object. Let's then create a new function called promisify() and include it into the utilities.js module (so we can use it later in
our web spider application):
var Promise = require('bluebird');
module.exports.promisify = function(callbackBasedApi) { return function promisified() {
var args = [].slice.call(arguments);
return new Promise(function(resolve, reject) { //[1] args.push(function(err, result) { //[2] if(err) { return reject(err); //[3] } if(arguments.length <= 2) { //[4] resolve(result); } else { resolve([].slice.call(arguments, 1));
});
callbackBasedApi.apply(null, args); //[5] });
} };
The preceding function returns another function called promisified(),
which represents the promisified version of the callbackBasedApi given in
the input. This is how it works:
1. The promisified() function creates a new promise using the Promise
constructor and immediately returns it back to the caller.
2. In the function passed to the Promise constructor, we make sure to pass to callbackBasedApi, a special callback. As we know that the callback always
comes last, we simply append it to the argument list (args) provided to the promisified() function.
3. In the special callback, if we receive an error, we immediately reject the promise.
4. If no error is received, we resolve the promise with a value or an array of values, depending on how many results are passed to the callback. 5. At last, we simply invoke the callbackBasedApi with the list of
arguments we have built.
Most of the promise implementations already provide, out-of-the-box, some sort of helper to convert a Node.js style API to one returning a promise. For example, Q has Q.denodeify() and Q.nbind(),
Bluebird has Promise.promisify(), and When.js has node.lift().
Sequential execution
After a little bit of necessary theory, we are now ready to convert our web spider application to use promises. Let's start directly from version 2, the one downloading in sequence the links of a web page.
In the spider.js module, the very first step required is to load our promises
implementation (we will use it later) and promisify the callback-based functions that we plan to use:
var Promise = require('bluebird'); var utilities = require('./utilities');
var mkdirp = utilities.promisify(require('mkdirp')); var fs = require('fs');
var readFile = utilities.promisify(fs.readFile); var writeFile = utilities.promisify(fs.writeFile); Now, we can start converting the download() function:
function download(url, filename) { console.log('Downloading ' + url); var body; return request(url) .then(function(results) { body = results[1]; return mkdirp(path.dirname(filename)); }) .then(function() {
return writeFile(filename, body); })
.then(function() {
console.log('Downloaded and saved: ' + url); return body;
}); }
We can see straightaway how elegant some sequential code implemented with promises is; we simply have an intuitive chain of then() functions. The final
return value of the download() function is the promise returned by the last then() invocation in the chain. This makes sure that the caller receives a promise that fulfills with body only after all the operations (request, mkdirp, writeFile) have completed.
Next, it's the turn of the spider() function:
function spider(url, nesting) {
var filename = utilities.urlToFilename(url); return readFile(filename, 'utf8')
.then(
function(body) {
return spiderLinks(url, body, nesting); }, function(err) { if(err.code !== 'ENOENT') { throw err; }
.then(function(body) {
return spiderLinks(url, body, nesting); });
} ); }
The important thing to notice here is that we also registered an onRejected()
function for the promise returned by readFile(), to handle the case when a page was not already downloaded (file does not exist). Also, it's interesting to see how we were able to use throw to propagate the error from within the handler.
Now that we have converted our spider() function as well, we can modify its main
invocation as follows: spider(process.argv[2], 1) .then(function() { console.log('Download complete'); }) .catch(function(err) { console.log(err); });
Note how we used, for the first time, the syntactic sugar catch to handle any error
situation originated from the spider() function. If we look again at all the code we
have written so far in this section, we would be pleasantly surprised by the fact that we didn't include any error propagation logic like we would be forced to do by using callbacks. This is clearly an enormous advantage, as it greatly reduces the boilerplate in our code and the chances of missing any asynchronous error.
Now, the only missing bit to complete the version 2 of our web spider application is the spiderLinks() function, which we are going to see in a moment.
Sequential iteration
The web spider code so far was mainly an overview of what promises are and how they are used, demonstrating how simple and elegant it is to implement a sequential execution flow using promises. However, the code we considered so far involves only the execution of a known set of asynchronous operations. So, the missing piece that will complete our exploration of sequential execution flows is to see how we can implement an iteration using promises. Again, the spiderLinks() function of web
Let's add the missing piece to the code we wrote so far: function spiderLinks(currentUrl, body, nesting) { var promise = Promise.resolve(); //[1] if(nesting === 0) {
return promise; }
var links = utilities.getPageLinks(currentUrl, body); links.forEach(function(link) { //[2]
promise = promise.then(function() { return spider(link, nesting - 1); });
});
return promise; }
To iterate asynchronously over all the links of a web page, we had to dynamically build a chain of promises:
1. First, we defined an "empty" promise, resolving to undefined. This promise
is just used as a starting point to build our chain.
2. Then, in a loop, we update the promise variable with a new promise
obtained by invoking then() on the previous promise in the chain.
This is actually our asynchronous iteration pattern using promises.
This way, at the end of the loop, the promise variable will contain the promise of the
last then() invocation in the loop, so it will resolve only when all the promises in the
chain have been resolved.
With this, we completely converted our web spider version 2 to use promises. We should now be able to try it out again.