Node模塊系統(tǒng)及其模式

模塊是構(gòu)建應(yīng)用程序的基礎(chǔ)屹篓,也使得函數(shù)和變量私有化注盈,不直接對外暴露出來苫亦,接下來我們就要介紹Node的模塊化系統(tǒng)和它最常用的模式

module的本質(zhì)

我們都知道条舔,JavaScript有一個很大的缺陷就是缺少namespacing的概念枫耳,程序運行在全局作用域下,很容易被內(nèi)部應(yīng)用程序的代碼或者是第三方依賴程序的數(shù)據(jù)所污染孟抗,一個很典型的解決方案就使通過IIFE來解決迁杨,本質(zhì)上是利用閉包來解決

const module = (() => {
    const privateOne = () => {
        // ... 
    }

    const privateTwo = () => {
        // ... 
    }

    const exported = {
        publicOne: () => {
            // ...
        },
        publicTwo: []
    }

    return exported;
})()

console.log(module);

通過上面的代碼,我們可以看出凄硼,module變量包含的只有對外暴露的API铅协,然而剩下的module內(nèi)容是對外不可見的,而這個也是Node module system最核心的思想摊沉。

Node modules 說明

CommonJS是一個致力于將JavaScript生態(tài)系統(tǒng)標準化的一個組織狐史,它最出名的一個提議就是我們眾所周知的CommonJS modules,Node在本規(guī)范的基礎(chǔ)上構(gòu)建了他自己的模塊系統(tǒng),并且添加了一些自定義擴展说墨,為了描述它是怎么工作的骏全,我們可以使用上面所提到的module的本質(zhì)的思想,自己做一個類似的實現(xiàn)尼斧。

自制一個module loader

下面的代碼主要是模仿Node原始的require()函數(shù)的功能

首先姜贡,我們創(chuàng)建一個函數(shù)用來加載一個module的內(nèi)容,將它包裹在一個私有的作用域中

function loadModule(filename, module, require) {
    const warppedSrc = `(function(module, mexports, require) {
        ${fs.readFileSync(filename, 'utf-8')}
    })(module, module.exports, require)`

    eval(warppedSrc);
}

module的源代碼被包裝到一個函數(shù)中棺棵,如同IIFE那樣楼咳,這里的區(qū)別在于我們傳遞了一些變量給module潘悼,特指modulemodule.exportsrequire爬橡,注意的是我們的exports變量實質(zhì)上是又module.exports初始化的治唤,我們接下來會繼續(xù)討論這個

*在這個例子中,需要注意的是糙申,我們使用了類似eval()或者是node的vm模塊宾添,它們可能會導致一些代碼注入攻擊的安全性問題,所以我們需要特別注意和避免

接下來柜裸,讓我們通過實現(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ù)的行為,當然疙挺,它只是具有一個雛形扛邑,而沒有完全準確的反映真實的require函數(shù)的行為,但是它可以讓我們很好的理解Node模塊系統(tǒng)的內(nèi)部機制铐然,一個模塊怎么被定義和被夾在蔬崩,我們的自制模塊系統(tǒng)具備下面的功能

  • 模塊名被作為參數(shù)傳入,首先要做的事情時調(diào)用require.resolve方法根據(jù)傳入的模塊名生成module id(通過指定的resolve算法來生成)
  • 如果該模塊已經(jīng)被加載過了搀暑,那么直接會從緩存中獲得
  • 如果該模塊還沒有被加載過沥阳,我們會初始化一個module對象,其中包含兩個屬性自点,一個是module id桐罕,另外一個屬性是exports,它的初始值為一個空對象桂敛,該屬性會被用于保存模塊的export的公共的API代碼
  • 將該module進行cache
  • 調(diào)用我們上面定義的loadModule函數(shù)來獲取模塊的源代碼功炮,將初始化的module對象作為參數(shù)傳入,因為module是對象术唬,引用類型薪伏,所以模塊可以利用module.exports或者是替換module.exports來暴露它的公共API
  • 最后,返回給調(diào)用者module.exports的內(nèi)容碴开,也就是該模塊的公共API

