all articles

further reading on how Node.js Module requires

2016-12-05 @sunderls

require node.js module

写node.js程序的时候,一直用require用惯了,可曾想过require是如何实现的呢?今天我们再回顾一下。

CommonJS

CommonJS是Node.js诞生时出现的非浏览器环境下运行js的一种规范(wiki),其中包含Modules,Package, Promises,System等,其中作为js程序猿最熟悉的应该就是Modules了,因为这是require被定义的地方。

CommonJS Modules 1.1.1

规范详细描述在这里: http://wiki.commonjs.org/wiki/Modules/1.1.1 先附上官方例子(虽然这个看上去有点过于简单)。

math.js

exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

increment.js

var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

program.js

var inc = require('increment').increment;
var a = 1;
inc(a); // 2

module.id == "program";

总之 exports, require, module这几个关键词,使得js代码可以模块化。接下来对协议进行详解。

1. require

1.1 require是个方法

1.2 require返回的是外部模块的api。

1.3 require遇到循环应用的时候,返回当时准备好的的就行。

准备如下例子

a.js

exports.name = 'a';

var b = require('./b');

console.log(`a.js => b.name === '${b.name}'`);
console.log(`a.js => b.date === '${b.date}'`);

exports.date = 'a-date';

b.js

exports.name='b';

let a = require('./a');

console.log(`b.js => a.name === '${a.name}'`);
console.log(`b.js => a.date === '${a.date}'`);

setTimeout(() => console.log(`b.js (1 sec afeter) => a.date === '${a.date}'`), 1000);

exports.date = 'b-date';

$> node a.js得到如下结果:

b.js => a.name === 'a'
b.js => a.date === 'undefined'
a.js => b.name === 'b'
a.js => b.date === 'b-date'
b.js (1 sec afeter) => a.date === 'a-date'

可以看到a中用到了b,b中又用到了a。注意require引用的是一个module,不会重新初始化,b中在获取a的时候a已经初始化了。b中第一次打印a.date的时候还没有值,1秒过后再打印的时候就有了。 文档这部分允许了循环引用,因为module不会重复初始化不会陷入死循环。

1.4. require如果不能返回对象必须报错

1.5. require 可以有一个main属性表示初始的module

1.5.1 This attribute, when feasible, should be read-only, don't delete. (这句话没有看懂)
1.5.2 main要不是undefined,要不是当前的module

在a.js中添加console.log(require.main)可以得到如下结果:

Module {
  id: '.',
  exports: { name: 'a', date: 'a-date' },
  parent: null,
  filename: 'path/to/a.js'
  loaded: false,
  children:
   [ Module {
       id: 'path/to/b.js',
       exports: [Object],
       parent: [Circular],
       filename: 'path/to/b.js',
       loaded: true,
       children: [],
       paths: [Object] } ],
  paths:
   [
     // every /node_modules path, all the way above
   ] }

注意在b.js中的require.main也得到的上述结果。

1.6 require可以有paths属性,从高到低列出顶层module的目录,可以帮助引用模块。

在a.js中测试发现没有这个属性。原来node.js 0.5时代开始就删除了这个属性。http://nodejs.jp/nodejs.org_ja/docs/v0.4/api/modules.html#loading_from_the_require.paths_Folders

2. Module Context(模块环境)

2.1 模块环境中须有一个直接可以使用的require(定义如上)

2.2 模块环境中须有一个直接可以使用的exports,这是一个对象包含对外的api

2.2.1 module只能用exports来公开api

2.3 模块环境中须有一个直接可以使用的module,这是一个对象,这个对象:

2.3.1 必须有一个id属性可以用来require
2.3.2 可以有一个uri属性来表示来源地

测试可以发现,a.js module === require.main, b.js module !== require.main,这个可以用来判定module是否是被引用。

3. Module Identifiers(模块标识符)

3.1 模块标识符有一些列terms组成,terms之间由/划分

3.2 term必须是camelCase的识别符,或者., 或者..

3.3 模块标识符可以没有类似于.js的文件名后缀

3.4 模块标识符可以是相对的或者顶层。如果标识符开头是...就代表相对

3.5 顶层标识符通过conceptual module name space root进行查找

3.6 相对表示符通过require被调用的位置开始相对查找

这简单的说纪实require('./../../a')这样的相对位置引用是可以的。嗯。

4. Unspecified (未定义)

4.1 模块存储方式不做规范
4.2 是否支持PATH不做规范

node.js的module

但是node.js其实已经不是严格common.js的了!!https://github.com/nodejs/node-v0.x-archive/issues/5132#issuecomment-15432598

因为,因为NPM啊!也就是说node.js之后的包管理是向其他服务器语言看齐,比如java的maven之类的,不再纠结于commonjs了。

