The Web Security Topics Series

Modules

[...]

Creating and Using Modules

Modules are stored in files, with a single module per file. A module is just some JavaScript code. The module mechanism ensures that any variables that are declared inside the module are inaccessible from outside it. To make an object available outside the module, it must be explicitly exported by assigning it to the exports property of a special variable called module. The data hiding is done by wrapping the module in a closure, but the module system does this transparently, so you do not need to be concerned about the implementation.

If a program needs to use a module, it must call the require function, passing it a path name that points to the file in which the module is stored. The value returned by this call to require is the object that was assigned to module.exports in the file.

[...]

The object that was used as a prototype for counters in Listing 14 in the previous chapter could usefully be made into a module. This can be done by storing the code shown in Listing 23 in a file, which might be called counter-module.js.

Listing 23

module.exports = {

  setUp: function(max, min) {
    // ...
  },

  next: function() { 
    // ...
  },

  previous: function() { 
    // ...
  },

  rewind: function() {
    // ...
  },

  value: function() {
    // ...
  }  
}

The object literal comprising the five methods that implement the counter is assigned to module.exports instead of being stored in a variable as before. (Some programmers prefer to assign it to a variable first and then assign the value of that variable to module.exports at the bottom of the file, so that it is clear what is being exported. This is just a stylistic preference as there is no advantage either way.)

Any program that needs to use counters can load this module and use the result exactly like any other object. It can be assigned to a variable (and objects imported from modules usually are) and used to create other objects, as shown in Listing 24, or its methods may be called.

Listing 24

var counter = require('counter-module')

var randomAcessCounter = Object.create(counter, {
  goTo: {
    // ...
  }  
})

Clearly, being able to use modules is much more convenient and maintainable than having to copy and paste the code of reused objects into every program that uses them.

[...]

Listing 25 shows how a module for inserting commas or points into long numbers could export the two functions described at the end of the chapter on Functions.

Listing 25###

var applyToInteger = function (f, n) {
      // ...
}

var insertSeparators = function (separator, n) {
      // ...
}

var curry = function(f) {
      // ...
}

var insertSeparatorsFunction = curry(insertSeparators)

module.exports.insertCommas = insertSeparatorsFunction(',')

module.exports.insertPoints = insertSeparatorsFunction('.')

The auxiliary functions applyToInteger, insertSeparators and curry are not exported, so there is no way of accessing them from outside the module, but the two exported functions are available by way of the object returned by require, as in Listing 26.

Listing 26

var separators = require('separators-module'),
    insertCommas = separators.insertCommas,
    insertPoints = separators.insertPoints

Now insertCommas and insertPoints can be used just as before. There is no requirement that the names of the variables used to hold the imported functions should be the same as the property names in the exported module, but they often are.

[...]

A module can export a function that takes arguments and returns a value – an object or another function – that depends on the value of the argument. When the module is required, this function can be called immediately with an argument.

As an example, suppose that instead of exporting insertCommas and insertPoints, as in Listing 25, separators-module exports the function previously assigned to insertSeparatorsFunction. That is, the last three lines of code in Listing 25 are replaced by

module.exports = curry(insertSeparators)

The value of require('separators-module') would then be that function, so the following code would make sense, and assign the expected function to insertCommas.

var insertCommas = require('separators-module)(',')

Most often, when this technique is used, the module consists entirely of the assignment of a function taking an argument to module.exports. Somewhere in the body of the function, an object with methods is returned. Since this object’s properties are closures they can refer to the argument to the module. The module can provide some abstract functions. When an object is passed into the module, these functions may become specific operations on that object.

To make this vague description more concrete, consider the module in Listing 27, which implements a map function that applies some function to each value in a sequence, and returns an array of the results.

Listing 27

module.exports = function(obj) {
  return {
    map: function(f) {
      if (obj.isEmpty()) return []
      var maps = function(o) {
        if (o.atEnd())
          return [f(o.value)]
        else
          return [f(o.value)].concat(maps(o.next()))
      }
      return maps(obj.rewind())
    }
  }
}

Each value is generated by calling the value property of the object passed into the module; the object’s next member is called to move through the sequence, and isEmpty and atEnd are used to identify the special case of an empty sequence, and to determine when the process should terminate. An auxiliary function called maps has been used to perform the actual iteration recursively. A while loop could be used instead.

The map function will work on any object that has a value property and next, isEmpty and atEnd methods. It might be used to map a function on a sequence of messages retrieved from a queue or records retrieved from a database, provided the methods are synchronous. (An asynchronous version is left as an exercise.)

As Listing 28 shows, it’s easy to add the missing pair of methods to the protoCounter prototype from the previous chapter.

Listing 28

var protoCounter = {

  // setUp, next, previous, rewind as before

  atEnd: function() {
    return this._current == this._max
  },

  isEmpty: function() {
    this._max < this._min
  }
}

// add value property as before

The value returned by isEmpty should always be false, because setUp throws an error unless there is at least one possible value. However, since it is possible to assign to _min and _max, it is necessary to perform the test.

With these added methods, it is possible to create objects that can be passed into the module that exports map. If this module is held in a file called iterator.js, a small counter could be created and passed to the module in the manner shown in Listing 29.

Listing 29

var c = Object.create(protoCounter)
c.setUp(4)
var c_iterator = require('iterator')(c)

The object assigned to c_iterator has a map method that applies its argument, a function, to each element of the sequence generated by c, so c_iterator.map(function(n) { return 2*n }) would be equal to [2, 4, 6, 8].

[...]


End of Extract


[Extracted from Javascript on the Server Using Node.js and Express by Nigel Chapman and Jenny Chapman]