看到這里毅该,我們會發(fā)現(xiàn),其實在Node 模塊系統(tǒng)沒有想象中的那么難潦牛,真正的技巧在于將模塊的代碼進行包裝眶掌,以及創(chuàng)建一個運行時的虛擬環(huán)境。

定義一個模塊

通過觀察我們自制的require()函數(shù)的工作機制巴碗,我們應(yīng)該很清楚的知道如何定義一個模塊

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)容將會被緩存并返回母赵。

定義全局變量

即使所有的變量和函數(shù)都在模塊本身的作用域內(nèi)聲明的,但是仍然可以定義全局變量具滴,事實上凹嘲,模塊系統(tǒng)暴露一個用來定義全局變量的特殊變量global,任何分配到這個變量的變量都會自動的變成全局變量

需要注意的是构韵,污染全局作用域是一個很不好的事情周蹭,甚至使得讓模塊系統(tǒng)的優(yōu)點消失,所以只有當你自己知道你要做什么時候疲恢,才去使用它

module.exports VS exports

很多不熟悉Node的開發(fā)同學凶朗,會對于module.exportsexports非常的困惑,通過上面的代碼我們很直觀的明白显拳,exports只是module.exports的一個引用棚愤,而且在模塊加載之前它本質(zhì)上只是一個簡單的對象

這意味著我們可以將新屬性掛載到exports引用上

exports.hello = () => {
    console.log('hello');
}

如果是對exports重新賦值,也不會有影響杂数,因為這個時候exports是一個新的對象宛畦,而不再是module.exports的引用,所以不會改變module.exports的內(nèi)容耍休。所以下面的代碼是錯誤的

exports = () => {
    console.log('hello');
}

如果你想暴露的不是一個對象刃永,或者是函數(shù)、實例或者是一個字符串羊精,那可以通過module.exports來做

module.exports = () => {
    console.log('hello');
}

require函數(shù)是同步的

另外一個重要的我們需要注意的細節(jié)是,我們自建的require函數(shù)是同步的囚玫,事實上喧锦,它返回模塊內(nèi)容的方法很簡單,并且不需要回調(diào)函數(shù)抓督。Node內(nèi)置的require()函數(shù)也是如此燃少。因此,對于module.exports內(nèi)容必須是同步的

// incorret code
setTimeout(() => {
    module.exports = function(){}
}, 100)

這個性質(zhì)對于我們定義模塊的方法十分重要铃在,使得限制我們在定義模塊的時候使用同步的代碼阵具。這也是為什么Node提供了很多同步API給我們的最重要的原因之一

如果我們需要定義一個異步操作來進行初始化的模塊,我們也可以這么做定铜,但是這種方法的問題是阳液,我們不能保證require進來的模塊能夠準備好,后續(xù)我們會討論這個問題的解決方案

其實揣炕,在早期的Node版本里帘皿,是有異步的require方法的,但是因為它的初始化時間和異步I/O所帶來的性能消耗而廢除了

resolving 算法

相依性地獄(dependency hell)描述的是由于軟件之間的依賴性不能被滿足從而導致的問題畸陡,軟件的依賴反過來取決于其他的依賴鹰溜,但是需要不同的兼容版本虽填。Node很好的解決了這個問題通過加載不同版本的模塊,具體取決于該模塊從哪里被加載曹动。這個特性的所有優(yōu)點都能在npm上體現(xiàn)斋日,并且也在require函數(shù)的resolving 算法中使用

然我們來快速連接下這個算法,我們都知道墓陈,resolve()函數(shù)獲取模塊名作為輸入桑驱,然后返回一個模塊的全路徑,該路金用于加載它的代碼也作為該模塊唯一的標識跛蛋。resolcing算法可以分為以下三個主要分支

  • 文件模塊(File modules)熬的,如果模塊名是以"/"開始,則被認為是絕對路徑開始赊级,如果是以"./"開始押框,則表示為相對路徑,它從使用該模塊的位置開始計算加載模塊的位置
  • 核心模塊(core modules)理逊,如果模塊名不是"/"橡伞、"./"開始的話,該算法會首先去搜索Node的核心模塊
  • 包模塊(package modules)晋被,如果通過模塊名沒有在核心模塊中找到兑徘,那么就會繼續(xù)在當前目錄下的node_modules文件夾下尋找匹配的模塊,如果沒有羡洛,則一級一級往上照挂脑,直到到達文件系統(tǒng)的根目錄

