Node.js 的模块系统和模式

Load modules

JavaScript 的 global namespace 和 Node.js CommonJS modules 标准

JavaScript: bad feature Node.js: No global namespace, using modules

modules and modules pattern

文件和模块一对一

传入模块标识字符串

var http = require(‘http’)

也可以指定其中的一种对象,而不是所有对象

var spawn = require(‘child_process’).spawn

Node 本身的模块,或是在 node_modules 下面的模块,可以直接使用 module 的 identifier

否则需要使用 / 指定路径

require(‘./mymodule’); 或者全路径
require(‘/path/to/mymodule’);

扩展名可以是 .js, .node, .json

模块加载过程

http://nodejs.org/api/modules.html

Core Modules are preloaded when a Node process starts

模块循环引用的问题

http://nodejs.org/api/modules.html

require

原来 require 返回的就是 module.exports 对象 NODE_PATH 环境变量

require is synchronous

As a consequence, any assignment to module.exports must be synchronous as well. For example, the following code is incorrect:

setTimeout(function() {
    module.exports = function() { ... };
}, 100);

The resolving algorithm

dependency hell

require.resolve 的完整解析过程

模块的导出:exports

导出一个 function constructor 导出N个functions and/or variables

导出 object/class 导出 instance

单次加载的问题

导出对象的封装

var Hello = require(‘./singleobject’).Hello

模块的导出:module.exports

module.exports vs exports

The variable exports is just a reference to the initial value of module.exports.

This means that we can only attach new properties to the object referenced by the exports variable, as shown in the follwoing code:

exports.hello = function () {
    console.log('Hello');
}

Reassigning the exports variable doesn’t have any effect, becasue it doesn’t change the contents of module.exports, it will only reassign the variable itself. The following code is therefore wrong:

exports = function () {
    console.log('Hello');
}

If we want to export something other than an object literal, as form example a function, an instance, or even a string, we have to reassign module.exports as follows:

module.exports = function() {
    console.log('Hello');
}

作为 class 导出

The module cache

模块复用:node_modules

package.json
git
npm init
npm adduser
npm publish

本地测试:npm install . -g

测试和发布自己的 Module

Summary

Node 抛弃了 JavaScript 默认的全局命名空间,反而使用了 CommonJS 的模块来代替。这让我们可以更好地组织代码,从而避免了安全性问题和bug。你可以使用 require() 来加载一个 core module,一个第三方的 module,或者你自己的一个来自文件或文件夹的 module。

你可以使用相对或者绝对文件路径来加载一个非核心 module。同样你也可以通过名称来加载 module,如果你把它放到了 node_modules 目录下,或者是使用 NPM 安装的。

你也可以导出自己的对象(函数,属性)等作为 module API,来创建自己的 module。

Module definition patterns

模块系统,除了作为加载依赖的机制之外,还是一个用来做定义API的工具。跟其他 API 设计的相关问题一样,主要因素是考虑私有功能和公开功能的平衡。目标是最大化实现信息隐藏以及API的可用性,同时还要平衡其他软件质量,像可扩展性以及代码复用。

Named exports

导出公开 API 的最基本的方法是采用 named exports,主要就是把我们希望公开的所有值 都赋给由 exports(或者module.exports)所引用的那个对象。在这种方式下,最终所导出的对象就变成了一个一套相关功能的容器或者命名空间。

// logger.js
exports.info = function (message) {
    console.log('info: ' + message);
};

exports.verbose = function (message) {
    console.log('verbose: ' + message);
};

导出的函数然后就在加载的模块中作为属性可以使用,如下:

// main.js
var logger = require('./logger');
logger.info('This is an informational message');
logger.verbose('This is a verbose message');

大多数 Node.js 的核心模块都采用这种模式。

Exporting a function

把整个 module.exports 变量重新赋给一个函数也是最常见的模块定义模式之一。只导出一个函数为模块提供了一个非常清晰的入口,理解和使用起来都非常容易,非常符合 ”’small surface area”’原则。这种定义模式在社区中也被称为 substack pattern(James Halliday 的昵称是substack)。

//logger.js
module.exports = function (message) {
    console.log('info: ' + message);
};

