NodeのModuleソース分析

18220 ワード

nodeプロジェクトでは、requireとmodule.exportsの使用が非常に一般的で、jsモジュール化による効率が大幅に向上しました.requireの背後にはどうやって実行されているのか気になりましたが、最近はこの部分のソースコードをよく見て、他の人の文章を参考にしました.幸いなことに、nodeのModuleはJavaScriptで書いてあります.
Common Js仕様
common Js仕様はjsモジュール化の一里塚と言えます.現在npm上のカバンは基本的にこの規格をサポートしています.Common Jsにおいて:
  • ファイルは一つのモジュールであり、単独のスコープを持っています.
  • 一般的に定義された変数、関数、オブジェクトはこのモジュール内に属します.
  • は、requireによってモジュールをロードする.
  • は、exportとmodule.exportsを通じてモジュールの内容を暴露する.
  • 一つ挙げますかa.js
    var aParam = 23; 
    
    exports.value = aParam;
    
    module.exports = {
        calculate: function(param){
            return aParam + param;
        },
        getA: function(){
            return aParam;
        },
        value: aParam
    };
    入り口のファイル:index.js
    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簡単な例です.ファイルの実行結果としてexportsから出てくる変数が、require方法によってjsファイルをロードできることを知りました.
    考え方を考える
    今はソースコードを見ていません.使用経験によって、requireとmoduleの特徴を整理できます.
  • requireおよびmoduleは、露出されたグローバルオブジェクトである.
  • requireは、1つのパラメータ(path)を受け取り、このパラメータは相対パスであってもよく、モジュールを持参するか、またはpackage.jsonのプラグインであってもよい.
  • このパラメータは、完全なファイル名でない場合、あいまいなマッチングを実現することができる.(拡張子マッチングとパスマッチング)
  • はファイルにマッチングした後、ファイルをコンパイルする.
  • ファイルのexportからの変数は、require方法の戻り値として作用します.
  • これはかなり小さいプロジェクトで、需要は全部並べました.とりあえず自分で実践を急ぎません.条件が足りないので書けません.大神さんのコードはどう書きますか?
    ソースの入手
    ソースを見る前に、公式文書を準備してください.中には多くの原生アプリが使われています.ModuleソースはNodeプロジェクトの中で、一つだけModuleファイルがあります.全部含まれています.このファイルは他の依存性もいくつか紹介しています.
    依存導入
    //      
    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キーワードを使って導入されています.
    モジュール
    /**
     *    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の主な属性:(ロードされたファイルはここでファイルと略称する)
  • id''''124124;ファイルの絶対パス
  • exportファイル暴露の変数は、デフォルトは{}です.
  • parentファイルのスケジューラのmodule例
  • filenameファイルの絶対パス
  • loadedがロードされているかどうか
  • .
  • 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;
    };
    _.ロード方法はファイルの主な流れをロードする方法です.
  • relove Filenameメソッドは、入力された相対パスまたはパケット名からファイルの絶対パスを算出する.
  • .
  • キャッシュに直接キャッシュ結果があれば、
  • モジュールを持参した場合、直接結果を返します.
  • そうでなければ、moduleのインスタンスを作成してキャッシュし、ファイルをロードしようと試みた後、ファイルの露出結果を返します.
  • これはrequire方法のワークフローです.ここで重要なポイントはファイルがロードされてキャッシュされた結果です.
    この方法で重要な2つのステップ:
  • Module.ursoveFilename()処理ファイルパス
  • 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;
    };
    この方法の主な流れ:
  • モジュールの中にあればファイル名に戻ります.
  • は、このファイルの可能なパスを算出する
  • .
  • は、可能な経路の中から真の経路を探し出し、引き返す.
  • がないとエラーが発生します.
  • なぜ可能なパスを算出するかというと、ファイルの出所が多いからです.相対的なパスの下のファイル、システムはモジュールを持っています.また、nodemumodulesの中のカバンかもしれません.第三のnodemumodulesフォルダの位置が確定できないなら、パスの計算をします.
    パス計算
    パス計算の二つの方法:
    /**
     *          
     * @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;
    };
    この中でどうやって可能なパスを計算しますか?
  • モジュールを持参する場合、可能な経路は[]です.
  • パスは相対パスではありません.おそらく経路はnode環境の持ち込みパケット(グローバル実装のnpm i XXX-g)です.
  • 调节者がいないと、プロジェクトのnodemoduleのカバンかもしれません.
  • そうでなければ、使用者の経路から絶対パスを算出します.
  • 二つ目の方法はModule.unode ModulePathsと推測項目の中のnodemoodulesフォルダのパスです.
    この計算方法はとても面白いです.多く見てみる価値があります.
    正確にマッチ
    その後、可能なすべての経路において、唯一の結果を見つけるにはどうすればいいですか?
    /**
     *                 
     * @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;
    };
    正確なパスマッチングの過程は比較的簡単で、すべてのテストです.
  • ファイルの拡張子があいまいです.js、json、node
  • 現在のパスがキャッシュにあるなら、そのままキャッシュに戻ります.
  • はすべてのパスを順次巡回します.
  • ファイルがパスと直接一致しているかどうか
  • パスと拡張子名が
  • に一致するかどうか
  • は、package.jsonのカバンかどうか
  • です.
  • ディレクトリ名+index+拡張子名
  • が存在するかどうか
  • は、見つかったファイルパスをキャッシュに戻し、
  • に戻る.
  • そうでなければ、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.export通信を通じて、例えば:
    (function (exports, require, module, __filename, __dirname) {
        //      
    });
    このように、jsファイルでは、requireとmoduleは、実際のグローバル変数ではなく、注入された変数であることが分かりました.このように砂の箱を閉じてモジュール化されています.
    初期化
    最後に現在のモジュールを初期化します.
    // 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モジュールの特徴を整理します.
  • は、comonjs仕様ではモジュールごとにModuleの例です.
  • requireメソッドを呼び出してモジュールファイルをロードします.
  • .ファイルの絶対パスを解析する.
  • .レノベLookupaths解析ファイルケンの絶対パス
  • _findPathマッチングは、ファイルの絶対パス
  • を見つけることを試みる.
  • ロード解析ファイル
  • Module.uextension[extension]は、拡張機能が異なる場合は、ロードを試みることができます.
  • _comple沙箱コンパイル
  • requireの戻り値はmodule.exportian{};
  • である.
    使用上の注意点
    そして、ソースの読み取りを通して、私もいくつかの点に気づいていませんでした.
  • Moduleはグローバルオブジェクトです.requireは違います.
  • require方法によって許容される経路は、次のようにすることができる.
  • システムのモジュール(fs、path)
  • グローバル実装モジュール
  • プロジェクトのインストールモジュール
  • は、モジュールファイル
  • に相対/絶対パスを介してマッチングすることができる.
  • パスファイルは、拡張子を書かなくてもいいです.デフォルトサポート(.js,json,.node)
  • パスはindex(./components=]./components/index)
  • を書かなくてもいいです.
  • は、サンドボックスのコンパイルによってモジュール化され、リキッドとモディはサンドボックスに注入されたオブジェクトである.
  • モジュールをロードするとキャッシュされます.二次コンパイルはできません.
  • requireの戻り値はmodule.eports{}である.
  • export=module.exports;
  • 最後に私はソースに対して中国語の注釈を行いました.
    参照
  • require()ソースコードの解読
  • NodeJSモジュールロード方法requireソース分析
  • ソースコード解析Node.jsの中の一つのファイルがrequireされた後に発生した物語
  • 【NodeJS】浅分析負荷モジュールの機構