最近看到「乞丐版」的Promise
實現,因此想實現一個「乞丐版」的CommonJS
規範的模塊加載。但願由此:html
CommonJS
規範;npm
上的模塊包;CommonJS
規範相信你們都不陌生,Node.js
正是由於實現了CommonJS
規範,纔有了模塊加載能力,和在此基礎上出現的蓬勃的生態。簡言之:前端
exports
或module.exports
對外暴露方法、屬性或對象。require
引用,若是屢次引用同一個模塊,會使用緩存而不是從新加載。若是還不熟悉CommonJS
規範,建議先閱讀CommonJS Modules和Node.js Modules文檔說明。node
首先,咱們初始化一個自定義的Module
對象,用於包裝文件對應的模塊。git
class Module {
constructor(id) {
this.id = id;
this.filename = id;
this.loaded = false;
this.exports = {};
this.children = [];
this.parent = null;
this.require = makeRequire.call(this);
}
}
複製代碼
這裏主要講解下this.exports
和this.require
,其它屬性主要都是用於輔助模塊的加載。github
this.exports
保存的是文件解析出來的模塊對象(你能夠認爲就是你在模塊文件中定義的module.exports
)。它在初始化的時候是個空對象,這也能說明爲何在循環依賴(circular require)時,在編譯階段取不到目標模塊屬性的值。舉個小板慄:面試
// a.js
const b = require('./b');
exports.value = 'a';
console.log(b.value); // a.value: undefined
console.log(b.valueFn()); // a.value: a
複製代碼
// b.js
const a = require('./a');
exports.value = `a.value: ${a.value}`; // 編譯階段,a.value === undefined
exports.valueFn = function valueFn() {
return `a.value: ${a.value}`; // 運行階段,a.value === a
};
複製代碼
this.require
是用於模塊加載的方法(就是你在模塊代碼中用的那個require),經過它咱們能夠加載模塊依賴的其它子模塊。npm
接下來咱們看下require
的實現。json
咱們知道,當咱們使用相對路徑require
一個模塊時,其相對的是當前模塊的__dirname
,這也就是爲何咱們須要爲每一個模塊都定義一個獨立的require
方法的緣由。api
const cache = {};
function makeRequire() {
const context = this;
const dirname = path.dirname(context.filename);
function resolve(request) {}
function require(id) {
const filename = resolve(id);
let module;
if (cache[filename]) {
module = cache[filename];
if (!~context.children.indexOf(module)) {
context.children.push(module);
}
} else {
module = new Module(filename);
(module.parent = context).children.push(module);
(cache[filename] = module).compile();
}
return module.exports;
}
require.cache = cache;
require.resolve = resolve;
return require;
}
複製代碼
注意這裏執行的前後順序:緩存
cache
裏面查找目標模塊是否已存在?查找依據是模塊文件的完整路徑。Module
對象,同時推入父模塊的children
中。module
對象存入cache
中。module
對象的compile
方法,此時模塊代碼纔會真正被解析和執行。module.exports
,即咱們在模塊中對外暴露的方法、屬性或對象。第3和第4的順序很重要,若是這兩步反過來了,則會致使在循環依賴(circular require)時進入死循環。
上述代碼中有一個require.resolve
方法,用於解析模塊完整文件路徑。正是這個方法,幫助咱們找到了千千萬萬的模塊,而不須要每次都寫完整的路徑。
在Node.js
官方文檔中,用完善的僞代碼描述了該查找過程:
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. LOAD_NODE_MODULES(X, dirname(Y))
5. THROW "not found"
複製代碼
對應的實現代碼:
const coreModules = { os, }; // and other core modules
const extensions = ['', '.js', '.json', '.node'];
const NODE_MODULES = 'node_modules';
const REGEXP_RELATIVE = /^\.{0,2}\//;
function resolve(request) {
if (coreModules[request]) {
return request;
}
let filename;
if (REGEXP_RELATIVE.test(request)) {
let absPath = path.resolve(dirname, request);
filename = loadAsFile(absPath) || loadAsDirectory(absPath);
} else {
filename = loadNodeModules(request, dirname);
}
if (!filename) {
throw new Error(`Can not find module '${request}'`);
}
return filename;
}
複製代碼
若是對如何從目錄、文件或node_modules
中查找的過程感興趣,請看後面的完整代碼。這些過程也是根據Node.js
官方文檔中的僞代碼實現的。
最後,咱們須要把文件中的代碼編譯成JS環境中真正可執行的代碼。
function compile() {
const __filename = this.filename;
const __dirname = path.dirname(__filename);
let code = fs.readFile(__filename);
if (path.extname(__filename).toLowerCase() === '.json') {
code = 'module.exports=' + code;
}
const wrapper = new Function('exports', 'require', 'module', '__filename', '__dirname', code);
wrapper.call(this, this.exports, this.require, this, __filename, __dirname);
this.loaded = true;
}
複製代碼
在compile
方法中,咱們主要作了:
json
格式文件的支持。new Function
生成一個方法。module
、module.exports
、require
、__dirname
、__filename
做爲參數,執行該方法loaded
標記爲true
。這裏完整的實現了一個能夠運行在QuickJs引擎之上的CommonJS
模塊加載器。QuickJs引擎實現了ES6
的模塊加載功能,可是沒有提供CommonJS
模塊加載的功能。
固然,若是你真的在面試的時候遇到了這個問題,建議仍是拿Node.js
源碼中實現的版原本交差。