这种模式还可以扩展成把导出的函数作为其他公开API的命名空间。在提供给模块一个清晰的单一入口的同时,还能够让我们导出其他二级的或者更加高级用例的功能。下面代码就演示了如何使用导出的函数作为命名空间,来扩展我们之前定义的模块:

module.exports.verbose = function (message) {
    console.log('verbose: ' + message);
};

下面代码演示的是如何使用我们刚刚定义的模块:

// main.js
var logger = require('./logger');
logger('This is an informational message');
logger.verbose('This is a verbose message');

尽管只导出一个函数看起来可能会比较局限,但实际上这是一个把重点放在单一功能上非常好的方法,什么是对这个模块最重要的。其他次要的就作为导出功能的属性。

Pattern(substack): expose the main functionality of a module by exporting only one function. Use the exported function as namespace to expose any auxiliary functionality.

Exporting a constructor

一个模块导出了一个 constructor,是一个模块导出了函数的一个特例。区别在于使用这种新的模式,我们能够让用户使用这个 constructor 创建一个新的实例,但同时我们还给他们提供了扩展原型和打造新类的能力。

// logger.js
function Logger(name) {
    this.name = name;
};
Logger.prototype.log = function (message) {
    console.log('[' + this.name + ']' + message);
};
Logger.prototype.info = function (message) {
    this.log('info: ' + message);
};
Logger.prototype.verbose = function (message) {
    this.log('verbose: ' + message);
};
module.exports = Logger;

我们可以像下面这样使用上面的模块:

var Logger = require('./logger');
var dbLogger = new Logger('DB');
dbLogger.info('This is an information message');
var accessLogger = new Logger('ACCESS');
accessLogger.verbose('This is a verbose message');

跟 substack 模式相比,导出一个 constructor 尽管暴露了更多模块内部的内容,但是对了扩展功能来说非常强大。

不使用 new 调用的 guard,把模块作为 factory 的小把戏:

function Logger(name) {
    if (!(this instanceof Logger)) {
        return new Logger(name);
    }
    this.name = name;
};

该技术能够让我们把模块作为一个工厂:

var Logger = require('./logger');
var dbLogger = Logger('DB');
accesssLogger.verbose('This is a verbose message');

Exporting an instance

我们可以轻松利用 require() 的缓存机制定义一个有状态的实例,从 constructor 或者工厂创建的一个有状态的对象,可以跨不同的模块进行共享。

function Logger(name) {
    this.count = 0;
    this.name = name;
};
Logger.prototype.log = function(message) {
    this.count++;
    console.log('[' + this.name + ']' + messsage);
};
module.exports = new Logger('DEFAULT');

然后这个新定义的模块就可以像下面这样使用:

var logger = require('./logger');
logger.log('This is an informational message');

因为模块都是被缓存的,所以导入 logger 模块的每一个模块实际上取到的都是对象的同一个实例,从而实现了状态共享。很像 Singleton,但是并不保证在整个应用程序中这个实例的唯一性。因为在一个程序的依赖树中,模块可能被安装很多次。这就导致了同一个逻辑模块的多个实例。 (后面会讲更多有光状态实例的问题)

对这种模式的一个扩展是再把 construcotr 导出来,这样既可以扩展,也可以创建新对象。

module.exports.Logger = Logger;

然后我们就可以使用导出的 constructor 来创建该类的其他实例:

var customLogger = new logger.Logger('CUSTOM');
customLogger.log('This is an informational message');

Modifying other modules or the global scope

一个模块也可以什么也不导出。可以修改全局区域和其中的任何对象,包括缓存中的其他模块。 这通常被叫做 monkey patching,通常指的是在运行时修改已有模块来改变或者扩展它们的行为或者应用一些临时性的修复。

下面演示了如何给另一个模块增加一个新的功能:

// patcher.js, ./looger 是另一个模块
require('./logger').customMessage = function() {
    console.log('This is a new functionality');
};

使用新的 patcher 模块可以很容易地写出下面这样的代码:

// file main.js

require('./patcher');
var logger = require('./logger');
logger.customMessage();

回复