JavaScript 標(biāo)準(zhǔn)參考教程(alpha)
CommonJS規(guī)范
來自《JavaScript 標(biāo)準(zhǔn)參考教程(alpha)》,by 阮一峰
目錄
AMD規(guī)范與CommonJS規(guī)范的兼容性
概述
Node 應(yīng)用由模塊組成,采用 CommonJS 模塊規(guī)范。
每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。
// example.jsvarx=5;varaddX=function(value){returnvalue+x;};
上面代碼中,變量x和函數(shù)addX,是當(dāng)前文件example.js私有的,其他文件不可見。
如果想在多個文件分享變量,必須定義為global對象的屬性。
global.warning=true;
上面代碼的warning變量,可以被所有文件讀取。當(dāng)然,這樣寫法是不推薦的。
CommonJS規(guī)范規(guī)定,每個模塊內(nèi)部,module變量代表當(dāng)前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。
varx=5;varaddX=function(value){returnvalue+x;};module.exports.x=x;module.exports.addX=addX;
上面代碼通過module.exports輸出變量x和函數(shù)addX。
require方法用于加載模塊。
varexample=require('./example.js');console.log(example.x);// 5console.log(example.addX(1));// 6
require方法的詳細(xì)解釋參見《Require命令》一節(jié)。
CommonJS模塊的特點如下。
所有代碼都運行在模塊作用域,不會污染全局作用域。
模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
module對象
Node內(nèi)部提供一個Module構(gòu)建函數(shù)。所有模塊都是Module的實例。
functionModule(id,parent){this.id=id;this.exports={};this.parent=parent;// ...
每個模塊內(nèi)部,都有一個module對象,代表當(dāng)前模塊。它有以下屬性。
module.id模塊的識別符,通常是帶有絕對路徑的模塊文件名。
module.filename模塊的文件名,帶有絕對路徑。
module.loaded返回一個布爾值,表示模塊是否已經(jīng)完成加載。
module.parent返回一個對象,表示調(diào)用該模塊的模塊。
module.children返回一個數(shù)組,表示該模塊要用到的其他模塊。
module.exports表示模塊對外輸出的值。
下面是一個示例文件,最后一行輸出module變量。
// example.jsvarjquery=require('jquery');exports.$=jquery;console.log(module);
執(zhí)行這個文件,命令行會輸出如下信息。
{id:'.',exports:{'$':[Function]},parent:null,filename:'/path/to/example.js',loaded:false,children:[{id:'/path/to/node_modules/jquery/dist/jquery.js',exports:[Function],parent:[Circular],filename:'/path/to/node_modules/jquery/dist/jquery.js',loaded:true,children:[],paths:[Object]}],paths:['/home/user/deleted/node_modules','/home/user/node_modules','/home/node_modules','/node_modules']}
如果在命令行下調(diào)用某個模塊,比如node something.js,那么module.parent就是null。如果是在腳本之中調(diào)用,比如require('./something.js'),那么module.parent就是調(diào)用它的模塊。利用這一點,可以判斷當(dāng)前模塊是否為入口腳本。
if(!module.parent){// ran with `node something.js`app.listen(8088,function(){console.log('app listening on port 8088');})}else{// used with `require('/.something.js')`module.exports=app;}
module.exports屬性
module.exports屬性表示當(dāng)前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取module.exports變量。
varEventEmitter=require('events').EventEmitter;module.exports=newEventEmitter();setTimeout(function(){module.exports.emit('ready');},1000);
上面模塊會在加載后1秒后,發(fā)出ready事件。其他文件監(jiān)聽該事件,可以寫成下面這樣。
vara=require('./a');a.on('ready',function(){console.log('module a is ready');});
exports變量
為了方便,Node為每個模塊提供一個exports變量,指向module.exports。這等同在每個模塊頭部,有一行這樣的命令。
varexports=module.exports;
造成的結(jié)果是,在對外輸出模塊接口時,可以向exports對象添加方法。
exports.area=function(r){returnMath.PI*r*r;};exports.circumference=function(r){return2*Math.PI*r;};
注意,不能直接將exports變量指向一個值,因為這樣等于切斷了exports與module.exports的聯(lián)系。
exports=function(x){console.log(x)};
上面這樣的寫法是無效的,因為exports不再指向module.exports了。
下面的寫法也是無效的。
exports.hello=function(){return'hello';};module.exports='Hello world';
上面代碼中,hello函數(shù)是無法對外輸出的,因為module.exports被重新賦值了。
這意味著,如果一個模塊的對外接口,就是一個單一的值,不能使用exports輸出,只能使用module.exports輸出。
module.exports=function(x){console.log(x);};
如果你覺得,exports與module.exports之間的區(qū)別很難分清,一個簡單的處理方法,就是放棄使用exports,只使用module.exports。
AMD規(guī)范與CommonJS規(guī)范的兼容性
CommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。AMD規(guī)范則是非同步加載模塊,允許指定回調(diào)函數(shù)。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。但是,如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。
AMD規(guī)范使用define方法定義模塊,下面就是一個例子:
define(['package/lib'],function(lib){functionfoo(){lib.log('hello world!');}return{foo:foo};});
AMD規(guī)范允許輸出的模塊兼容CommonJS規(guī)范,這時define方法需要寫成下面這樣:
define(function(require,exports,module){varsomeModule=require("someModule");varanotherModule=require("anotherModule");someModule.doTehAwesome();anotherModule.doMoarAwesome();exports.asplode=function(){someModule.doTehAwesome();anotherModule.doMoarAwesome();};});
require命令
基本用法
Node使用CommonJS模塊規(guī)范,內(nèi)置的require命令用于加載模塊文件。
require命令的基本功能是,讀入并執(zhí)行一個JavaScript文件,然后返回該模塊的exports對象。如果沒有發(fā)現(xiàn)指定模塊,會報錯。
// example.jsvarinvisible=function(){console.log("invisible");}exports.message="hi";exports.say=function(){console.log(message);}
運行下面的命令,可以輸出exports對象。
varexample=require('./example.js');example// {//? message: "hi",//? say: [Function]// }
如果模塊輸出的是一個函數(shù),那就不能定義在exports對象上面,而要定義在module.exports變量上面。
module.exports=function(){console.log("hello world")}require('./example2.js')()
上面代碼中,require命令調(diào)用自身,等于是執(zhí)行module.exports,因此會輸出 hello world。
加載規(guī)則
require命令用于加載文件,后綴名默認(rèn)為.js。
varfoo=require('foo');//? 等同于varfoo=require('foo.js');
根據(jù)參數(shù)的不同格式,require命令去不同路徑尋找模塊文件。
(1)如果參數(shù)字符串以“/”開頭,則表示加載的是一個位于絕對路徑的模塊文件。比如,require('/home/marco/foo.js')將加載/home/marco/foo.js。
(2)如果參數(shù)字符串以“./”開頭,則表示加載的是一個位于相對路徑(跟當(dāng)前執(zhí)行腳本的位置相比)的模塊文件。比如,require('./circle')將加載當(dāng)前腳本同一目錄的circle.js。
(3)如果參數(shù)字符串不以“./“或”/“開頭,則表示加載的是一個默認(rèn)提供的核心模塊(位于Node的系統(tǒng)安裝目錄中),或者一個位于各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。
舉例來說,腳本/home/user/projects/foo.js執(zhí)行了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
這樣設(shè)計的目的是,使得不同的模塊可以將所依賴的模塊本地化。
(4)如果參數(shù)字符串不以“./“或”/“開頭,而且是一個路徑,比如require('example-module/path/to/file'),則將先找到example-module的位置,然后再以它為參數(shù),找到后續(xù)路徑。
(5)如果指定的模塊文件沒有發(fā)現(xiàn),Node會嘗試為文件名添加.js、.json、.node后,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯后的二進(jìn)制文件解析。
(6)如果想得到require命令加載的確切文件名,使用require.resolve()方法。
目錄的加載規(guī)則
通常,我們會把相關(guān)的文件會放在一個目錄里面,便于組織。這時,最好為該目錄設(shè)置一個入口文件,讓require方法可以通過這個入口文件,加載整個目錄。
在目錄中放置一個package.json文件,并且將入口文件寫入main字段。下面是一個例子。
// package.json{"name":"some-library","main":"./lib/some-library.js"}
require發(fā)現(xiàn)參數(shù)字符串指向一個目錄以后,會自動查看該目錄的package.json文件,然后加載main字段指定的入口文件。如果package.json文件沒有main字段,或者根本就沒有package.json文件,則會加載該目錄下的index.js文件或index.node文件。
模塊的緩存
第一次加載某個模塊時,Node會緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的module.exports屬性。
require('./example.js');require('./example.js').message="hello";require('./example.js').message// "hello"
上面代碼中,連續(xù)三次使用require命令,加載同一個模塊。第二次加載的時候,為輸出的對象添加了一個message屬性。但是第三次加載的時候,這個message屬性依然存在,這就證明require命令并沒有重新加載模塊文件,而是輸出了緩存。
如果想要多次執(zhí)行某個模塊,可以讓該模塊輸出一個函數(shù),然后每次require這個模塊的時候,重新執(zhí)行一下輸出的函數(shù)。
所有緩存的模塊保存在require.cache之中,如果想刪除模塊的緩存,可以像下面這樣寫。
// 刪除指定模塊的緩存deleterequire.cache[moduleName];// 刪除所有模塊的緩存Object.keys(require.cache).forEach(function(key){deleterequire.cache[key];})
注意,緩存是根據(jù)絕對路徑識別模塊的,如果同樣的模塊名,但是保存在不同的路徑,require命令還是會重新加載該模塊。
環(huán)境變量NODE_PATH
Node執(zhí)行一個腳本時,會先查看環(huán)境變量NODE_PATH。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時,Node會去這些路徑查找。
可以將NODE_PATH添加到.bashrc。
exportNODE_PATH="/usr/local/lib/node"
所以,如果遇到復(fù)雜的相對路徑,比如下面這樣。
varmyModule=require('../../../../lib/myModule');
有兩種解決方法,一是將該文件加入node_modules目錄,二是修改NODE_PATH環(huán)境變量,package.json文件可以采用下面的寫法。
{"name":"node_path","version":"1.0.0","description":"","main":"index.js","scripts":{"start":"NODE_PATH=lib node index.js"},"author":"","license":"ISC"}
NODE_PATH是歷史遺留下來的一個路徑解決方案,通常不應(yīng)該使用,而應(yīng)該使用node_modules目錄機(jī)制。
模塊的循環(huán)加載
如果發(fā)生模塊的循環(huán)加載,即A加載B,B又加載A,則B將加載A的不完整版本。
// a.jsexports.x='a1';console.log('a.js ',require('./b.js').x);exports.x='a2';// b.jsexports.x='b1';console.log('b.js ',require('./a.js').x);exports.x='b2';// main.jsconsole.log('main.js ',require('./a.js').x);console.log('main.js ',require('./b.js').x);
上面代碼是三個JavaScript文件。其中,a.js加載了b.js,而b.js又加載a.js。這時,Node返回a.js的不完整版本,所以執(zhí)行結(jié)果如下。
$node main.jsb.js? a1a.js? b2main.js? a2main.js? b2
修改main.js,再次加載a.js和b.js。
// main.jsconsole.log('main.js ',require('./a.js').x);console.log('main.js ',require('./b.js').x);console.log('main.js ',require('./a.js').x);console.log('main.js ',require('./b.js').x);
執(zhí)行上面代碼,結(jié)果如下。
$node main.jsb.js? a1a.js? b2main.js? a2main.js? b2main.js? a2main.js? b2
上面代碼中,第二次加載a.js和b.js時,會直接從緩存讀取exports屬性,所以a.js和b.js內(nèi)部的console.log語句都不會執(zhí)行了。
require.main
require方法有一個main屬性,可以用來判斷模塊是直接執(zhí)行,還是被調(diào)用執(zhí)行。
直接執(zhí)行的時候(node module.js),require.main屬性指向模塊本身。
require.main===module// true
調(diào)用執(zhí)行的時候(通過require加載該腳本執(zhí)行),上面的表達(dá)式返回false。
模塊的加載機(jī)制
CommonJS模塊的加載機(jī)制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值。請看下面這個例子。
下面是一個模塊文件lib.js。
// lib.jsvarcounter=3;functionincCounter(){counter++;}module.exports={counter:counter,incCounter:incCounter,};
上面代碼輸出內(nèi)部變量counter和改寫這個變量的內(nèi)部方法incCounter。
然后,加載上面的模塊。
// main.jsvarcounter=require('./lib').counter;varincCounter=require('./lib').incCounter;console.log(counter);// 3incCounter();console.log(counter);// 3
上面代碼說明,counter輸出以后,lib.js模塊內(nèi)部的變化就影響不到counter了。
require的內(nèi)部處理流程
require命令是CommonJS規(guī)范之中,用來加載其他模塊的命令。它其實不是一個全局命令,而是指向當(dāng)前模塊的module.require命令,而后者又調(diào)用Node的內(nèi)部命令Module._load。
Module._load=function(request,parent,isMain){// 1. 檢查 Module._cache,是否緩存之中有指定模塊// 2. 如果緩存之中沒有,就創(chuàng)建一個新的Module實例// 3. 將它保存到緩存// 4. 使用 module.load() 加載指定的模塊文件,//? ? 讀取文件內(nèi)容之后,使用 module.compile() 執(zhí)行文件代碼// 5. 如果加載/解析過程報錯,就從緩存刪除該模塊// 6. 返回該模塊的 module.exports};
上面的第4步,采用module.compile()執(zhí)行指定模塊的腳本,邏輯如下。
Module.prototype._compile=function(content,filename){// 1. 生成一個require函數(shù),指向module.require// 2. 加載其他輔助方法到require// 3. 將文件內(nèi)容放到一個函數(shù)之中,該函數(shù)可調(diào)用 require// 4. 執(zhí)行該函數(shù)};
上面的第1步和第2步,require函數(shù)及其輔助方法主要如下。
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據(jù)文件的后綴名,調(diào)用不同的執(zhí)行函數(shù)
一旦require函數(shù)準(zhǔn)備完畢,整個所要加載的腳本內(nèi)容,就被放到一個新的函數(shù)之中,這樣可以避免污染全局環(huán)境。該函數(shù)的參數(shù)包括require、module、exports,以及其他一些參數(shù)。
(function(exports,require,module,__filename,__dirname){// YOUR CODE INJECTED HERE!});
Module._compile方法是同步執(zhí)行的,所以Module._load要等它執(zhí)行完成,才會向用戶返回module.exports的值。
參考鏈接
Addy Osmani,Writing Modular JavaScript With AMD, CommonJS & ES Harmony
Pony Foo,A Gentle Browserify Walkthrough
Nico Reed,What is require?
Fred K. Schott,The Node.js Way - How require() Actually Works
留言
版權(quán)聲明| last modified on 2013-08-13