您好,登錄后才能下訂單哦!
小編給大家分享一下Node模塊系統(tǒng)及其模式的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
模塊是構(gòu)建應(yīng)用程序的基礎(chǔ),也使得函數(shù)和變量私有化,不直接對(duì)外暴露出來,接下來我們就要介紹Node的模塊化系統(tǒng)和它最常用的模式
為了讓Node.js的文件可以相互調(diào)用,Node.js提供了一個(gè)簡(jiǎn)單的模塊系統(tǒng)。
模塊是Node.js 應(yīng)用程序的基本組成部分,文件和模塊是一一對(duì)應(yīng)的。換言之,一個(gè) Node.js 文件就是一個(gè)模塊,這個(gè)文件可能是JavaScript 代碼、JSON 或者編譯過的C/C++ 擴(kuò)展。
module的本質(zhì)
我們都知道,JavaScript有一個(gè)很大的缺陷就是缺少namespacing的概念,程序運(yùn)行在全局作用域下,很容易被內(nèi)部應(yīng)用程序的代碼或者是第三方依賴程序的數(shù)據(jù)所污染,一個(gè)很典型的解決方案就使通過IIFE來解決,本質(zhì)上是利用閉包來解決
const module = (() => { const privateOne = () => { // ... } const privateTwo = () => { // ... } const exported = { publicOne: () => { // ... }, publicTwo: [] } return exported; })() console.log(module);
通過上面的代碼,我們可以看出,module變量包含的只有對(duì)外暴露的API,然而剩下的module內(nèi)容是對(duì)外不可見的,而這個(gè)也是Node module system最核心的思想。
Node modules 說明
CommonJS是一個(gè)致力于將JavaScript生態(tài)系統(tǒng)標(biāo)準(zhǔn)化的一個(gè)組織,它最出名的一個(gè)提議就是我們眾所周知的CommonJS modules,Node在本規(guī)范的基礎(chǔ)上構(gòu)建了他自己的模塊系統(tǒng),并且添加了一些自定義擴(kuò)展,為了描述它是怎么工作的,我們可以使用上面所提到的module的本質(zhì)的思想,自己做一個(gè)類似的實(shí)現(xiàn)。
自制一個(gè)module loader
下面的代碼主要是模仿Node原始的require()函數(shù)的功能
首先,我們創(chuàng)建一個(gè)函數(shù)用來加載一個(gè)module的內(nèi)容,將它包裹在一個(gè)私有的作用域中
function loadModule(filename, module, require) { const warppedSrc = `(function(module, mexports, require) { ${fs.readFileSync(filename, 'utf-8')} })(module, module.exports, require)` eval(warppedSrc); }
module的源代碼被包裝到一個(gè)函數(shù)中,如同IIFE那樣,這里的區(qū)別在于我們傳遞了一些變量給module,特指module、module.exports和require,注意的是我們的exports變量實(shí)質(zhì)上是又module.exports初始化的,我們接下來會(huì)繼續(xù)討論這個(gè)
*在這個(gè)例子中,需要注意的是,我們使用了類似eval()或者是node的vm模塊,它們可能會(huì)導(dǎo)致一些代碼注入攻擊的安全性問題,所以我們需要特別注意和避免
接下來,讓我們通過實(shí)現(xiàn)我們的require()函數(shù),來看看這些變量怎么被引入的
const require = (moduleName) => { console.log(`Required invoked for module: ${moduleName}`); const id = require.resolve(moduleName); if(require.cache[id]) { return require.cache[id].exports; } // module structure data const module = { exports: {}, id: id } // uodate cache require.cache[id] = module; // load the module loadModule(id, module, require); // return exported variables return module.exports; } require.cache = {}; require.resolve = (moduleName) => { // resolve a full module id from the moduleName }
上面的函數(shù)模擬了Nodejs原生用來加載模塊的require函數(shù)的行為,當(dāng)然,它只是具有一個(gè)雛形,而沒有完全準(zhǔn)確的反映真實(shí)的require函數(shù)的行為,但是它可以讓我們很好的理解Node模塊系統(tǒng)的內(nèi)部機(jī)制,一個(gè)模塊怎么被定義和被夾在,我們的自制模塊系統(tǒng)具備下面的功能
模塊名被作為參數(shù)傳入,首先要做的事情時(shí)調(diào)用require.resolve方法根據(jù)傳入的模塊名生成module id(通過指定的resolve算法來生成)
如果該模塊已經(jīng)被加載過了,那么直接會(huì)從緩存中獲得
如果該模塊還沒有被加載過,我們會(huì)初始化一個(gè)module對(duì)象,其中包含兩個(gè)屬性,一個(gè)是module id,另外一個(gè)屬性是exports,它的初始值為一個(gè)空對(duì)象,該屬性會(huì)被用于保存模塊的export的公共的API代碼
將該module進(jìn)行cache
調(diào)用我們上面定義的loadModule函數(shù)來獲取模塊的源代碼,將初始化的module對(duì)象作為參數(shù)傳入,因?yàn)閙odule是對(duì)象,引用類型,所以模塊可以利用module.exports或者是替換module.exports來暴露它的公共API
最后,返回給調(diào)用者module.exports的內(nèi)容,也就是該模塊的公共API
看到這里,我們會(huì)發(fā)現(xiàn),其實(shí)在Node 模塊系統(tǒng)沒有想象中的那么難,真正的技巧在于將模塊的代碼進(jìn)行包裝,以及創(chuàng)建一個(gè)運(yùn)行時(shí)的虛擬環(huán)境。
定義一個(gè)模塊
通過觀察我們自制的require()函數(shù)的工作機(jī)制,我們應(yīng)該很清楚的知道如何定義一個(gè)模塊
const dependency = require('./anotherModule'); function log() { console.log(`get another ${dependency.username}`); } module.exports.run = () => { log(); } // anotherModule.js module.exports = { username: 'wingerwang' }
最重要的是要記住在模塊里面,除了被分配給module.exports的變量,其他的都是該模塊私有的,在使用require()加載后,這些變量的內(nèi)容將會(huì)被緩存并返回。
定義全局變量
即使所有的變量和函數(shù)都在模塊本身的作用域內(nèi)聲明的,但是仍然可以定義全局變量,事實(shí)上,模塊系統(tǒng)暴露一個(gè)用來定義全局變量的特殊變量global,任何分配到這個(gè)變量的變量都會(huì)自動(dòng)的變成全局變量
需要注意的是,污染全局作用域是一個(gè)很不好的事情,甚至使得讓模塊系統(tǒng)的優(yōu)點(diǎn)消失,所以只有當(dāng)你自己知道你要做什么時(shí)候,才去使用它
module.exports VS exports
很多不熟悉Node的開發(fā)同學(xué),會(huì)對(duì)于module.exports和exports非常的困惑,通過上面的代碼我們很直觀的明白,exports只是module.exports的一個(gè)引用,而且在模塊加載之前它本質(zhì)上只是一個(gè)簡(jiǎn)單的對(duì)象
這意味著我們可以將新屬性掛載到exports引用上
exports.hello = () => { console.log('hello'); }
如果是對(duì)exports重新賦值,也不會(huì)有影響,因?yàn)檫@個(gè)時(shí)候exports是一個(gè)新的對(duì)象,而不再是module.exports的引用,所以不會(huì)改變module.exports的內(nèi)容。所以下面的代碼是錯(cuò)誤的
exports = () => { console.log('hello'); }
如果你想暴露的不是一個(gè)對(duì)象,或者是函數(shù)、實(shí)例或者是一個(gè)字符串,那可以通過module.exports來做
module.exports = () => { console.log('hello'); }
require函數(shù)是同步的
另外一個(gè)重要的我們需要注意的細(xì)節(jié)是,我們自建的require函數(shù)是同步的,事實(shí)上,它返回模塊內(nèi)容的方法很簡(jiǎn)單,并且不需要回調(diào)函數(shù)。Node內(nèi)置的require()函數(shù)也是如此。因此,對(duì)于module.exports內(nèi)容必須是同步的
// incorret code setTimeout(() => { module.exports = function(){} }, 100)
這個(gè)性質(zhì)對(duì)于我們定義模塊的方法十分重要,使得限制我們?cè)诙x模塊的時(shí)候使用同步的代碼。這也是為什么Node提供了很多同步API給我們的最重要的原因之一
如果我們需要定義一個(gè)異步操作來進(jìn)行初始化的模塊,我們也可以這么做,但是這種方法的問題是,我們不能保證require進(jìn)來的模塊能夠準(zhǔn)備好,后續(xù)我們會(huì)討論這個(gè)問題的解決方案
其實(shí),在早期的Node版本里,是有異步的require方法的,但是因?yàn)樗某跏蓟瘯r(shí)間和異步I/O所帶來的性能消耗而廢除了
resolving 算法
相依性地獄(dependency hell)描述的是由于軟件之間的依賴性不能被滿足從而導(dǎo)致的問題,軟件的依賴反過來取決于其他的依賴,但是需要不同的兼容版本。Node很好的解決了這個(gè)問題通過加載不同版本的模塊,具體取決于該模塊從哪里被加載。這個(gè)特性的所有優(yōu)點(diǎn)都能在npm上體現(xiàn),并且也在require函數(shù)的resolving 算法中使用
然我們來快速連接下這個(gè)算法,我們都知道,resolve()函數(shù)獲取模塊名作為輸入,然后返回一個(gè)模塊的全路徑,該路金用于加載它的代碼也作為該模塊唯一的標(biāo)識(shí)。resolcing算法可以分為以下三個(gè)主要分支
文件模塊(File modules),如果模塊名是以"/"開始,則被認(rèn)為是絕對(duì)路徑開始,如果是以"./"開始,則表示為相對(duì)路徑,它從使用該模塊的位置開始計(jì)算加載模塊的位置
核心模塊(core modules),如果模塊名不是"/"、"./"開始的話,該算法會(huì)首先去搜索Node的核心模塊
包模塊(package modules),如果通過模塊名沒有在核心模塊中找到,那么就會(huì)繼續(xù)在當(dāng)前目錄下的node_modules文件夾下尋找匹配的模塊,如果沒有,則一級(jí)一級(jí)往上照,直到到達(dá)文件系統(tǒng)的根目錄
對(duì)于文件和包模塊,單個(gè)文件和文件夾可以匹配到模塊名,特別的,算法將嘗試匹配一下內(nèi)容
<moduleName>.js
<moduleName>/index.js
在<moduleName>/package main中指定的目錄/文件
算法文檔
每個(gè)包通過npm安裝的依賴會(huì)放在node_modules文件夾下,這就意味著,按照我們剛剛算法的描述,每個(gè)包都會(huì)有它自己私有的依賴。
myApp ├── foo.js └── node_modules ├── depA │ └── index.js └── depB │ ├── bar.js ├── node_modules ├── depA │ └── index.js └── depC ├── foobar.js └── node_modules └── depA └── index.js
通過看上面的文件夾結(jié)構(gòu),myApp、depb和depC都依賴depA,但是他們都有自己私有的依賴版本,根據(jù)上面所說的算法的規(guī)則,當(dāng)使用require('depA')會(huì)根據(jù)加載的模塊的位置加載不同的文件
myApp/foo.js 加載的是 /myApp/node_modules/depA/index.js
myApp/node_modules/depB/bar.js 加載的是 /myApp/node_modules/depB/node_modules/depA/index.js
myApp/node_modules/depB/depC/foobar.js 加載的是 /myApp/node_modules/depB/depC/node_modules/depA/index.js
resolving算法是保證Node依賴管理的核心部分,它的存在使得即便應(yīng)用程序擁有成百上千個(gè)包的情況下也不會(huì)出現(xiàn)沖突和版本不兼容的問題
當(dāng)我們使用require()時(shí),resolving算法對(duì)于我們是透明的,然后,如果需要的話,也可以在模塊中直接通過調(diào)用require.resolve()來使用
模塊緩存(module cache)
每個(gè)模塊都會(huì)在它第一次被require的時(shí)候加載和計(jì)算,然后隨后的require會(huì)返回緩存的版本,這一點(diǎn)通過看我們自制的require函數(shù)會(huì)非常清楚,緩存是提高性能的重要手段,而且他也帶來了一些其他的好處
使得在模塊依賴關(guān)系中,循環(huán)依賴變得可行
它保證了在給定的包中,require相同的模塊總是會(huì)返回相同的實(shí)例
模塊的緩存通過變量require.cache暴露出來,所以如果需要的話,可以直接獲取,一個(gè)很常見的使用場(chǎng)景是通過刪除require.cache的key值使得某個(gè)模塊的緩存失效,但是不建議在非測(cè)試環(huán)境下去使用這個(gè)功能
循環(huán)依賴
很多人會(huì)認(rèn)為循環(huán)依賴是自身設(shè)計(jì)的問題,但是這確實(shí)是在真實(shí)的項(xiàng)目中會(huì)發(fā)生的問題,所以我們很有必要去弄清楚在Node內(nèi)部是怎么工作的。然我們通過我們自制的require函數(shù)來看看有沒有什么問題
定義兩個(gè)模塊
// a.js exports.loaded = false; const b = require('./b.js'); module.exports = { bWasLoaded: b.loaded, loaded: true } // b.js exports.loaded = false; const a = require('./a.js'); module.exports = { aWasLoaded: a.loaded, loaded: true }
在main.js中調(diào)用
const a = require('./a'); const b = require('./b'); console.log(a); console.log(b);
最后的結(jié)果是
{ bWasLoaded: true, loaded: true }
{ aWasLoaded: false, loaded: true }
這個(gè)結(jié)果揭示了循環(huán)依賴的注意事項(xiàng),雖然在main主模塊require兩個(gè)模塊的時(shí)候,它們已經(jīng)完成了初始化,但是a.js模塊是沒有完成的,這種狀態(tài)將會(huì)持續(xù)到它把模塊b.js加載完,這種情況需要我們值得注意
其實(shí)造成這個(gè)的原因主要是因?yàn)榫彺娴脑颍?dāng)我們先引入a.js的時(shí)候,到達(dá)去引入b.js的時(shí)候,這個(gè)時(shí)候require.cache已經(jīng)有了關(guān)于a.js的緩存,所以在b.js模塊中,去引入a.js的時(shí)候,直接返回的是require.cache中關(guān)于a.js的緩存,也就是不完全的a.js模塊,對(duì)于b.js也是一樣的操作,才會(huì)得出上面的結(jié)果
模塊定義技巧
模塊系統(tǒng)除了成為一個(gè)加載依賴的機(jī)制意外,也是一個(gè)很好的工具去定義API,對(duì)于API設(shè)計(jì)的主要問題,是去考慮私有和公有功能的平衡,最大的隱藏內(nèi)部實(shí)現(xiàn)細(xì)節(jié),對(duì)外暴露出API的可用性,而且還需要對(duì)軟件的擴(kuò)展性和可用性等的平衡
接下來來介紹幾種在Node中常見的定義模塊的方法
命名導(dǎo)出
這也是最常見的一種方法,通過將值掛載到exports或者是module.exports上,通過這種方法,對(duì)外暴露的對(duì)象成為了一個(gè)容器或者是命名空間
// logger.js exports.info = function(message) { console.log('info:' + message); } exports.verbose = function(message) { console.log('verbose:' + message) }
// main.js const logger = require('./logger.js'); logger.info('hello'); logger.verbose('world');
很多Node的核心模塊都使用的這種模式
其實(shí)在CommonJS規(guī)范中,只允許使用exports對(duì)外暴露公共成員,因此該方法是唯一的真的符合CommmonJS規(guī)范的,對(duì)于通過module.exports去暴露的,都是Node的一個(gè)擴(kuò)展功能
函數(shù)導(dǎo)出
另一個(gè)很常見的就是將整個(gè)module.exports作為一個(gè)函數(shù)對(duì)外暴露,它主要的優(yōu)點(diǎn)在于只暴露了一個(gè)函數(shù),使得提供了一個(gè)很清晰的模塊的入口,易于理解和使用,這種模式也被社區(qū)稱為substack pattern
// logger.js module.exports = function(message) { // ... }
該模式的的一個(gè)擴(kuò)展就是將上面提到的命名導(dǎo)出組合起來,雖然它仍然只是提供了一個(gè)入口點(diǎn),但是可以使用次要的功能
module.exports.verbose = function(message) { // ... }
雖然看起來暴露一個(gè)函數(shù)是一個(gè)限制,但是它是一個(gè)很完美的方式,把重點(diǎn)放在一個(gè)函數(shù)中,代表該函數(shù)是這個(gè)模塊最重要的功能,而且使得內(nèi)部私有變量屬性變的更透明
Node的模塊化也鼓勵(lì)我們使用單一職責(zé)原則,每個(gè)模塊應(yīng)該對(duì)單個(gè)功能負(fù)責(zé),從而保證模塊的復(fù)用性
構(gòu)造函數(shù)導(dǎo)出
將構(gòu)造函數(shù)導(dǎo)出,是一個(gè)函數(shù)導(dǎo)出的特例,但是區(qū)別在于它可以使得用戶通過它區(qū)創(chuàng)建一個(gè)實(shí)例,但是我們?nèi)匀焕^承了它的prototype屬性,類似于類的概念
class Logger { constructor(name) { this.name = name; } log(message) { // ... } info(message) { // ... } verbose(message) { // ... } }
const Logger = require('./logger'); const dbLogger = new Logger('DB'); // ...
實(shí)例導(dǎo)出
我們可以利用require的緩存機(jī)制輕松的定義從構(gòu)造函數(shù)或者是工廠實(shí)例化的實(shí)例,可以在不同的模塊中共享
// count.js function Count() { this.count = 0; } Count.prototype.add = function() { this.count++; } module.exports = new Count(); // a.js const count = require('./count'); count.add(); console.log(count.count) // b.js const count = require('./count'); count.add(); console.log(count.count) // main.js const a = require('./a'); const b = require('./b');
輸出的結(jié)果是
1
2
該模式很像單例模式,它并不保證整個(gè)應(yīng)用程序的實(shí)例的唯一性,因?yàn)橐粋€(gè)模塊很可能存在一個(gè)依賴樹,所以可能會(huì)有多個(gè)依賴,但是不是在同一個(gè)package中
修改其他的模塊或者全局作用域
一個(gè)模塊甚至可以導(dǎo)出任何東西這可以看起來有點(diǎn)不合適;但是,我們不應(yīng)該忘記一個(gè)模塊可以修改全局范圍和其中的任何對(duì)象,包括緩存中的其他模塊。請(qǐng)注意,這些通常被認(rèn)為是不好的做法,但是由于這種模式在某些情況下(例如測(cè)試)可能是有用和安全的,有時(shí)確實(shí)可以利用這一特性,這是值得了解和理解的。我們說一個(gè)模塊可以修改全局范圍內(nèi)的其他模塊或?qū)ο?。它通常是指在運(yùn)行時(shí)修改現(xiàn)有對(duì)象以更改或擴(kuò)展其行為或應(yīng)用的臨時(shí)更改。
以下示例顯示了我們?nèi)绾蜗蛄硪粋€(gè)模塊添加新函數(shù)
// file patcher.js // ./logger is another module require('./logger').customMessage = () => console.log('This is a new functionality');
// file main.js require('./patcher'); const logger = require('./logger'); logger.customMessage();
在上述代碼中,必須首先引入patcher程序才能使用logger模塊。
上面的寫法是很危險(xiǎn)的。主要考慮的是擁有修改全局命名空間或其他模塊的模塊是具有副作用的操作。換句話說,它會(huì)影響其范圍之外的實(shí)體的狀態(tài),這可能導(dǎo)致不可預(yù)測(cè)的后果,特別是當(dāng)多個(gè)模塊與相同的實(shí)體進(jìn)行交互時(shí)。想象一下,有兩個(gè)不同的模塊嘗試設(shè)置相同的全局變量,或者修改同一個(gè)模塊的相同屬性,效果可能是不可預(yù)測(cè)的(哪個(gè)模塊勝出?),但最重要的是它會(huì)對(duì)在整個(gè)應(yīng)用程序產(chǎn)生影響。
以上是“Node模塊系統(tǒng)及其模式的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。