node.js的require()及基本用法
2009年,Node.js 項(xiàng)目誕生,所有模塊一律為 CommonJS 格式。
時(shí)至今日,Node.js 的模塊倉(cāng)庫(kù) npmjs.com ,已經(jīng)存放了15萬(wàn)個(gè)模塊,其中絕大部分都是 CommonJS 格式。
這種格式的核心就是 require 語(yǔ)句,模塊通過(guò)它加載。學(xué)習(xí) Node.js ,必學(xué)如何使用 require 語(yǔ)句。本文通過(guò)源碼分析,詳細(xì)介紹 require 語(yǔ)句的內(nèi)部運(yùn)行機(jī)制,幫你理解 Node.js 的模塊機(jī)制。
一、require() 的基本用法
分析源碼之前,先介紹 require 語(yǔ)句的內(nèi)部邏輯。
當(dāng) Node 遇到 require(X) 時(shí),按下面的順序處理。 (1)如果 X 是內(nèi)置模塊(比如 require('http')) a. 返回該模塊。 b. 不再繼續(xù)執(zhí)行。 (2)如果 X 以 "./" 或者 "/" 或者 "../" 開頭 a. 根據(jù) X 所在的父模塊,確定 X 的絕對(duì)路徑。 b. 將 X 當(dāng)成文件,依次查找下面文件,只要其中有一個(gè)存在,就返回該文件,不再繼續(xù)執(zhí)行。 X X.js X.json X.node c. 將 X 當(dāng)成目錄,依次查找下面文件,只要其中有一個(gè)存在,就返回該文件,不再繼續(xù)執(zhí)行。 X/package.json(main字段) X/index.js X/index.json X/index.node (3)如果 X 不帶路徑 a. 根據(jù) X 所在的父模塊,確定 X 可能的安裝目錄。 b. 依次在每個(gè)目錄中,將 X 當(dāng)成文件名或目錄名加載。 (4) 拋出 "not found"
請(qǐng)看一個(gè)例子。
當(dāng)前腳本文件 /home/ry/projects/foo.js 執(zhí)行了 require('bar') ,這屬于上面的第三種情況。Node 內(nèi)部運(yùn)行過(guò)程如下。
首先,確定 x 的絕對(duì)路徑可能是下面這些位置,依次搜索每一個(gè)目錄。
/home/ry/projects/node_modules/bar /home/ry/node_modules/bar /home/node_modules/bar /node_modules/bar
搜索時(shí),Node 先將 bar 當(dāng)成文件名,依次嘗試加載下面這些文件,只要有一個(gè)成功就返回。
bar bar.js bar.json bar.node
如果都不成功,說(shuō)明 bar 可能是目錄名,于是依次嘗試加載下面這些文件。
bar/package.json(main字段) bar/index.js bar/index.json bar/index.node
如果在所有目錄中,都無(wú)法找到 bar 對(duì)應(yīng)的文件或目錄,就拋出一個(gè)錯(cuò)誤。
二、Module 構(gòu)造函數(shù)
了解內(nèi)部邏輯以后,下面就來(lái)看源碼。
require 的源碼在 Node 的 lib/module.js 文件。為了便于理解,本文引用的源碼是簡(jiǎn)化過(guò)的,并且刪除了原作者的注釋。
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; this.filename = null; this.loaded = false; this.children = []; } module.exports = Module; var module = new Module(filename, parent);
上面代碼中,Node 定義了一個(gè)構(gòu)造函數(shù) Module,所有的模塊都是 Module 的實(shí)例。可以看到,當(dāng)前模塊(module.js)也是 Module 的一個(gè)實(shí)例。
每個(gè)實(shí)例都有自己的屬性。下面通過(guò)一個(gè)例子,看看這些屬性的值是什么。新建一個(gè)腳本文件 a.js 。
// a.js console.log('module.id: ', module.id); console.log('module.exports: ', module.exports); console.log('module.parent: ', module.parent); console.log('module.filename: ', module.filename); console.log('module.loaded: ', module.loaded); console.log('module.children: ', module.children); console.log('module.paths: ', module.paths);
運(yùn)行這個(gè)腳本。
$ node a.js module.id: . module.exports: {} module.parent: null module.filename: /home/ruanyf/tmp/a.js module.loaded: false module.children: [] module.paths: [ '/home/ruanyf/tmp/node_modules', '/home/ruanyf/node_modules', '/home/node_modules', '/node_modules' ]
可以看到,如果沒有父模塊,直接調(diào)用當(dāng)前模塊,parent 屬性就是 null,id 屬性就是一個(gè)點(diǎn)。filename 屬性是模塊的絕對(duì)路徑,path 屬性是一個(gè)數(shù)組,包含了模塊可能的位置。另外,輸出這些內(nèi)容時(shí),模塊還沒有全部加載,所以 loaded 屬性為 false 。
新建另一個(gè)腳本文件 b.js,讓其調(diào)用 a.js 。
// b.js var a = require('./a.js');
運(yùn)行 b.js
$ node b.js module.id: /home/ruanyf/tmp/a.js module.exports: {} module.parent: { object } module.filename: /home/ruanyf/tmp/a.js module.loaded: false module.children: [] module.paths: [ '/home/ruanyf/tmp/node_modules', '/home/ruanyf/node_modules', '/home/node_modules', '/node_modules' ]
上面代碼中,由于 a.js 被 b.js 調(diào)用,所以 parent 屬性指向 b.js 模塊,id 屬性和 filename 屬性一致,都是模塊的絕對(duì)路徑。
三、模塊實(shí)例的 require 方法
每個(gè)模塊實(shí)例都有一個(gè) require 方法。
Module.prototype.require = function(path) { return Module._load(path, this); };
由此可知,require 并不是全局性命令,而是每個(gè)模塊提供的一個(gè)內(nèi)部方法,也就是說(shuō),只有在模塊內(nèi)部才能使用 require 命令(唯一的例外是 REPL 環(huán)境)。另外,require 其實(shí)內(nèi)部調(diào)用 Module._load 方法。
下面來(lái)看 Module._load 的源碼。
Module._load = function(request, parent, isMain) { // 計(jì)算絕對(duì)路徑 var filename = Module._resolveFilename(request, parent); // 第一步:如果有緩存,取出緩存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否為內(nèi)置模塊 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } // 第三步:生成模塊實(shí)例,存入緩存 var module = new Module(filename, parent); Module._cache[filename] = module; // 第四步:加載模塊 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:輸出模塊的exports屬性 return module.exports; };
上面代碼中,首先解析出模塊的絕對(duì)路徑(filename),以它作為模塊的識(shí)別符。然后,如果模塊已經(jīng)在緩存中,就從緩存取出;如果不在緩存中,就加載模塊。
因此,Module._load 的關(guān)鍵步驟是兩個(gè)。
?Module._resolveFilename() :確定模塊的絕對(duì)路徑 ?module.load():加載模塊
四、模塊的絕對(duì)路徑
下面是 Module._resolveFilename 方法的源碼。
Module._resolveFilename = function(request, parent) { // 第一步:如果是內(nèi)置模塊,不含路徑返回 if (NativeModule.exists(request)) { return request; } // 第二步:確定所有可能的路徑 var resolvedModule = Module._resolveLookupPaths(request, parent); var id = resolvedModule[0]; var paths = resolvedModule[1]; // 第三步:確定哪一個(gè)路徑為真 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; };
上面代碼中,在 Module.resolveFilename 方法內(nèi)部,又調(diào)用了兩個(gè)方法 Module.resolveLookupPaths() 和 Module._findPath() ,前者用來(lái)列出可能的路徑,后者用來(lái)確認(rèn)哪一個(gè)路徑為真。
為了簡(jiǎn)潔起見,這里只給出 Module._resolveLookupPaths() 的運(yùn)行結(jié)果。
[ '/home/ruanyf/tmp/node_modules', '/home/ruanyf/node_modules', '/home/node_modules', '/node_modules' '/home/ruanyf/.node_modules', '/home/ruanyf/.node_libraries', '$Prefix/lib/node' ]
上面的數(shù)組,就是模塊所有可能的路徑?;旧鲜牵瑥漠?dāng)前路徑開始一級(jí)級(jí)向上尋找 node_modules 子目錄。最后那三個(gè)路徑,主要是為了歷史原因保持兼容,實(shí)際上已經(jīng)很少用了。
有了可能的路徑以后,下面就是 Module._findPath() 的源碼,用來(lái)確定到底哪一個(gè)是正確路徑。
Module._findPath = function(request, paths) { // 列出所有可能的后綴名:.js,.json, .node var exts = Object.keys(Module._extensions); // 如果是絕對(duì)路徑,就不再搜索 if (request.charAt(0) === '/') { paths = ['']; } // 是否有后綴的目錄斜杠 var trailingSlash = (request.slice(-1) === '/'); // 第一步:如果當(dāng)前路徑已在緩存中,就直接返回緩存 var cacheKey = JSON.stringify({request: request, paths: paths}); if (Module._pathCache[cacheKey]) { return Module._pathCache[cacheKey]; } // 第二步:依次遍歷所有路徑 for (var i = 0, PL = paths.length; i < PL; i++) { var basePath = path.resolve(paths[i], request); var filename; if (!trailingSlash) { // 第三步:是否存在該模塊文件 filename = tryFile(basePath); if (!filename && !trailingSlash) { // 第四步:該模塊文件加上后綴名,是否存在 filename = tryExtensions(basePath, exts); } } // 第五步:目錄中是否存在 package.json if (!filename) { filename = tryPackage(basePath, exts); } if (!filename) { // 第六步:是否存在目錄名 + index + 后綴名 filename = tryExtensions(path.resolve(basePath, 'index'), exts); } // 第七步:將找到的文件路徑存入返回緩存,然后返回 if (filename) { Module._pathCache[cacheKey] = filename; return filename; } } // 第八步:沒有找到文件,返回false return false; };
經(jīng)過(guò)上面代碼,就可以找到模塊的絕對(duì)路徑了。
有時(shí)在項(xiàng)目代碼中,需要調(diào)用模塊的絕對(duì)路徑,那么除了 module.filename ,Node 還提供一個(gè) require.resolve 方法,供外部調(diào)用,用于從模塊名取到絕對(duì)路徑。
require.resolve = function(request) { return Module._resolveFilename(request, self); }; // 用法 require.resolve('a.js') // 返回 /home/ruanyf/tmp/a.js
五、加載模塊
有了模塊的絕對(duì)路徑,就可以加載該模塊了。下面是 module.load 方法的源碼。
Module.prototype.load = function(filename) { var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; };
上面代碼中,首先確定模塊的后綴名,不同的后綴名對(duì)應(yīng)不同的加載方法。下面是 .js 和 .json 后綴名對(duì)應(yīng)的處理方法。
Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(stripBOM(content), filename); }; 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; } };
這里只討論 js 文件的加載。首先,將模塊文件讀取成字符串,然后剝離 utf8 編碼特有的BOM文件頭,最后編譯該模塊。
module._compile 方法用于模塊的編譯。
Module.prototype._compile = function(content, filename) { var self = this; var args = [self.exports, require, self, filename, dirname]; return compiledWrapper.apply(self.exports, args); };
上面的代碼基本等同于下面的形式。
(function (exports, require, module, __filename, __dirname) { // 模塊源碼 });
也就是說(shuō),模塊的加載實(shí)質(zhì)上就是,注入exports、require、module三個(gè)全局變量,然后執(zhí)行模塊的源碼,然后將模塊的 exports 變量的值輸出。
到此這篇關(guān)于node.js的require()的文章就介紹到這了,更多相關(guān)node.js的require()內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用Node.js制作爬取大眾點(diǎn)評(píng)的爬蟲
相信每位用過(guò)大眾點(diǎn)評(píng)的人都知道,大眾點(diǎn)評(píng)上有很多美食餐館的信息,所以這篇文章給大家分享利用Node.js實(shí)現(xiàn)爬取大眾點(diǎn)評(píng)的爬蟲,正好可以拿來(lái)練練手Node.js。感興趣的可以參考借鑒。2016-09-09詳解一個(gè)基于套接字實(shí)現(xiàn)長(zhǎng)連接的express
這篇文章主要介紹了詳解一個(gè)基于套接字實(shí)現(xiàn)長(zhǎng)連接的express,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03node靜態(tài)服務(wù)器實(shí)現(xiàn)靜態(tài)讀取文件或文件夾
這篇文章主要介紹了node靜態(tài)服務(wù)器實(shí)現(xiàn)靜態(tài)讀取文件或文件夾,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Node.js中的process.nextTick使用實(shí)例
這篇文章主要介紹了Node.js中的process.nextTick使用實(shí)例,nextTick函數(shù)有什么用、怎么用、和setTimeout有什么區(qū)別呢,本文就講解了這些知識(shí),需要的朋友可以參考下2015-06-06Node.js檢測(cè)端口(port)是否被占用的簡(jiǎn)單示例
大家有沒有遇到過(guò)在開啟本地服務(wù)時(shí),有這么一種情況:當(dāng)前端口已經(jīng)被另一個(gè)項(xiàng)目使用了,導(dǎo)致服務(wù)開啟失敗。那么接下來(lái),我們通過(guò)簡(jiǎn)簡(jiǎn)單單的示例代碼來(lái)檢測(cè)端口是否已經(jīng)被占用。有需要的朋友們可以參考借鑒。2016-09-09淺談Koa2框架利用CORS完成跨域ajax請(qǐng)求
這篇文章主要介紹了淺談Koa2框架利用CORS完成跨域ajax請(qǐng)求,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03