對于文件和包模塊,單個文件和文件夾可以匹配到模塊名欲侮,特別的崭闲,算法將嘗試匹配一下內(nèi)容

  • <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

通過看上面的文件夾結(jié)構(gòu)宛渐,myApp、depb和depC都依賴depA获搏,但是他們都有自己私有的依賴版本虑粥,根據(jù)上面所說的算法的規(guī)則如孝,當使用require('depA')會根據(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)用程序擁有成百上千個包的情況下也不會出現(xiàn)沖突和版本不兼容的問題

當我們使用require()時舀奶,resolving算法對于我們是透明的暑竟,然后,如果需要的話,也可以在模塊中直接通過調(diào)用require.resolve()來使用

模塊緩存(module cache)

每個模塊都會在它第一次被require的時候加載和計算但荤,然后隨后的require會返回緩存的版本罗岖,這一點通過看我們自制的require函數(shù)會非常清楚,緩存是提高性能的重要手段腹躁,而且他也帶來了一些其他的好處

  • 使得在模塊依賴關(guān)系中桑包,循環(huán)依賴變得可行
  • 它保證了在給定的包中,require相同的模塊總是會返回相同的實例

模塊的緩存通過變量require.cache暴露出來纺非,所以如果需要的話哑了,可以直接獲取,一個很常見的使用場景是通過刪除require.cache的key值使得某個模塊的緩存失效烧颖,但是不建議在非測試環(huán)境下去使用這個功能

循環(huán)依賴

很多人會認為循環(huán)依賴是自身設(shè)計的問題弱左,但是這確實是在真實的項目中會發(fā)生的問題,所以我們很有必要去弄清楚在Node內(nèi)部是怎么工作的炕淮。然我們通過我們自制的require函數(shù)來看看有沒有什么問題

定義兩個模塊

// 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 }

這個結(jié)果揭示了循環(huán)依賴的注意事項拆火,雖然在main主模塊require兩個模塊的時候,它們已經(jīng)完成了初始化涂圆,但是a.js模塊是沒有完成的们镜,這種狀態(tài)將會持續(xù)到它把模塊b.js加載完,這種情況需要我們值得注意

其實造成這個的原因主要是因為緩存的原因润歉,當我們先引入a.js的時候模狭,到達去引入b.js的時候,這個時候require.cache已經(jīng)有了關(guān)于a.js的緩存踩衩,所以在b.js模塊中嚼鹉,去引入a.js的時候,直接返回的是require.cache中關(guān)于a.js的緩存九妈,也就是不完全的a.js模塊反砌,對于b.js也是一樣的操作,才會得出上面的結(jié)果

模塊定義技巧

模塊系統(tǒng)除了成為一個加載依賴的機制意外萌朱,也是一個很好的工具去定義API,對于API設(shè)計的主要問題策菜,是去考慮私有和公有功能的平衡晶疼,最大的隱藏內(nèi)部實現(xiàn)細節(jié),對外暴露出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規(guī)范中寒匙,只允許使用exports對外暴露公共成員,因此該方法是唯一的真的符合CommmonJS規(guī)范的,對于通過module.exports去暴露的锄弱,都是Node的一個擴展功能

函數(shù)導出

另一個很常見的就是將整個module.exports作為一個函數(shù)對外暴露考蕾,它主要的優(yōu)點在于只暴露了一個函數(shù),使得提供了一個很清晰的模塊的入口会宪,易于理解和使用肖卧,這種模式也被社區(qū)稱為substack pattern

// logger.js
module.exports = function(message) {
    // ...
}

該模式的的一個擴展就是將上面提到的命名導出組合起來,雖然它仍然只是提供了一個入口點掸鹅,但是可以使用次要的功能

module.exports.verbose = function(message) {
    // ...
}

