Node中的Module源碼分析

在node項目中,require和module.exports使用很是廣泛,js模塊化帶來的效率大大提高。一直很好奇require背後是怎樣運行的,最近仔細看了看這部分的源碼,而後參考了其餘人的文章,還好node中的Module是JavaScript寫的能夠看懂。javascript

CommonJs規範

commonJs規範能夠說是js模塊化中的里程碑,目前npm上面的包基本都支持該規範。在CommonJs中:html

  1. 一個文件就是一個模塊,擁有單獨的做用域;
  2. 普通方式定義的變量、函數、對象都屬於該模塊內;
  3. 經過require來加載模塊;
  4. 經過exports和module.exports來暴露模塊中的內容;

舉個🌰:a.jsjava

var aParam = 23; 

exports.value = aParam;

module.exports = {
    calculate: function(param){
        return aParam + param;
    },
    getA: function(){
        return aParam;
    },
    value: aParam
};

而後入口文件:index.jsnode

var a = require('./a');

console.log(a);
// {calculate: [Function], getA: [Function], value: 23}

console.log(a.calculate(2));
// 25

console.log(a.value);
// 23

ok很簡單的小例子,咱們知道了經過require方法能夠加載一個js文件,該文件中exports出來的變量會當作文件運行結果。git

思考思路

如今沒有看源碼,咱們憑藉着使用經驗能夠梳理一下require和module的特色及實現思路:github

  1. require和module是暴露的全局對象。
  2. require接受一個參數(path),這個參數能夠是相對路徑,也能夠是自帶模塊或者是package.json中的插件。
  3. 這個參數若是不是完整的文件名能夠實現模糊匹配。(後綴匹配和路徑匹配)
  4. 匹配到文件以後,編譯該文件。
  5. 文件exports出來的變量做爲require方法的返回值。

這至關因而一個小項目,需求都列出來了。先不着急本身動手實踐,由於缺乏一些條件寫不出來,看看大神的代碼怎麼寫的。npm

源碼入手

再看源碼以前,官方文檔必定要準備好,裏面用到了不少原生的api。Module源碼在Node項目中,只有一個Module文件,全都包含在裏面。該文件也引入了一些其餘的依賴。json

依賴引入

// 原生的模塊
var NativeModule = require('native_module');
var util = require('util');
// vm沙箱
var runInThisContext = require('vm').runInThisContext;
var runInNewContext = require('vm').runInNewContext;
var assert = require('assert').ok;

var fs = require('fs');

function hasOwnProperty(obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
}

文件開頭引入了一些要用的工具類和方法,注意這裏的文件引入是直接使用require關鍵字引入的。bootstrap

Module模塊

/**
 * 大寫的Module其實是module的工廠
 * @param id 路徑 
 * @param parent 調用者的module對象
 * @constructor
 */
function Module(id, parent) {
    this.id = id; // 文件驗重的表示,字符串形式的絕對路徑
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
        parent.children.push(this);
    }

    this.filename = null;
    this.loaded = false;
    this.children = [];
}

module.exports = Module;

// 初始化一些變量
Module._contextLoad = (+process.env['NODE_MODULE_CONTEXTS'] > 0);
Module._cache = {};
Module._pathCache = {};
Module._extensions = {};
// node_module文件夾可能的路徑
var modulePaths = [];
Module.globalPaths = [];

Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;

var path = require('path');

而後定義了Module方法,Module是個工廠方法,module是工廠的實例。Module中主要的屬性:(被加載的文件這裏簡稱文件)api

  1. id '.' || 文件的絕對路徑
  2. exports 文件暴露的變量,默認是{};
  3. parent 文件的調用者的module實例
  4. filename 文件的絕對路徑
  5. loaded 是否加載完成
  6. children 調用文件的關係集合

而後初始化了一些變量,下面會用到。

require方法

/**
 * 暴露的require方法
 * @param path 文件的相對路徑或者是自帶模塊的名字
 */
