node中的CommonJS

whynotgonow發表於2019-02-17

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物件,它代表模組自身,而exportsmodule的屬性。

在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 原生模組

httpfspathevents等模組,是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.jsindex.jsonindex.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

通過exportsmodule.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.exportsexports
  • 匯出物件用module.exports,匯出多個方法和變數用exports

7 參考文獻

相關文章