模塊是構(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潘悼,特指module
、module.exports
和require
爬橡,注意的是我們的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.exports
和exports
非常的困惑,通過上面的代碼我們很直觀的明白显拳,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)注~