Module.prototype.require = function(path) {
    return Module._load(path, this);
};

require方法其實是調用了Module的私有方法。

/**
 * 私有的加載文件
 * @param request 須要的文件名字或路徑
 * @param parent 對應文件的父節點
 * @param isMain 是不是入口調用
 * @private
 */
Module._load = function(request, parent, isMain) {

    // 根據路徑解析出filename
    var filename = Module._resolveFilename(request, parent);

    // 從Module緩存中取出是否有緩存
    var cachedModule = Module._cache[filename];

    if (cachedModule) {
        return cachedModule.exports;
    }

    // 系統自帶模塊中是否有該文件
    if (NativeModule.exists(filename)) {

        // 單獨對repl模塊處理
        if (filename == 'repl') {
            // 編譯
            var replModule = new Module('repl');
            replModule._compile(NativeModule.getSource('repl'), 'repl.js');
            NativeModule._cache.repl = replModule;
            return replModule.exports;
        }

        // 其餘自帶模塊
        return NativeModule.require(filename);
    }

    // 不是自帶模塊 生成一個實例
    var module = new Module(filename, parent);

    // 若是是主程序入口 則肯定當前位置
    if (isMain) {
        process.mainModule = module;
        module.id = '.';
    }

    // 緩存實例
    Module._cache[filename] = module;

    // 嘗試加載該文件module,若是有錯誤則回滾該module
    var hadException = true;

    try {
        module.load(filename);
        hadException = false;
    } finally {
        if (hadException) {
            delete Module._cache[filename];
        }
    }

    // 暴露
    return module.exports;
};

_load方法是加載文件的主要流程的方法:

  1. resloveFilename方法根據輸入的相對路徑或者是包名字算出文件的絕對路徑(filename)。
  2. 若是緩存中有直接返回緩存結果;
  3. 若是是自帶模塊直接返回結果;
  4. 不然建立一個module實例並緩存,嘗試加載該文件,而後返回文件的暴露結果。

這就是require方法的工做主流程,這裏很重要一點:文件被加載過會緩存結果。

該方法中重要的兩個步驟:

  1. Module._resolveFilename() 處理文件路徑
  2. module.load() 加載文件

咱們一個個來看。

處理路徑

/**
 * 肯定模塊的絕對路徑(filename)
 * @param request 文件相對路徑
 * @param parent 調用者的Module實例
 * @returns filename
 * @private
 */
Module._resolveFilename = function(request, parent) {
    // 若是是自帶模塊 filename 就是 request
    if (NativeModule.exists(request)) {
        return request;
    }

    // 肯定request可能的路徑有哪些
    var resolvedModule = Module._resolveLookupPaths(request, parent);
    var id = resolvedModule[0];
    var paths = resolvedModule[1];

    var filename = Module._findPath(request, paths);
    if (!filename) {
        var err = new Error("Cannot find module '" + request + "'");
        err.code = 'MODULE_NOT_FOUND';
        throw err;
    }

    // 解析後的文件的絕對路徑
    return filename;
};

_resolveFilename處理文件的絕對路徑方法,這個方法主要流程:

  1. 自帶模塊裏面有的話 返回文件名;
  2. 算出這個文件可能的路徑;
  3. 在可能路徑中找出真正的路徑並返回;
  4. 沒有的話報錯。

爲何要算出可能的路徑呢,由於文件來源不少:相對路徑下的文件;系統自帶模塊;還有多是node_modules中的包。若是是第三種node_modules文件夾位置沒法肯定,因此要進行路徑測算。

路徑測算

路徑測算兩個方法:

/**
 * 查找文件可能的路徑
 * @param request 文件的相對路徑
 * @param parent 父節點的Module實例
 * @returns [request, paths] [文件的相對路徑, 可能的路徑(Array)]
 * @private
 */
