JavaScript參考流程

JavaScript 標(biāo)準(zhǔn)參考教程(alpha)

草稿二:Node.js

CommonJS規(guī)范

GitHub

TOP

CommonJS規(guī)范

來自《JavaScript 標(biāo)準(zhǔn)參考教程(alpha)》,by 阮一峰

目錄

概述

module對象

module.exports屬性

exports變量

AMD規(guī)范與CommonJS規(guī)范的兼容性

require命令

基本用法

加載規(guī)則

目錄的加載規(guī)則

模塊的緩存

環(huán)境變量NODE_PATH

模塊的循環(huán)加載

require.main

模塊的加載機(jī)制

require的內(nèi)部處理流程

參考鏈接

概述

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

留言

comments powered byDisqus

版權(quán)聲明| last modified on 2013-08-13

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 模塊通常是指編程語言所提供的代碼組織機(jī)制,利用此機(jī)制可將程序拆解為獨立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,259評論 0 0
  • Node.js是目前非常火熱的技術(shù),但是它的誕生經(jīng)歷卻很奇特。 眾所周知,在Netscape設(shè)計出JavaScri...
    w_zhuan閱讀 3,737評論 2 41
  • topics: 1.The Node.js philosophy 2.The reactor pattern 3....
    宮若石閱讀 1,245評論 0 1
  • 模塊 Node 有簡單的模塊加載系統(tǒng)。在 Node 里,文件和模塊是一一對應(yīng)的。下面例子里,foo.js加載同一個...
    保川閱讀 697評論 0 0
  • 什么是模塊化開發(fā)? 前端開發(fā)中,起初只要在script標(biāo)簽中嵌入幾十上百行代碼就能實現(xiàn)一些基本的交互效果,后來js...
    半世韶華憶闌珊閱讀 722評論 0 0

友情鏈接更多精彩內(nèi)容