(一):什麼是Node.jsjavascript
Node.js從2009年誕生至今,已經發展了兩年有餘,其成長的速度有目共睹。從在github的訪問量超過Rails,到去年末Node.jsS創始人Ryan Dalh加盟Joyent得到企業資助,再到今年發佈Windows移植版本,Node.js的前景得到了技術社區的確定。InfoQ一直在關注Node.js的發展,在今年的兩次Qcon大會(北京站和杭州站)都有專門的講座。爲了更好地促進Node.js在國內的技術推廣,咱們決定開設「深刻淺出Node.js」專欄,邀請來自Node.js領域的佈道師、開發人員、技術專家來說述Node.js的各方面內容,讓讀者對Node.js有更深刻的瞭解,而且可以積極投入到新技術的討論和實踐中。html
專欄的第一篇文章《什麼是Node.js》嘗試從各個角度來闡述Node.js的基本概念、發展歷史、優點等,對該領域不熟悉的開發人員能夠經過本文了解Node.js的一些基礎知識。前端
從名字提及java
有關Node.js的技術報道愈來愈多,Node.js的寫法也是五花八門,有寫成NodeJS的,有寫成Nodejs的,到底哪種寫法最標準呢,咱們不妨遵循官方的說法。在Node.js的官方網站上,一直將其項目稱之爲」Node「或者」Node.js「,沒有發現其餘的說法,」Node「用的最多,考慮到Node這個單詞的意思和用途太普遍,容易讓開發人員誤解,咱們採用了第二種稱呼——」Node.js「,js的後綴點出了Node項目的本意,其餘的名稱五花八門,沒有確切的出處,咱們不推薦使用。node
Node.js不是JS應用、而是JS運行平臺python
看到Node.js這個名字,初學者可能會誤覺得這是一個Javascript應用,事實上,Node.js採用C++語言編寫而成,是一個Javascript的運行環境。爲何採用C++語言呢?據Node.js創始人Ryan Dahl回憶,他最初但願採用Ruby來寫Node.js,可是後來發現Ruby虛擬機的性能不能知足他的要求,後來他嘗試採用V8引擎,因此選擇了C++語言。既然不是Javascript應用,爲什麼叫.js呢?由於Node.js是一個Javascript的運行環境。提到Javascript,你們首先想到的是平常使用的瀏覽器,現代瀏覽器包含了各類組件,包括渲染引擎、Javascript引擎等,其中Javascript引擎負責解釋執行網頁中的Javascript代碼。做爲Web前端最重要的語言之一,Javascript一直是前端工程師的專利。不過,Node.js是一個後端的Javascript運行環境(支持的系統包括*nux、Windows),這意味着你能夠編寫系統級或者服務器端的Javascript代碼,交給Node.js來解釋執行,簡單的命令相似於:nginx
#node helloworld.js |
Node.js採用了Google Chrome瀏覽器的V8引擎,性能很好,同時還提供了不少系統級的API,如文件操做、網絡編程等。瀏覽器端的Javascript代碼在運行時會受到各類安全性的限制,對客戶系統的操做有限。相比之下,Node.js則是一個全面的後臺運行時,爲Javascript提供了其餘語言可以實現的許多功能。c++
Node.js採用事件驅動、異步編程,爲網絡服務而設計git
事件驅動這個詞並不陌生,在某些傳統語言的網絡編程中,咱們會用到回調函數,好比當socket資源達到某種狀態時,註冊的回調函數就會執行。Node.js的設計思想中以事件驅動爲核心,它提供的絕大多數API都是基於事件的、異步的風格。以Net模塊爲例,其中的net.Socket對象就有如下事件:connect、data、end、timeout、drain、error、close等,使用Node.js的開發人員須要根據本身的業務邏輯註冊相應的回調函數。這些回調函數都是異步執行的,這意味着雖然在代碼結構中,這些函數看似是依次註冊的,可是它們並不依賴於自身出現的順序,而是等待相應的事件觸發。事件驅動、異步編程的設計(感興趣的讀者能夠查閱筆者的另外一篇文章《Node.js的異步編程風格》),重要的優點在於,充分利用了系統資源,執行代碼無須阻塞等待某種操做完成,有限的資源能夠用於其餘的任務。此類設計很是適合於後端的網絡服務編程,Node.js的目標也在於此。在服務器開發中,併發的請求處理是個大問題,阻塞式的函數會致使資源浪費和時間延遲。經過事件註冊、異步函數,開發人員能夠提升資源的利用率,性能也會改善。程序員
從Node.js提供的支持模塊中,咱們能夠看到包括文件操做在內的許多函數都是異步執行的,這和傳統語言存在區別,並且爲了方便服務器開發,Node.js的網絡模塊特別多,包括HTTP、DNS、NET、UDP、HTTPS、TLS等,開發人員能夠在此基礎上快速構建Web服務器。以簡單的helloworld.js爲例:
var http = require('http'); |
上面的代碼搭建了一個簡單的http服務器(運行示例部署在http://helloworld.cnodejs.net/中,讀者能夠訪問),在本地監聽80端口,對於任意的http請求,服務器都返回一個頭部狀態碼爲200、Content-Type'值爲text/plain'的」Hello World「文字響應。從這個小例子中,咱們能夠看出幾點:
Node.js的網絡編程比較便利,提供的模塊(在這裏是http)開放了容易上手的API接口,短短几行代碼就能夠構建服務器。
體現了事件驅動、異步編程,在createServer函數的參數中指定了一個回調函數(採用Javascript的匿名函數實現),當有http請求發送過來時,Node.js就會調用該回調函數來處理請求並響應。固然,這個例子相對簡單,沒有太多的事件註冊,在之後的文章中讀者會看到更多的實際例子。
Node.js的特色
下面咱們來講說Node.js的特色。事件驅動、異步編程的特色剛纔已經詳細說過了,這裏再也不重複。
Node.js的性能不錯。按照創始人Ryan Dahl的說法,性能是Node.js考慮的重要因素,選擇C++和V8而不是Ruby或者其餘的虛擬機也是基於性能的目的。Node.js在設計上也是比較大膽,它以單進程、單線程模式運行(很吃驚,對吧?這和Javascript的運行方式一致),事件驅動機制是Node.js經過內部單線程高效率地維護事件循環隊列來實現的,沒有多線程的資源佔用和上下文切換,這意味着面對大規模的http請求,Node.js憑藉事件驅動搞定一切,習慣了傳統語言的網絡服務開發人員可能對多線程併發和協做很是熟悉,可是面對Node.js,咱們須要接受和理解它的特色。由此咱們是否能夠推測出這樣的設計會致使負載的壓力集中在CPU(事件循環處理?)而不是內存(還記得Java虛擬機拋出OutOfMemory異常的日子嗎?),眼見爲實,不如來看看淘寶共享數據平臺團隊對Node.js的性能測試:
物理機配置:RHEL 5.二、CPU 2.2GHz、內存4G
Node.js應用場景:MemCache代理,每次取100字節數據
鏈接池大小:50
併發用戶數:100
測試結果(socket模式):內存(30M)、QPS(16700)、CPU(95%)
從上面的結果,咱們能夠看到在這樣的測試場景下,qps可以達到16700次,內存僅佔用30M(其中V8堆佔用22M),CPU則達到95%,可能成爲瓶頸。此外,還有很多實踐者對Node.js作了性能分析,總的來講,它的性能讓人信服,也是受歡迎的重要緣由。既然Node.js採用單進程、單線程模式,那麼在現在多核硬件流行的環境中,單核性能出色的Node.js如何利用多核CPU呢?創始人Ryan Dahl建議,運行多個Node.js進程,利用某些通訊機制來協調各項任務。目前,已經有很多第三方的Node.js多進程支持模塊發佈,專欄後面的文章會詳細講述Node.js在多核CPU下的編程。
Node.js的另外一個特色是它支持的編程語言是Javascript。關於動態語言和靜態語言的優缺點比較在這裏再也不展開討論。只說三點:
Javascript做爲前端工程師的主力語言,在技術社區中有至關的號召力。並且,隨着Web技術的不斷髮展,特別是前端的重要性增長,很多前端工程師開始試水」後臺應用「,在許多采用Node.js的企業中,工程師都表示由於習慣了Javascript,因此選擇Node.js。
Javascript的匿名函數和閉包特性很是適合事件驅動、異步編程,從helloworld例子中咱們能夠看到回調函數採用了匿名函數的形式來實現,很方便。閉包的做用則更大,看下面的代碼示例:
var hostRequest = http.request(requestOptions,function(response) { |
在上面的代碼中,咱們須要在end事件中處理responseHTML變量,因爲Javascript的閉包特性,咱們能夠在兩個回調函數以外定義responseHTML變量,而後在data事件對應的回調函數中不斷修改其值,並最終在end事件中訪問處理。
Javascript在動態語言中性能較好,有開發人員對Javacript、Python、Ruby等動態語言作了性能分析,發現Javascript的性能要好於其餘語言,再加上V8引擎也是同類的佼佼者,因此Node.js的性能也受益其中。
Node.js發展簡史
2009年2月,Ryan Dahl在博客上宣佈準備基於V8建立一個輕量級的Web服務器並提供一套庫。
2009年5月,Ryan Dahl在GitHub上發佈了最第一版本的部分Node.js包,隨後幾個月裏,有人開始使用Node.js開發應用。
2009年11月和2010年4月,兩屆JSConf大會都安排了Node.js的講座。
2010年年末,Node.js得到雲計算服務商Joyent資助,創始人Ryan Dahl加入Joyent全職負責Node.js的發展。
2011年7月,Node.js在微軟的支持下發布Windows版本。
Node.js應用案例
雖然Node.js誕生剛剛兩年多,可是其發展勢頭逐漸趕超Ruby/Rails,咱們在這裏列舉了部分企業應用Node.js的案例,聽聽來自客戶的聲音。
在社交網站LinkedIn最新發布的移動應用中,NodeJS是該移動應用的後臺基礎。LinkedIn移動開發主管Kiran Prasad對媒體表示,其整個移動軟件平臺都由NodeJS構建而成:
LinkedIn內部使用了大量的技術,可是在移動服務器這一塊,咱們徹底基於Node。
(使用它的緣由)第一,是由於其靈活性。第二,若是你瞭解Node,就會發現它最擅長的事情是與其餘服務通訊。移動應用必須與咱們的平臺API和數據庫交互。咱們沒有作太多數據分析。相比以前採用的Ruby on Rails技術,開發團隊發現Node在性能方面提升不少。他們在每臺物理機上跑了15個虛擬服務器(15個實例),其中4個實例便可處理雙倍流量。容量評估基於負載測試的結果。
企業社會化服務網站Yammer則利用Node建立了針對其自身平臺的跨域代理服務器,第三方的開發人員能夠經過該服務器實現從自身域託管的Javascript代碼與Yammer平臺API的AJAX通訊。Yammer平臺技術主管Jim Patterson對Node的優勢和缺點提出了本身的見解:
(優勢)由於Node是基於事件驅動和無阻塞的,因此很是適合處理併發請求,所以構建在Node上的代理服務器相比其餘技術實現(如Ruby)的服務器表現要好得多。此外,與Node代理服務器交互的客戶端代碼是由javascript語言編寫的,所以客戶端和服務器端都用同一種語言編寫,這是很是美妙的事情。
(缺點)Node是一個相對新的開源項目,因此不太穩定,它老是一直在變,並且缺乏足夠多的第三方庫支持。看起來,就像是Ruby/Rails當年的樣子。
知名項目託管網站GitHub也嘗試了Node應用。該Node應用稱爲NodeLoad,是一個存檔下載服務器(每當你下載某個存儲分支的tarball或者zip文件時就會用到它)。GitHub以前的存檔下載服務器採用Ruby編寫。在舊系統中,下載存檔的請求會建立一個Resque任務。該任務實際上在存檔服務器上運行一個git archive命令,從某個文件服務器中取出數據。而後,初始的請求分配給你一個小型Ruby Sinatra應用等待該任務。它其實只是在檢查memcache flag是否存在,而後再重定向到最終的下載地址上。舊系統運行大約3個Sinatra實例和3個Resque worker。GitHub的開發人員以爲這是Node應用的好機會。Node基於事件驅動,相比Ruby的阻塞模型,Node可以更好地處理git存檔。在編寫新下載服務器過程當中,開發人員以爲Node很是適合該功能,此外,他們還裏利用了Node庫socket.io來監控下載狀態。
不只在國外,Node的優勢也一樣吸引了國內開發人員的注意,淘寶就實際應用了Node技術:
MyFOX 是一個數據處理中間件,負責從一個MySQL集羣中提取數據、計算並輸出統計結果。用戶提交一段SQL語句,MyFOX根據該SQL命令的語義,生成各個數據庫分片所須要執行的查詢語句,併發送至各個分片,再將結果進行彙總和計算。 MyFOX的特色是CPU密集,無文件IO,並只處理只讀數據。起初MyFOX使用PHP編寫,但遇到許多問題。例如PHP是單線程的,MySQL又須要阻塞查詢,所以很難併發請求數據,後來的解決方案是使用nginx和dirzzle,並基於HTTP協議實現接口,並經過curl_multi_get命 令進行請求。不過MyFOX項目組最終仍是決定使用Node.js來實現MyFOX。
選擇Node.js有許多方面的緣由,好比考慮了興趣及社區發展,同時也但願能夠提升併發能力,榨乾CPU。例如,頻繁地打開和關閉鏈接會讓大量端口處於等待狀態,當併發數量上去以後,時常會由於端口不夠用(處於TIME_WAIT狀態)而致使鏈接失敗。以前每每是經過修改系統設置來減小等待時間以繞開這個錯誤,然而使用鏈接池即可以很好地解決這個問題。此外,之前MyFOX會在某些緩存失效的狀況下出現十分密集的訪問壓力,使用 Node.js即可以共享查詢狀態,讓某些請求「等待片刻」,以便系統從新填充緩存內容。
小結
本文簡要介紹了Node.js的基本知識,包括概念、特色、歷史、案例等等。做爲一個僅僅2歲的平臺,Node.js的發展勢頭有目共睹,愈來愈多的企業開始關注並嘗試Node.js,先後端開發人員應該瞭解相關的內容。
(二):Node.js&NPM的安裝與配置
Node.js安裝與配置
Node.js已經誕生兩年有餘,因爲一直處於快速開發中,過去的一些安裝配置介紹多數針對0.4.x版本而言的,並不是適合最新的0.6.x的版本狀況了,對此,咱們將在0.6.x的版本上介紹Node.js的安裝和配置。(本文一概以0.6.1爲例,0.6的其他版本,只需替換版本號便可。從http://nodejs.org/#download能夠查看到最新的二進制版本和源代碼)。
Windows平臺下的Node.js安裝
在過去,Node.js一直不支持在Windows平臺下原生編譯,須要藉助Cygwin或MinGW來模擬POSIX系統,才能編譯安裝。幸運的是2011年6月微軟開始與Joyent合做移植Node.js到Windows平臺上(http://www.infoq.com/cn/news/2011/06/node-exe ),此次合做的成果最終呈如今0.6.x的穩定版的發佈上。此次的版本發佈使得Node.js在Windows平臺上的性能大幅度提升,使用方面也更容易和輕巧,徹底擺脫掉Cygwin或MinGW等實驗室式的環境,而且在某些細節方面,表現出比Linux下更高的性能,細節參見http://www.infoq.com/news/2011/11/Nodejs-Windows。
在Windows(Windows7)平臺下,我將介紹二種安裝Node.js的方法,即普通和文藝安裝方法。
普通的安裝方法
普通安裝方法其實就是最簡單的方法了,對於大多Windows用戶而言,都是不太喜歡折騰的人,你能夠從這裏(http://nodejs.org/dist/v0.6.1/node-v0.6.1.msi )直接下載到Node.js編譯好的msi文件。而後雙擊便可在程序的引導下完成安裝。
在命令行中直接運行:
node -v |
命令行將打印出:
v0.6.1 |
該引導步驟會將node.exe文件安裝到C:\Program Files (x86)\nodejs\目錄下,並將該目錄添加進PATH環境變量。
文藝的安裝方法
Windows平臺下的文藝安裝方法主要提供給那些熱愛折騰,喜歡編譯的同窗們。在編譯源碼以前須要注意的是你的Windows系統是否包含編譯源碼的工具。Node.js的源碼主要由C++代碼和JavaScript代碼構成,可是卻用gyp工具(http://code.google.com/p/gyp/ )來作源碼的項目管理,該工具採用Python語言寫成的。在Windows平臺上,Node.js採用gyp來生成Visual Studio Solution文件,最終經過VC++的編譯器將其編譯爲二進制文件。因此,你須要知足如下兩個條件:
Python(Node.js建議使用2.6或更高版本,不推薦3.0),能夠從這裏(http://python.org/)獲取。
VC++ 編譯器,包含在Visual Studio 2010中(VC++ 2010 Express亦可),VS2010能夠從這裏(http://msdn.microsoft.com/en-us/vstudio/hh388567)找到。
下載Node.js的0.6.1版本的源碼壓縮包(http://nodejs.org/dist/v0.6.1/node-v0.6.1.tar.gz )並解壓之。
經過命令行進入解壓的源碼目錄,執行vcbuild.bat release命令,而後經歷了漫長的等待後,編譯完成後,在Release目錄下能夠找到編譯好的node.exe文件。經過命令行執行node -v。
命令行返回結果爲:
v0.6.1 |
事實上,若是你的編譯環境中存在WiX工具集(http://wix.sourceforge.net/ ),執行vcbuild.bat msi release命令,你將會在Relase目錄下找到node.msi。
是的,咱們回到了一開始的普通安裝方法。所謂文藝就是多走一些路,多看一些風景罷了。
Unix/Linux平臺下的Node.js安裝
因爲Node.js尚處於v0.x.x的版本的快速發展中,Unix/Linux平臺的發行版都不會預置Node的二進制文件,經過源碼進行編譯安裝是目前最好的選擇。並且用Unix/Linux系統的同窗們多數都是文藝程序員,本節只介紹如何經過源碼進行編譯和安裝。
安裝條件
如同在Windows平臺下同樣,Node.js依然是採用gyp工具管理生成項目的,不一樣的是經過make工具進行最終的編譯。因此Unix/Linux平臺下你須要如下幾個必備條件,才能確保編譯完成:
Python。用於gyp,能夠經過在shell下執行python命令,查看是否已安裝python,並確認版本是否符合需求(2.6或更高版本,但不推薦3.0)。
源代碼編譯器,一般 Unix/Linux平臺都自帶了C++的編譯器(GCC/G++)。若是沒有,請經過當前發行版的軟件包安裝工具安裝make,g++這些編譯工具。
Debian/Ubuntu下的工具是apt-get
RedHat/centOS下經過yum命令
Mac OS X下你可能須要安裝xcode來得到編譯器
其次,若是你計劃在Node.js中啓用網絡加密,OpenSSL的加密庫也是必須的。該加密庫是libssl-dev,能夠經過apt-get install libssl-dev等命令安裝。
檢查環境並安裝
完成以上預備條件後,咱們獲取源碼並進行環境檢查吧:
檢查環境並安裝
wget http://nodejs.org/dist/v0.6.1/node-v0.6.1.tar.gz |
若是檢查沒有經過,請確認上面提到的三個條件是否知足。若是configure命令執行成功,就能夠進行編譯了:
make |
Nodejs經過make工具進行編譯和安裝(若是make install不成功,請使用sudo以確保擁有權限)。完成以上兩步後,檢查一下是否安裝成功:
node -v |
檢查是否返回:
v0.6.1 |
至此,Nodejs已經編譯並安裝完成。如需卸載,能夠執行make uninstall進行卸載。
小結
以上介紹了*nix和Windows平臺下Nodejs的安裝,以後能夠如同Nodejs官方網站上介紹的那樣,編寫example.js文件。
var http = require('http'); |
在命令行中執行它:
node example.js |
你就能夠經過瀏覽器訪問http://127.0.0.1:1337獲得Hello World的響應。
安裝NPM
NPM的全稱是Node Package Manager,若是你熟悉ruby的gem,Python的PyPL、setuptools,PHP的pear,那麼你就知道NPM的做用是什麼了。沒錯,它就是Nodejs的包管理器。Nodejs自身提供了基本的模塊。可是在這些基本模塊上開發實際應用須要較多的工做。所幸的是筆者執筆此文的時候NPM上已經有了5112個Nodejs庫或框架,這些庫從各個方面能夠幫助Nodejs的開發者完成較爲複雜的應用。這些庫的數量和活躍也從側面反映出Nodejs社區的發展是十分神速和活躍的。下面我將介紹安裝NPM和經過NPM安裝Nodejs的第三方庫,以及在大陸的網絡環境下,如何更好的利用NPM。
Unix/Linux下安裝NPM
就像NPM的官網(http://npmjs.org/)上介紹的那樣,安裝NPM僅僅是一行命令的事情:
curl http://npmjs.org/install.sh | sh |
這裏詳解一下這句命令的意思,curl http://npmjs.org/install.sh是經過curl命令獲取這個安裝shell腳本,按後經過管道符| 將獲取的腳本交由sh命令來執行。這裏若是沒有權限會安裝不成功,須要加上sudo來確保權限:
curl http://npmjs.org/install.sh | sudo sh |
安裝成功後執行npm命令,會獲得一下的提示:
Usage: npm <command> |
咱們以underscore爲例,來展現下經過npm安裝第三方包的過程。
npm install underscore |
返回:
underscore@1.2.2 ./node_modules/underscore |
因爲一些特殊的網絡環境,直接經過npm install命令安裝第三方庫的時候,常常會出現卡死的狀態。幸運的是國內CNode社區的@fire9同窗利用空餘時間搭建了一個鏡像的NPM資源庫,服務器架設在日本,能夠繞過某些沒必要要的網絡問題。你能夠經過如下這條命令來安裝第三方庫:
npm --registry "http://npm.hacknodejs.com/" install underscore |
若是你想將它設爲默認的資源庫,運行下面這條命令便可:
npm config set registry "http://npm.hacknodejs.com/"
設置以後每次安裝時就能夠不用帶上—registry參數。值得一提的是還有另外一個鏡像可用,該鏡像地址是http://registry.npmjs.vitecho.com,如需使用,替換上面兩行命令的地址便可。
Windows下安裝NPM
因爲Nodejs最初在Linux開發下的歷史緣由,致使NPM一開始也不支持Windows環境,可是隨着Nodejs成功移植到到Windows平臺,NPM在Windows下的需求亦是日漸增長。下面開始Windows下的NPM之旅吧。
安裝GIT工具
因爲github網站不支持直接下載打包了全部submodule的源碼包,因此須要經過git工具來簽出全部的源碼。從http://code.google.com/p/msysgit/downloads/list,能夠下載到msysgit這個Windows平臺下的git客戶端工具(最新版本文件爲Git-1.7.7.1-preview20111027.exe)。在下載以後雙擊安裝。
下載NPM源碼
打開命令行工具(CMD),執行如下命令,能夠經過msysgit簽出NPM的全部源碼和依賴代碼並安裝npm。
git clone --recursive git://github.com/isaacs/npm.git |
在執行這段代碼以前,請確保node.exe是跟經過node.msi的方式安裝的,或者在PATH環境變量中。這段命令也會將npm加入到PATH環境變量中去,以後能夠隨處執行npm命令。若是安裝中遇到權限方面的錯誤,請確保cmd命令行工具是經過管理員身份運行的。安裝成功後,執行如下命令:
npm install underscore |
返回:
underscore@1.2.2 ./node_modules/underscore |
如此,Windows平臺下的NPM安裝完畢。若是遭遇網絡問題沒法安裝,請參照Linux下的NPM命令,添加鏡像地址。
(三):深刻Node.js的模塊機制
專欄的第三篇文章《深刻Node.js的模塊機制》。以前介紹了Node.js安裝的基礎知識,本文將深刻Node.js的模塊機制。
Node.js模塊的實現
以前在網上查閱了許多介紹Node.js的文章,惋惜對於Node.js的模塊機制大都着墨很少。在後續介紹模塊的使用以前,我認爲有必要深刻一下Node.js的模塊機制。
CommonJS規範
早在Netscape誕生不久後,JavaScript就一直在探索本地編程的路,Rhino是其表明產物。無奈那時服務端JavaScript走的路均是參考衆多服務器端語言來實現的,在這樣的背景之下,一沒有特點,二沒有實用價值。可是隨着JavaScript在前端的應用愈來愈普遍,以及服務端JavaScript的推進,JavaScript現有的規範十分薄弱,不利於JavaScript大規模的應用。那些以JavaScript爲宿主語言的環境中,只有自己的基礎原生對象和類型,更多的對象和API都取決於宿主的提供,因此,咱們能夠看到JavaScript缺乏這些功能:
JavaScript沒有模塊系統。沒有原生的支持密閉做用域或依賴管理。
JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。
JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。
JavaScript沒有包管理系統。不能自動加載和安裝依賴。
因而便有了CommonJS(http://www.commonjs.org)規範的出現,其目標是爲了構建JavaScript在包括Web服務器,桌面,命令行工具,及瀏覽器方面的生態系統。
CommonJS制定瞭解決這些問題的一些規範,而Node.js就是這些規範的一種實現。Node.js自身實現了require方法做爲其引入模塊的方法,同時NPM也基於CommonJS定義的包規範,實現了依賴管理和模塊自動安裝等功能。這裏咱們將深刻一下Node.js的require機制和NPM基於包規範的應用。
簡單模塊定義和使用
在Node.js中,定義一個模塊十分方便。咱們以計算圓形的面積和周長兩個方法爲例,來表現Node.js中模塊的定義方式。
var PI = Math.PI; |
將這個文件存爲circle.js,並新建一個app.js文件,並寫入如下代碼:
var circle = require('./circle.js'); |
能夠看到模塊調用也十分方便,只須要require須要調用的文件便可。
在require了這個文件以後,定義在exports對象上的方法即可以隨意調用。Node.js將模塊的定義和調用都封裝得極其簡單方便,從API對用戶友好這一個角度來講,Node.js的模塊機制是很是優秀的。
模塊載入策略
Node.js的模塊分爲兩類,一類爲原生(核心)模塊,一類爲文件模塊。原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。另外一類文件模塊是動態加載的,加載速度比原生模塊慢。可是Node.js對原生模塊和文件模塊都進行了緩存,因而在第二次require時,是不會有重複開銷的。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。
node app.js |
因爲經過命令行加載啓動的文件幾乎都爲文件模塊。咱們從Node.js如何加載文件模塊開始談起。加載文件模塊的工做,主要由原生模塊module來實現和完成,該原生模塊在啓動時已經被加載,進程直接調用到runMain靜態方法。
// bootstrap main module. |
_load靜態方法在分析文件名以後執行
var module = new Module(id, parent); |
並根據文件路徑緩存當前模塊對象,該模塊實例對象則根據文件名加載。
module.load(filename); |
實際上在文件模塊中,又分爲3類模塊。這三類文件模塊之後綴來區分,Node.js會根據後綴名來決定加載方法。
.js。經過fs模塊同步讀取js文件並編譯執行。
.node。經過C/C++進行編寫的Addon。經過dlopen方法進行加載。
.json。讀取文件,調用JSON.parse解析加載。
這裏咱們將詳細描述js後綴的編譯過程。Node.js在編譯js文件的過程當中實際完成的步驟有對js文件內容進行頭尾包裝。以app.js爲例,包裝以後的app.js將會變成如下形式:
(function (exports, require, module, __filename, __dirname) { |
這段代碼會經過vm原生模塊的runInThisContext方法執行(相似eval,只是具備明確上下文,不污染全局),返回爲一個具體的function對象。最後傳入module對象的exports,require方法,module,文件名,目錄名做爲實參並執行。
這就是爲何require並無定義在app.js 文件中,可是這個方法卻存在的緣由。從Node.js的API文檔中能夠看到還有__filename、__dirname、module、exports幾個沒有定義可是卻存在的變量。其中__filename和__dirname在查找文件路徑的過程當中分析獲得後傳入的。module變量是這個模塊對象自身,exports是在module的構造函數中初始化的一個空對象({},而不是null)。
在這個主文件中,能夠經過require方法去引入其他的模塊。而其實這個require方法實際調用的就是load方法。
load方法在載入、編譯、緩存了module後,返回module的exports對象。這就是circle.js文件中只有定義在exports對象上的方法才能被外部調用的緣由。
以上所描述的模塊載入機制均定義在lib/module.js中。
require方法中的文件查找策略
因爲Node.js中存在4類模塊(原生模塊和3種文件模塊),儘管require方法極其簡單,可是內部的加載倒是十分複雜的,其加載優先級也各自不一樣。
從文件模塊緩存中加載
儘管原生模塊與文件模塊的優先級不一樣,可是都不會優先於從文件模塊的緩存中加載已經存在的模塊。
從原生模塊加載
原生模塊的優先級僅次於文件模塊緩存的優先級。require方法在解析文件名以後,優先檢查模塊是否在原生模塊列表中。以http模塊爲例,儘管在目錄下存在一個http/http.js/http.node/http.json文件,require(「http」)都不會從這些文件中加載,而是從原生模塊中加載。
原生模塊也有一個緩存區,一樣也是優先從緩存區加載。若是緩存區沒有被加載過,則調用原生模塊的加載方式進行加載和執行。
從文件加載
當文件模塊緩存中不存在,並且不是原生模塊的時候,Node.js會解析require方法傳入的參數,並從文件系統中加載實際的文件,加載過程當中的包裝和編譯細節在前一節中已經介紹過,這裏咱們將詳細描述查找文件模塊的過程,其中,也有一些細節值得知曉。
require方法接受如下幾種參數的傳遞:
http、fs、path等,原生模塊。
./mod或../mod,相對路徑的文件模塊。
/pathtomodule/mod,絕對路徑的文件模塊。
mod,非原生模塊的文件模塊。
在進入路徑查找以前有必要描述一下module path這個Node.js中的概念。對於每個被加載的文件模塊,建立這個模塊對象的時候,這個模塊便會有一個paths屬性,其值根據當前文件的路徑計算獲得。咱們建立modulepath.js這樣一個文件,其內容爲:
console.log(module.paths); |
咱們將其放到任意一個目錄中執行node modulepath.js命令,將獲得如下的輸出結果。
[ '/home/jackson/research/node_modules', |
Windows下:
[ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ] |
能夠看出module path的生成規則爲:從當前文件目錄開始查找node_modules目錄;而後依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。
除此以外還有一個全局module path,是當前node執行文件的相對目錄(../../lib/node)。若是在環境變量中設置了HOME目錄和NODE_PATH目錄的話,整個路徑還包含NODE_PATH和HOME目錄下的.node_libraries與.node_modules。其最終值大體以下:
[NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node] |
下圖是筆者從源代碼中整理出來的整個文件查找流程:
簡而言之,若是require絕對路徑的文件,查找時不會去遍歷每個node_modules目錄,其速度最快。其他流程以下:
從module path數組中取出第一個目錄做爲查找基準。
直接從目錄中查找該文件,若是存在,則結束查找。若是不存在,則進行下一條查找。
嘗試添加.js、.json、.node後綴後查找,若是存在文件,則結束查找。若是不存在,則進行下一條。
嘗試將require的參數做爲一個包來進行查找,讀取目錄下的package.json文件,取得main參數指定的文件。
嘗試查找該文件,若是存在,則結束查找。若是不存在,則進行第3條查找。
若是繼續失敗,則取出module path數組中的下一個目錄做爲基準查找,循環第1至5個步驟。
若是繼續失敗,循環第1至6個步驟,直到module path中的最後一個值。
若是仍然失敗,則拋出異常。
整個查找過程十分相似原型鏈的查找和做用域的查找。所幸Node.js對路徑查找實現了緩存機制,不然因爲每次判斷路徑都是同步阻塞式進行,會致使嚴重的性能消耗。
包結構
前面提到,JavaScript缺乏包結構。CommonJS致力於改變這種現狀,因而定義了包的結構規範(http://wiki.commonjs.org/wiki/Packages/1.0 )。而NPM的出現則是爲了在CommonJS規範的基礎上,實現解決包的安裝卸載,依賴管理,版本管理等問題。require的查找機制明瞭以後,咱們來看一下包的細節。
一個符合CommonJS規範的包應該是以下這種結構:
一個package.json文件應該存在於包頂級目錄下
二進制文件應該包含在bin目錄下。
JavaScript代碼應該包含在lib目錄下。
文檔應該在doc目錄下。
單元測試應該在test目錄下。
由上文的require的查找過程能夠知道,Node.js在沒有找到目標文件時,會將當前目錄看成一個包來嘗試加載,因此在package.json文件中最重要的一個字段就是main。而實際上,這一處是Node.js的擴展,標準定義中並不包含此字段,對於require,只須要main屬性便可。可是在除此以外包須要接受安裝、卸載、依賴管理,版本管理等流程,因此CommonJS爲package.json文件定義了以下一些必須的字段:
name。包名,須要在NPM上是惟一的。不能帶有空格。
description。包簡介。一般會顯示在一些列表中。
version。版本號。一個語義化的版本號(http://semver.org/ ),一般爲x.y.z。該版本號十分重要,經常用於一些版本控制的場合。
keywords。關鍵字數組。用於NPM中的分類搜索。
maintainers。包維護者的數組。數組元素是一個包含name、email、web三個屬性的JSON對象。
contributors。包貢獻者的數組。第一個就是包的做者本人。在開源社區,若是提交的patch被merge進master分支的話,就應當加上這個貢獻patch的人。格式包含name和email。如:
"contributors": [{ |
bugs。一個能夠提交bug的URL地址。能夠是郵件地址(mailto:mailxx@domain),也能夠是網頁地址(http://url)。
licenses。包所使用的許可證。例如:
"licenses": [{ |
repositories。託管源代碼的地址數組。
dependencies。當前包須要的依賴。這個屬性十分重要,NPM會經過這個屬性,幫你自動加載依賴的包。
如下是Express框架的package.json文件,值得參考。
{ |
除了前面提到的幾個必選字段外,咱們還發現了一些額外的字段,如bin、scripts、engines、devDependencies、author。這裏能夠重點說起一下scripts字段。包管理器(NPM)在對包進行安裝或者卸載的時候須要進行一些編譯或者清除的工做,scripts字段的對象指明瞭在進行操做時運行哪一個文件,或者執行拿條命令。以下爲一個較全面的scripts案例:
"scripts": { |
若是你完善了本身的JavaScript庫,使之實現了CommonJS的包規範,那麼你能夠經過NPM來發布本身的包,爲NPM上5000+的基礎上再加一個模塊。
npm publish <folder> |
命令十分簡單。可是在這以前你須要經過npm adduser命令在NPM上註冊一個賬戶,以便後續包的維護。NPM會分析該文件夾下的package.json文件,而後上傳目錄到NPM的站點上。用戶在使用你的包時,也十分簡明:
npm install <package> |
甚至對於NPM沒法安裝的包(由於某些奇怪的網絡緣由),能夠經過github手動下載其穩定版本,解壓以後經過如下命令進行安裝:
npm install <package.json folder> |
只需將路徑指向package.json存在的目錄便可。而後在代碼中require('package')便可使用。
Node.js中的require內部流程之複雜,而方法調用之簡單,實在值得歎爲觀止。更多NPM使用技巧能夠參見http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence。
Node.js模塊與前端模塊的異同
一般有一些模塊能夠同時適用於先後端,可是在瀏覽器端經過script標籤的載入JavaScript文件的方式與Node.js不一樣。Node.js在載入到最終的執行中,進行了包裝,使得每一個文件中的變量自然的造成在一個閉包之中,不會污染全局變量。而瀏覽器端則一般是裸露的JavaScript代碼片斷。因此爲了解決先後端一致性的問題,類庫開發者須要將類庫代碼包裝在一個閉包內。如下代碼片斷抽取自著名類庫underscore的定義方式。
(function () { |
首先,它經過function定義構建了一個閉包,將this做爲上下文對象直接call調用,以免內部變量污染到全局做用域。續而經過判斷exports是否存在來決定將局部變量_綁定給exports,而且根據define變量是否存在,做爲處理在實現了AMD規範環境(http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition)下的使用案例。僅只當處於瀏覽器的環境中的時候,this指向的是全局對象(window對象),纔將_變量賦在全局對象上,做爲一個全局對象的方法導出,以供外部調用。
因此在設計先後端通用的JavaScript類庫時,都有着如下相似的判斷:
if (typeof exports !== "undefined") { |
即,若是exports對象存在,則將局部變量掛載在exports對象上,若是不存在,則掛載在全局對象上。
對於更多前端的模塊實現能夠參考國內淘寶玉伯的seajs(http://seajs.com/),或者思科杜歡的oye(http://www.w3cgroup.com/oye/)。
(四):Node.js的事件機制
專欄的第四篇文章《Node.js的事件機制》。以前介紹了Node.js的模塊機制,本文將深刻Node.js的事件部分。
Node.js的事件機制
Node.js在其Github代碼倉庫(https://github.com/joyent/node)上有着一句短短的介紹:Evented I/O for V8 JavaScript。這句近似廣告語的句子卻道盡了Node.js自身的特點所在:基於V8引擎實現的事件驅動IO。在本文的這部份內容中,我來揭開這Evented這個關鍵詞的一切奧祕吧。
Node.js可以在衆多的後端JavaScript技術之中脫穎而出,正是因其基於事件的特色而受到歡迎。拿Rhino來作比較,能夠看出Rhino引擎支持的後端JavaScript擺脫不掉其餘語言同步執行的影響,致使JavaScript在後端編程與前端編程之間有着十分顯著的差異,在編程模型上沒法造成統一。在前端編程中,事件的應用十分普遍,DOM上的各類事件。在Ajax大規模應用以後,異步請求更獲得普遍的認同,而Ajax亦是基於事件機制的。在Rhino中,文件讀取等操做,均是同步操做進行的。在這類單線程的編程模型下,若是採用同步機制,沒法與PHP之類的服務端腳本語言的成熟度媲美,性能也沒有值得可圈可點的部分。直到Ryan Dahl在2009年推出Node.js後,後端JavaScript才走出其迷局。Node.js的推出,我以爲該變了兩個情況:
統一了先後端JavaScript的編程模型。
利用事件機制充分利用用異步IO突破單線程編程模型的性能瓶頸,使得JavaScript在後端達到實用價值。
有了第二次瀏覽器大戰中的佼佼者V8的適時助力,使得Node.js在短短的兩年內達到可觀的運行效率,並迅速被你們接受。這一點從Node.js項目在Github上的流行度和NPM上的庫的數量可見一斑。
至於Node.js爲什麼會選擇Evented I/O for V8 JavaScript的結構和形式來實現,能夠參見一下2011年初對做者Ryan Dahl的一次採訪:http://bostinno.com/2011/01/31/node-js-interview-4-questions-with-creator-ryan-dahl/ 。
事件機制的實現
Node.js中大部分的模塊,都繼承自Event模塊(http://nodejs.org/docs/latest/api/events.html )。Event模塊(events.EventEmitter)是一個簡單的事件監聽器模式的實現。具備addListener/on,once,removeListener,removeAllListeners,emit等基本的事件監聽模式的方法實現。它與前端DOM樹上的事件並不相同,由於它不存在冒泡,逐層捕獲等屬於DOM的事件行爲,也沒有preventDefault()、stopPropagation()、 stopImmediatePropagation() 等處理事件傳遞的方法。
從另外一個角度來看,事件偵聽器模式也是一種事件鉤子(hook)的機制,利用事件鉤子導出內部數據或狀態給外部調用者。Node.js中的不少對象,大多具備黑盒的特色,功能點較少,若是不經過事件鉤子的形式,對象運行期間的中間值或內部狀態,是咱們沒法獲取到的。這種經過事件鉤子的方式,可使編程者不用關注組件是如何啓動和執行的,只需關注在須要的事件點上便可。
var options = { |
在這段HTTP request的代碼中,程序員只須要將視線放在error,data這些業務事件點便可,至於內部的流程如何,無需過於關注。
值得一提的是若是對一個事件添加了超過10個偵聽器,將會獲得一條警告,這一處設計與Node.js自身單線程運行有關,設計者認爲偵聽器太多,可能致使內存泄漏,因此存在這樣一個警告。調用:
emitter.setMaxListeners(0); |
能夠將這個限制去掉。
其次,爲了提高Node.js的程序的健壯性,EventEmitter對象對error事件進行了特殊對待。若是運行期間的錯誤觸發了error事件。EventEmitter會檢查是否有對error事件添加過偵聽器,若是添加了,這個錯誤將會交由該偵聽器處理,不然,這個錯誤將會做爲異常拋出。若是外部沒有捕獲這個異常,將會引發線程的退出。
事件機制的進階應用
繼承event.EventEmitter
實現一個繼承了EventEmitter類是十分簡單的,如下是Node.js中流對象繼承EventEmitter的例子:
function Stream() { |
Node.js在工具模塊中封裝了繼承的方法,因此此處能夠很便利地調用。程序員能夠經過這樣的方式輕鬆繼承EventEmitter對象,利用事件機制,能夠幫助你解決一些問題。
多事件之間協做
在略微大一點的應用中,數據與Web服務器之間的分離是必然的,如新浪微博、Facebook、Twitter等。這樣的優點在於數據源統一,而且能夠爲相同數據源制定各類豐富的客戶端程序。以Web應用爲例,在渲染一張頁面的時候,一般須要從多個數據源拉取數據,並最終渲染至客戶端。Node.js在這種場景中能夠很天然很方便的同時並行發起對多個數據源的請求。
api.getUser("username", function (profile) { |
Node.js經過異步機制使請求之間無阻塞,達到並行請求的目的,有效的調用下層資源。可是,這個場景中的問題是對於多個事件響應結果的協調並不是被Node.js原生優雅地支持。爲了達到三個請求都獲得結果後才進行下一個步驟,程序也許會被變成如下狀況:
api.getUser("username", function (profile) { |
這將致使請求變爲串行進行,沒法最大化利用底層的API服務器。
爲解決這類問題,我曾寫做一個模塊(EventProxy,https://github.com/JacksonTian/eventproxy)來實現多事件協做,如下爲上面代碼的改進版:
var proxy = new EventProxy(); |
EventProxy也是一個簡單的事件偵聽者模式的實現,因爲底層實現跟Node.js的EventEmitter不一樣,沒法合併進Node.js中。可是卻提供了比EventEmitter更強大的功能,且API保持與EventEmitter一致,與Node.js的思路保持契合,並能夠適用在前端中。
這裏的all方法是指偵聽完profile、timeline、skin三個方法後,執行回調函數,並將偵聽接收到的數據傳入。
最後還介紹一種解決多事件協做的方案:Jscex(https://github.com/JeffreyZhao/jscex )。Jscex經過運行時編譯的思路(須要時也可在運行前編譯),將同步思惟的代碼轉換爲最終異步的代碼來執行,能夠在編寫代碼的時候經過同步思惟來寫,能夠享受到同步思惟的便利寫做,異步執行的高效性能。若是經過Jscex編寫,將會是如下形式:
var data = $await(Task.whenAll({ |
此節感謝Jscex做者@老趙(http://blog.zhaojie.me/)的指正和幫助。
利用事件隊列解決雪崩問題
所謂雪崩問題,是在緩存失效的情景下,大併發高訪問量同時涌入數據庫中查詢,數據庫沒法同時承受如此大的查詢請求,進而往前影響到網站總體響應緩慢。那麼在Node.js中如何應付這種情景呢。
var select = function (callback) { |
以上是一句數據庫查詢的調用,若是站點恰好啓動,這時候緩存中是不存在數據的,而若是訪問量巨大,同一句SQL會被髮送到數據庫中反覆查詢,影響到服務的總體性能。一個改進是添加一個狀態鎖。
var status = "ready"; |
可是這種情景,連續的屢次調用select發,只有第一次調用是生效的,後續的select是沒有數據服務的。因此這個時候引入事件隊列吧:
var proxy = new EventProxy(); |
這裏利用了EventProxy對象的once方法,將全部請求的回調都壓入事件隊列中,並利用其執行一次就會將監視器移除的特色,保證每個回調只會被執行一次。對於相同的SQL語句,保證在同一個查詢開始到結束的時間中永遠只有一次,在這查詢期間到來的調用,只需在隊列中等待數據就緒便可,節省了重複的數據庫調用開銷。因爲Node.js單線程執行的緣由,此處無需擔憂狀態問題。這種方式其實也能夠應用到其餘遠程調用的場景中,即便外部沒有緩存策略,也能有效節省重複開銷。此處也能夠用EventEmitter替代EventProxy,不過可能存在偵聽器過多,引起警告,須要調用setMaxListeners(0)移除掉警告,或者設更大的警告閥值。