Module._resolveLookupPaths = function(request, parent) {
    // 自帶模塊 返回request
    if (NativeModule.exists(request)) {
        return [request, []];
    }

    // 判斷路徑開頭 不是相對路徑 補充可能的路徑(依賴包裏的路徑)
    var start = request.substring(0, 2);
    if (start !== './' && start !== '..') {
        var paths = modulePaths;
        if (parent) {
            if (!parent.paths) parent.paths = [];
            paths = parent.paths.concat(paths);
        }
        return [request, paths];
    }

    // 沒有調用者 
    if (!parent || !parent.id || !parent.filename) {

        var mainPaths = ['.'].concat(modulePaths);
        mainPaths = Module._nodeModulePaths('.').concat(mainPaths);
        return [request, mainPaths];
    }

    // 路徑是結尾是不是index
    var isIndex = /^index\.\w+?$/.test(path.basename(parent.filename));
    // 肯定調用者(parent)的絕對路徑
    var parentIdPath = isIndex ? parent.id : path.dirname(parent.id);
    var id = path.resolve(parentIdPath, request);

    if (parentIdPath === '.' && id.indexOf('/') === -1) {
        id = './' + id;
    }

    return [id, [path.dirname(parent.filename)]];
};

/**
 * 解析出node_modules目錄可能的路徑
 * @param from 當前模塊的路徑
 * @returns [Array]
 * @private
 */
Module._nodeModulePaths = function(from) {
    // 從from解析出絕對路徑
    from = path.resolve(from);

    // 根據操做系統不一樣兼容處理 解析出可能的路徑
    var splitRe = process.platform === 'win32' ? /[\/\\]/ : /\//;
    var paths = [];
    var parts = from.split(splitRe);

    /**
     * 這段比較有意思
     * 咱們假設form的絕對路徑是:
     * /Users/aus/Documents/node
     * 則項目的node_module文件夾路徑多是:
     * /Users/aus/Documents/node/node_modules
     * /Users/aus/Documents/node_modules
     * /Users/aus/node_modules
     * /Users/node_modules
     * /node_modules
     */
    for (var tip = parts.length - 1; tip >= 0; tip--) {
        // don't search in .../node_modules/node_modules
        if (parts[tip] === 'node_modules') continue;
        var dir = parts.slice(0, tip + 1).concat('node_modules').join(path.sep);
        paths.push(dir);
    }

    return paths;
};

這裏面如何算出可能的路徑:

  1. 自帶模塊可能路徑爲[]。
  2. 路徑不是相對路徑,可能路徑是node環境的自帶包(全局安裝的包 npm i XXX -g)。
  3. 沒有調用者的話,多是項目node_module中的包。
  4. 不然根據調用者(parent)的路徑算出絕對路徑。

第二個方法Module._nodeModulePaths則是推測項目中node_modules文件夾的路徑。

這塊測算路徑挺有意思的,值得多看一看。

精確匹配

而後如何在全部可能的路徑中找到惟一的結果:

/**
 * 根據全部可能的路徑肯定真實的路徑
 * @param request 文件的相對路徑
 * @param paths 可能的路徑
 * @returns filename 文件的絕對路徑
 * @private
 */
Module._findPath = function(request, paths) {
    var exts = Object.keys(Module._extensions);

    // 若是是絕對路徑,就再也不搜索
    if (request.charAt(0) === '/') {
        paths = [''];
    }

    // 是否有後綴的目錄斜槓
    var trailingSlash = (request.slice(-1) === '/');

    // 若是當前路徑已在緩存中,就直接返回緩存
    var cacheKey = JSON.stringify({request: request, paths: paths});
    if (Module._pathCache[cacheKey]) {
        return Module._pathCache[cacheKey];
    }

    // For each path
    for (var i = 0, PL = paths.length; i < PL; i++) {
        var basePath = path.resolve(paths[i], request);
        var filename;

        if (!trailingSlash) {
            // try to join the request to the path
            filename = tryFile(basePath);

            if (!filename && !trailingSlash) {
                // try it with each of the extensions
                filename = tryExtensions(basePath, exts);
            }
        }

        // 是不是package.json中的文件
        if (!filename) {
            filename = tryPackage(basePath, exts);
        }

        // 是否存在目錄名 + index + 後綴名
        if (!filename) {
            // try it with each of the extensions at "index"
            filename = tryExtensions(path.resolve(basePath, 'index'), exts);
        }

        // 找到文件路徑了 緩存
        if (filename) {
            Module._pathCache[cacheKey] = filename;
            return filename;
        }
    }

    // 404
    return false;
};

