對於JS自己而言,他的規範是薄弱的,具備如下不足:html
CommonJS 是一種使用普遍的JavaScript模塊化規範,核心思想是經過require方法來同步地加載依賴的其餘模塊,經過 module.exports 導出須要暴露的接口。前端
在CommonJS規範中,存在require()
方法,這個方法接受模塊標識,以此引入一個模塊的API到當前上下文中。 模塊引用的示例代碼以下:node
const path = require("path");
複製代碼
上下文提供了exports
對象用於導出當前模塊的方法或者變量,而且它是惟一導出的出口。json
在模塊中,還存在一個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)
}
複製代碼
模塊標識其實就是傳遞給require()
方法的參數,他必須是符合小駝峯命名的字符串,或者以.
、..
開頭的相對路徑,或者絕對路徑。模塊化
CommonJS的構建的這套模塊導出和引入機制使得用戶徹底不考慮變量污染,命名空間等方案與此相比相形見絀。函數
http
、fs
、path
、events
等模塊,是Node提供的模塊,這些模塊在Node源代碼的編譯過程當中被編譯成二進制。在Node進程啓動時,部分原生代碼就被直接加載進內存中,因此原生模塊引入時,文件定位和編譯執行這個兩個步驟能夠省略掉,而且在路徑分析中優先判斷, 因此加載速度最快。原生模塊經過名稱來加載。性能
在硬盤的某個位置,在運行時動態加載,須要完成的路徑分析、文件定位、編譯執行過程,速度比原生模塊慢。
文件模塊經過名稱或路徑來加載,文件模塊的後綴有三種,以下
require
函數只指定名稱則視爲從node_modules
下面加載文件,這樣的話你能夠移動模塊而不須要修改引用的模塊路徑module.paths
和全局目錄全局目錄
window若是在環境變量中設置了NODE_PATH
變量,並將變量設置爲一個有效的磁盤目錄,require在本地找不到此模塊時向在此目錄下找這個模塊。
UNIX操做系統中會從 $HOME/.node_modules
$HOME/.node_libraries
目錄下尋找
Node對引入過的模塊都會進行緩存,以減小二次引入時的開銷,與前端瀏覽器緩存靜態腳本不一樣,瀏覽器僅緩存文件,而Node緩存的是編譯和執行後的對象。
不管是原生模塊仍是文件模塊等, require()
方法對相同模塊的加載都一概採用緩存優先的方式,這是第一優先級的。
緩存優先策略,以下圖:
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
中的路徑,直到找到目標文件爲止。因此當前文件的路徑約深,模塊查找耗時越多。因此第三方模塊加載速度最慢。
嘗試過程當中須要調用fs模塊同步阻塞判斷文件是否存在,由於是單線程,會引發性能問題。
訣竅是: 若是是.node和.json文件,傳遞時帶上擴展名.
require()
分析文件擴展名以後,可能沒有查找到對應文件,但卻獲得一個目錄,此時Node會將該目錄當作一個包來處理。首先,Node會在當前目錄下查找package.json
,從中取出main
屬性指定的文件進行定位。 若是文件缺乏擴展名,將會進入擴展名分析的步驟。 若是main
屬性指定的文件名錯誤,或者根本沒有package.json
,Node會將index
當作默認文件名,而後依次查找index.js
、index.json
、index.node
。
若是在目錄分析中沒有定位成功任何文件,則進入下一個模塊路徑進行查找。若是模塊路徑數組都被遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗的異常。
以下圖:
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會建一個模塊對象,而後根據路徑載入並編譯。對於不一樣的文件擴展名,載入的方法也不一樣,具體以下所示:
JSON.parse()
解析返回結果。在編譯過程當中,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
複製代碼
exports
VS module.exports
經過exports
和module.exports
對外公開的方法均可以訪問,但有區別。
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 */
複製代碼
舉個栗子,入下:
// 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() // 只能做爲函數調用
複製代碼
使用這樣的好處是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 */
複製代碼
module.exports
和exports
module.exports
,導出多個方法和變量用exports