node中的CommonJS

1 JS模塊化的不足

對於JS自己而言,他的規範是薄弱的,具備如下不足:html

  • 沒有模塊系統,不支持封閉的做用域和依賴管理
  • 沒有標準庫,沒有文件系統和IO流API
  • 也沒有包管理系統

2 CommonJS的功能

  • 封裝功能
  • 封閉做用域
  • 可能解決依賴問題
  • 工做效率更高,重構方便

3 CommonJS的模塊規範

CommonJS 是一種使用普遍的JavaScript模塊化規範,核心思想是經過require方法來同步地加載依賴的其餘模塊,經過 module.exports 導出須要暴露的接口。前端

3.1 模塊引用

在CommonJS規範中,存在require()方法,這個方法接受模塊標識,以此引入一個模塊的API到當前上下文中。 模塊引用的示例代碼以下:node

const path = require("path");
複製代碼

3.2 模塊定義

上下文提供了exports對象用於導出當前模塊的方法或者變量,而且它是惟一導出的出口。json

在模塊中,還存在一個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 參考文獻

相關文章
相關標籤/搜索