精確匹配路徑過程則相對簡單,就是一個個試:

  1. 文件後綴模糊匹配 .js,.json, .node
  2. 若是當前路徑已在緩存中,就直接返回緩存;
  3. 依次遍歷全部路徑
  4. 文件是否在與路徑直接匹配
  5. 路徑加上後綴名是否匹配到
  6. 是不是package.json中的包
  7. 是否存在目錄名 + index + 後綴名
  8. 將找到的文件路徑存入返回緩存,而後返回
  9. 不然404

經過上面的處理咱們能夠將一個須要加載的文件絕對路徑算出來,下一步只要編譯該文件便可。

編譯文件

來到了module.load這個方法

/**
 * 根據文件路徑名 嘗試不一樣擴展名解析文件
 * @param filename 文件路徑
 */
Module.prototype.load = function(filename) {

    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;
};

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
    var content = fs.readFileSync(filename, 'utf8');
    module._compile(stripBOM(content), filename);
};

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
    var content = fs.readFileSync(filename, 'utf8');
    try {
        module.exports = JSON.parse(stripBOM(content));
    } catch (err) {
        err.message = filename + ': ' + err.message;
        throw err;
    }
};

//Native extension for .node
Module._extensions['.node'] = process.dlopen;

找到了文件要根據文件的不一樣後綴來處理;

其中js和json文件處理方法仔細看。

文件編碼處理

/**
 * 剝離 utf8 編碼特有的BOM文件頭
 * @param content
 * @returns content 處理後的
 */
function stripBOM(content) {
    // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
    // because the buffer-to-string conversion in `fs.readFileSync()`
    // translates it to FEFF, the UTF-16 BOM.
    if (content.charCodeAt(0) === 0xFEFF) {
        content = content.slice(1);
    }
    return content;
}

獲取到文件,將文件轉成二進制流要處理頭信息。

爲何要處理頭信息,看這裏

沙箱編譯

ok,到這裏準備工做作完了要到了真正的編譯環節。

/**
 * 將文件在沙箱裏運行 將暴露的變量提取出來
 * @param content 文件字節流
 * @param filename 文件路徑
 * @returns {*}
 * @private
 */
Module.prototype._compile = function(content, filename) {
    var self = this;
    // remove shebang
    content = content.replace(/^\#\!.*/, '');

    function require(path) {
        return self.require(path);
    }

    require.resolve = function(request) {
        return Module._resolveFilename(request, self);
    };

    Object.defineProperty(require, 'paths', { get: function() {
        throw new Error('require.paths is removed. Use ' +
            'node_modules folders, or the NODE_PATH ' +
            'environment variable instead.');
    }});

    require.main = process.mainModule;

    // Enable support to add extra extension types
    require.extensions = Module._extensions;
    require.registerExtension = function() {
        throw new Error('require.registerExtension() removed. Use ' +
            'require.extensions instead.');
    };

    require.cache = Module._cache;

    var dirname = path.dirname(filename);

    if (Module._contextLoad) {
        if (self.id !== '.') {
            debug('load submodule');
            // not root module
            var sandbox = {};
            for (var k in global) {
                sandbox[k] = global[k];
            }
            sandbox.require = require;
            sandbox.exports = self.exports;
            sandbox.__filename = filename;
            sandbox.__dirname = dirname;
            sandbox.module = self;
            sandbox.global = sandbox;
            sandbox.root = root;

            return runInNewContext(content, sandbox, { filename: filename });
        }

        debug('load root module');
        // root module
        global.require = require;
        global.exports = self.exports;
        global.__filename = filename;
        global.__dirname = dirname;
        global.module = self;

        return runInThisContext(content, { filename: filename });
    }

    // create wrapper function
    var wrapper = Module.wrap(content);

    var compiledWrapper = runInThisContext(wrapper, { filename: filename });
    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) {
            global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0);
        }
    }
    var args = [self.exports, require, self, filename, dirname];
    return compiledWrapper.apply(self.exports, args);
};

