Node.js的模块机制可以让我们很方便地将js代码按照功能进行封装。在一个模块中我们使用require()方法引入另一个模块,使用module.exports向外暴露方法、对象、变量供其他模块引用。新建.js文件的时候,并没有引入require方法和module变量,这些是从哪里来的呢?
module对象
在 Node.js 模块系统中,每个文件都被视为独立的模块,并且有一个module对象与之对应。举个栗子,下面是circle.js
的内容
// circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
console.log(module);
将module
打印出来,看看是什么内容,运行node circle.js
可以看到打印出来一个对象,类名是Module
,包含了id
,exports
,filename
,parent
,children
等属性,每个属性解释如下:
- module.id 模块的标识符。 通常是完全解析后的文件名
- module.exports 一个对象,包含了模块对外暴露的方法、局部变量、类等
- module.filename 模块的完全解析后的文件名。
- module.parent 最先引用该模块的模块。
- module.children 被该模块引用的模块对象。
Tips:exports.area 是 module.exports.area的简单写法
新建一个foo.js文件,引用circle.js文件
//foo.js
const circle = require('./circle.js');
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);
看下运行效果:
可以看到,通过require()方法,可以成功引用circle.js方法中暴露的area()方法。并且foo.js对应的module对象成为了circle.js对应circle对象的parent(父摸块)。
module对象从何而来
module
对象在circle.js
中并没有引用就直接使用了,它从哪来的?
有兴趣的同学可以看下面的源码分析,否则可以直接跳到总结。
源码分析
从node/module.js
源码可以看下引入一个文件的时候Node.js做了什么。
// 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);
};
require方法在校验了路径参数是否合法后,将路径参数传给了加载方法 _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);
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;
var hadException = true;
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
return module.exports;
};
可以看到对新引入的文件,在_load()方法中新建了module对象,然后调用了新建对象的load()方法,参数是被引入的文件名(解析后的),返回结果是新建module对象的exports属性
// 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;
};
这里会根据不同文件类型,引用不同的加载方法,我们仅以js文件为例
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
调用_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) {
// remove shebang
content = content.replace(shebangRe, '');
// create wrapper function
var wrapper = Module.wrap(content);
var compiledWrapper = runInThisContext(wrapper,
{ filename: filename, lineOffset: 0 });
if (global.v8debug) {
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) {
// Installing this dummy debug event listener tells V8 to start
// the debugger. Without it, the setBreakPoint() fails with an
// 'illegal access' error.
global.v8debug.Debug.setListener(function() {});
global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0);
}
}
const dirname = path.dirname(filename);
const require = internalModule.makeRequireFunction.call(this);
const args = [this.exports, require, this, filename, dirname];
const depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
const result = compiledWrapper.apply(this.exports, args);
if (depth === 0) stat.cache = null;
return result;
};
在_compile()方法中可以看到,js文件内容先被wrap(包装)了一下,然后才使用runInThisContext来运行包装后的代码。我们看下wrap()方法是怎么包装的
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
可以看到就是讲引入文件的首尾加了些代码,讲内容包在一个function里。回到前面的_compile方法,传入runInThisContext()方法的参数都是新建module对象的属性或对象本身, require是使用module作为上下文新建的方法。
总结
使用require()方法引入一个.js文件模块,简单说经历了下面过程:
1,Node首先根据文件名看下缓存中有没有module对象,如果有就返回,没有就新建一个module对象并保存在缓存中。
2,加载文件内容后,Node使用一个如下的函数包装器将其包装:
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
3, 使用新建的module对象作为上下文,整理参数传入上面包装好的函数
4, 返回module.exports属性
参考文献
1,https://www.jianshu.com/p/609489e8c929#
2,http://nodejs.cn/api/modules.html