接下来我们来看看node.js的module系统(官方: https://nodejs.org/api/modules.html) ,commonjs的内容基本还是在其中,但是多了node.js自身的特性,以下详细说明。

1. require的运作逻辑

1.1 require 层层向上查询目标module

具体是利用require.resolve()来定位目标js文件,其算法为:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.json is a file, parse X.json to a JavaScript Object.  STOP
4. If X.node is a file, load X.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIRS + DIR
   d. let I = I - 1
5. return DIRS

↑还是比较清楚的,简单说明如下,在path Y下require(X)的时候,node顺次做了如下工作:

  • 1.如果X是内置组件,直接返回就ok了
  • 2.如果X是相对路径,比如./, ../, /,就直接找到目标位置,对于文件或者文件夹进行分别处理
    • 2.1 如果是文件,尝试作为js文件load
    • 2.2 如果是文件夹:
      • 2.2.1 如果有package.json,找到其中的main字段,作为文件load
      • 2.2.2 如果有index.js,作为文件load
      • 2.2.3 如果有index.json 作为文件load
      • 2.2.4 如果有index.node 作为文件load
  • 3.从Y开始一直向上,查找每一个路径下的node_modules总是否有X(按照1和2的方式)。
  • 4.如果系统变量中有NODE_PATH的话,也会被查找。
  • 4.没找到的话,会报错'MODULE_NOT_FOUND'

1.2 module是被缓存的。

require过后的module是缓存的,只会有一个module,所以如果想要在module中每一次被调用都被执行的话,须要export一个function,然后去执行这个function。

但是,这个文件名来的,根据1中的查询方法,有可能在不同情况下有不同的文件名,这种情况下,会出现多次加载。比如在区分大小写的系统下。

2 module的内部实现。

终于到了最想要说的部分了,node.js会在通过一个wrapper在加载js文件过后将其包装为一个module:

2.1 wrapper

(function (exports, require, module, __filename, __dirname) {
  // Your module code actually lives in here
});

哦哦哦! 用了一个closure把require和module传了进去,注意还有两个不怎么听过的__filename, __dirname,也是可以用的。nice!

2.2 module 对象

属性/方法 desc
module.children 就是这个module require的那些module
module.exports 就是module 暴露给引用者的内容
module.filename 文件名
module.loaded load完成没,比如前面的循环引用的例子中,就会出现load没完成的时候
module.parent 第一个require 这个module的module
module.require(id) 就是require的真身,没怎么用过,貌似可以在load完成后再让其require,但是在其他module中只能看到exports,所以module本身需要被export。

3. module.js

从文件夹在到module创建,都是用了 module.js这个node.js内置的文件,这一点在 之前的文章[理解前端开发中的unit test 和 e2e test] http://tech.colla.me/show/understanding_unit_test_and_e2e_test_in_frond_end_development 中已经有用到了,当时代码如下:

const Module = require('module');

// adjust folder
file = file.replace('require((.*)?)', 'require(../$1)');

new Module()._compile('\'use strict\';const test = require(\'./lib/unitTest.js\');' + file);

module.js github地址: https://github.com/nodejs/node/blob/master/lib/module.js ,以下稍微详细说明。

3.1 Module contructor

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}
module.exports = Module;

github

module.js中export了这么一个构造方法,显然是用来new的,其中可以看到之前列出的所有属性。

3.2 Module.prototype.require

// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};

github

require方法在这里被定义,实际上调用了Module的类方法_load,把module本身作为第二个参数传入了进去

3.3 Module._load

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}

github

这里尝试resolve了文件位置过后,调用了module.load实例方法,

// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

github

这里调用了Module._extensions类方法来处理不同的文件扩展:

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path._makeLong(filename));
};

github

可以看到,js 文件采用的是module._compile,json是JSON.parse, .nodeprocess.dlopen,我们主要看module._compile

3.4 module._compile

// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
  ...(omit here)...
  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  if (process._debugWaitConnect && process._eval == null) {
    if (!resolvedArgv) {
      // we enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (filename === resolvedArgv) {
      delete process._debugWaitConnect;
      const Debug = vm.runInDebugContext('Debug');
      Debug.setBreakPoint(compiledWrapper, 0, 0);
    }
  }
  var dirname = path.dirname(filename);
  var require = internalModule.makeRequireFunction.call(this);
  var args = [this.exports, require, this, filename, dirname];
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result = compiledWrapper.apply(this.exports, args);
  if (depth === 0) stat.cache = null;
  return result;
};

首先,对于js文件内容,需要把最开始说的wrapper 闭包的头尾部分给补充完毕才行,这一部分是在NativeModule中,

  NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

github

然后包装好的文本就可以送到vm中执行了,这部分就应该是v8引擎的事情了吧,总之脱离了js的范畴。看上面的wrapper可以看到,执行完后得到的是一个wrapper方法,最后对这个方法传入参数执行。

var args = [this.exports, require, this, filename, dirname];
var result = compiledWrapper.apply(this.exports, args);

最后得到的结果返回module, DONE!

回过头来看e2e test文章中的代码是不是清楚明了很多?

// 悄悄利用`_compile`注入额外的require。
new Module()._compile('\'use strict\';const test = require(\'./lib/unitTest.js\');' + file);

总结

想当然的用require用了这么久,终于稍微仔细地看了下require是怎么实现的。最后总结一下就是:

node.js首先查找到目标文件 ==> 然后在文件内容中添加闭包的头尾 ==> 通过原生引擎编译得到一个Module的实例返回。

而利用这个Module有的时候也能得到意想不到的便利。