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