雖然看起來暴露一個函數(shù)是一個限制塞帐,但是它是一個很完美的方式,把重點放在一個函數(shù)中巍沙,代表該函數(shù)是這個模塊最重要的功能葵姥,而且使得內(nèi)部私有變量屬性變的更透明

Node的模塊化也鼓勵我們使用單一職責原則,每個模塊應(yīng)該對單個功能負責句携,從而保證模塊的復用性

構(gòu)造函數(shù)導出

將構(gòu)造函數(shù)導出榔幸,是一個函數(shù)導出的特例,但是區(qū)別在于它可以使得用戶通過它區(qū)創(chuàng)建一個實例务甥,但是我們?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');
// ...

實例導出

我們可以利用require的緩存機制輕松的定義從構(gòu)造函數(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

該模式很像單例模式敞临,它并不保證整個應(yīng)用程序的實例的唯一性态辛,因為一個模塊很可能存在一個依賴樹,所以可能會有多個依賴挺尿,但是不是在同一個package中

修改其他的模塊或者全局作用域

一個模塊甚至可以導出任何東西這可以看起來有點不合適;但是奏黑,我們不應(yīng)該忘記一個模塊可以修改全局范圍和其中的任何對象,包括緩存中的其他模塊编矾。請注意熟史,這些通常被認為是不好的做法,但是由于這種模式在某些情況下(例如測試)可能是有用和安全的窄俏,有時確實可以利用這一特性蹂匹,這是值得了解和理解的。我們說一個模塊可以修改全局范圍內(nèi)的其他模塊或?qū)ο蟀简凇KǔJ侵冈谶\行時修改現(xiàn)有對象以更改或擴展其行為或應(yīng)用的臨時更改限寞。

以下示例顯示了我們?nèi)绾蜗蛄硪粋€模塊添加新函數(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模塊仰坦。

上面的寫法是很危險的履植。主要考慮的是擁有修改全局命名空間或其他模塊的模塊是具有副作用的操作。換句話說悄晃,它會影響其范圍之外的實體的狀態(tài)玫霎,這可能導致不可預測的后果,特別是當多個模塊與相同的實體進行交互時。想象一下庶近,有兩個不同的模塊嘗試設(shè)置相同的全局變量翁脆,或者修改同一個模塊的相同屬性,效果可能是不可預測的(哪個模塊勝出拦盹?)鹃祖,但最重要的是它會對在整個應(yīng)用程序產(chǎn)生影響。

原文地址 滿意的給個關(guān)注~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末普舆,一起剝皮案震驚了整個濱河市恬口,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沼侣,老刑警劉巖祖能,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛾洛,居然都是意外死亡养铸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門轧膘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钞螟,“玉大人,你說我怎么就攤上這事谎碍×郾酰” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵蟆淀,是天一觀的道長拯啦。 經(jīng)常有香客問我,道長熔任,這世上最難降的妖魔是什么褒链? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮疑苔,結(jié)果婚禮上甫匹,老公的妹妹穿的比我還像新娘。我一直安慰自己惦费,他們只是感情好赛惩,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趁餐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪篮绰。 梳的紋絲不亂的頭發(fā)上后雷,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音,去河邊找鬼臀突。 笑死勉抓,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的候学。 我是一名探鬼主播藕筋,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼梳码!你這毒婦竟也來了隐圾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掰茶,失蹤者是張志新(化名)和其女友劉穎暇藏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體濒蒋,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡盐碱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沪伙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓮顽。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖围橡,靈堂內(nèi)的尸體忽然破棺而出暖混,到底是詐尸還是另有隱情,我是刑警寧澤某饰,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布儒恋,位于F島的核電站,受9級特大地震影響黔漂,放射性物質(zhì)發(fā)生泄漏诫尽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一炬守、第九天 我趴在偏房一處隱蔽的房頂上張望牧嫉。 院中可真熱鬧,春花似錦减途、人聲如沸酣藻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辽剧。三九已至,卻和暖如春税产,著一層夾襖步出監(jiān)牢的瞬間怕轿,已是汗流浹背偷崩。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撞羽,地道東北人阐斜。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像诀紊,于是被迫代替她去往敵國和親谒出。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容