Статьи

Node.js для программистов на PHP # 2: модули, пакеты и клубничный домик

PHP знает, как группировать классы в общем пространстве имен и создавать распространяемые пакеты, используя PEAR. Node.js также очень хорош в организации кода в модульные и многоразовые наборы, но есть и ключевые отличия. Я объясню их в ближайшее время, но прежде чем я хотел бы поговорить о куклах.

Клубничный-песочное-Playset-ягодно-кафе

Проблема с куклой

Моей 4-летней дочери недавно предложили подержанный кукольный набор, в который входят 4 куклы, различная одежда, мебель, пони и клубничный домик. Она любит одевать и раздевать куклы, и я рада видеть, как она учится делать это идеально — намного лучше, чем она умеет одеваться сама.

Проблема в том, что она не знает, как убрать одежду, когда она заканчивает играть, точно так же, как она оставляет свой шарф, свитер и кепку на полу, когда она возвращается домой. Так что моя грустная обязанность убрать кукольную одежду в клубничный дом, и это боль. Видите ли, каждый предмет одежды должен быть аккуратно сложен и убран в определенное место, иначе «это не хорошо». Таким образом, розовые носки должны идти в шкаф в спальне в клубничном домике, а не в розовое платье — нет, этот должен входить в сундук.

Если хотите мое мнение, складывать и складывать кукольную одежду настолько скучно, что эту задачу следует поручить компьютеру. Моя дочь, вероятно, никогда этого не поймет, но я уверен, что вы поймете.

Включая скрипты в PHP и Node.js

PHP knows how to categorize classes using namespaces. Here is an example:

<?php
// in house/bedroom/closet/sock.php
namespace House\Bedroom\Closet;

class Sock
{
  protected static $acceptedColors = array('pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia');
  protected $color;

  public function __construct($color)
  {
    if (!in_array($color, self::$acceptedColors)) {
      throw new Exception('Only Pink color and the likes are valid socks colors');
    }
    $this->color = $color;
  }

  public function fold()
  {
    // some code here
  }
}

PHP namespaces are a very simple feature. At compile time, the PHP parser prefixes all the class names in the file using the declared namespace. So Sock (the class name) becomes House\Bedroom\Closet\Sock (the fully qualified class name) — it’s just string manipulation. It’s enough to prevent name collision, but it often makes the code harder to read than it really should:

<?php
require __DIR__ . '/house/bedroom/closet/sock.php';
$mySock = new House\Bedroom\Closet\Sock('pink');

The Node.js equivalent of the Sock class declarations is straightforward. In JavaScript, classes are functions, and methods are properties of the function object’s prototype. This may be disturbing at first, but you will quickly get used to it.

// in house/bedroom/closet/sock.js
var accepted_colors = ['pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia'];
var Sock = function(color) {
  if (accepted_colors.indexOf(color) == -1) {
    throw 'Only Pink color and the likes are valid socks colors';
  }
  this.color = color;
};
Sock.prototype.fold = function() {
  // some code here
}
exports.Sock = Sock;

The last line is more specific to Node.js. Node provides a global object called exports. The script adds the newly defined Sock class as a property of this exports object. In practice, this last line «exports» the class to the outside, and defines the public API of the script.

To import this exports object into another variable, Node provides the require() function:

var sock = require('./house/bedroom/closet/sock.js');
var my_sock = new sock.Sock('pink');

Although the code looks similar to the PHP version, what happens here is a little different. Node’s require() function does not just load and execute the socks.js code directly. Instead, it returns a copy of the exports object defined in the socks.js script.

Tip: Node doesn’t need the .js ending in the file path passed as parameter to require(). Node finds the script even if you just call require('./house/bedroom/closet/sock'). The suffix will be omitted in further examples.

require Is The Gate To The Module System

To be fair, exports is not really a global object. It’s local to a Node module. And what’s a module? In short, it’s a closure. Here is what the call to require actually does behind the curtain:

var addSockToExports = function(exports) {
  var accepted_colors = ['pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia'];
  var Sock = function(color) {
    if (accepted_colors.indexOf(color) == -1) {
      throw 'Only Pink color and the likes are valid socks colors';
    }
    this.color = color;
  };
  Sock.prototype.fold = function() {
    // some code here
  }
  exports.Sock = Sock;
}
var exports = {};
addSockToExports(exports);
return exports;

require encapsulates the sock.js code into a closure, and executes it using the exports object as a parameter. That’s why exports is said to be a pseudoglobal: it is seen as a global variable from within a module, but in fact it is local to this module. So all the variables defined in a module (like accepted_colors and Sock) are isolated and don’t pollute the global namespace. Only the properties added to the exports parameter make it to the outside:

console.dir(require('./house/bedroom/closet/sock.js'));
// { Sock: [Function] }

The accepted_colors variable is therefore private and cannot be accessed by any outside script that requires the sock module. You can imagine how this can be used to create private classes, a concept nowhere to be found in PHP.

Node.js isn’t at the origin of the module concept. It’s part of an initiative called CommonJS, and the CommonJS «module» feature has several implementations. Node.js offers one of them, adds some syntactic sugar and a few bonus features.

Modules Are Smart Objects

There is one major difference between the PHP and the JavaScript versions of the doll socks example. In JavaScript, sock is just the name of the variable storing the require() return value. This «namespace» is nowhere to be found in the original sock.js script. In other terms, in JavaScript it’s the caller who adds the namespace, while in PHP the callee must be namespaced.

That makes aliasing trivial in Node:

var stocking = require('./house/bedroom/closet/sock.js');
var my_sock = new stocking.Sock('pink');

This is the equivalent of the use ... as statement in PHP:

<?php
require __DIR__ . '/house/bedroom/closet/sock.php';
use House\Bedroom\Closet\Sock as Stocking;
$mySock = new Stocking\Sock('pink');

By the way, if a module exports just a single class, you may want to use an alternative exporting method called module.exports:

// in house/bedroom/closet/sock.js
var accepted_colors = ['pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia'];
var Sock = function(color) {
  if (accepted_colors.indexOf(color) == -1) {
    throw 'Only Pink color and the likes are valid socks colors';
  }
  this.color = color;
};
Sock.prototype.fold = function() {
  // some code here
}
module.exports = Sock;  //  <= this line changed

// in main script
var Sock = require('./house/bedroom/closet/sock.js');
var my_sock = new Sock('pink');

Where does that module object come from? Just like exports, it is passed to the module as a pseudoglobal. To be completely fair, the result of a call to require('sock') looks more like the following:

var module = { exports: {} };
(function(module, exports){
  var accepted_colors = ['pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia'];
  var Sock = function(color) {
    if (accepted_colors.indexOf(color) == -1) {
      throw 'Only Pink color and the likes are valid socks colors';
    }
    this.color = color;
  };
  Sock.prototype.fold = function() {
    // some code here
  }
  module.exports = Sock;
})(module, module.exports);
return module.exports;

Yes, that’s an anonymous function calling itself. For an average PHP developer, this may be looking like voodoo, but the truth is to be found farther East.

Node’s Modules Are Matryoshka Dolls

You know these Russian nesting dolls called Matryoshka dolls? Well, Node modules are a bit like that. Small modules can nest inside larger modules:

// in house/bedroom/closet/sock.js
var accepted_colors = ['pink', 'rose', 'lavender', 'purple', 'bubble-gum', 'fuchsia'];
var Sock = function(color) {
  if (accepted_colors.indexOf(color) == -1) {
    throw 'Only Pink color and the likes are valid socks colors';
  }
  this.color = color;
};
Sock.prototype.fold = function() {
  // some code here
}
module.exports = Sock;

// in house/entrance/chest/shoe.js
var Shoe = function() {};
Shoe.prototype.fold = function() {
  // some code here
}
module.exports = Shoe;

// in house/index.js
module.exports = {
  Sock: require('./bedroom/closet/sock'),
  Shoe: require('./entrance/chest/shoe');
}

// in main script
var clothes = require('./house'); // loads house/index.js
var sock = new clothes.Sock('pink');
var shoe = new clothes.Shoe();

You can require a module and reexport it. And since the require() function is in fact a pseudoglobal, it executes in the context (and using the file path) of the current module. That makes nesting of modules extremely easy, without necessarily using long fully qualified class names as in PHP. Besides, it keeps the code readable, while PHP code following the PSR-0 standard is harder to read due to namespaces matching the directory structure.

There is much more to tell about Node’s module system. But doll clothes need to be put away. Node.js module documentation is a must read if you really need to know more.

The Doll House

Once every cloth is neatly folded at the right place, the strawberry doll house can be closed. It has a nice handle, so I could easily give the strawberry house to another kid to freak their dad out. The same goes for libraries: you can package several scripts together, and distribute the result across your company, or the Internet.

PHP’s doll house market is called PEAR. It should be the default distribution method used by library developers, but it’s more and more replaced by git clone or composer. That’s probably because PEAR has many drawbacks. First, it installs libraries user- (or system-) wide, which means that all projects must share the same version of a given library. Second, creating PEAR packages is a pain due to a very verbose XML package description format. Third, publishing packages is not an easy task (having your own package registered on the main PEAR channel is heavy, and hosting your own channel is heavy, too). Last, many PEAR packages have not been updated for a while — that’s not the preferred package registry anymore.

Node, on the other hand, comes with a very powerful Node Package Manager. npm is both a distribution utility and the central repository for Node public packages. Check the list of available packages (about 7000 packages available as of writing this post) in the node registry.

If you want to publish your own package for other developers using npm, you don’t need anybody’s permission, you don’t even need to describe all the files in your package. You just need a simple package.json at the root of the package looking like the following:

{
  "name": "strawberry-house",
  "version": "0.0.1",
  "description": "A doll house complete with dolls, clothes, and a pony",
  "author": "Francois Zaninotto"
}

The npm JSON package description format allows for much more metadata, but these four lines are enough to publish the package to the node public registry. To do so, just call:

npm publish .

Of course, you can also choose to publish a package only to your own private registry.

To install a public node package, just call npm install:

npm install my-little-pony

This downloads and installs the my-little-pony package, together with all its dependencies, under the node_modules/ subdirectory, under the current path.

Even better, if you define your own list of dependencies in the package.json file, npm will download and install all of them for you when you call:

npm install

Multiple Modules and Package versions

The greatest thing about both the module and package system of Node.js is that it solves version conflicts in the most elegant way: by allowing several versions of a module or package in the same application.

Let’s say you use the strawberry-house package AND the girl-toys packages in a project. girl-toys itselfs depends on the strawberry-house package, and there are chances that it doesn’t require the same version than the one you’re using directly.

It doesn’t matter: npm will install the girl-toys dependencies in its own directory, and let you use your own version:

app.js                   // your own app
node_modules/
  strawberry-house/      // the one you use directly
  girl-toys/
    node_modules/
      strawberry-house/  // the one used by girl-toys
      my-little-pony/

With Node’s encapsulated module system, you’ll never have to worry about version conflicts anymore.

Conclusion

I learned how to fold doll clothes, I’m sure you’ll learn how to use Node’s powerful module system. Both are easy, and programmers tend to have more fun with the latter. As compared to the equivalent features in PHP, Node modules and packages feel very refreshing. That’s probably why the Node community is so active, and the number of available packages is growing so rapidly.