文章提綱:
第一部分:介紹模塊規范及之間區別
第二部分:以 node.js 實現模塊化規范 源碼,深入學習。
一、模塊規范
define(id?, dependencies?, factory); //id :可選參數,它指的是模塊的名字。 //dependencies:可選參數,定義中模塊所依賴模塊的數組。 //factory:模塊初始化要執行的函數或對象
1 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { 2 exports.verb = function() { 3 return beta.verb(); 4 //Or: 5 //return require("beta").verb(); 6 } 7 });
1.2、require() 函數
require([module], callback); //module:一個數組,里面的成員就是要加載的模塊. //callback:模塊加載成功之后的回調函數。
需要注意的是 ,module 有多少個元素,callback 就有多少個傳參,位置一一對應。
使用的栗子:
require(["a","b","c"],function(a,b,c){ //code here });
define(factory); //factory:模塊初始化要執行的函數或對象,為函數時,表示是模塊的構造方法。
//執行該構造方法,可以得到模塊向外提供的接口。
//factory 方法在執行時,默認會傳入三個參數:require、exports 和 module。
//其中require用來獲取其他模塊提供的接口,exports用來向外提供模塊接口,module是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法。
使用的栗子:
1 define(function(require, exports, module) { 2 var a = require('./a') 3 a.doSomething() 4 // 此處略去 100 行 5 var b = require('./b') // 依賴可以就近書寫 6 b.doSomething() 7 // … 8 // 對外提供 doSomething 方法 9 exports.doSomething = function() {}; 10 });
而調用CMD編寫的模塊的方法是:
1 seajs.use("a")//調用a模塊 2 //這里就設計到SeaJS的使用了: 3 //- 引入sea.js的庫 4 //- 如何變成模塊? 5 // - define 6 //- 如何調用模塊? 7 // -sea.js.use 8 //- 如何依賴模塊? 9 // -require
1 //定義模塊 math.js 2 var random=Math.random()*10; 3 function printRandom(){ 4 console.log(random) 5 } 6 7 function printIntRandom(){ 8 console.log(Math.floor(random)) 9 } 10 //模塊輸出 11 module.exports={ 12 printRandom:printRandom, 13 printIntRandom:printIntRandom 14 } 15 //加載模塊 math.js 16 var math=require('math') 17 //調用模塊提供的方法 18 math.printIntRandom() 19 math.printRandom()
4、模塊規范之間的區別
A、首先說一下 CommonJS與其它兩種的區別:CommonJS采用的就是同步加載方式,而其它兩種都是異步的。
舉個栗子:
commonJS中:
1 var math = require('math'); 2 math.add(2, 3);
第二行 math.add(2, 3),在第一行 require('math') 之后運行,因此必須等 math.js 加載完成。也就是說,如果加載時間很長,整個應用就會停在那里等。
AMD中:
1 require(['math'], function (math) { 2 math.add(2, 3); 3 }); 4 5 console.log("222");
這個是不會阻遏后面語句的執行的,等到什么時候 math 模塊加載出來進行回調函數就可以了。
PS:由於 Node.js 主要用於服務器編程,模塊文件一般都已經存在於本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以 CommonJS 規范比較適用。但是,如果是瀏覽器環境,要從服務器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用 AMD 規范。
通常我們在使用一個模塊的時候在 js 中都是這樣引用的:
var math = require('math'); math.add(2, 3);
從 require 方法本身是如何實現的入手,一步一步看:(代碼全部來自 node.js [https://github.com/nodejs/node] 源碼)
require 方法封裝在 node 源碼中的 lib 文件夾里的 module.js 中
1 // Loads a module at the given file path. Returns that module's 2 // `exports` property. 3 // 給定一個模塊目錄,返回該模塊的 exports 屬性 4 Module.prototype.require = function(path) { 5 // assert() 頭部引入,主要用於斷言,如果表達式不符合預期,就拋出一個錯誤。 6 // assert方法接受兩個參數,當第一個參數對應的布爾值為true時,不會有任何提示,返回undefined。 7 // 當第一個參數對應的布爾值為false時,會拋出一個錯誤,該錯誤的提示信息就是第二個參數設定的字符串。 8 assert(path, 'missing path'); //斷言是否有path 9 assert(typeof path === 'string', 'path must be a string'); //斷言 path是否是個字符串 10 11 return Module._load(path, this, /* isMain */ false); //require方法主要是為了引出_load方法。 12 //_load函數三個參數: path 當前加載的模塊名稱,parent 父親模塊,其實是誰導入了該模塊, 13 // /* isMain */ false 是不是主入口文件 14 };
1 // Check the cache for the requested file. 2 // 1. If a module already exists in the cache: return its exports object. 3 // 2. If the module is native: call `NativeModule.require()` with the 4 // filename and return the result. 5 // 3. Otherwise, create a new module for the file and save it to the cache. 6 // Then have it load the file contents before returning its exports 7 // object. 8 // 從緩存中查找所要加載的模塊 9 // 1. 如果一個模塊已經存在於緩存中:直接返回它的exports對象 10 // 2. 如果模塊是一個本地模塊,調用'NativeModule.require()'方法,filename作為參數,並返回結果 11 // 3. 否則,使用這個文件創建一個新模塊並把它加入緩存中。在加載它只會返回exports對象。 12 // _load函數三個參數: path 當前加載的模塊名稱,parent 父親模塊,/* isMain */ false 是不是主入口文件 13 Module._load = function(request, parent, isMain) { 14 if (parent) { 15 //頭部引入了 Module._debug = util.debuglog('module');const debug = Module._debug; 16 // 這個方法用來打印出調試信息,具體可以看 https://chyingp.gitbooks.io/nodejs/%E6%A8%A1%E5%9D%97/util.html 17 debug('Module._load REQUEST %s parent: %s', request, parent.id); 18 19 } 20 21 // 找到當前的需要解析的文件名 22 var filename = Module._resolveFilename(request, parent, isMain); 23 24 //如果已經有的緩存,直接返回緩存的exports 25 var cachedModule = Module._cache[filename]; 26 if (cachedModule) { 27 return cachedModule.exports; 28 } 29 30 //如果模塊是一個內部模塊,調用內部方法'NativeModule.require()'方法,filename作為參數,並返回結果 31 if (NativeModule.nonInternalExists(filename)) { 32 debug('load native module %s', request); 33 return NativeModule.require(filename); 34 } 35 36 //創建一個新模塊 37 var module = new Module(filename, parent); 38 39 //是否為主模塊, 40 if (isMain) { 41 //主模塊的話,需要將當前的module賦值給process.mainModule 42 process.mainModule = module; 43 //主模塊的id特殊的賦值為"." 44 module.id = '.'; 45 } 46 47 //並把新模塊加入緩存中 48 Module._cache[filename] = module; 49 50 //嘗試導入模塊的操作 51 tryModuleLoad(module, filename); 52 53 // 返回新創建模塊中的exports,也就是暴露在外面的方法屬性等。 54 return module.exports; 55 };
Module._load 中調用了 Module._resolveFilename() 方法
1 // 負責具體filename的文件查找 2 // 參數 request 當前加載的模塊名稱,parent 父親模塊,/* isMain */ false 是不是主入口文件 3 Module._resolveFilename = function(request, parent, isMain) { 4 5 //NativeModule用於管理js模塊,頭部引入的。 6 //NativeModule.nonInternalExists()用來判斷是否是原生模塊且不是內部模塊, 7 //所謂內部模塊就是指 lib/internal 文件目錄下的模塊,像fs等。 8 //滿足 是原生模塊且不是內部模塊,則直接返回 當前加載的模塊名稱request。 9 if (NativeModule.nonInternalExists(request)) { 10 return request; 11 } 12 13 // Module._resolveLookupPaths()函數返回一個數組[id , paths], 14 // paths是一個 可能 包含這個模塊的文件夾路徑(絕對路徑)數組 15 var paths = Module._resolveLookupPaths(request, parent, true); 16 17 // look up the filename first, since that's the cache key. 18 // 確定哪一個路徑為真,並且添加到緩存中 19 var filename = Module._findPath(request, paths, isMain); 20 21 // 如果沒有找到模塊,報錯 22 if (!filename) { 23 var err = new Error(`Cannot find module '${request}'`); 24 err.code = 'MODULE_NOT_FOUND'; 25 throw err; 26 } 27 28 // 找到模塊則直接返回 29 return filename; 30 };
Module._resolveFilename 調用了 Module._resolveLookupPaths() 方法 和 Module._findPath() 方法。
這兩個方法主要是對模塊路徑的查找,這里要說一下 node 模塊路徑解析,方便對下面兩個函數的理解,大家可以對照着理解。
根據require函數的參數形式的不同,比如說直接引一個文件名 require("moduleA"),或者是路徑require("./moduleA")等,查找方式會有一些變化:
從 Y 路徑的模塊 require(X) 1. 如果 X 是一個核心模塊, a. 返回核心模塊 //核心模塊是指node.js下lib的內容 b. 結束 2. 如果 X 是以 './' 或 '/' 或 '../' 開頭 a. 加載文件(Y + X) b. 加載目錄(Y + X) 3. 加載Node模塊(X, dirname(Y)) // 導入一個NODE_MODULE,返回 4. 拋出 "未找到" // 上述都沒找到,直接排出沒找到的異常。 加載文件(X) 1. 如果 X 是一個文件,加載 X 作為 JavaScript 文本。結束 2. 如果 X.js 是一個文件,加載 X.js 作為 JavaScript 文本。結束 3. 如果 X.json 是一個文件,解析 X.json 成一個 JavaScript 對象。結束 4. 如果 X.node 是一個文件,加載 X.node 作為二進制插件。結束 加載目錄(X) 1. 如果 X/package.json 是一個文件, a. 解析 X/package.json,查找 "main" 字段 b. let M = X + (json main 字段) c. 加載文件(M) 2. 如果 X/index.js 是一個文件,加載 X/index.js 作為 JavaScript 文本。結束 3. 如果 X/index.json 是一個文件,解析 X/index.json 成一個 JavaScript 對象。結束 4. 如果 X/index.node 是一個文件,加載 X/index.node 作為二進制插件。結束 加載Node模塊(X, START) 1. let DIRS=NODE_MODULES_PATHS(START) //得到 node_module 文件目錄 2. for each DIR in DIRS: // 遍歷所有的路徑 直到找到 x ,x 可能是 文件或者是目錄 a. 加載文件(DIR/X) b. 加載目錄(DIR/X) NODE_MODULES_PATHS(START) //具體NODE_MODULES文件目錄算法 1. let PARTS = path split(START) 2. let I = count of PARTS - 1 3. let DIRS = [] 4. while I >= 0, a. if PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 .. I] + "node_modules") c. DIRS = DIRS + DIR d. let I = I - 1 5. return DIRS
1、Module._resolveLookupPaths() 方法
1 // 'index.' character codes 2 var indexChars = [ 105, 110, 100, 101, 120, 46 ]; 3 var indexLen = indexChars.length; 4 //_resolveLookupPaths() 方法用來查找模塊,返回一個數組,數組第一項為模塊名稱即request,數組第二項返回一個可能包含這個模塊的文件夾路徑數組 5 // 6 //處理了如下幾種情況: 7 // 1、是原生模塊且不是內部模塊 8 // 2、如果路徑不以"./" 或者'..'開頭或者只有一個字符串,即是引用模塊名的方式,即require('moduleA'); 9 // 2.1以 '/' 為前綴的模塊是文件的絕對路徑。 例如,require('/home/marco/foo.js') 會加載 /home/marco/foo.js 文件。 10 // 2.2以 './' 為前綴的模塊是相對於調用 require() 的文件的。 也就是說,circle.js 必須和 foo.js 在同一目錄下以便於 require('./circle') 找到它。 11 // 2.3當沒有以 '/'、'./' 或 '../' 開頭來表示文件時,這個模塊必須是一個核心模塊或加載自 node_modules 目錄。 12 Module._resolveLookupPaths = function(request, parent, newReturn) { //request 當前加載的模塊名稱,parent 父親模塊 13 14 //NativeModule用於管理js模塊,頭部引入的。 15 //NativeModule.nonInternalExists()用來判斷是否是原生模塊且不是內部模塊,所謂內部模塊就是指 lib/internal 文件目錄下的模塊,像fs等。 16 if (NativeModule.nonInternalExists(request)) { 17 debug('looking for %j in []', request); 18 19 //滿足 是原生模塊且不是內部模塊,也就是說是node.js下lib文件夾下的模塊, 20 //但不包含lib/internal 文件目錄下的模塊,並且newReturn 為true,則返回null , 21 //如果newReturn 為false 則返回[request, []]。 22 return (newReturn ? null : [request, []]); 23 } 24 25 // Check for relative path 26 // 檢查相關路徑 27 // 如果路徑不以"./"或者'..'開頭或者只有一個字符串,即是引用模塊名的方式,即require('moduleA'); 28 if (request.length < 2 || 29 request.charCodeAt(0) !== 46/*.*/ || 30 (request.charCodeAt(1) !== 46/*.*/ && 31 request.charCodeAt(1) !== 47/*/*/)) { 32 //全局變量,在Module._initPaths 函數中賦值的變量,modulePaths記錄了全局加載依賴的根目錄 33 var paths = modulePaths; 34 35 // 設置一下父親的路徑,其實就是誰導入了當前模塊 36 if (parent) { 37 if (!parent.paths) 38 paths = parent.paths = []; 39 else 40 paths = parent.paths.concat(paths); 41 } 42 43 // Maintain backwards compat with certain broken uses of require('.') 44 // by putting the module's directory in front of the lookup paths. 45 // 如果只有一個字符串,且是 . 46 if (request === '.') { 47 if (parent && parent.filename) { 48 paths.unshift(path.dirname(parent.filename)); 49 } else { 50 paths.unshift(path.resolve(request)); 51 } 52 } 53 54 debug('looking for %j in %j', request, paths); 55 56 //直接返回 57 return (newReturn ? (paths.length > 0 ? paths : null) : [request, paths]); 58 } 59 60 // with --eval, parent.id is not set and parent.filename is null 61 // 處理父親模塊為空的情況 62 if (!parent || !parent.id || !parent.filename) { 63 // make require('./path/to/foo') work - normally the path is taken 64 // from realpath(__filename) but with eval there is no filename 65 // 生成新的目錄, 在系統目錄 modulePaths,當前目錄 和 "node_modules" 作為候選的路徑 66 var mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths); 67 68 debug('looking for %j in %j', request, mainPaths); 69 //直接返回 70 return (newReturn ? mainPaths : [request, mainPaths]); 71 } 72 73 // Is the parent an index module? 74 // We can assume the parent has a valid extension, 75 // as it already has been accepted as a module. 76 // 處理父親模塊是否為index模塊,即 path/index.js 或者 X/index.json等 帶有index字樣的module 77 const base = path.basename(parent.filename); // path.basename()返回路徑中的最后一部分 78 var parentIdPath; 79 if (base.length > indexLen) { 80 var i = 0; 81 82 //檢查 引入的模塊名中是否有 "index." 字段,如果有, i === indexLen。 83 for (; i < indexLen; ++i) { 84 if (indexChars[i] !== base.charCodeAt(i)) 85 break; 86 } 87 88 // 匹配 "index." 成功,查看是否有多余字段以及剩余部分的匹配情況 89 if (i === indexLen) { 90 // We matched 'index.', let's validate the rest 91 for (; i < base.length; ++i) { 92 const code = base.charCodeAt(i); 93 94 // 如果模塊名中有 除了 _, 0-9,A-Z,a-z 的字符 則跳出,繼續下一次循環 95 if (code !== 95/*_*/ && 96 (code < 48/*0*/ || code > 57/*9*/) && 97 (code < 65/*A*/ || code > 90/*Z*/) && 98 (code < 97/*a*/ || code > 122/*z*/)) 99 break; 100 } 101 102 103 if (i === base.length) { 104 // Is an index module 105 parentIdPath = parent.id; 106 } else { 107 // Not an index module 108 parentIdPath = path.dirname(parent.id); //path.dirname() 返回路徑中代表文件夾的部分 109 } 110 } else { 111 // Not an index module 112 parentIdPath = path.dirname(parent.id); 113 } 114 } else { 115 // Not an index module 116 parentIdPath = path.dirname(parent.id); 117 } 118 119 //拼出絕對路徑 120 //path.resolve([from ...], to) 將 to 參數解析為絕對路徑。 121 //eg:path.resolve('/foo/bar', './baz') 輸出'/foo/bar/baz' 122 var id = path.resolve(parentIdPath, request); 123 124 // make sure require('./path') and require('path') get distinct ids, even 125 // when called from the toplevel js file 126 // 確保require('./path')和require('path')兩種形式的,獲得不同的 ids 127 if (parentIdPath === '.' && id.indexOf('/') === -1) { 128 id = './' + id; 129 } 130 131 debug('RELATIVE: requested: %s set ID to: %s from %s', request, id, 132 parent.id); 133 //path.dirname() 返回路徑中代表文件夾的部分 134 var parentDir = [path.dirname(parent.filename)]; 135 136 debug('looking for %j in %j', id, parentDir); 137 138 // 當我們以"./" 等方式require時,都是以當前引用他的模塊,也就是父親模塊為對象路徑的 139 return (newReturn ? parentDir : [id, parentDir]); 140 };
2、Module._findPath() 方法
1 var warned = false; 2 //_findPath用於從可能的路徑中確定哪一個路徑為真,並且添加到緩存中 3 //參數request 當前加載的模塊名稱, 4 //paths ,Module._resolveLookupPaths()函數返回一個數組[id , paths],即模塊可能在的所有路徑, 5 // /* isMain */ false 是不是主入口文件 6 Module._findPath = function(request, paths, isMain) { 7 8 //path.isAbsolute()判斷參數 path 是否是絕對路徑。 9 if (path.isAbsolute(request)) { 10 paths = ['']; 11 } else if (!paths || paths.length === 0) { 12 return false; 13 } 14 15 16 var cacheKey = request + '\x00' + 17 (paths.length === 1 ? paths[0] : paths.join('\x00')); 18 var entry = Module._pathCache[cacheKey]; 19 20 //判斷是否在緩存中,如果有則直接返回 21 if (entry) 22 return entry; 23 24 //如果不在緩存中,則開始查找 25 var exts; 26 // 當前加載的模塊名稱大於0位並且最后一位是 / ,即是否有后綴的目錄斜杠 27 var trailingSlash = request.length > 0 && 28 request.charCodeAt(request.length - 1) === 47/*/*/; 29 30 // For each path 31 // 循環每一個可能的路徑paths 32 for (var i = 0; i < paths.length; i++) { 33 34 // Don't search further if path doesn't exist 35 // 如果路徑存在就繼續執行,不存在就繼續檢驗下一個路徑 stat 獲取路徑狀態 36 const curPath = paths[i]; 37 if (curPath && stat(curPath) < 1) continue; 38 var basePath = path.resolve(curPath, request); //生成絕對路徑 39 var filename; 40 41 //stat 頭部定義的函數,用來獲取路徑狀態,判斷路徑類型,是文件還是文件夾 42 var rc = stat(basePath); 43 //如果沒有后綴的目錄斜杠,那么就有可能是文件或者是文件夾名 44 if (!trailingSlash) { 45 // 若是文件 46 if (rc === 0) { // File. 47 48 // 如果是使用模塊的符號路徑而不是真實路徑,並且不是主入口文件 49 if (preserveSymlinks && !isMain) { 50 filename = path.resolve(basePath); 51 } else { 52 filename = toRealPath(basePath); //獲取當前執行文件的真實路徑 53 } 54 55 // 若是目錄 56 } else if (rc === 1) { // Directory. 57 if (exts === undefined) 58 //目錄中是否存在 package.json 59 //通過package.json文件,返回相應路徑 60 exts = Object.keys(Module._extensions); 61 filename = tryPackage(basePath, exts, isMain); 62 } 63 64 // 如果嘗試了上面都沒有得到filename 匹配所有擴展名進行嘗試,是否存在 65 if (!filename) { 66 // try it with each of the extensions 67 if (exts === undefined) 68 exts = Object.keys(Module._extensions); 69 // 該模塊文件加上后綴名js .json .node進行嘗試,是否存在 70 filename = tryExtensions(basePath, exts, isMain); 71 } 72 } 73 74 // 如果仍然沒有得到filename,並且路徑類型是文件夾 75 if (!filename && rc === 1) { // Directory. 76 if (exts === undefined) 77 // 目錄中是否存在 package.json 78 // 通過package.json文件,返回相應路徑 79 exts = Object.keys(Module._extensions); 80 filename = tryPackage(basePath, exts, isMain); 81 } 82 83 // 如果仍然沒有得到filename,並且路徑類型是文件夾 84 if (!filename && rc === 1) { // Directory. 85 // try it with each of the extensions at "index" 86 // 是否存在目錄名 + index + 后綴名 87 // 嘗試 index.js index.json index.node 88 if (exts === undefined) 89 exts = Object.keys(Module._extensions); 90 91 //tryExtensions()頭部定義方法,用來檢查文件加上js node json后綴是否存在 92 filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain); 93 } 94 95 96 if (filename) { 97 // Warn once if '.' resolved outside the module dir 98 if (request === '.' && i > 0) { 99 if (!warned) { 100 warned = true; 101 process.emitWarning( 102 'warning: require(\'.\') resolved outside the package ' + 103 'directory. This functionality is deprecated and will be removed ' + 104 'soon.', 105 'DeprecationWarning', 'DEP0019'); 106 } 107 } 108 109 // 將找到的文件路徑存入返回緩存,然后返回 110 Module._pathCache[cacheKey] = filename; 111 return filename; 112 } 113 } 114 115 // 所以從這里可以看出,對於具體的文件的優先級: 116 // 1. 具體文件。 117 // 2. 加上后綴。 118 // 3. package.json 119 // 4 index加上后綴 120 // 可能的路徑以當前文件夾,nodejs系統文件夾和node_module中的文件夾為候選,以上述順序找到任意一個, 121 // 就直接返回 122 123 // 沒有找到文件,返回false 124 return false; 125 };
Module._load 中還調用了 tryModuleLoad() 方法
1 function tryModuleLoad(module, filename) { 2 var threw = true; 3 4 //try catch一下,如果裝載失敗,就會從cache中將這個模塊刪除。 5 try { 6 7 //做真正的導入模塊的操作 8 module.load(filename); 9 threw = false; 10 } finally { 11 if (threw) { 12 delete Module._cache[filename]; 13 } 14 } 15 }
tryModuleLoad() 中調用了 Module.prototype.load() 方法
1 // Given a file name, pass it to the proper extension handler. 2 // 指定一個文件名,導入模塊,調用適當擴展處理函數,當前主要是js,json,和node 3 Module.prototype.load = function(filename) { 4 debug('load %j for module %j', filename, this.id); 5 6 assert(!this.loaded); //斷言 確保當前模塊沒有被載入 7 this.filename = filename; // 賦值當前模塊的文件名 8 9 // Module._nodeModulePaths主要決定paths參數的值的方法。獲取node_modules文件夾所在路徑。 10 // path.dirname() 方法返回一個 path 的目錄名 path.dirname('/foo/bar/baz/asdf/quux') 11 // 返回: '/foo/bar/baz/asdf' 12 this.paths = Module._nodeModulePaths(path.dirname(filename)); 13 14 //當前文件的后綴 15 var extension = path.extname(filename) || '.js'; 16 17 //如果沒有后綴,默認為 .js 18 if (!Module._extensions[extension]) extension = '.js'; 19 20 //根據不同的后綴,執行不同的函數 21 Module._extensions[extension](this, filename); 22 this.loaded = true; 23 };
Module.prototype.load() 中調用了 Module._nodeModulePaths() 和 Module._extensions 方法
1、Module._nodeModulePaths() 根據操作系統的不同,返回不同的函數
1 //path 模塊的默認操作會根據 Node.js 應用程序運行的操作系統的不同而變化。 2 //比如,當運行在 Windows 操作系統上時,path 模塊會認為使用的是 Windows 風格的路徑。 3 //例如,對 Windows 文件路徑 C:\temp\myfile.html 使用 path.basename() 函數, 4 //運行在 POSIX 上與運行在 Windows 上會產生不同的結果: 5 //在 POSIX 上: 6 //path.basename('C:\\temp\\myfile.html'); 7 // 返回: 'C:\\temp\\myfile.html' 8 // 9 // 在 Windows 上: 10 //path.basename('C:\\temp\\myfile.html'); 11 // 返回: 'myfile.html' 12 // 13 // 以下就是根據不同的操作系統返回不同的路徑格式 ,具體可以了解http://nodejs.cn/api/path.html 14 // 15 // 16 // Module._nodeModulePaths主要決定paths參數的值的方法。獲取node_modules文件夾所在路徑。 17 // 'node_modules' character codes reversed 18 var nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; 19 var nmLen = nmChars.length; 20 if (process.platform === 'win32') { 21 // 'from' is the __dirname of the module. 22 Module._nodeModulePaths = function(from) { 23 // guarantee that 'from' is absolute. 24 from = path.resolve(from); 25 26 // note: this approach *only* works when the path is guaranteed 27 // to be absolute. Doing a fully-edge-case-correct path.split 28 // that works on both Windows and Posix is non-trivial. 29 30 // return root node_modules when path is 'D:\\'. 31 // path.resolve will make sure from.length >=3 in Windows. 32 if (from.charCodeAt(from.length - 1) === 92/*\*/ && 33 from.charCodeAt(from.length - 2) === 58/*:*/) 34 return [from + 'node_modules']; 35 36 const paths = []; 37 var p = 0; 38 var last = from.length; 39 for (var i = from.length - 1; i >= 0; --i) { 40 const code = from.charCodeAt(i); 41 // The path segment separator check ('\' and '/') was used to get 42 // node_modules path for every path segment. 43 // Use colon as an extra condition since we can get node_modules 44 // path for dirver root like 'C:\node_modules' and don't need to 45 // parse driver name. 46 if (code === 92/*\*/ || code === 47/*/*/ || code === 58/*:*/) { 47 if (p !== nmLen) 48 paths.push(from.slice(0, last) + '\\node_modules'); 49 last = i; 50 p = 0; 51 } else if (p !== -1) { 52 if (nmChars[p] === code) { 53 ++p; 54 } else { 55 p = -1; 56 } 57 } 58 } 59 60 return paths; 61 }; 62 } else { // posix 63 // 'from' is the __dirname of the module. 64 Module._nodeModulePaths = function(from) { 65 // guarantee that 'from' is absolute. 66 from = path.resolve(from); 67 // Return early not only to avoid unnecessary work, but to *avoid* returning 68 // an array of two items for a root: [ '//node_modules', '/node_modules' ] 69 if (from === '/') 70 return ['/node_modules']; 71 72 // note: this approach *only* works when the path is guaranteed 73 // to be absolute. Doing a fully-edge-case-correct path.split 74 // that works on both Windows and Posix is non-trivial. 75 const paths = []; 76 var p = 0; 77 var last = from.length; 78 for (var i = from.length - 1; i >= 0; --i) { 79 const code = from.charCodeAt(i); 80 if (code === 47/*/*/) { 81 if (p !== nmLen) 82 paths.push(from.slice(0, last) + '/node_modules'); 83 last = i; 84 p = 0; 85 } else if (p !== -1) { 86 if (nmChars[p] === code) { 87 ++p; 88 } else { 89 p = -1; 90 } 91 } 92 } 93 94 // Append /node_modules to handle root paths. 95 paths.push('/node_modules'); 96 97 return paths; 98 }; 99 }
2、Module._extensions 方法
1 // 根據不同的文件類型,三種后綴,Node.js會進行不同的處理和執行 2 // 對於.js的文件會,先同步讀取文件,然后通過module._compile解釋執行。 3 // 對於.json文件的處理,先同步的讀入文件的內容,無異常的話直接將模塊的exports賦值為json文件的內容 4 // 對於.node文件的打開處理,通常為C/C++文件。 5 // Native extension for .js 6 Module._extensions['.js'] = function(module, filename) { 7 // 同步讀取文件 8 var content = fs.readFileSync(filename, 'utf8'); 9 10 // internalModule.stripBOM()剝離 utf8 編碼特有的BOM文件頭, 11 // 然后通過module._compile解釋執行 12 module._compile(internalModule.stripBOM(content), filename); 13 }; 14 15 16 // Native extension for .json 17 Module._extensions['.json'] = function(module, filename) { 18 // 同步的讀入文件的內容 19 var content = fs.readFileSync(filename, 'utf8'); 20 try { 21 // internalModule.stripBOM()剝離 utf8 編碼特有的BOM文件頭, 22 // 然后將模塊的exports賦值為json文件的內容 23 module.exports = JSON.parse(internalModule.stripBOM(content)); 24 } catch (err) { 25 // 異常處理 26 err.message = filename + ': ' + err.message; 27 throw err; 28 } 29 }; 30 31 32 //Native extension for .node 33 Module._extensions['.node'] = function(module, filename) { 34 // 對於.node文件的打開處理,通常為C/C++文件。 35 return process.dlopen(module, path._makeLong(filename)); 36 };
針對 .js 后綴的,在 Module._extensions 還調用了 module._compile() 方法
1 // Resolved path to process.argv[1] will be lazily placed here 2 // (needed for setting breakpoint when called with --debug-brk) 3 var resolvedArgv; 4 // Run the file contents in the correct scope or sandbox. Expose 5 // the correct helper variables (require, module, exports) to 6 // the file. 7 // Returns exception, if any. 8 // 此方法用於模塊的編譯。 9 // 參數content 主要是模塊js文件的主要內容,filename 是js文件的文件名 10 Module.prototype._compile = function(content, filename) { 11 // Remove shebang 12 // Shebang(也稱為 Hashbang )是一個由井號和嘆號構成的字符序列 #! 13 var contLen = content.length; 14 if (contLen >= 2) { 15 // 如果content 開頭有Shebang 16 if (content.charCodeAt(0) === 35/*#*/ && 17 content.charCodeAt(1) === 33/*!*/) { 18 if (contLen === 2) { 19 // Exact match 20 content = ''; 21 } else { 22 // Find end of shebang line and slice it off 23 // 找到以shebang開頭的句子的結尾,並將其分開,留下剩余部分 賦值給content 24 var i = 2; 25 for (; i < contLen; ++i) { 26 var code = content.charCodeAt(i); 27 if (code === 10/*\n*/ || code === 13/*\r*/) 28 break; 29 } 30 if (i === contLen) 31 content = ''; 32 else { 33 // Note that this actually includes the newline character(s) in the 34 // new output. This duplicates the behavior of the regular expression 35 // that was previously used to replace the shebang line 36 content = content.slice(i); 37 } 38 } 39 } 40 } 41 42 // create wrapper function 43 // Module.wrap頭部引入,主要用來給content內容包裝頭尾,類似於 44 // (function (exports, require, module, __filename, __dirname) { 45 // -----模塊源碼 content----- 46 // }); 47 var wrapper = Module.wrap(content); 48 49 // 包裝好的文本就可以送到vm中執行了,這部分就應該是v8引擎的事情, 50 // runInThisContext將被包裝后的源字符串轉成可執行函數,runInThisContext的作用,類似eval 51 var compiledWrapper = vm.runInThisContext(wrapper, { 52 filename: filename, 53 lineOffset: 0, 54 displayErrors: true 55 }); 56 57 var inspectorWrapper = null; 58 // 處理debug模式, 59 if (process._debugWaitConnect && process._eval == null) { 60 if (!resolvedArgv) { 61 // we enter the repl if we're not given a filename argument. 62 if (process.argv[1]) { 63 resolvedArgv = Module._resolveFilename(process.argv[1], null, false); 64 } else { 65 resolvedArgv = 'repl'; 66 } 67 } 68 69 // Set breakpoint on module start 70 if (filename === resolvedArgv) { 71 delete process._debugWaitConnect; 72 inspectorWrapper = getInspectorCallWrapper(); 73 if (!inspectorWrapper) { 74 const Debug = vm.runInDebugContext('Debug'); 75 Debug.setBreakPoint(compiledWrapper, 0, 0); 76 } 77 } 78 } 79 80 // 獲取當前的文件的路徑 81 var dirname = path.dirname(filename); 82 83 //生成require方法 84 var require = internalModule.makeRequireFunction(this); 85 86 //依賴模塊 87 var depth = internalModule.requireDepth; 88 if (depth === 0) stat.cache = new Map(); 89 var result; 90 91 //直接調用content經過包裝后的wrapper函數,將module模塊中的exports,生成的require, 92 //this也就是新創建的module,filename, dirname作為參數傳遞給模塊 93 //類似於 94 //(function (exports, require, module, __filename, __dirname) { 95 // -----模塊源碼 content----- 96 // })( this.exports, require, this, filename, dirname); 97 // 這就是為什么我們可以直接在module文件中,直接訪問exports, module, require函數的原因 98 if (inspectorWrapper) { 99 result = inspectorWrapper(compiledWrapper, this.exports, this.exports, 100 require, this, filename, dirname); 101 } else { 102 result = compiledWrapper.call(this.exports, this.exports, require, this, 103 filename, dirname); 104 } 105 if (depth === 0) stat.cache = null; 106 return result; 107 };
Module.prototype._compile 中調用了 Module.wrap 這個方法就是用了給 content 包裝的主要函數, 它來自頭部的引用:
1 //Module.wrapper和Module.wrap的方法寫在下面, 2 //給傳入進去的script也就是咱們的content --js文件內容套了一個殼,使其最后變成類似於如下的樣子: 3 // 4 //(function (exports, require, module, __filename, __dirname) { 5 // -----模塊源碼----- 6 // }); 7 // 8 // NativeModule.wrap = function(script) { 9 // return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; 10 // }; 11 12 // NativeModule.wrapper = [ 13 // '(function (exports, require, module, __filename, __dirname) { ', 14 // '\n});' 15 // ]; 16 Module.wrapper = NativeModule.wrapper; 17 Module.wrap = NativeModule.wrap;

(圖一)
現在咱們再看這個圖,梳理一下剛才的代碼,就清晰多了。
(圖二)

在 bootstrap_node.js 中定義了一個 NativeModule 對象,用於加載核心模塊,如 module.js、http.js 等即 lib 文件夾下的 排除 lib/internal 目錄下的 js 模塊。
在這個 NativeModule 對象中也定義了 require 方法,compile 方法、wrap 方法(用於包裝頭尾)等 都和上面的 module.js 中的相應的方法意思是一樣的,可以下載源碼了解一下。
