node的模块机制
?
Node.js模塊的實現(xiàn)
之前在網(wǎng)上查閱了許多介紹Node.js的文章,可惜對于Node.js的模塊機制大都著墨不多。在后續(xù)介紹模塊的使用之前,我認(rèn)為有必要深入一下Node.js的模塊機制。
CommonJS規(guī)范
早在Netscape誕生不久后,JavaScript就一直在探索本地編程的路,Rhino是其代表產(chǎn)物。無奈那時服務(wù)端JavaScript走的路均是參考眾多服務(wù)器端語言來實現(xiàn)的,在這樣的背景之下,一沒有特色,二沒有實用價值。但是隨著JavaScript在前端的應(yīng)用越來越廣泛,以及服務(wù)端JavaScript的推動,JavaScript現(xiàn)有的規(guī)范十分薄弱,不利于JavaScript大規(guī)模的應(yīng)用。那些以JavaScript為宿主語言的環(huán)境中,只有本身的基礎(chǔ)原生對象和類型,更多的對象和API都取決于宿主的提供,所以,我們可以看到JavaScript缺少這些功能:
- JavaScript沒有模塊系統(tǒng)。沒有原生的支持密閉作用域或依賴管理。
- JavaScript沒有標(biāo)準(zhǔn)庫。除了一些核心庫外,沒有文件系統(tǒng)的API,沒有IO流API等。
- JavaScript沒有標(biāo)準(zhǔn)接口。沒有如Web Server或者數(shù)據(jù)庫的統(tǒng)一接口。
- JavaScript沒有包管理系統(tǒng)。不能自動加載和安裝依賴
于是便有了CommonJS(http://www.commonjs.org)規(guī)范的出現(xiàn),其目標(biāo)是為了構(gòu)建JavaScript在包括Web服務(wù)器,桌面,命令行工具,及瀏覽器方面的生態(tài)系統(tǒng)。
CommonJS制定了解決這些問題的一些規(guī)范,而Node.js就是這些規(guī)范的一種實現(xiàn)。Node.js自身實現(xiàn)了require方法作為其引入模塊的方法,同時NPM也基于CommonJS定義的包規(guī)范,實現(xiàn)了依賴管理和模塊自動安裝等功能。這里我們將深入一下Node.js的require機制和NPM基于包規(guī)范的應(yīng)用。
簡單模塊定義和使用
在Node.js中,定義一個模塊十分方便。我們以計算圓形的面積和周長兩個方法為例,來表現(xiàn)Node.js中模塊的定義方式。
var PI = Math.PI; exports.area = function (r) {return PI * r * r; }; exports.circumference = function (r) {return 2 * PI * r; };將這個文件存為circle.js,并新建一個app.js文件,并寫入以下代碼:
var circle = require('./circle.js'); console.log( 'The area of a circle of radius 4 is ' + circle.area(4));可以看到模塊調(diào)用也十分方便,只需要require需要調(diào)用的文件即可。
在require了這個文件之后,定義在exports對象上的方法便可以隨意調(diào)用。Node.js將模塊的定義和調(diào)用都封裝得極其簡單方便,從API對用戶友好這一個角度來說,Node.js的模塊機制是非常優(yōu)秀的。
模塊載入策略
Node.js的模塊分為兩類,一類為原生(核心)模塊,一類為文件模塊。原生模塊在Node.js源代碼編譯的時候編譯進了二進制執(zhí)行文件,加載的速度最快。另一類文件模塊是動態(tài)加載的,加載速度比原生模塊慢。但是Node.js對原生模塊和文件模塊都進行了緩存,于是在第二次require時,是不會有重復(fù)開銷的。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。
node app.js由于通過命令行加載啟動的文件幾乎都為文件模塊。我們從Node.js如何加載文件模塊開始談起。加載文件模塊的工作,主要由原生模塊module來實現(xiàn)和完成,該原生模塊在啟動時已經(jīng)被加載,進程直接調(diào)用到runMain靜態(tài)方法。
// bootstrap main module. Module.runMain = function () {// Load the main module--the command line argument.Module._load(process.argv[1], null, true); };_load靜態(tài)方法在分析文件名之后執(zhí)行
var module = new Module(id, parent);并根據(jù)文件路徑緩存當(dāng)前模塊對象,該模塊實例對象則根據(jù)文件名加載。
module.load(filename);實際上在文件模塊中,又分為3類模塊。這三類文件模塊以后綴來區(qū)分,Node.js會根據(jù)后綴名來決定加載方法。
- .js。通過fs模塊同步讀取js文件并編譯執(zhí)行。
- .node。通過C/C++進行編寫的Addon。通過dlopen方法進行加載。
- .json。讀取文件,調(diào)用JSON.parse解析加載。
這里我們將詳細描述js后綴的編譯過程。Node.js在編譯js文件的過程中實際完成的步驟有對js文件內(nèi)容進行頭尾包裝。以app.js為例,包裝之后的app.js將會變成以下形式:
(function (exports, require, module, __filename, __dirname) {var circle = require('./circle.js');console.log('The area of a circle of radius 4 is ' + circle.area(4)); });這段代碼會通過vm原生模塊的runInThisContext方法執(zhí)行(類似eval,只是具有明確上下文,不污染全局),返回為一個具體的function對象。最后傳入module對象的exports,require方法,module,文件名,目錄名作為實參并執(zhí)行。
這就是為什么require并沒有定義在app.js 文件中,但是這個方法卻存在的原因。從Node.js的API文檔中可以看到還有__filename、__dirname、module、exports幾個沒有定義但是卻存在的變量。其中__filename和__dirname在查找文件路徑的過程中分析得到后傳入的。module變量是這個模塊對象自身,exports是在module的構(gòu)造函數(shù)中初始化的一個空對象({},而不是null)。
在這個主文件中,可以通過require方法去引入其余的模塊。而其實這個require方法實際調(diào)用的就是load方法。
load方法在載入、編譯、緩存了module后,返回module的exports對象。這就是circle.js文件中只有定義在exports對象上的方法才能被外部調(diào)用的原因。
以上所描述的模塊載入機制均定義在lib/module.js中。
require方法中的文件查找策略
由于Node.js中存在4類模塊(原生模塊和3種文件模塊),盡管require方法極其簡單,但是內(nèi)部的加載卻是十分復(fù)雜的,其加載優(yōu)先級也各自不同。
從文件模塊緩存中加載
盡管原生模塊與文件模塊的優(yōu)先級不同,但是都不會優(yōu)先于從文件模塊的緩存中加載已經(jīng)存在的模塊。
從原生模塊加載
原生模塊的優(yōu)先級僅次于文件模塊緩存的優(yōu)先級。require方法在解析文件名之后,優(yōu)先檢查模塊是否在原生模塊列表中。以http模塊為例,盡管在目錄下存在一個http/http.js/http.node/http.json文件,require(“http”)都不會從這些文件中加載,而是從原生模塊中加載。
原生模塊也有一個緩存區(qū),同樣也是優(yōu)先從緩存區(qū)加載。如果緩存區(qū)沒有被加載過,則調(diào)用原生模塊的加載方式進行加載和執(zhí)行。
從文件加載
當(dāng)文件模塊緩存中不存在,而且不是原生模塊的時候,Node.js會解析require方法傳入的參數(shù),并從文件系統(tǒng)中加載實際的文件,加載過程中的包裝和編譯細節(jié)在前一節(jié)中已經(jīng)介紹過,這里我們將詳細描述查找文件模塊的過程,其中,也有一些細節(jié)值得知曉。
require方法接受以下幾種參數(shù)的傳遞:
- http、fs、path等,原生模塊。
- ./mod或../mod,相對路徑的文件模塊。
- /pathtomodule/mod,絕對路徑的文件模塊。
- mod,非原生模塊的文件模塊。
在進入路徑查找之前有必要描述一下module path這個Node.js中的概念。對于每一個被加載的文件模塊,創(chuàng)建這個模塊對象的時候,這個模塊便會有一個paths屬性,其值根據(jù)當(dāng)前文件的路徑計算得到。我們創(chuàng)建modulepath.js這樣一個文件,其內(nèi)容為:
console.log(module.paths);我們將其放到任意一個目錄中執(zhí)行node modulepath.js命令,將得到以下的輸出結(jié)果。
[ '/home/jackson/research/node_modules', '/home/jackson/node_modules', '/home/node_modules', '/node_modules' ]Windows下:
[ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ]可以看出module path的生成規(guī)則為:從當(dāng)前文件目錄開始查找node_modules目錄;然后依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。
除此之外還有一個全局module path,是當(dāng)前node執(zhí)行文件的相對目錄(../../lib/node)。如果在環(huán)境變量中設(shè)置了HOME目錄和NODE_PATH目錄的話,整個路徑還包含NODE_PATH和HOME目錄下的.node_libraries與.node_modules。其最終值大致如下:
[NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node]下圖是筆者從源代碼中整理出來的整個文件查找流程:
簡而言之,如果require絕對路徑的文件,查找時不會去遍歷每一個node_modules目錄,其速度最快。其余流程如下:
整個查找過程十分類似原型鏈的查找和作用域的查找。所幸Node.js對路徑查找實現(xiàn)了緩存機制,否則由于每次判斷路徑都是同步阻塞式進行,會導(dǎo)致嚴(yán)重的性能消耗。
包結(jié)構(gòu)
前面提到,JavaScript缺少包結(jié)構(gòu)。CommonJS致力于改變這種現(xiàn)狀,于是定義了包的結(jié)構(gòu)規(guī)范(http://wiki.commonjs.org/wiki/Packages/1.0?)。而NPM的出現(xiàn)則是為了在CommonJS規(guī)范的基礎(chǔ)上,實現(xiàn)解決包的安裝卸載,依賴管理,版本管理等問題。require的查找機制明了之后,我們來看一下包的細節(jié)。
一個符合CommonJS規(guī)范的包應(yīng)該是如下這種結(jié)構(gòu):
- 一個package.json文件應(yīng)該存在于包頂級目錄下
- 二進制文件應(yīng)該包含在bin目錄下。
- JavaScript代碼應(yīng)該包含在lib目錄下。
- 文檔應(yīng)該在doc目錄下。
- 單元測試應(yīng)該在test目錄下。
由上文的require的查找過程可以知道,Node.js在沒有找到目標(biāo)文件時,會將當(dāng)前目錄當(dāng)作一個包來嘗試加載,所以在package.json文件中最重要的一個字段就是main。而實際上,這一處是Node.js的擴展,標(biāo)準(zhǔn)定義中并不包含此字段,對于require,只需要main屬性即可。但是在除此之外包需要接受安裝、卸載、依賴管理,版本管理等流程,所以CommonJS為package.json文件定義了如下一些必須的字段:
- name。包名,需要在NPM上是唯一的。不能帶有空格。
- description。包簡介。通常會顯示在一些列表中。
- version。版本號。一個語義化的版本號(http://semver.org/?),通常為x.y.z。該版本號十分重要,常常用于一些版本控制的場合。
- keywords。關(guān)鍵字?jǐn)?shù)組。用于NPM中的分類搜索。
- maintainers。包維護者的數(shù)組。數(shù)組元素是一個包含name、email、web三個屬性的JSON對象。
- contributors。包貢獻者的數(shù)組。第一個就是包的作者本人。在開源社區(qū),如果提交的patch被merge進master分支的話,就應(yīng)當(dāng)加上這個貢獻patch的人。格式包含name和email。如:
- bugs。一個可以提交bug的URL地址。可以是郵件地址(mailto:mailxx@domain),也可以是網(wǎng)頁地址(http://url)。
- licenses。包所使用的許可證。例如:
- repositories。托管源代碼的地址數(shù)組。
- dependencies。當(dāng)前包需要的依賴。這個屬性十分重要,NPM會通過這個屬性,幫你自動加載依賴的包。
以下是Express框架的package.json文件,值得參考。
{"name": "express","description": "Sinatra inspired web development framework","version": "3.0.0alpha1-pre","author": "TJ Holowaychuk除了前面提到的幾個必選字段外,我們還發(fā)現(xiàn)了一些額外的字段,如bin、scripts、engines、devDependencies、author。這里可以重點提及一下scripts字段。包管理器(NPM)在對包進行安裝或者卸載的時候需要進行一些編譯或者清除的工作,scripts字段的對象指明了在進行操作時運行哪個文件,或者執(zhí)行拿條命令。如下為一個較全面的scripts案例:
"scripts": {"install": "install.js","uninstall": "uninstall.js","build": "build.js","doc": "make-doc.js","test": "test.js", }如果你完善了自己的JavaScript庫,使之實現(xiàn)了CommonJS的包規(guī)范,那么你可以通過NPM來發(fā)布自己的包,為NPM上5000+的基礎(chǔ)上再加一個模塊。
npm publish <folder>命令十分簡單。但是在這之前你需要通過npm adduser命令在NPM上注冊一個帳戶,以便后續(xù)包的維護。NPM會分析該文件夾下的package.json文件,然后上傳目錄到NPM的站點上。用戶在使用你的包時,也十分簡明:
npm install <package>甚至對于NPM無法安裝的包(因為某些奇怪的網(wǎng)絡(luò)原因),可以通過github手動下載其穩(wěn)定版本,解壓之后通過以下命令進行安裝:
npm install <package.json folder>只需將路徑指向package.json存在的目錄即可。然后在代碼中require('package')即可使用。
Node.js中的require內(nèi)部流程之復(fù)雜,而方法調(diào)用之簡單,實在值得嘆為觀止。更多NPM使用技巧可以參見http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence。
Node.js模塊與前端模塊的異同
通常有一些模塊可以同時適用于前后端,但是在瀏覽器端通過script標(biāo)簽的載入JavaScript文件的方式與Node.js不同。Node.js在載入到最終的執(zhí)行中,進行了包裝,使得每個文件中的變量天然的形成在一個閉包之中,不會污染全局變量。而瀏覽器端則通常是裸露的JavaScript代碼片段。所以為了解決前后端一致性的問題,類庫開發(fā)者需要將類庫代碼包裝在一個閉包內(nèi)。以下代碼片段抽取自著名類庫underscore的定義方式。
(function () {// Establish the root object, `window` in the browser, or `global` on the server.var root = this;var _ = function (obj) {return new wrapper(obj);};if (typeof exports !== 'undefined') {if (typeof module !== 'undefined' && module.exports) {exports = module.exports = _;}exports._ = _;} else if (typeof define === 'function' && define.amd) {// Register as a named module with AMD.define('underscore', function () {return _;});} else {root['_'] = _;} }).call(this);首先,它通過function定義構(gòu)建了一個閉包,將this作為上下文對象直接call調(diào)用,以避免內(nèi)部變量污染到全局作用域。續(xù)而通過判斷exports是否存在來決定將局部變量_綁定給exports,并且根據(jù)define變量是否存在,作為處理在實現(xiàn)了AMD規(guī)范環(huán)境(http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition)下的使用案例。僅只當(dāng)處于瀏覽器的環(huán)境中的時候,this指向的是全局對象(window對象),才將_變量賦在全局對象上,作為一個全局對象的方法導(dǎo)出,以供外部調(diào)用。
所以在設(shè)計前后端通用的JavaScript類庫時,都有著以下類似的判斷:
if (typeof exports !== "undefined") {exports.EventProxy = EventProxy; } else {this.EventProxy = EventProxy; }即,如果exports對象存在,則將局部變量掛載在exports對象上,如果不存在,則掛載在全局對象上。
轉(zhuǎn)載于:https://www.cnblogs.com/xiongmaoblog/p/7284264.html
總結(jié)
- 上一篇: DB2安装教程
- 下一篇: 多个表添加几个相同的字段