Node.js design pattern一書中對Node的Module模組機制這一塊,我覺得講的挺透徹和易懂,這裡根據自己理解做下總結。本文轉發自本人github。
loadModule
自定義一個簡單的模組載入方法loadModule
,基本思路跟nodejs一致,將載入的模組內容包裹在一個函式裡面實現變數的隔離,保證模組內的變數都是私有的。
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
函式。
作為對比,在nodejs原始碼中, wrap
是這樣實現的
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
`(function (exports, require, module, __filename, __dirname) { `,
`
});`
];
複製程式碼
最終對該warpper
的解析實現在Module.prototype._compile
方法中,其中用到了vm
模組對wrapper
指令碼進行處理。vm
實現的功能與eval
函式類似,但比eval
函式更強大。
模組引用require()
對模組的引用我們通過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
方法通過模組路徑載入模組內容。 - 返回模組中暴露的介面以供呼叫。
可以通過下圖更加直觀的瞭解其中的關係。
module.exports vs exports
通過上述程式碼和圖示可知,我們寫的模組中的exports
其實是對module.exports
的引用。 因此我們可以通過exports
新增屬性來給module.exports
引用的物件新增屬性。
exports.hello = () => { console.log(`Hello`) };
複製程式碼
但如果給exports
重新賦值,則會失去module.exports
的引用
exports = () => { console.log(`Hello`) };
複製程式碼
此時exports !== module.exports
,意味著通過exports
暴露的介面是無效的,沒有新增到module metadata中的exports
中。