1 JS模組化的不足
對於JS本身而言,他的規範是薄弱的,具有以下不足:
- 沒有模組系統,不支援封閉的作用域和依賴管理
- 沒有標準庫,沒有檔案系統和IO流API
- 也沒有包管理系統
2 CommonJS的功能
- 封裝功能
- 封閉作用域
- 可能解決依賴問題
- 工作效率更高,重構方便
3 CommonJS的模組規範
CommonJS 是一種使用廣泛的JavaScript模組化規範,核心思想是通過require方法來同步地載入依賴的其他模組,通過 module.exports 匯出需要暴露的介面。
3.1 模組引用
在CommonJS規範中,存在require()
方法,這個方法接受模組標識,以此引入一個模組的API到當前上下文中。
模組引用的示例程式碼如下:
const path = require("path");
複製程式碼
3.2 模組定義
上下文提供了exports
物件用於匯出當前模組的方法或者變數,並且它是唯一匯出的出口。
在模組中,還存在一個module
物件,它代表模組自身,而exports
是module
的屬性。
在Node中,一個檔案就是一個模組,將方法掛載在exports
物件作為屬性即可定義匯出的方式,如下:
// math.js
exports.add = function(){
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while(i < l){
sum += args[i++]
}
return sum;
}
複製程式碼
在另外一個檔案中,我們通過require()
方法引入模組後,就能呼叫定義的屬性或方法:
var math = require("math");
exports.increment = function(val){
return math.add(val, 1)
}
複製程式碼
3.3 模組標識
模組標識其實就是傳遞給require()
方法的引數,他必須是符合小駝峰命名的字串,或者以.
、..
開頭的相對路徑,或者絕對路徑。
CommonJS的構建的這套模組匯出和引入機制使得使用者完全不考慮變數汙染,名稱空間等方案與此相比相形見絀。
4 Node的模組實現
4.1 在Node中引入模組的步驟
- (1) 路徑分析
- (2) 檔案定位
- (3) 編譯執行
4.2 模組分類
4.2.1 原生模組
http
、fs
、path
、events
等模組,是Node提供的模組,這些模組在Node原始碼的編譯過程中被編譯成二進位制。在Node程式啟動時,部分原生程式碼就被直接載入進記憶體中,所以原生模組引入時,檔案定位和編譯執行這個兩個步驟可以省略掉,並且在路徑分析中優先判斷, 所以載入速度最快。原生模組通過名稱來載入。
4.2.2 檔案模組
在硬碟的某個位置,在執行時動態載入,需要完成的路徑分析、檔案定位、編譯執行過程,速度比原生模組慢。
檔案模組通過名稱或路徑來載入,檔案模組的字尾有三種,如下
- .js -- 需要先讀入記憶體再執行
- .json -- fs 讀入記憶體 轉化成JSON物件
- .node -- 經過編譯後的二進位制C/C++擴充套件模組檔案,可以直接使用
4.2.3 第三方模組
- 如果
require
函式只指定名稱則視為從node_modules
下面載入檔案,這樣的話你可以移動模組而不需要修改引用的模組路徑 - 第三方模組的查詢路徑包括
module.paths
和全域性目錄 - 載入最慢
全域性目錄
window如果在環境變數中設定了NODE_PATH
變數,並將變數設定為一個有效的磁碟目錄,require在本地找不到此模組時向在此目錄下找這個模組。
UNIX作業系統中會從 $HOME/.node_modules
$HOME/.node_libraries
目錄下尋找
4.3 載入策略
4.3.1 優先從快取載入
Node對引入過的模組都會進行快取,以減少二次引入時的開銷,與前端瀏覽器快取靜態指令碼不同,瀏覽器僅快取檔案,而Node快取的是編譯和執行後的物件。
不論是原生模組還是檔案模組等, require()
方法對相同模組的載入都一律採用快取優先的方式,這是第一優先順序的。
快取優先策略,如下圖:
4.3.2 路徑分析和檔案定位
module.paths
模組路徑
console.log(module.paths)
[ '/Users/**/Documents/framework/article/node中的CommonJS/node_modules',
'/Users/****/Documents/framework/article/node_modules',
'/Users/**/Documents/framework/node_modules',
'/Users/**/Documents/node_modules',
'/Users/**/node_modules',
'/Users/node_modules',
'/node_modules' ]
複製程式碼
在載入過程中,Node會逐個嘗試module.paths
中的路徑,直到找到目標檔案為止。所以當前檔案的路徑約深,模組查詢耗時越多。所以第三方模組載入速度最慢。
檔案定位
- (1) 副檔名 副檔名順序: .js > .node > .json
嘗試過程中需要呼叫fs模組同步阻塞判斷檔案是否存在,因為是單執行緒,會引起效能問題。
訣竅是: 如果是.node和.json檔案,傳遞時帶上副檔名.
- (2) 目錄分析和包
require()
分析副檔名之後,可能沒有查詢到對應檔案,但卻得到一個目錄,此時Node會將該目錄當做一個包來處理。
首先,Node會在當前目錄下查詢package.json
,從中取出main
屬性指定的檔案進行定位。
如果檔案缺少副檔名,將會進入副檔名分析的步驟。
如果main
屬性指定的檔名錯誤,或者根本沒有package.json
,Node會將index
當做預設檔名,然後依次查詢index.js
、index.json
、index.node
。
如果在目錄分析中沒有定位成功任何檔案,則進入下一個模組路徑進行查詢。如果模組路徑陣列都被遍歷完畢,依然沒有查詢到目標檔案,則會丟擲查詢失敗的異常。
4.3.3 檔案模組查詢規則總結
如下圖:
5 模組編譯(檔案模組)
5.1 module
的屬性
在Node中,每個檔案模組都是一個物件,定義如下:
console.log(module)
/*
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/.../article/015_node中的CommonJS/tempCodeRunnerFile.js',
loaded: false,
children: [],
paths:
[ '/Users/.../article/015_node中的CommonJS/node_modules',
'/Users/.../article/node_modules',
'/Users/.../node_modules',
'/Users/.../node_modules',
'/Users/.../node_modules',
'/Users/node_modules',
'/node_modules' ] }
*/
複製程式碼
編譯和執行是引入檔案模組的最後一個階段。定位到具體檔案後,Node會建一個模組物件,然後根據路徑載入並編譯。對於不同的副檔名,載入的方法也不同,具體如下所示:
- .js 檔案。通過 fs 模組同步讀取檔案後編譯執行。
- .node 檔案。這是用 **C/C++編寫的擴充套件檔案,通過dlopen()**方法載入最後編譯生成的檔案。
- .json 檔案。通過 fs 模組同步讀取檔案後,用
JSON.parse()
解析返回結果。 - 其餘副檔名檔案。他們都被當做**.js**檔案載入
5.2 js模組的編譯
在編譯過程中,Node對獲取的JS檔案內容進行了頭尾包裝,這樣,每個檔案模組之間都進行了作用域隔離。如下:
(function(exports, require, module, __filename, __dirname){
})
複製程式碼
模擬
require
方法的原理,如下:
// b.js
console.log('b.js')
exports.name = "b"
// a.js
let fs = require('fs');
let path = require('path')
let b = require2('./b.js')
function require2(mod) {
let filename = path.join(__dirname, mod);
let content = fs.readFileSync(filename, 'utf8');
let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + "\n return module.exports")
let module = {
exports: {}
}
return fn(module.exports, require2, module, __filename, __dirname)
}
// b.js
複製程式碼
6 exports
VS module.exports
通過exports
和module.exports
對外公開的方法都可以訪問,但有區別。
6.1 聯絡
exports
僅僅是 module.exports
的一個地址引用。
nodejs 只會匯出 module.exports
的指向,如果 exports
指向變了,那就僅僅是 exports 不在指向 module.exports
,於是不會再被匯出。
舉個栗子,如下:
// test3.js
let counter = 0;
exports.printNextCount = function () {
counter += 2;
console.log(counter);
}
module.exports = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter)
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/*
{ printNextCount: [Function] }
[Function]
false
*/
// test3_require.js
let Counter = require('./test3.js')
let counterObj = new Counter();
counterObj.printNextCount();
/*
10
*/
複製程式碼
6.2 區別
6.2.1 根本區別
- exports 返回的是模組函式
- module.exports 返回的是模組物件本身,返回的是一個類
舉個栗子,入下:
// test1.js
let counter = 0;
exports.temp = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter);
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/*
{ temp: [Function] } // 是一個函式可以直接呼叫
{ temp: [Function] } // 是一個函式可以直接呼叫
true
*/
// test1_require.js
// 只能作為函式呼叫
let counter = require('./test1')
console.log(counter) // { temp: [Function] }
counter.temp() // 只能作為函式呼叫
複製程式碼
6.2.2 使用區別
- exports 的方法可以直接呼叫
- module.exports 需要new物件之後才可以呼叫
使用這樣的好處是exports只能對外暴露單個函式,但是module.exports卻能暴露一個類
舉個栗子,如下:
// test2.js
let counter = 0;
module.exports = function () {
counter += 10;
this.printNextCount = function () {
console.log(counter);
}
}
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
/*
{}
[Function] // 是一個類,需要new才能呼叫
false
*/
// test2_require.js
let Counter = require('./test2');
// 直接呼叫報錯
// console.log(Counter.printNextCount()) // TypeError: Counter.printNextCount is not a function
// new一個物件再呼叫
let counterObj = new Counter();
counterObj.printNextCount();
/*
10
*/
複製程式碼
6.3 使用建議
- 最好別分別定義
module.exports
和exports
- 匯出物件用
module.exports
,匯出多個方法和變數用exports