這裏編譯主要是沙箱(沙盒)編譯,文件的內容被嵌入到一個閉包盒子中,盒子中的運行結果不會對外部環境產生影響,經過module.exports通訊,好比這樣:

(function (exports, require, module, __filename, __dirname) {
    //原始文件內容
});

這樣發現,在js文件裏,require和module都是注入的變量,而不是真正的全局變量。經過這樣閉包沙盒實現模塊化。(這樣看起來是否是跟requireJs很像)

初始化

最後初始化一下當前模塊

// bootstrap main module.
Module.runMain = function() {
    // Load the main module--the command line argument.
    Module._load(process.argv[1], null, true);
    // Handle any nextTicks added in the first tick of the program
    process._tickCallback();
};

/**
 * 初始化node全局依賴
 * @private
 */
Module._initPaths = function() {
    var isWindows = process.platform === 'win32';

    if (isWindows) {
        var homeDir = process.env.USERPROFILE;
    } else {
        var homeDir = process.env.HOME;
    }

    var paths = [path.resolve(process.execPath, '..', '..', 'lib', 'node')];

    if (homeDir) {
        paths.unshift(path.resolve(homeDir, '.node_libraries'));
        paths.unshift(path.resolve(homeDir, '.node_modules'));
    }

    var nodePath = process.env['NODE_PATH'];
    if (nodePath) {
        paths = nodePath.split(path.delimiter).concat(paths);
    }

    modulePaths = paths;

    // clone as a read-only copy, for introspection.
    Module.globalPaths = modulePaths.slice(0);
};

// bootstrap repl
Module.requireRepl = function() {
    return Module._load('repl', '.');
};

Module._initPaths();

// backwards compatibility
Module.Module = Module;

總結

Module模塊特色

整理一下Module模塊的特色:

  1. 在commonjs規範中每一個模塊都是一個Module實例。
  2. require方法調用__load方法加載模塊文件

    1. _resolveFilename解析文件的絕對路徑

      1. _resolveLookupPaths解析文件肯能的絕對路徑
      2. _findPath匹配嘗試找到文件絕對路徑
    2. load解析文件

      1. Module._extensions[extension]不一樣後綴嘗試加載

        1. _compile沙箱編譯
  3. require的返回值是module.exports || {};

使用注意點

而後經過源碼的閱讀,我也注意到了幾個以前沒有注意到的點:

  1. Module是全局對象,require不是。
  2. require方法接受的路徑能夠是:

    1. 系統自帶模塊(fs,path)
    2. 全局安裝的模塊
    3. 項目安裝的模塊(node_modules)
    4. 能夠經過相對/絕對路徑匹配到模塊文件
    5. 路徑文件能夠不寫後綴,默認支持(.js, .json, .node)
    6. 路徑能夠不寫index(./components => ./components/index)
  3. 經過沙箱編譯方式實現模塊化,require和module都是沙箱中注入的對象。
  4. 模塊加載以後會被緩存,不會二次編譯。
  5. require的返回值是module.eports || {};
  6. exports = module.exports;

最後我對源碼進行了中文註釋,方法重寫排序,須要的github自取。

參考

  1. require()源碼解讀
  2. NodeJS 模塊加載方法 require 源碼分析
  3. 經過源碼解析 Node.js 中一個文件被 require 後所發生的故事
  4. 【NodeJS】淺析加載模塊的機制
相關文章
相關標籤/搜索