早期的JavaScript因爲缺少模塊系統。要編寫JS腳本,必須依賴HTML對其進行管理,嚴重製約了JavaScript的發展。而CommonJS規範的提出,賦予了JavaScript開發大型應用程序的基礎能力。其中NodeJS借鑑CommonJS的Modules規範實現了一套簡單易用的模塊系統,爲JavaScript在服務端的開發開闢了道路。html
CommonJS的模塊規範定義三個了部分:node
模塊引用:模塊所在的上下文提供require方法,可以接受模塊標識爲參數引入一個模塊的API到當前模塊的上下文中。json
模塊定義:在模塊中,存在一個module對象以表明模塊自己,同時存在exports做用模塊屬性的引用。api
模塊標識:模塊標識即爲require方法的參數,它要求必須爲小駝峯命名的字符串,相對路徑或絕對路徑。數組
模塊引用:在Node模塊的上下文中,存在require方法,可以對模塊進行引入,如const fs = require("fs");
。瀏覽器
模塊定義:在Node中,以單個文件做爲模塊的基礎單位,即一個文件爲一個模塊,全部掛載到exports對象上的方法屬性即爲導出。緩存
// person.js
exports.name = "vincent";
exports.say = function() {
console.log("hello world");
};
// driver.js
const person = require("person");
exports.say = function() {
person.say();
console.log(`I am ${person.name}`);
};
複製代碼
模塊標識:Node將爲模塊分爲兩類,一類是由Node提供的內建模塊,也稱爲核心模塊;另外一類是用戶編寫的模塊,稱爲用戶或第三方模塊。而Node中的模塊標識符主要分爲如下幾類。bash
以上即是Node對CommonJS模塊規範的實現概覽。但實際上Node對模塊規範進行了必定的取捨,在require
和exports
module
過程當中加入了自身的特點,下面讓咱們來深刻了解一下:app
require
實現的核心邏輯(省略了部分代碼)Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
// 存在父級模塊時,拼接路徑做爲臨時緩存索引(查詢真實路徑)
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
// 在緩存中查詢模塊
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
// 將模塊push到父級模塊的children數組中
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 刪除臨時索引
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 查詢模塊真實路徑,策略同步驟二
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
// 緩存存在時,返回module.exports
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 緩存不存在時,優先查找核心模塊
const mod = loadNativeModule(filename, request, experimentalModules);
// 若是能夠被開發者直接reuqire, 那麼直接返回module.exports
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 生成模塊實例並緩存
const module = new Module(filename, parent);
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
// 是否加載成功,默認失敗
let threw = true;
try {
// 加載模塊,根據文件後綴名使用對應方法
// .js -> fs.readFileSync -> compile (下一個章節會說明)
// .json -> fs.readFileSync -> JSON.parse
// .node -> fs.readFileSync -> dlopen (C/C++模塊)
// 其餘類型省略
module.load(filename);
threw = false;
} finally {
// 加載失敗,刪除緩存及其索引
if (threw) {
delete Module._cache[filename];
if (parent !== undefined) {
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
}
return module.exports;
};
複製代碼
Module._findPath = function(request, paths, isMain) {
// 絕對路徑
const absoluteRequest = path.isAbsolute(request);
if (absoluteRequest) {
paths = [''];
} else if (!paths || paths.length === 0) {
return false;
}
// 嘗試經過路徑緩存索引獲取
const cacheKey = request + '\x00' +
(paths.length === 1 ? paths[0] : paths.join('\x00'));
const entry = Module._pathCache[cacheKey];
if (entry) return entry;
// 判斷路徑是否以":/"或/結尾
var exts;
var trailingSlash = request.length > 0 &&
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
if (!trailingSlash) {
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
}
// For each path
for (var i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
if (curPath && stat(curPath) < 1) continue;
var basePath = resolveExports(curPath, request, absoluteRequest);
var filename;
// 查詢文件類型
var rc = stat(basePath);
if (!trailingSlash) {
// 文件是否存在
if (rc === 0) { // File.
// 嘗試根據模塊類型獲取真實路徑,代碼省略
filename = findPath();
}
// 嘗試給文件添加後綴名
if (!filename) {
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
}
}
// 當前文件路徑爲文件目錄且後綴名不存在,嘗試獲取filename/index[.extension]
if (!filename && rc === 1) { // Directory.
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryPackage(basePath, exts, isMain, request);
}
// 緩存路徑並返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 沒有找到文件,返回false
return false;
};
複製代碼
// (function(exports, require, module, __filename, __dirname) {\n
JS文件代碼...
// \n})
複製代碼
這樣每一個模塊文件直接都進行了做用域隔離。包裝事後的代碼經過vm原生模塊的
runInThisContext()
返回一個具體的function對象。最後將當前模塊對象的module
自身引用,require
方法,exports
屬性及一些等全局屬性做爲參數傳入function
中執行。這就是這些變量沒有定義卻在每一個模塊文件中存在的緣由。框架
剛開始接觸Node時,對於module.exports
和exports
的關係會存在一些疑惑。它們均可以掛載屬性方法,做爲當前模塊的導出。但它們分別表示什麼?又有什麼區別呢?觀察下面的代碼:
// a.js
exports.name = "vincent";
module.exports.name = "ziwen.fu";
exports.age = 24;
// b.js
const a = require("a");
console.log(a); // { "name": "ziwen.fu", age: 24 };
複製代碼
從前面的模塊源碼解析中,咱們能夠得知Node模塊最終導出的上module.exports
的值,而從上面的代碼咱們能夠肯定,module.exports
的初始值爲{}
,而exports
是做爲module.exports
的引用,掛載到exports
上的屬性方法,最終會由module.exports
導出。繼續觀察下面的代碼:
// a.js
exports = "from exports";
module.exports = "from module.exports";
// b.js
const a = require("a");
console.log(a); // from module.exports
複製代碼
從代碼運行結果能夠看出,直接對module.exports
和exports
進行賦值,最終模塊導出的是module.exports
的值。從上一小結可知,exports
在當前模塊上下文中是做爲形參傳入,直接改變形參的引用,並不能改變做用域外的值。測試代碼以下:
const myModule = function(myExports) {
myExports = "24";
console.log(myExports);
};
const myExports = "8";
myModule(myExports); // 24
console.log(myExports); // 8
複製代碼
下面這段來自Node官網的示例:
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
// main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
// console.log
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
複製代碼
官方的解釋是,當main.js加載a.js時,a.js嘗試加b.js。此時b.js又嘗試去加載a.js。爲了不死循環,a.js導出了一個未完成的副本,使b.js完成加載,隨後b.js導出給a.js完成整個過程。目前ES6 Module 已經提供瞭解決方案,Node的 ES6模塊也已經進入測驗階段,這裏就不作過多介紹了。
上面介紹了Node模塊的基礎機制,大多數狀況下咱們可能都會使用依賴前置的方式去require
模塊,即提早引入當前模塊所需的模塊,並它置於代碼頂部。但有時候,存在部分模塊,程序不須要當即使用它們,這時候動態引入是一個更好的選擇。下面是Node Web服務框架egg.js中加載器(Loader)實現的相關代碼,它提供了一個不同的思路:
// egg-core/lib/loader/utils
loadFile(filepath) {
// filepath來自於require.resolve(path)的定位
try {
// 非JavaScript模塊,同步讀取文件
// Module._extension爲Node模塊支持後綴名數組
const extname = path.extname(filepath);
if (extname && !Module._extensions[extname]) {
return fs.readFileSync(filepath);
}
// JavaScript模塊直接require
const obj = require(filepath);
if (!obj) return obj;
// ES6模塊返回處理
if (obj.__esModule) return 'default' in obj ? obj.default : obj;
return obj;
} catch (err) {
// ...
}
}
// egg-core/lib/loader/context_loader
// 代理上下文中對象app.context
// property對應項目文件名
Object.defineProperty(app.context, property, {
get() {
// 查詢緩存
if (!this[CLASSLOADER]) {
this[CLASSLOADER] = new Map();
}
const classLoader = this[CLASSLOADER];
// 獲取模塊對象實例並緩存
let instance = classLoader.get(property);
if (!instance) {
instance = getInstance(target, this);
classLoader.set(property, instance);
}
return instance;
},
});
複製代碼
egg-loader的思路是經過代理app.context上的property,動態的去require
模塊並加以包裝以後掛載到上下文對象上。其中property是來源於require.resolve
定位的模塊文件名,藉助緩存機制,使得程序在運行過程當中可以按需引入模塊,同時也減小了開發者引入模塊以及維護模塊名及路徑的成本。
目前Node的模塊機制中,require
模塊是基於readFileSync
實現的同步API,對於大文件的引入存在諸多不便。而正在試驗過程當中的ES Module
支持異步動態引入,同時也解決了循環依賴的問題,將來可能將普遍應用到Node模塊機制中。