早在Netscape誕生不久后豹绪,JavaScript就一直在探索本地編程的路损痰,Rhino是其代表產(chǎn)物女坑。無奈那時服務(wù)端JavaScript走的路均是參考眾多服務(wù)器端語言來實現(xiàn)的尘盼,在這樣的背景之下肖油,一沒有特色兼吓,二沒有實用價值。但是隨著JavaScript在前端的應(yīng)用越來越廣泛森枪,以及服務(wù)端JavaScript的推動视搏,JavaScript現(xiàn)有的規(guī)范十分薄弱审孽,不利于JavaScript大規(guī)模的應(yīng)用。那些以JavaScript為宿主語言的環(huán)境中浑娜,只有本身的基礎(chǔ)原生對象和類型佑力,更多的對象和API都取決于宿主的提供,所以筋遭,我們可以看到JavaScript缺少這些功能:
JavaScript沒有模塊系統(tǒng)打颤。沒有原生的支持密閉作用域或依賴管理。
JavaScript沒有標(biāo)準(zhǔn)庫漓滔。除了一些核心庫外编饺,沒有文件系統(tǒng)的API禁荸,沒有IO流API等辖所。
JavaScript沒有標(biāo)準(zhǔn)接口。沒有如Web Server或者數(shù)據(jù)庫的統(tǒng)一接口羽氮。
JavaScript沒有包管理系統(tǒng)豁鲤。不能自動加載和安裝依賴秽誊。
于是便有了CommonJS(http://www.commonjs.org)規(guī)范的出現(xiàn),其目標(biāo)是為了構(gòu)建JavaScript在包括Web服務(wù)器琳骡,桌面锅论,命令行工具,及瀏覽器方面的生態(tài)系統(tǒng)楣号。
CommonJS制定了解決這些問題的一些規(guī)范棍厌,而Node.js就是這些規(guī)范的一種實現(xiàn)。Node.js自身實現(xiàn)了require方法作為其引入模塊的方法竖席,同時NPM也基于CommonJS定義的包規(guī)范,實現(xiàn)了依賴管理和模塊自動安裝等功能敬肚。
一毕荐,CommonJS的模塊規(guī)范
Node借鑒CommonJS的Modules規(guī)范實現(xiàn)了一套模塊系統(tǒng)憎亚,所以先來看看CommonJS的模塊規(guī)范。
CommonJS對模塊的定義十分簡單弄慰,主要分為模塊引用第美、模塊定義和模塊標(biāo)識3個部分。
1. 模塊引用
模塊引用的示例代碼如下:
var math = require('math');
在CommonJS規(guī)范中陆爽,存在require()方法什往,這個方法接受模塊標(biāo)識,以此引入一個模塊的API到當(dāng)前上下文中慌闭。
2. 模塊定義
在模塊中别威,上下文提供require()方法來引入外部模塊躯舔。對應(yīng)引入的功能,上下文提供了exports對象用于導(dǎo)出當(dāng)前模塊的方法或者變量省古,并且它是唯一導(dǎo)出的出口粥庄。在模塊中,還存在一個module對象豺妓,它代表模塊自身惜互,而exports是module的屬性。在Node中琳拭,一個文件就是一個模塊训堆,將方法掛載在exports對象上作為屬性即可定義導(dǎo)出的方式:
// math.js
exports.add = function () {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) { sum += args[i++]; }
return sum;
};
在另一個文件中,我們通過require()方法引入模塊后臀栈,就能調(diào)用定義的屬性或方法了:
// program.js
var math = require('math');
exports.increment = function (val) { return math.add(val, 1);};
3.模塊標(biāo)識
模塊標(biāo)識其實就是傳遞給require()方法的參數(shù)蔫慧,它必須是符合小駝峰命名的字符串,或者以.权薯、..開頭的相對路徑姑躲,或者絕對路徑。它可以沒有文件名后綴.js盟蚣。模塊的定義十分簡單黍析,接口也十分簡潔。它的意義在于將類聚的方法和變量等限定在私有的作用域中屎开,同時支持引入和導(dǎo)出功能以順暢地連接上下游依賴阐枣。每個模塊具有獨立的空間,它們互不干擾奄抽,在引用時也顯得干凈利落蔼两。
二,Node的模塊實現(xiàn)
Node在實現(xiàn)中并非完全按照規(guī)范實現(xiàn)逞度,而是對模塊規(guī)范進行了一定的取舍额划,同時也增加了少許自身需要的特性。盡管規(guī)范中exports档泽、require和module聽起來十分簡單俊戳,但是Node在實現(xiàn)它們的過程中究竟經(jīng)歷了什么,這個過程需要知曉馆匿。
在Node中引入模塊抑胎,需要經(jīng)歷如下3個步驟。
1. 路徑分析
2. 文件定位
3. 編譯執(zhí)行
在Node中渐北,模塊分為兩類:一類是Node提供的模塊阿逃,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊盆昙。
? 核心模塊部分在Node源代碼的編譯過程中羽历,編譯進了二進制執(zhí)行文件。在Node進程啟動時淡喜,部分核心模塊就被直接加載進內(nèi)存中秕磷,所以這部分核心模塊引入時,文件定位和編譯執(zhí)行這兩個步驟可以省略掉炼团,并且在路徑分析中優(yōu)先判斷澎嚣,所以它的加載速度是最快的。
? 文件模塊則是在運行時動態(tài)加載瘟芝,需要完整的路徑分析易桃、文件定位、編譯執(zhí)行過程锌俱,速度比核心模塊慢晤郑。
1.優(yōu)先從緩存加載
與前端瀏覽器會緩存靜態(tài)腳本文件以提高性能一樣,Node對引入過的模塊都會進行緩存贸宏,以減少二次引入時的開銷造寝。不同的地方在于,瀏覽器僅僅緩存文件吭练,而Node緩存的是編譯和執(zhí)行之后的對象诫龙。不論是核心模塊還是文件模塊,require()方法對相同模塊的二次加載都一律采用緩存優(yōu)先的方式鲫咽,這是第一優(yōu)先級的签赃。不同之處在于核心模塊的緩存檢查先于文件模塊的緩存檢查。
模塊加載的優(yōu)先級是:緩存模塊 > 核心模塊 > 用戶自定義模塊分尸。
2.路徑分析和文件定位
因為標(biāo)識符有幾種形式锦聊,對于不同的標(biāo)識符,模塊的查找和定位有不同程度上的差異箩绍。
a.模塊標(biāo)識符分析
Node基于一個模塊標(biāo)識符進行模塊查找孔庭。模塊標(biāo)識符在Node中主要分為以下幾類。
核心模塊伶选,如http、fs尖昏、path等仰税。
.或..開始的相對路徑文件模塊。
以/開始的絕對路徑文件模塊抽诉。
非路徑形式的文件模塊陨簇,如自定義的connect模塊
? 核心模塊
核心模塊的優(yōu)先級僅次于緩存加載,它在Node的源代碼編譯過程中已經(jīng)編譯為二進制代碼,其加載過程最快河绽。如果試圖加載一個與核心模塊標(biāo)識符相同的自定義模塊己单,那是不會成功的。如果自己編寫了一個http用戶模塊耙饰,想要加載成功纹笼,必須選擇一個不同的標(biāo)識符或者換用路徑的方式。
? 路徑形式的文件模塊
以.苟跪、..和/開始的標(biāo)識符廷痘,這里都被當(dāng)做文件模塊來處理。在分析路徑模塊時件已,require()方法會將路徑轉(zhuǎn)為真實路徑笋额,并以真實路徑作為索引,將編譯執(zhí)行后的結(jié)果存放到緩存中篷扩,以使二次加載時更快兄猩。由于文件模塊給Node指明了確切的文件位置,所以在查找過程中可以節(jié)約大量時間鉴未,其加載速度慢于核心模塊枢冤。
? 自定義模塊
自定義模塊指的是非核心模塊,也不是路徑形式的標(biāo)識符歼狼。它是一種特殊的文件模塊掏导,可能是一個文件或者包的形式。這類模塊的查找是最費時的羽峰,也是所有方式中最慢的一種趟咆。
b.文件定位
從緩存加載的優(yōu)化策略使得二次引入時不需要路徑分析、文件定位和編譯執(zhí)行的過程梅屉,大大提高了再次加載模塊時的效率值纱。但在文件的定位過程中,還有一些細(xì)節(jié)需要注意坯汤,這主要包括文件擴展名的分析虐唠、目錄和包的處理。
? 文件擴展名分析
CommonJS模塊規(guī)范也允許在標(biāo)識符中不包含文件擴展名惰聂,這種情況下疆偿,Node會按.js、.json搓幌、.node的次序補足擴展名杆故,依次嘗試。在嘗試的過程中溉愁,需要調(diào)用fs模塊同步阻塞式地判斷文件是否存在处铛。因為Node是單線程的,所以這里是一個會引起性能問題的地方。小訣竅是:如果是.node和.json文件撤蟆,在傳遞給require()的標(biāo)識符中帶上擴展名奕塑,會加快一點速度。
require加載無文件類型的優(yōu)先級:.js > .json > .node
? 目錄分析和包
在分析標(biāo)識符的過程中家肯,require()通過分析文件擴展名之后龄砰,可能沒有查找到對應(yīng)文件,但卻得到一個目錄息楔,此時Node會將目錄當(dāng)做一個包來處理寝贡。
在這個過程中,Node對CommonJS包規(guī)范進行了一定程度的支持值依。首先圃泡,Node在當(dāng)前目錄下查找package.json(CommonJS包規(guī)范定義的包描述文件),通過JSON.parse()解析出包描述對象愿险,從中取出main屬性指定的文件名進行定位颇蜡。如果文件名缺少擴展名,將會進入擴展名分析的步驟辆亏。而如果main屬性指定的文件名錯誤风秤,或者壓根沒有package.json文件,Node會將index當(dāng)做默認(rèn)文件名扮叨,然后依次查找index.js缤弦、index.node、index.json彻磁。
如果在目錄分析的過程中沒有定位成功任何文件碍沐,則自定義模塊進入下一個模塊路徑進行查找。如果模塊路徑數(shù)組都被遍歷完畢衷蜓,依然沒有查找到目標(biāo)文件累提,則會拋出查找失敗的異常。
c.模塊編譯
在Node中磁浇,每個文件模塊都是一個對象斋陪,它的定義如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
編譯和執(zhí)行是引入文件模塊的最后一個階段。定位到具體的文件后置吓,Node會新建一個模塊對象无虚,然后根據(jù)路徑載入并編譯。對于不同的文件擴展名衍锚,其載入方法也有所不同友题,具體如下所示。
? .js文件构拳。
通過fs模塊同步讀取文件后編譯執(zhí)行咆爽。
? .node文件。
這是用C/C++編寫的擴展文件置森,通過dlopen()方法加載最后編譯生成的文件斗埂。
? .json文件。
通過fs模塊同步讀取文件后凫海,用JSON.parse()解析返回結(jié)果呛凶。
? 其余擴展名文件。
它們都被當(dāng)做.js文件載入行贪。
每一個編譯成功的模塊都會將其文件路徑作為索引緩存在Module._cache對象上漾稀,以提高二次引入的性能。
JavaScript模塊的編譯
回到CommonJS模塊規(guī)范建瘫,我們知道每個模塊文件中存在著require崭捍、exports、module這3個變量啰脚,但是它們在模塊文件中并沒有定義殷蛇,那么從何而來呢?甚至在Node的API文檔中橄浓,我們知道每個模塊中還有__filename粒梦、__dirname這兩個變量的存在,它們又是從何而來的呢荸实?如果我們把直接定義模塊的過程放諸在瀏覽器端匀们,會存在污染全局變量的情況。
事實上准给,在編譯的過程中泄朴,Node對獲取的JavaScript文件內(nèi)容進行了頭尾包裝。在頭部添加了(function (exports, require, module, __filename, __dirname) {\n圆存,在尾部添加了\n});叼旋。一個正常的JavaScript文件會被包裝成如下的樣子:
(function (exports, require, module, __filename, __dirname) {
var math = require('math');
exports.area = function (radius) {
return Math.PI * radius * radius;
};
});
這樣每個模塊文件之間都進行了作用域隔離。包裝之后的代碼會通過vm原生模塊的runInThisContext()方法執(zhí)行(類似eval沦辙,只是具有明確上下文夫植,不污染全局),返回一個具體的function對象油讯。最后详民,將當(dāng)前模塊對象的exports屬性、require()方法陌兑、module(模塊對象自身)沈跨,以及在文件定位中得到的完整文件路徑和文件目錄作為參數(shù)傳遞給這個function()執(zhí)行。
三兔综,包和NPM
在模塊之外饿凛,包和NPM則是將模塊聯(lián)系起來的一種機制狞玛。
CommonJS的包規(guī)范的定義其實也十分簡單,它由包結(jié)構(gòu)和包描述文件兩個部分組成涧窒,前者用于組織包中的各種文件心肪,后者則用于描述包的相關(guān)信息,以供外部讀取分析纠吴。
包結(jié)構(gòu)
包實際上是一個存檔文件硬鞍,即一個目錄直接打包為.zip或tar.gz格式的文件,安裝后解壓還原為目錄戴已。完全符合CommonJS規(guī)范的包目錄應(yīng)該包含如下這些文件固该。
package.json:包描述文件。
bin:用于存放可執(zhí)行二進制文件的目錄糖儡。
lib:用于存放JavaScript代碼的目錄伐坏。
doc:用于存放文檔的目錄。
test:用于存放單元測試用例的代碼握联。
包描述文件
包描述文件用于表達非代碼相關(guān)的信息著淆,它是一個JSON格式的文件——package.json,位于包的根目錄下拴疤,是包的重要組成部分永部。而NPM的所有行為都與包描述文件的字段息息相關(guān)。
這個可以看看NPM官網(wǎng)對package.json的定義規(guī)范呐矾。
可以通過npm adduser, npm publish把自己的package上傳到npm倉庫苔埋。
本文內(nèi)容源自https://blog.csdn.net/u012422829/article/details/52760981,https://blog.csdn.net/qbian/article/details/79367500