在Node 中引入模塊,需要經(jīng)歷三個(gè)步驟
- 路徑分析
- 文件定位
- 編譯執(zhí)行
在Node中,模塊分為兩類:一類是Node提供的模塊,稱為核心模塊;一類是用戶編寫的模塊,稱為文件模塊。
核心模塊部分在Node源碼的編譯過(guò)程中,編譯進(jìn)了二進(jìn)制執(zhí)行文件。在Node進(jìn)程啟動(dòng)時(shí),部分核心模塊就被直接加載進(jìn)內(nèi)存中,所以這部分核心模塊引入時(shí),文件定位和編譯執(zhí)行兩個(gè)步驟可以省略掉,并且在路徑分析中優(yōu)先判斷,所以它的加載速度是最快的。
文件模塊則是在運(yùn)行時(shí)動(dòng)態(tài)加載,需要完整的路徑分析、文件定位、編譯執(zhí)行過(guò)程,速度比核心模塊慢。
優(yōu)先從緩存加載
無(wú)論是核心模塊還是文件模塊,require() 方法對(duì)相同模塊的二次加載都一律采用緩存優(yōu)先的方式,不同之處在于核心模塊的緩存檢查優(yōu)先于文件模塊的緩存檢查。
路徑分析和文件定位
模塊標(biāo)識(shí)符分析
require()方法接受一個(gè)標(biāo)識(shí)符作為參數(shù)。標(biāo)識(shí)符在Node中主要分為一下幾類:
核心模塊,例如http、fs、path等
.或..開始的相對(duì)路徑文件模塊
以/開始的絕對(duì)路徑文件模塊
非路徑形式的文件模塊,例如自定義模塊
核心模塊
核心模塊加載優(yōu)先級(jí)僅次于緩存,它在Node源碼編譯過(guò)程中已經(jīng)編譯為二進(jìn)制代碼,其加載過(guò)程最快
路徑形式的文件模塊
在分析路徑模塊時(shí),require()方法會(huì)將路徑轉(zhuǎn)換為真實(shí)的路徑,并以真實(shí)路徑作為索引,將編譯執(zhí)行后的結(jié)果放在緩存中,文件模塊給Node 指明了確切的文件位置,所以查找過(guò)程中可以節(jié)約大量時(shí)間,其加載速度慢于核心模塊
自定義模塊
自定義模塊指的是非核心模塊,也不是路徑形式的標(biāo)識(shí)符。它是一種特殊的文件模塊,可能是一個(gè)文件或者包的形式,這類模塊是加載最慢的一種。
*** Node 定位文件模塊的查找策略*** 具體表現(xiàn)為一個(gè)路徑組成的數(shù)組??梢允謩?dòng)嘗試一下:
在任意目錄下執(zhí)行輸入node進(jìn)入node環(huán)境,然后輸入module.paths
在mac 下會(huì)得到下面一個(gè)數(shù)組輸出
> module.paths
[ '/Users/fanrongrong/repl/node_modules',
'/Users/fanrongrong/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/fanrongrong/.node_modules',
'/Users/fanrongrong/.node_libraries',
'/Users/fanrongrong/.nvm/versions/node/v8.9.3/lib/node' ]
>
模塊路徑的生成規(guī)則
當(dāng)前文件目錄下的node_modules目錄
父目錄下的node_modules目錄
父目錄的父目錄下的node_modules目錄,沿著路徑逐級(jí)遞歸,直到跟目錄
mac 會(huì)查找用戶模塊下的.node_modules和.node_libraries 目錄,window會(huì)查找環(huán)境變量$HOME下的這兩個(gè)目錄
node 的安裝目錄下的node_modules (全局安裝的包默認(rèn)在這里,可以通過(guò)npm root -g查看路徑)
文件定位
文件擴(kuò)展名分析
require()在分析標(biāo)識(shí)符的過(guò)程中,允許在標(biāo)識(shí)符中不包含文件擴(kuò)展名,這種情況下Node會(huì)按照.js、.json、.node的次序補(bǔ)足擴(kuò)展名
目錄分析和包
在分析標(biāo)識(shí)符的過(guò)程中,require()通過(guò)分析文件擴(kuò)展名之后,可能沒(méi)有查找到對(duì)應(yīng)的文件卻得到了一個(gè)目錄,此時(shí)Node會(huì)將目錄當(dāng)作一個(gè)包來(lái)處理。
包處理規(guī)則:Node在當(dāng)前目錄查找package.json文件,通過(guò)JSON.parse()解析出包的描述對(duì)象,從中取出main屬性指定的文件名進(jìn)行定位,如果文件名缺少擴(kuò)展名,會(huì)進(jìn)入擴(kuò)展名分析步驟,如果main指定的文件名錯(cuò)誤,或沒(méi)有package.json文件,Node會(huì)將index當(dāng)作默認(rèn)文件名,然后依次查找index.js、index.json、index.node,如果目錄分析中沒(méi)有定位到任何文件,在自定義模塊進(jìn)入下一個(gè)模塊的路徑分析,如果模塊的路徑數(shù)組都遍歷完依然沒(méi)有找的目標(biāo)文件,則會(huì)拋出查找失敗的異常。
模塊編譯
去看源碼
Node會(huì)新建一個(gè)模塊對(duì)象,然后根據(jù)路徑載入并編譯,對(duì)于不同的文件擴(kuò)展名,載入的方式也不同。
- .js 文件。通過(guò)fs模塊同步讀取文件后編譯執(zhí)行
- .node 文件。這是用c++編寫的擴(kuò)展文件,通過(guò)
dlopen()方法加載最后編譯生成的文件 - .json文件。通過(guò)fs模塊同步讀取文件后,用
JSON.parse()解析返回結(jié)果 - 其余文件均當(dāng)作js文件引入
每一個(gè)編譯成功的模塊都會(huì)將其文件路徑作為索引緩存在Module._cache對(duì)象上。
其中,Module._extensions 會(huì)被賦值給require()的extensions屬性。通過(guò)console(require.extensions)可以查看已有的加載方式;
JavaScript模塊的編譯
在編譯過(guò)程中,Node對(duì)獲取的JavaScript 文件內(nèi)容進(jìn)行包裝,如下:
(function (exports, require, module, __filename, __dirname) {
var math = require('math')
exports.area = function () {
return Math.PI * radius * radius2
}
})
包與NPM
包結(jié)構(gòu)
- package.json: 包描述文件
- bin : 存放可執(zhí)行二進(jìn)制文件目錄
- lib: 存放JavaScript 代碼的目錄
包描述文件
- main 模塊引入方法require方法會(huì)優(yōu)先檢查這個(gè)字段,并將它入口,如果不存在就查找index.js、index.node、index.json
- bin 配置好bin 字段當(dāng)npm install 包名 -g 時(shí)可以將腳本添加到執(zhí)行路徑中
安裝依賴包
全局安裝模式
- 根據(jù)bin字段的配置,將實(shí)際的腳本鏈接到與node 可執(zhí)行文件相同的路徑下:
"bin": {
"express": "./bin/express"
}
通過(guò)全局安裝的模塊都會(huì)被安裝到一個(gè)統(tǒng)一目錄下,一搬都是node的安裝目錄下的lib/node_modules 下
node的包引用
node commonjs 規(guī)范引用包方式
node 在8.0版之前都是遵循commenjs 規(guī)范進(jìn)行的包引用(exports/module.exprots, require)
exports是module.exports 的一個(gè)引用,所以導(dǎo)出時(shí)可以使用exports.xxxx = xxx 的方式,而不能使用exports = xxxx的方式,可以使用module.exports = xxx的方式
這種方式是運(yùn)行時(shí)加載,換句話說(shuō)是在 NodeJS 腳本執(zhí)行時(shí)才加載進(jìn)來(lái)
node 8.0 之后加入ES方式的引用包方式(import, export)
使用ES方式需要在啟動(dòng)node時(shí)加入?yún)?shù) --experimental-modules
這種方式引用是在靜態(tài)分析時(shí)候就確定了引用關(guān)系,就像目標(biāo)模塊建立了一個(gè)符號(hào)鏈接,或者說(shuō)建立了一個(gè)指針。這種加載方式加載效率應(yīng)該略高于 CommonJS。
例子:
// a.js
var n = 3;
exports.n = 3
exports.add = () => {
n++;
}
// b.js
var mod = require('./a.js');
console.log(mod.n);
mod.add();
console.log(mod.n);
// a.mjs
export let n = 3;
export let add = () => {
n++;
}
// b.mjs
import {
n,
add
} from './a.mjs'
console.log(n);
add();
console.log(n);