1 概述
CommonJS對模塊的定義十分簡單,主要分為模塊定義、模塊引用和模塊標識。Nodejs的模塊系統就遵循了CommonJS規范。但Node在實現中並非完全按照CommonJS規范實現,而是對模塊規范進行了一定的取舍。下面,我們結合Node來深入了解CommonJS規范。
2 模塊定義
CommonJS規范規定,一個文件就是一個模塊,用module變量代表當前模塊。 Node在其內部提供一個Module的構建函數。所有模塊都是Module的實例。實例代碼如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; this.filename = null; this.loaded = false; this.children = []; } module.exports = Module; var module = new Module(filename, parent);
每個模塊內部,都有一個module對象,代表當前模塊。它的屬性如下:
- module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名。
- module.filename 模塊的文件名,帶有絕對路徑。
- module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
- module.parent 返回一個對象,表示調用該模塊的模塊。
- module.children 返回一個數組,表示該模塊要用到的其他模塊。
- module.exports 初始值為一個空對象{},表示模塊對外輸出的接口。
2.1 module.exports屬性
module.exports屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取module.exports變量。
例如,我們在moduleA.js文件中定義funA方法,並用module.exports變量把該方法暴露出,實例代碼如下:
//moduleA.js module.exports.funcA= function(){ console.log('This is moduleA!'); }
然后,在moduleB模塊中加載引入moduleA模塊,便可以使用funA方法了,示例代碼如下:
//moduleB.js var a = require('./moduleA'); a.funcA();//打印'This is moduleA!'
2.2 exports變量
為了方便,Node為每個模塊提供一個exports變量,指向module.exports。在模塊內部大概是這樣的:
var exports = module.exports={};
在對外輸出模塊接口時,可以向exports對象添加方法。
//a.js var funA=function(){ console.log('This is module a!'); }; exports.funA=funA;//等同於module.exports.funA=funA;
exports 賦值其實是給 module.exports 這個空對象添加myName屬性而已,為什么是給exports添加屬性,而不直接exports= funA呢?
因為, exports 是指向的 module.exports 的引用。如果直接是給exports賦值而不是添加屬性的話,exports 就不再指向module.exports 了。當exports 被改變的時候,module.exports將不會被改變,而模塊導出的時候,真正導出的執行是module.exports,而不是exports。例如,將a.js改為:
//a.js var funA=function(){ console.log('This is module a!'); }; exports =funA;
這樣是無效的。因為,前面是通過給 exports 添加屬性,而現在對 exports 指向的內存做了修改,exports 和 module.exports 不再指向同一塊內存,即 module.exports 指向的那塊內存並沒有做任何改變,仍然為一個空對象 {},所以funA方法輸出無效。
如果覺得module.exports和exports難以分清的話,個人建議可以全部使用module.exports來應對所有的情況,並盡量減少犯錯的機會。
3 模塊引用
require函數的基本功能是,讀入並執行一個JavaScript文件,然后返回該模塊的exports對象。當我們用require()獲取module時,Node會根據module.id找到對應的module,並返回module. exports,這樣就實現了模塊的輸出。
require函數使用一個參數,參數值可以帶有完整路徑的模塊的文件名,也可以為模塊名。
假如,有三個文件:一個是a.js(存放路徑:home/a.js),一個是b.js(存放路徑:home/user/b.js), 一個是c.js(存放路徑:home/user/c.js)。我們在a.js文件中引用三個模塊,實例代碼如下:
var httpModule=require('HTTP');//用 “模塊名”加載服務模塊http var b=require('./user/b');//用“相對路徑”加載文件b.js var b=require('../ home/user/c');//用“絕對路徑”加載文件c.js
4 模塊標識
模塊標識就是傳遞給require方法的參數,必須符合小駝峰命名的字符串,或者以.、..開頭的相對路徑,或者絕對路徑,默認文件名后綴.js。在Node實現中,正是基於這樣一個標識符進行模塊查找的,如果沒有發現指定模塊會報錯。
根據參數的不同格式,require命令去不同路徑尋找模塊文件。加載規則如下:
(1)如果參數字符串以“/”開頭,則表示加載的是一個位於絕對路徑的模塊文件。比如,require('/home/marco/foo.js')將加載/home/marco/foo.js。
(2)如果參數字符串以“./”開頭,則表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。比如,require('./circle')將加載當前腳本同一目錄的circle.js。
(3)如果參數字符串不以“./“或”/“開頭,則表示加載的是一個默認提供的核心模塊(位於Node的系統安裝目錄中),或者一個位於各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。
舉例來說,腳本/home/user/projects/foo.js執行了require('bar.js')命令,Node會依次搜索以下文件。
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
這樣設計的目的是,使得不同的模塊可以將所依賴的模塊本地化。
(4)如果參數字符串不以“./“或”/“開頭,而且是一個路徑,比如require('example-module/path/to/file'),則將先找到example-module的位置,然后再以它為參數,找到后續路徑。
(5)如果指定的模塊文件沒有發現,Node會嘗試為文件名添加.js、.json、.node后,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯后的二進制文件解析。
(6)如果想得到require命令加載的確切文件名,使用require.resolve()方法。
CommonJS是同步的,意味着你想調用模塊里的方法,必須先用require加載模塊。這對服務器端的Nodejs來說不是問題,因為模塊的JS文件都在本地硬盤上,CPU的讀取時間非常快,同步不是問題。但如果是瀏覽器環境,要從服務器加載模塊。模塊的加載將取決於網速,如果采用同步,網絡情緒不穩定時,頁面可能卡住,這就必須采用異步模式。所以,就有了 AMD解決方案。下一篇我們開始介紹模塊化規范的AMD規范;
參考鏈接
[1] https://en.wikipedia.org/wiki/CommonJS
[2] https://nodejs.org/api/modules.html#modules_modules
[3] http://javascript.ruanyifeng.com/nodejs/module.html#toc2