爲了讓Node.js的文件能夠相互調用,Node.js提供了一個簡單的模塊系統。 html
模塊是Node.js 應用程序的基本組成部分,文件和模塊是一一對應的。換言之, node
一個 Node.js 文件就是一個模塊,這個文件多是JavaScript 代碼、JSON 或者編譯過的C/C++ 擴展。git
Node.js中沒有全局做用域,只有模塊做用域 github
——外部訪問不到內部,內部訪問不到外部npm
模塊引⼊三部曲:json
var 自定義變量名稱 = require (「 模塊」 )
一、加載文件模塊,並執行裏面的代碼; bootstrap
二、拿到被加載的文件模塊導出的模塊對象。數組
var net = require(「net」);var fs = require(「fs」);緩存
require('/文件名');//絕對路徑require('./文件名');//相對路徑 app
require('../文件名')
若是直接引入會怎樣呢?var test = require(「test」);
模塊標識符分析:對於不一樣的標識符,模塊的查找和定位不一樣。
require()方法會將路徑解析爲真 實路徑,並以真實路徑進行加 載編譯
文件定位:
代碼追蹤棧:
Module.prototype.require --> Module.load --> Module.resolveFilename -->
Module.resolveLookupPaths --> Module._fifindPath --> fifileName(⽂件絕對路徑)
經過給定的path加載⼀個模塊,並返回該模塊的exports屬性。
const assert = require('assert').ok; ... // Loads a module at the given file path. Returns that module's 'exports' property Module.prototype.require = function(path) { assert(path, "missing path");//path不能爲空 assert(typeof path === "string", "path must be a string");//path必須是字 符串類型 return Module._load(path, this, false);//加載模塊並返回exports }
assert
assert是Node.js中的斷⾔模塊: 提供簡單的斷⾔測試功能,主要⽤於內部使⽤,也能夠
require('assert') 後在外部進⾏使⽤。
模塊⽅法:
實例:
調⽤Module._resolveFilename獲取⽂件絕對路徑,而且根據該絕對路徑添加緩存以及編譯模塊。
Module._load = function(request, parent, isMain) { //... var filename = Module.resolveFilename(request, parent); //路徑解析,絕對路徑 //... }
獲取⽂件絕對路徑。
Module._resolveFilename = function(request, parent){ //是原⽣模塊而且不是原⽣內部模塊則直接返回 if(NativeModule.nonInternalExists(request)){ return 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; }
NativeModule.nonInternalExists
nonInternalExists是Node.js原⽣模塊提供的⽅法,⽤於判斷:是原⽣模塊而且不是原⽣內部模塊。
實現⽅法⾃⾏欣賞:
NativeModule.nonInternalExists = function(id){ return NativeModule.exists(id) && !NativeModule.isInternal(id); } NativeModule.isInternal = function(id){ return id.startsWith('internal/'); }
node/lib/module.js ⽂件開頭引⼊的兩個原⽣內部模塊 const internalModule =require('internal/module'); //internal/module 便是路徑名也是id const internalUtil =require('internal/util');
也就是說在咱們⾃⼰的代碼⾥⾯是請求不到Node.js源碼⾥⾯ lib/internal/*.js
這些⽂件的,⽐如 require("internal/module")
運⾏時會報錯 Error: Cannot find module'internal/module'
。
特例 require("internal/repl")
能夠執⾏,具體什麼應⽤場景,請⾃⾏查找。
寫個測試⽂件,在⾥⾯打印 process.moduleLoadList
,能夠查看已經加載的原⽣模塊。
計算全部可能的路徑,對於核⼼模塊、相對路徑、絕對路徑、⾃定義模塊返回不一樣的數組。實現代碼相對較複雜不作分析,只看執⾏結果
根據⽂件可能路徑定位⽂件絕對路徑,包括後綴的補全(.js , .json, .node)
Module._findPath = function(request, paths){ //絕對路徑,將 paths 清空 if(path.isAbsolute(request)){ paths = ['']; } //第⼀步:若是當前路徑已在緩存中,直接返回緩存 var cacheKey = JSON.stringify({request: request, paths: paths}); if (Module._pathCache[cacheKey]) { return Module._pathCache[cacheKey]; } //獲取後綴名:.js, .json, .node const exts = Object.keys(Module._extensions); //模塊路徑是否以/結尾,若是路徑以/結尾,那麼就是⽂件夾 const trailingSlash = request.slice(-1) === '/'; // 第⼆步,依次遍歷全部路徑 for (var i = 0, PL = paths.length; i < PL; i++) { // Don't search further if path doesn't exist if (paths[i] && stat(paths[i]) < 1) continue;var basePath = path.resolve(paths[i], request); var filename; if (!trailingSlash) { // 模塊路徑⾮「/」結尾,那麼多是⽂件,也多是⽂件夾 const rc = stat(basePath); // 判斷⽂件類型,是⼀個⽂件仍是⽬錄 if (rc === 0) { //a. 若是是⼀個⽂件,則轉換爲真實路徑 filename = toRealPath(basePath); } else if (rc === 1) { //b. 若是是⼀個⽬錄,則調⽤tryPackage⽅法讀取該⽬錄下的 package.json⽂件,把⾥⾯的 main屬性設置爲filename filename = tryPackage(basePath, exts); } //c. 若是沒有讀到路徑上的⽂件,則經過tryExtensions嘗試在該路徑後依次加上.js,.json 和.node後 綴,判斷是否存在,若存在則返回加上後綴後的路徑 if (!filename) { filename = tryExtensions(basePath, exts); } } //第三步:若是依然不存在,則一樣調⽤tryPackage⽅法讀取該⽬錄下的package.json⽂件,把⾥⾯的 main屬性設置爲filename if (!filename) { filename = tryPackage(basePath, exts); } //第四步: 若是依然不存在,則嘗試在該路徑後依次加上index.js,index.json和index.node,判斷是 否 存在,若存在則返回拼接後的路徑。 if (!filename) { // try it with each of the extensions at "index" filename = tryExtensions(path.resolve(basePath, 'index'), exts); } //第五步:若解析成功,則把解析獲得的⽂件名cache起來,下次require就不⽤再次解析了 if (filename) { // Warn once if '.' resolved outside the module dir if (request === '.' && i > 0) { warned = internalUtil.printDeprecationMessage( 'warning: require(\'.\') resolved outside the package ' + 'directory. This functionality is deprecated and will be removed ' +'soon.', warned); } Module._pathCache[cacheKey] = filename; return filename; } } //第六步: 若解析失敗,則返回false return false; } //tryPackage function tryPackage(requestPath, exts, isMain) { var pkg = readPackage(requestPath); if (!pkg) return false; var filename = path.resolve(requestPath, pkg); return tryFile(filename, isMain) || //直接判斷這個⽂件是否存在並返回 tryExtensions(filename, exts, isMain) || //判斷分別以js,json,node等後綴結尾的⽂件是否存在 tryExtensions(path.resolve(filename, 'index'), exts, isMain); //判斷分別以${filename}/index.(js|json|node)等後綴結尾的⽂件是否存在 } //tryExtensions function tryExtensions(p, exts, isMain) { for (var i = 0; i < exts.length; i++) { const filename = tryFile(p + exts[i], isMain); if (filename) { return filename; } } return false; } //tryFile function tryFile(requestPath) { const rc = stat(requestPath); return rc === 0 && toRealPath(requestPath); } //toRealPath function toRealPath(requestPath) { return fs.realpathSync(requestPath, Module._realpathCache); }
查找策略
require()傳入的字符串最後一個字符不是/時:
若是是個文件夾,則查找該文件夾下是否有package.json文件,以及這個文件 當中的main字段對應的路徑(對應源碼當中的方法爲tryPackage):
require()傳入的字符串最後一個字符是/時,即require的是一個文件夾時:
Module._load = function(request, parent, isMain) { //解析⽂件絕對路徑 //第⼀步: 先檢查是否在⽂件模塊緩存中,若是有緩存,直接取緩存,Module._cache存放⽂件模塊 var filename = Module.resolveFilename(request, parent); var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; } //第⼆步: 檢測是不是原⽣模塊,若是是,使⽤原⽣模塊的加載⽅法 if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); return NativeModule.require(filename); } //第三步: 判斷⽆緩存且⾮原⽣模塊後,新建模塊實例 var module = new Module(filename, parent); if (isMain) { process.mainModule = module; module.id = '.'; } //加載模塊前,就將模塊緩存 Module._cache[filename] = module; var hadException = true; //第四步: 加載模塊 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; //加載失敗,刪除緩存 } } return module.exports; }
NativeModule.require
主要⽤來加載Node.js的⼀些原⽣模塊。
源碼:
NativeModule.require = function(id){ //一、判斷是不是⾃身 if(id == 'native_module'){ return NativeModule } //二、是否有緩存,原⽣模塊存放在NativeModule._cache中 var cached = NativeModule.getCached(id); if(cached){ return cached.exports; } //三、是不是原⽣模塊 if(!NativeModule.exists(id)){ throw new Error('No such native module ' + id); } //四、存放在模塊加載列表⾥ process.moduleLoadList.push('NativeModule ' + id); //五、載⼊該原⽣模塊、緩存、編譯、返回 var nativeModule = new NativeModule(id); nativeModule.cache(); nativeModule.compile(); return nativeModule.exports; } NativeModule.prototype.compile = function() { var source = NativeModule.getSource(this.id); source = NativeModule.wrap(source); var fn = runInThisContext(source, { filename: this.filename }); fn(this.exports, NativeModule.require, this, this.filename); this.loaded = true; }; NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) {\n','\n});' ]; NativeModule.prototype.cache = function() { NativeModule._cache[this.id] = this; };
經過步驟5找到對應的文件後Node會新建一個模塊對象,定義以下:
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 = []; }
根據路徑載入並編譯。對於不一樣的文件擴展名,其載入方法不一樣:
JS模塊編譯
Node對獲取的JavaScript文件內容進行頭尾包裝
二、包裝後的代碼會經過vm原生模塊的runInThisContext()方法,返回一個具體的 function對象。
三、將當前模塊對象的exports屬性、require()方法、module(模塊對象自身)以及 在文件定位中獲得的完整文件路徑和文件目錄做爲參數傳遞給這個function()執行。執 行後,模塊的exports屬性被返回給調用方。
Module.prototype.load = function(filename){ assert(!this.loaded); this.filename = filename; //獲取這個module路徑上全部可能的node_modules路徑 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; }
調⽤Module._extension⽅法加載不一樣格式的⽂件
如下爲js⽂件:
Module._extensions[".js"] = function(module, filename){ var content = fs.readFilSync(filename, 'utf8'); //同步讀取⽂件的⽂本內容 module._compile(internalModule.stripBOM(content), filename); //編譯 }
stripBOM內部原⽣模塊的⽅法
function stripBOM(content){ //檢測第⼀額字符是否爲BOM; //BOM:它常被⽤來當作標示⽂件是以UTF-八、UTF-16或UTF-32編碼的記號。 if(content.charCodeAt(0) === 0xFEFF){ content = content.slice(1); } return content; }
Module.prototype._compile = function(content, filename){ /** *⽂件頭部 *Module.wrapper = NativeModule.wrapper; *Module.wrap = NativeModule.wrap; */ var wrapper = Module.wrap(content); // vm.runInThisContext在⼀個v8的虛擬機內部執⾏wrapper後的代碼,相似於eval var compiledWrapper = runInThisContext(wrapper, { filename: filename, lineOffset: 0 }) //... const dirname = path.dirname(filename); /** *這個require並⾮是Module.prototype.require⽅法, *⽽是經過internalModule.makeRequireFunction從新構造出來的, *這個⽅法內部仍是依賴Module.prototype.require⽅法去加載模塊的, *同時還對這個require⽅法作了⼀些拓展。 */ const require = internalModule.makeRequireFunction.call(this); const args = [this.exports, require, this, filename, dirname]; const result = compiledWrapper.apply(this.exports, args); return result; } function makeRequireFunction() { const Module = this.constructor; const self = this; function require(path) { try { exports.requireDepth += 1; return self.require(path); } finally { exports.requireDepth -= 1; } } function resolve(request) { return Module._resolveFilename(request, self); } require.resolve = resolve; require.main = process.mainModule; // Enable support to add extra extension types. require.extensions = Module._extensions;require.cache = Module._cache; return require; }
以node index.js的形式啓動,模塊如何加載?
其實node啓動的原理跟require是⼀樣的,src/node.cc中的node::LoadEnvironment函數會被調⽤,
在該函數內則會接着調⽤lib/internal/bootstrap_node.js中的代碼,並執⾏startup函數,startup函
數會執⾏Module.runMain⽅法,⽽Module.runMain⽅法會執⾏Module._load⽅法,參數就是命令
⾏的第⼀個參數(⽐如: node index.js),如此,跟前⾯介紹的require就⾛到⼀起了。
// 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(); };
在 NodeJS 中想要導出模塊中的變量或者函數有三種方式
a.js
let name = "it6666.top"; function sum(a, b) { return a + b; } exports.str = name; exports.fn = sum;
b.js
let aModule = require("./07-a"); console.log(aModule); console.log(aModule.str); console.log(aModule.fn(10, 20));
運行結果以下所示:
a.js
let name = "it6666.top"; function sum(a, b) { return a + b; } module.exports.str = name; module.exports.fn = sum;
b.js 其實能夠不動的,我把返回值單獨的接收了一下而後在輸出打印。
let aModule = require("./07-a"); console.log(aModule); console.log(aModule.str); let res = aModule.fn(10, 20); console.log(res);
運行結果以下所示:
a.js
let name = "it6666.top"; function sum(a, b) { return a + b; } global.str = name; global.fn = sum;
b.js
let aModule = require("./07-a"); console.log(str); let res = fn(10, 20); console.log(res);
運行結果以下所示:
源碼:
https://github.com/nodejs/nod...