Node.js design pattern一书中对Node的Module模块机制这一块,我以为讲的挺透彻和易懂,这里根据本身理解作下总结。本文转发自本人github。javascript
自定义一个简单的模块加载方法loadModule
,基本思路跟nodejs一致,将加载的模块内容包裹在一个函数里面实现变量的隔离,保证模块内的变量都是私有的。java
function loadModule(filename, module, require) {
const wrappedSrc = `(function(module, exports, require) { ${fs.readFileSync(filename, 'utf8')} })(module, module.exports, require);`;
eval(wrappedSrc);
}
复制代码
这个例子经过eval
对wrappedSrc
进行计算,即经过eval
函数处理该字符串脚本。由于这里要把(function(module, exports, require) {
这串东西和 fs.readFileSync(filename, 'utf8')
加载的模块内容合并在一块儿做为新的整合代码再运行,因此必须借助eval
函数。node
做为对比,在nodejs源码中, wrap
是这样实现的git
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
复制代码
最终对该warpper
的解析实如今Module.prototype._compile
方法中,其中用到了vm
模块对wrapper
脚本进行处理。vm
实现的功能与eval
函数相似,但比eval
函数更强大。github
对模块的引用咱们经过require(..)
函数进行引用,如var http = require('http')
,该方法简单实现以下:缓存
const require = (moduleName) => {
console.log(`Require invoked for module: ${moduleName}`);
const id = require.resolve(moduleName);
if (require.cache[id]) { return require.cache[id].exports; }
// 1.module metadata
const module = {
exports: {},
id: id
}
// 2.require.cache
require.cache[id] = module;
// 3.load the module
loadModule(id, module, require);
// 4.return exported variables
return module.exports;
}
require.cache = {};
require.resolve = (moduleName) => {
/* resolve a full module id from the moduleName */
}
复制代码
module
对象用来保存经过loadModule
方法中加载模块中暴露出的接口。require(..)
时不会再调用loadModule
方法,直接从cache
中返回。loadModule
方法经过模块路径加载模块内容。能够经过下图更加直观的了解其中的关系。app
module.exports vs exports函数
经过上述代码和图示可知,咱们写的模块中的exports
实际上是对module.exports
的引用。 所以咱们能够经过exports
添加属性来给module.exports
引用的对象添加属性。ui
exports.hello = () => { console.log('Hello') };
复制代码
但若是给exports
从新赋值,则会失去module.exports
的引用spa
exports = () => { console.log('Hello') };
复制代码
此时exports !== module.exports
,意味着经过exports
暴露的接口是无效的,没有添加到module metadata中的exports
中。