浅析Node的模块化

概述

Node应用由模块组成,采用了CommonJS模块规范。因此根据这个规范,文件皆模块,每个文件都有自己的作用于,模块的导出通过module.exportsexports方式导出,导出后模块的导入通过require实现。

模块分类

Node的模块可分为三类

  • 核心模块(内置模块):如fs,http,path等,无需安装,直接引入即可
  • 第三方模块:一些别人写的第三方模块,需要通过node包管理工具安装
  • 自定义模块:自己项目中实现,通过绝对路径或者相对路径引入。

用法示例

1.通过module.exports可以导出一个整体对象

// a.js
module.exports = {
	a:1
};

// b.js 引入a.js
let a = require('./a')
console.log(a); // {a:1}

2.通过exports导出

// a.js
exports.a = 1;

// b.js 引入a.js
let a = require('./a')
console.log(a); // {a:1}

由于上述两种导出,结果一致。 接下来对exportsmodule.exports进行一些小测试

1.module.exports和exports默认是一个空对象

console.log(module.exports); // {}
console.log(exports); // {}

2.从下面可以看出,exports和module.exports实际上是指向的同一处引用地址.

console.log(exports === module.exports); // true

3.平时写法都是module.exports = {};而exports都是对某个属性进行赋值;尝试对exports进行重新赋值, 结果打印始终未空对象

// a.js
exports = 1;
// b.js
let a = require('./a')
console.log(a); // {}

// 上面的导出并没有作用到最终的导出对象,可大致理解成,node的模块化规范,默认导出的是module.exports,而exports只是引用了module.exports,所以修改了exports的取值并不会影响到module.exports,所以也不会影响到最终的效果

4.每个文件默认导出一个对象,多处引用,其他地方对内容进行改动会改动整体的取值。下面的例子中,b.js导入了a.js,并将a属性改成了99999, 在c.js中引入c后对a模块进行查看,发现a的值发生了改变。

// a.js
exports = 1;

// b.js
let a = require('./a');
a.a = 99999;

// c.js
let a = require('./a')
console.log(a); // {a:1}
let c = require('./c.js');
console.log(a); // {a: 99999}

进一步探索

经过上面的例子后,对过程处理一头雾水,并且为啥node环境下支持使用__dirname__filename呢?决定了解下里面的原理。

对require函数进行断点调试,然后进入查看, 发现内部的源码是一个helpers.js的文件,调用了里面的makeRequireFunction产生的require函数,该函数接收一个path路径参数,返回mod.require函数调用后的返回值。

// Invoke with makeRequireFunction(module) where |module| is the Module object
// to use as the context for the require() function.
function makeRequireFunction(mod) {
  const Module = mod.constructor;

  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

  function resolve(request, options) {
    if (typeof request !== 'string') {
      throw new ERR_INVALID_ARG_TYPE('request', 'string', request);
    }
    return Module._resolveFilename(request, mod, false, options);
  }

  require.resolve = resolve;

  function paths(request) {
    if (typeof request !== 'string') {
      throw new ERR_INVALID_ARG_TYPE('request', 'string', request);
    }
    return Module._resolveLookupPaths(request, mod, true);
  }

  resolve.paths = paths;

  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;

  require.cache = Module._cache;

  return require;
}

继续往下走,mod.require实际上是mod是由Module构造出来的实例,Module原型上绑了require方法。函数内调用了Module的静态方法_load

// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
  if (typeof id !== 'string') {
    throw new ERR_INVALID_ARG_TYPE('id', 'string', id);
  }
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  return Module._load(id, this, /* isMain */ false);
};
  1. Module._load加载模块。
  2. Module._resolveFilename将路径转换成绝对路径
  3. let module = new Module,创建一个模块,初始化id和exports对象等信息。
  4. tryModuleLoad,尝试加载模块
// 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) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

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

  // Don't call updateChildren(), Module constructor already does.
  // 创建模块
  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];
    }
  }
}
  1. 通过不同的后缀对文件进行处理

// 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));
  // 通过不同的后缀进行加载 .json  .js  .node .mjs
  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;

  if (experimentalModules) { // 是否是实验性模块
    // ...
  }
};
  1. 不同文件后缀处理逻辑不同
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};
Module.wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

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

// 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) {
  // content是自己编写的文件内容,通过读取后返回的字符串
  content = stripShebang(content);

  // create wrapper function
  // 结合Module.wrap函数,将内容进行包裹处理.
  var wrapper = Module.wrap(content);
  // 执行包裹后的函数
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  var inspectorWrapper = null;
  if (process._breakFirstLine && process._eval == null) {
    // ...省略
  }
  var dirname = path.dirname(filename);
  var require = makeRequireFunction(this);
  var depth = requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    // 执行函数
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  // 返回结果
  return result;
};

_compile函数的过程是先读取用户自己写的文件内容,然后将内容包裹成一个新的函数,并执行返回结果。

例如:

// 我编写的内容 my.js
console.log('hello world');
  1. 经过_compile处理第一步,读取到的content为’console.log(“hello world”)’;
  2. 经过Module.wrap的包裹, 得到一个字符串函数’(function (exports, require, module, __filename, __dirname) {console.log(‘hello world’);\n}’
  3. 该函数有5个参数, exports就是module.exports, require是导入模块函数, module即当前模块,__filename当前文件名,__dirname当前文件夹名
  4. 经过node内置模块vm的runInThisContext执行该代码,返回一个隔离作用域的函数compiledWrapper
  5. 执行compiledWrapper函数,并将this指向修改为this.exports, 实际上就是module.exports.

通过上面这些代码,就能明白用node运行时, exports和module.exports的关系,以及__filename和__dirname是如何产生的.