Node.js是目前非郴系火熱的技術(shù),但是它的誕生經(jīng)歷卻很奇特胁澳。
眾所周知该互,在Netscape設(shè)計(jì)出JavaScript后的短短幾個(gè)月,JavaScript事實(shí)上已經(jīng)是前端開發(fā)的唯一標(biāo)準(zhǔn)韭畸。
后來慢洋,微軟通過IE擊敗了Netscape后一統(tǒng)桌面塘雳,結(jié)果幾年時(shí)間,瀏覽器毫無進(jìn)步普筹。(2001年推出的古老的IE 6到今天仍然有人在使用0苊鳌)
沒有競(jìng)爭(zhēng)就沒有發(fā)展。微軟認(rèn)為IE6瀏覽器已經(jīng)非常完善太防,幾乎沒有可改進(jìn)之處妻顶,然后解散了IE6開發(fā)團(tuán)隊(duì)!而Google卻認(rèn)為支持現(xiàn)代Web應(yīng)用的新一代瀏覽器才剛剛起步蜒车,尤其是瀏覽器負(fù)責(zé)運(yùn)行JavaScript的引擎性能還可提升10倍讳嘱。
先是Mozilla借助已壯烈犧牲的Netscape遺產(chǎn)在2002年推出了Firefox瀏覽器,緊接著Apple于2003年在開源的KHTML瀏覽器的基礎(chǔ)上推出了WebKit內(nèi)核的Safari瀏覽器酿愧,不過僅限于Mac平臺(tái)沥潭。
隨后,Google也開始創(chuàng)建自家的瀏覽器嬉挡。他們也看中了WebKit內(nèi)核钝鸽,于是基于WebKit內(nèi)核推出了Chrome瀏覽器。
Chrome瀏覽器是跨Windows和Mac平臺(tái)的庞钢,并且拔恰,Google認(rèn)為要運(yùn)行現(xiàn)代Web應(yīng)用,瀏覽器必須有一個(gè)性能非常強(qiáng)勁的JavaScript引擎基括,于是Google自己開發(fā)了一個(gè)高性能JavaScript引擎颜懊,名字叫V8,以BSD許可證開源风皿。
現(xiàn)代瀏覽器大戰(zhàn)讓微軟的IE瀏覽器遠(yuǎn)遠(yuǎn)地落后了河爹,因?yàn)樗麄兘馍⒘俗钣薪?jīng)驗(yàn)、戰(zhàn)斗力最強(qiáng)的瀏覽器團(tuán)隊(duì)桐款!回過頭再追趕卻發(fā)現(xiàn)昌抠,支持HTML5的WebKit已經(jīng)成為手機(jī)端的標(biāo)準(zhǔn)了,IE瀏覽器從此與主流移動(dòng)端設(shè)備絕緣鲁僚。
瀏覽器大戰(zhàn)和Node有何關(guān)系?
話說有個(gè)叫Ryan Dahl的歪果仁裁厅,他的工作是用C/C++寫高性能Web服務(wù)冰沙。對(duì)于高性能,異步IO执虹、事件驅(qū)動(dòng)是基本原則拓挥,但是用C/C++寫就太痛苦了。于是這位仁兄開始設(shè)想用高級(jí)語言開發(fā)Web服務(wù)袋励。他評(píng)估了很多種高級(jí)語言侥啤,發(fā)現(xiàn)很多語言雖然同時(shí)提供了同步IO和異步IO当叭,但是開發(fā)人員一旦用了同步IO,他們就再也懶得寫異步IO了盖灸,所以蚁鳖,最終,Ryan瞄向了JavaScript赁炎。
因?yàn)镴avaScript是單線程執(zhí)行醉箕,根本不能進(jìn)行同步IO操作,所以徙垫,JavaScript的這一“缺陷”導(dǎo)致了它只能使用異步IO讥裤。
選定了開發(fā)語言,還要有運(yùn)行時(shí)引擎姻报。這位仁兄曾考慮過自己寫一個(gè)己英,不過明智地放棄了,因?yàn)閂8就是開源的JavaScript引擎吴旋。讓Google投資去優(yōu)化V8损肛,咱只負(fù)責(zé)改造一下拿來用,還不用付錢邮府,這個(gè)買賣很劃算鼻由。
于是在2009年,Ryan正式推出了基于JavaScript語言和V8引擎的開源Web服務(wù)器項(xiàng)目吁朦,命名為Node.js编检。雖然名字很土,但是仙辟,Node第一次把JavaScript帶入到后端服務(wù)器開發(fā)同波,加上世界上已經(jīng)有無數(shù)的JavaScript開發(fā)人員,所以Node一下子就火了起來叠国。
在Node上運(yùn)行的JavaScript相比其他后端開發(fā)語言有何優(yōu)勢(shì)未檩?
最大的優(yōu)勢(shì)是借助JavaScript天生的事件驅(qū)動(dòng)機(jī)制加V8高性能引擎,使編寫高性能Web服務(wù)輕而易舉粟焊。
其次冤狡,JavaScript語言本身是完善的函數(shù)式語言,在前端開發(fā)時(shí)项棠,開發(fā)人員往往寫得比較隨意悲雳,讓人感覺JavaScript就是個(gè)“玩具語言”。但是香追,在Node環(huán)境下合瓢,通過模塊化的JavaScript代碼,加上函數(shù)式編程透典,并且無需考慮瀏覽器兼容性問題晴楔,直接使用最新的ECMAScript 6標(biāo)準(zhǔn)顿苇,可以完全滿足工程上的需求。
我還聽說過io.js税弃,這又是什么鬼纪岁?
因?yàn)镹ode.js是開源項(xiàng)目,雖然由社區(qū)推動(dòng)钙皮,但幕后一直由Joyent公司資助蜂科。由于一群開發(fā)者對(duì)Joyent公司的策略不滿,于2014年從Node.js項(xiàng)目fork出了io.js項(xiàng)目短条,決定單獨(dú)發(fā)展导匣,但兩者實(shí)際上是兼容的。
然而中國(guó)有句古話茸时,叫做“分久必合贡定,合久必分”。分家后沒多久可都,Joyent公司表示要和解缓待,于是,io.js項(xiàng)目又決定回歸Node.js渠牲。
具體做法是將來io.js將首先添加新的特性旋炒,如果大家測(cè)試用得爽,就把新特性加入Node.js签杈。io.js是“嘗鮮版”瘫镇,而Node.js是線上穩(wěn)定版,相當(dāng)于Fedora Linux和RHEL的關(guān)系答姥。
安裝Node.js和npm
由于Node.js平臺(tái)是在后端運(yùn)行JavaScript代碼铣除,所以,必須首先在本機(jī)安裝Node環(huán)境鹦付。
安裝Node.js
目前Node.js的最新版本是6.2.x尚粘。首先,從Node.js官網(wǎng)下載對(duì)應(yīng)平臺(tái)的安裝程序敲长,網(wǎng)速慢的童鞋請(qǐng)移步國(guó)內(nèi)鏡像郎嫁。
在Windows上安裝時(shí)務(wù)必選擇全部組件,包括勾選Add to Path祈噪。
安裝完成后泽铛,在Windows環(huán)境下,請(qǐng)打開命令提示符钳降,然后輸入node -v,如果安裝正常腌巾,你應(yīng)該看到v6.2.0這樣的輸出:
C:\Users\IEUser>node -vv6.2.0
繼續(xù)在命令提示符輸入node遂填,此刻你將進(jìn)入Node.js的交互環(huán)境铲觉。在交互環(huán)境下,你可以輸入任意JavaScript語句吓坚,例如100+200撵幽,回車后將得到輸出結(jié)果。
要退出Node.js環(huán)境礁击,連按兩次Ctrl+C盐杂。
在Mac或Linux環(huán)境下,請(qǐng)打開終端哆窿,然后輸入node -v链烈,你應(yīng)該看到如下輸出:
$ node -vv6.2.0
如果版本號(hào)不是v6.2.x,說明Node.js版本不對(duì)挚躯,后面章節(jié)的代碼不保證能正常運(yùn)行强衡,請(qǐng)重新安裝最新版本。
npm
在正式開始Node.js學(xué)習(xí)之前码荔,我們先認(rèn)識(shí)一下npm漩勤。
npm是什么東東?npm其實(shí)是Node.js的包管理工具(package manager)缩搅。
為啥我們需要一個(gè)包管理工具呢越败?因?yàn)槲覀冊(cè)贜ode.js上開發(fā)時(shí),會(huì)用到很多別人寫的JavaScript代碼硼瓣。如果我們要使用別人寫的某個(gè)包究飞,每次都根據(jù)名稱搜索一下官方網(wǎng)站,下載代碼巨双,解壓噪猾,再使用,非常繁瑣筑累。于是一個(gè)集中管理的工具應(yīng)運(yùn)而生:大家都把自己開發(fā)的模塊打包后放到npm官網(wǎng)上袱蜡,如果要使用,直接通過npm安裝就可以直接用慢宗,不用管代碼存在哪坪蚁,應(yīng)該從哪下載。
更重要的是镜沽,如果我們要使用模塊A敏晤,而模塊A又依賴于模塊B,模塊B又依賴于模塊X和模塊Y缅茉,npm可以根據(jù)依賴關(guān)系嘴脾,把所有依賴的包都下載下來并管理起來。否則,靠我們自己手動(dòng)管理译打,肯定既麻煩又容易出錯(cuò)耗拓。
講了這么多,npm究竟在哪奏司?
其實(shí)npm已經(jīng)在Node.js安裝的時(shí)候順帶裝好了乔询。我們?cè)诿钐崾痉蛘呓K端輸入npm -v,應(yīng)該看到類似的輸出:
C:\>npm -v3.8.9
如果直接輸入npm韵洋,你會(huì)看到類似下面的輸出:
C:\> npmUsage: npm where? is one of: ...
上面的一大堆文字告訴你竿刁,npm需要跟上命令。現(xiàn)在我們不用關(guān)心這些命令搪缨,后面會(huì)一一講到食拜。目前,你只需要確保npm正確安裝了勉吻,能運(yùn)行就行监婶。
第一個(gè)Node程序
在前面的所有章節(jié)中,我們編寫的JavaScript代碼都是在瀏覽器中運(yùn)行的齿桃,因此惑惶,我們可以直接在瀏覽器中敲代碼,然后直接運(yùn)行短纵。
從本章開始带污,我們編寫的JavaScript代碼將不能在瀏覽器環(huán)境中執(zhí)行了,而是在Node環(huán)境中執(zhí)行香到,因此鱼冀,JavaScript代碼將直接在你的計(jì)算機(jī)上以命令行的方式運(yùn)行,所以悠就,我們要先選擇一個(gè)文本編輯器來編寫JavaScript代碼千绪,并且把它保存到本地硬盤的某個(gè)目錄,才能夠執(zhí)行梗脾。
那么問題來了:文本編輯器到底哪家強(qiáng)荸型?
推薦兩款文本編輯器:
一個(gè)是Sublime Text,免費(fèi)使用炸茧,但是不付費(fèi)會(huì)彈出提示框:
hello.js
一個(gè)是Notepad++瑞妇,免費(fèi)使用,有中文界面:
notepad-hello.js
請(qǐng)注意梭冠,用哪個(gè)都行辕狰,但是絕對(duì)不能用Word和寫字板,Windows自帶的記事本也強(qiáng)烈不推薦使用控漠。Word和寫字板保存的不是純文本文件蔓倍,而記事本會(huì)自作聰明地在文件開始的地方加上幾個(gè)特殊字符(UTF-8 BOM),結(jié)果經(jīng)常會(huì)導(dǎo)致程序運(yùn)行出現(xiàn)莫名其妙的錯(cuò)誤。
安裝好文本編輯器后偶翅,輸入以下代碼:
'use strict';console.log('Hello, world.');
第一行總是寫上'use strict';
是因?yàn)槲覀兛偸且試?yán)格模式運(yùn)行JavaScript代碼他去,避免各種潛在陷阱。然后倒堕,選擇一個(gè)目錄,例如C:\Workspace爆价,把文件保存為hello.js垦巴,就可以打開命令行窗口,把當(dāng)前目錄切換到hello.js所在目錄铭段,然后輸入以下命令運(yùn)行這個(gè)程序了:
C:\Workspace>node hello.js
Hello, world.
也可以保存為別的名字骤宣,比如first.js,但是必須要以.js結(jié)尾序愚。此外憔披,文件名只能是英文字母、數(shù)字和下劃線的組合爸吮。
如果當(dāng)前目錄下沒有hello.js這個(gè)文件芬膝,運(yùn)行node hello.js就會(huì)報(bào)錯(cuò):
C:\Workspace>node hello.jsmodule.js:338 throw err; ^Error: Cannot find module 'C:\Workspace\hello.js' at Function.Module._resolveFilename at Function.Module._load at Function.Module.runMain at startup at node.js
報(bào)錯(cuò)的意思就是,沒有找到hello.js這個(gè)文件形娇,因?yàn)槲募淮嬖诿趟_@個(gè)時(shí)候,就要檢查一下當(dāng)前目錄下是否有這個(gè)文件了桐早。
命令行模式和Node交互模式
請(qǐng)注意區(qū)分命令行模式和Node交互模式癣缅。看到類似C:\>是在Windows提供的命令行模式:
run-node-hello
在命令行模式下哄酝,可以執(zhí)行node進(jìn)入Node交互式環(huán)境友存,也可以執(zhí)行nodehello.js運(yùn)行一個(gè).js文件√招疲看到>是在Node交互式環(huán)境下:
node-interactive-env
在Node交互式環(huán)境下屡立,我們可以輸入JavaScript代碼并立刻執(zhí)行。此外万哪,在命令行模式運(yùn)行.js文件和在Node交互式環(huán)境下直接運(yùn)行JavaScript代碼有所不同侠驯。Node交互式環(huán)境會(huì)把每一行JavaScript代碼的結(jié)果自動(dòng)打印出來,但是奕巍,直接運(yùn)行JavaScript文件卻不會(huì)吟策。
例如,在Node交互式環(huán)境下的止,輸入:
> 100 + 200 + 300;
600
直接可以看到結(jié)果600檩坚。
但是,寫一個(gè)calc.js的文件,內(nèi)容如下:
100 + 200 + 300;
然后在命令行模式下執(zhí)行:
C:\Workspace>node calc.js
發(fā)現(xiàn)什么輸出都沒有匾委。這是正常的拖叙。想要輸出結(jié)果,必須自己用console.log()打印出來赂乐。把calc.js改造一下:
console.log(100+200+300);
再執(zhí)行薯鳍,就可以看到結(jié)果:
C:\Workspace>node calc.js
600
用文本編輯器寫JavaScript程序,然后保存為后綴為.js的文件挨措,就可以用node直接運(yùn)行這個(gè)程序了挖滤。
Node的交互模式和直接運(yùn)行.js文件有什么區(qū)別呢?
直接輸入node進(jìn)入交互模式浅役,相當(dāng)于啟動(dòng)了Node解釋器斩松,但是等待你一行一行地輸入源代碼,每輸入一行就執(zhí)行一行觉既。
直接運(yùn)行node hello.js文件相當(dāng)于啟動(dòng)了Node解釋器惧盹,然后一次性把hello.js文件的源代碼給執(zhí)行了,你是沒有機(jī)會(huì)以交互的方式輸入源代碼的瞪讼。
在編寫JavaScript代碼的時(shí)候钧椰,完全可以一邊在文本編輯器里寫代碼,一邊開一個(gè)Node交互式命令窗口符欠,在寫代碼的過程中演侯,把部分代碼粘到命令行去驗(yàn)證,事半功倍背亥!前提是得有個(gè)27'的超大顯示器秒际!
模塊
在計(jì)算機(jī)程序的開發(fā)過程中,隨著程序代碼越寫越多狡汉,在一個(gè)文件里代碼就會(huì)越來越長(zhǎng)娄徊,越來越不容易維護(hù)。
為了編寫可維護(hù)的代碼盾戴,我們把很多函數(shù)分組寄锐,分別放到不同的文件里,這樣尖啡,每個(gè)文件包含的代碼就相對(duì)較少橄仆,很多編程語言都采用這種組織代碼的方式。在Node環(huán)境中衅斩,一個(gè).js文件就稱之為一個(gè)模塊(module)盆顾。
使用模塊有什么好處?
最大的好處是大大提高了代碼的可維護(hù)性畏梆。其次您宪,編寫代碼不必從零開始奈懒。當(dāng)一個(gè)模塊編寫完畢,就可以被其他地方引用宪巨。我們?cè)诰帉懗绦虻臅r(shí)候磷杏,也經(jīng)常引用其他模塊,包括Node內(nèi)置的模塊和來自第三方的模塊捏卓。
使用模塊還可以避免函數(shù)名和變量名沖突极祸。相同名字的函數(shù)和變量完全可以分別存在不同的模塊中,因此怠晴,我們自己在編寫模塊時(shí)贿肩,不必考慮名字會(huì)與其他模塊沖突。
在上一節(jié)龄寞,我們編寫了一個(gè)hello.js文件,這個(gè)hello.js文件就是一個(gè)模塊汤功,模塊的名字就是文件名(去掉.js后綴)物邑,所以hello.js文件就是名為hello的模塊。
我們把hello.js改造一下滔金,創(chuàng)建一個(gè)函數(shù)色解,這樣我們就可以在其他地方調(diào)用這個(gè)函數(shù):
'use strict';vars ='Hello';functiongreet(name){console.log(s +', '+ name +'!');}module.exports = greet;
函數(shù)greet()是我們?cè)趆ello模塊中定義的,你可能注意到最后一行是一個(gè)奇怪的賦值語句餐茵,它的意思是科阎,把函數(shù)greet作為模塊的輸出暴露出去,這樣其他模塊就可以使用greet函數(shù)了忿族。
問題是其他模塊怎么使用hello模塊的這個(gè)greet函數(shù)呢锣笨?我們?cè)倬帉懸粋€(gè)main.js文件,調(diào)用hello模塊的greet函數(shù):
'use strict';// 引入hello模塊:vargreet =require('./hello');vars ='Michael';greet(s);// Hello, Michael!
注意到引入hello模塊用Node提供的require函數(shù):
vargreet =require('./hello');
引入的模塊作為變量保存在greet變量中道批,那greet變量到底是什么東西错英?其實(shí)變量greet就是在hello.js中我們用module.exports = greet;輸出的greet函數(shù)。所以隆豹,main.js就成功地引用了hello.js模塊中定義的greet()函數(shù)椭岩,接下來就可以直接使用它了。
在使用require()引入模塊的時(shí)候璃赡,請(qǐng)注意模塊的相對(duì)路徑判哥。因?yàn)閙ain.js
和hello.js位于同一個(gè)目錄,所以我們用了當(dāng)前目錄.:
vargreet =require('./hello');// 不要忘了寫相對(duì)目錄!
如果只寫模塊名:
vargreet =require('hello');
則Node會(huì)依次在內(nèi)置模塊碉考、全局模塊和當(dāng)前模塊下查找hello.js塌计,你很可能會(huì)得到一個(gè)錯(cuò)誤:
module.js throw err; ^Error: Cannot find module 'hello' at
Function.Module._resolveFilename at Function.Module._load ... at
Function.Module._load at Function.Module.runMain
遇到這個(gè)錯(cuò)誤,你要檢查:
模塊名是否寫對(duì)了
模塊文件是否存在
相對(duì)路徑是否寫對(duì)了
CommonJS規(guī)范
這種模塊加載機(jī)制被稱為CommonJS規(guī)范侯谁。在這個(gè)規(guī)范下夺荒,每個(gè).js文件都是一個(gè)模塊瞒渠,它們內(nèi)部各自使用的變量名和函數(shù)名都互不沖突,例如技扼,hello.js
和main.js都申明了全局變量var s = 'xxx'伍玖,但互不影響。
一個(gè)模塊想要對(duì)外暴露變量(函數(shù)也是變量)剿吻,可以用module.exports = variable;窍箍,一個(gè)模塊要引用其他模塊暴露的變量,用
varref =require('module_name');
就拿到了引用模塊的變量丽旅。
要在模塊中對(duì)外輸出變量椰棘,用:module.exports = variable;
輸出的變量可以是任意對(duì)象、函數(shù)榄笙、數(shù)組等等邪狞。要引入其他模塊輸出的對(duì)象,用:var foo = require('other_module');
引入的對(duì)象具體是什么茅撞,取決于引入模塊輸出的對(duì)象帆卓。
深入了解模塊原理
如果你想詳細(xì)地了解CommonJS的模塊實(shí)現(xiàn)原理,請(qǐng)繼續(xù)往下閱讀
當(dāng)我們編寫JavaScript代碼時(shí)米丘,我們可以申明全局變量:
vars ='global';
在瀏覽器中剑令,大量使用全局變量可不好。如果你在a.js中使用了全局變量s拄查,那么吁津,在b.js中也使用全局變量s,將造成沖突堕扶,b.js中對(duì)s賦值會(huì)改變a.js的運(yùn)行邏輯碍脏。
也就是說,JavaScript語言本身并沒有一種模塊機(jī)制來保證不同模塊可以使用相同的變量名稍算。
那Node.js是如何實(shí)現(xiàn)這一點(diǎn)的潮酒?
其實(shí)要實(shí)現(xiàn)“模塊”這個(gè)功能,并不需要語法層面的支持邪蛔。Node.js也并不會(huì)增加任何JavaScript語法急黎。實(shí)現(xiàn)“模塊”功能的奧妙就在于JavaScript是一種函數(shù)式編程語言,它支持閉包侧到。如果我們把一段JavaScript代碼用一個(gè)函數(shù)包裝起來勃教,這段代碼的所有“全局”變量就變成了函數(shù)內(nèi)部的局部變量。
請(qǐng)注意我們編寫的hello.js代碼是這樣的:
vars ='Hello';varname ='world';console.log(s +' '+ name +'!');
Node.js加載了hello.js后匠抗,它可以把代碼包裝一下故源,變成這樣執(zhí)行:
(function(){// 讀取的hello.js代碼:vars ='Hello';varname ='world';console.log(s +' '+ name +'!');// hello.js代碼結(jié)束})();
這樣一來,原來的全局變量s現(xiàn)在變成了匿名函數(shù)內(nèi)部的局部變量汞贸。如果Node.js繼續(xù)加載其他模塊绳军,這些模塊中定義的“全局”變量s也互不干擾印机。
所以,Node利用JavaScript的函數(shù)式編程的特性门驾,輕而易舉地實(shí)現(xiàn)了模塊的隔離射赛。
但是,模塊的輸出module.exports怎么實(shí)現(xiàn)奶是?
這個(gè)也很容易實(shí)現(xiàn)楣责,Node可以先準(zhǔn)備一個(gè)對(duì)象module:
// 準(zhǔn)備module對(duì)象:varmodule= {id:'hello',exports: {}};varload =function(module){// 讀取的hello.js代碼:functiongreet(name){console.log('Hello, '+ name +'!');? }module.exports = greet;// hello.js代碼結(jié)束returnmodule.exports;};varexported = load(module);// 保存module:save(module, exported);
可見,變量module是Node在加載js文件前準(zhǔn)備的一個(gè)變量聂沙,并將其傳入加載函數(shù)秆麸,我們?cè)趆ello.js中可以直接使用變量module原因就在于它實(shí)際上是函數(shù)的一個(gè)參數(shù):module.exports = greet;
通過把參數(shù)module傳遞給load()函數(shù),hello.js就順利地把一個(gè)變量傳遞給了Node執(zhí)行環(huán)境及汉,Node會(huì)把module變量保存到某個(gè)地方沮趣。
由于Node保存了所有導(dǎo)入的module,當(dāng)我們用require()獲取module時(shí)坷随,Node找到對(duì)應(yīng)的module房铭,把這個(gè)module的exports變量返回,這樣甸箱,另一個(gè)模塊就順利拿到了模塊的輸出:
vargreet =require('./hello');
以上是Node實(shí)現(xiàn)JavaScript模塊的一個(gè)簡(jiǎn)單的原理介紹。
module.exports vs exports
很多時(shí)候迅脐,你會(huì)看到芍殖,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:
方法一:對(duì)module.exports賦值:
// hello.jsfunctionhello(){console.log('Hello, world!');}functiongreet(name){console.log('Hello, '+ name +'!');}functionhello(){console.log('Hello, world!');}module.exports = {hello: hello,greet: greet};
方法二:直接使用exports:
// hello.jsfunctionhello(){console.log('Hello, world!');}functiongreet(name){console.log('Hello, '+ name +'!');}functionhello(){console.log('Hello, world!');}exports.hello = hello;exports.greet = greet;
但是你不可以直接對(duì)exports賦值:
// 代碼可以執(zhí)行谴蔑,但是模塊并沒有輸出任何變量:exports = {hello: hello,greet: greet};
如果你對(duì)上面的寫法感到十分困惑豌骏,不要著急,我們來分析Node的加載機(jī)制:
首先隐锭,Node會(huì)把整個(gè)待加載的hello.js文件放入一個(gè)包裝函數(shù)load中執(zhí)行窃躲。在執(zhí)行這個(gè)load()函數(shù)前,Node準(zhǔn)備好了module變量:
varmodule= {id:'hello',exports: {}};
load()函數(shù)最終返回module.exports:
varload =function(exports, module){// hello.js的文件內(nèi)容 ...// load函數(shù)返回:returnmodule.exports;};varexported = load(module.exports,module);
也就是說钦睡,默認(rèn)情況下蒂窒,Node準(zhǔn)備的exports變量和module.exports變量實(shí)際上是同一個(gè)變量,并且初始化為空對(duì)象{}荞怒,于是洒琢,我們可以寫:
exports.foo =function(){return'foo'; };exports.bar =function(){return'bar'; };
也可以寫:
module.exports.foo =function(){return'foo'; };module.exports.bar =function(){return'bar'; };
換句話說,Node默認(rèn)給你準(zhǔn)備了一個(gè)空對(duì)象{}褐桌,這樣你可以直接往里面加?xùn)|西衰抑。
但是,如果我們要輸出的是一個(gè)函數(shù)或數(shù)組荧嵌,那么呛踊,只能給module.exports
賦值:
module.exports =function(){return'foo'; };
給exports賦值是無效的砾淌,因?yàn)橘x值后,module.exports仍然是空對(duì)象{}谭网。
結(jié)論:
如果要輸出一個(gè)鍵值對(duì)象{}汪厨,可以利用exports這個(gè)已存在的空對(duì)象{},并繼續(xù)在上面添加新的鍵值蜻底;
如果要輸出一個(gè)函數(shù)或數(shù)組骄崩,必須直接對(duì)module.exports對(duì)象賦值。
所以我們可以得出結(jié)論:直接對(duì)module.exports賦值薄辅,可以應(yīng)對(duì)任何情況:
module.exports = {foo:function(){return'foo';? }};
或者:
module.exports =function(){return'foo'; };
最終要拂,我們強(qiáng)烈建議使用module.exports = xxx的方式來輸出模塊變量,這樣站楚,你只需要記憶一種方法脱惰。
基本模塊
因?yàn)镹ode.js是運(yùn)行在服務(wù)區(qū)端的JavaScript環(huán)境,服務(wù)器程序和瀏覽器程序相比窿春,最大的特點(diǎn)是沒有瀏覽器的安全限制了拉一,而且,服務(wù)器程序必須能接收網(wǎng)絡(luò)請(qǐng)求旧乞,讀寫文件蔚润,處理二進(jìn)制內(nèi)容,所以尺栖,Node.js內(nèi)置的常用模塊就是為了實(shí)現(xiàn)基本的服務(wù)器功能嫡纠。這些模塊在瀏覽器環(huán)境中是無法被執(zhí)行的,因?yàn)樗鼈兊牡讓哟a是用C/C++在Node.js運(yùn)行環(huán)境中實(shí)現(xiàn)的延赌。
global
在前面的JavaScript課程中除盏,我們已經(jīng)知道,JavaScript有且僅有一個(gè)全局對(duì)象挫以,在瀏覽器中者蠕,叫window對(duì)象。而在Node.js環(huán)境中掐松,也有唯一的全局對(duì)象踱侣,但不叫window,而叫g(shù)lobal大磺,這個(gè)對(duì)象的屬性和方法也和瀏覽器環(huán)境的window不同泻仙。進(jìn)入Node.js交互環(huán)境,可以直接輸入:
> global.console
Console {
log: [Function: bound ],
info: [Function: bound ],
warn: [Function: bound ],
error: [Function: bound ],
dir: [Function: bound ],
time: [Function: bound ],
timeEnd: [Function: bound ],
trace: [Function: bound trace],
assert: [Function: bound ],
Console: [Function: Console] }
process
process也是Node.js提供的一個(gè)對(duì)象量没,它代表當(dāng)前Node.js進(jìn)程玉转。通過process對(duì)象可以拿到許多有用信息:
> process === global.process;
true
> process.version;
'v5.2.0'
> process.platform;
'darwin'
> process.arch;
'x64'
> process.cwd(); //返回當(dāng)前工作目錄
'/Users/michael'
> process.chdir('/private/tmp'); // 切換當(dāng)前工作目錄
undefined
> process.cwd();
'/private/tmp'
JavaScript程序是由事件驅(qū)動(dòng)執(zhí)行的單線程模型,Node.js也不例外殴蹄。Node.js不斷執(zhí)行響應(yīng)事件的JavaScript函數(shù)究抓,直到?jīng)]有任何響應(yīng)事件的函數(shù)可以執(zhí)行時(shí)猾担,Node.js就退出了。
如果我們想要在下一次事件響應(yīng)中執(zhí)行代碼刺下,可以調(diào)用process.nextTick():
// test.js// process.nextTick()將在下一輪事件循環(huán)中調(diào)用:process.nextTick(function(){console.log('nextTick callback!');});console.log('nextTick was set!');
用Node執(zhí)行上面的代碼node test.js绑嘹,你會(huì)看到,打印輸出是:
nextTick was set!
nextTick callback!
這說明傳入process.nextTick()的函數(shù)不是立刻執(zhí)行橘茉,而是要等到下一次事件循環(huán)工腋。
Node.js進(jìn)程本身的事件就由process對(duì)象來處理。如果我們響應(yīng)exit事件畅卓,就可以在程序即將退出時(shí)執(zhí)行某個(gè)回調(diào)函數(shù):
// 程序即將退出時(shí)的回調(diào)函數(shù):process.on('exit',function(code){console.log('about to exit with code: '+ code);});
判斷JavaScript執(zhí)行環(huán)境
有很多JavaScript代碼既能在瀏覽器中執(zhí)行擅腰,也能在Node環(huán)境執(zhí)行,但有些時(shí)候翁潘,程序本身需要判斷自己到底是在什么環(huán)境下執(zhí)行的趁冈,常用的方式就是根據(jù)瀏覽器和Node環(huán)境提供的全局變量名稱來判斷:
if(typeof(window) ==='undefined') {console.log('node.js');}else{console.log('browser');}
后面,我們將介紹Node.js的常用內(nèi)置模塊拜马。
fs
Node.js內(nèi)置的fs模塊就是文件系統(tǒng)模塊渗勘,負(fù)責(zé)讀寫文件。
和所有其它JavaScript模塊不同的是俩莽,fs模塊同時(shí)提供了異步和同步的方法旺坠。
回顧一下什么是異步方法。因?yàn)镴avaScript的單線程模型扮超,執(zhí)行IO操作時(shí)取刃,JavaScript代碼無需等待,而是傳入回調(diào)函數(shù)后瞒津,繼續(xù)執(zhí)行后續(xù)JavaScript代碼蝉衣。比如jQuery提供的getJSON()操作:
$.getJSON('http://example.com/ajax',function(data){console.log('IO結(jié)果返回后執(zhí)行...');});console.log('不等待IO結(jié)果直接執(zhí)行后續(xù)代碼...');
而同步的IO操作則需要等待函數(shù)返回:
// 根據(jù)網(wǎng)絡(luò)耗時(shí)括尸,函數(shù)將執(zhí)行幾十毫秒~幾秒不等:vardata = getJSONSync('http://example.com/ajax');
同步操作的好處是代碼簡(jiǎn)單巷蚪,缺點(diǎn)是程序?qū)⒌却齀O操作,在等待時(shí)間內(nèi)濒翻,無法響應(yīng)其它任何事件屁柏。而異步讀取不用等待IO操作,但代碼較麻煩有送。
異步讀文件
按照J(rèn)avaScript的標(biāo)準(zhǔn)淌喻,異步讀取一個(gè)文本文件的代碼如下:
'use strict';varfs =require('fs');fs.readFile('sample.txt','utf-8',function(err, data){if(err) {console.log(err);? }else{console.log(data);? }});
請(qǐng)注意,sample.txt文件必須在當(dāng)前目錄下雀摘,且文件編碼為utf-8裸删。
異步讀取時(shí),傳入的回調(diào)函數(shù)接收兩個(gè)參數(shù)阵赠,當(dāng)正常讀取時(shí)涯塔,err參數(shù)為null肌稻,data參數(shù)為讀取到的String。當(dāng)讀取發(fā)生錯(cuò)誤時(shí)匕荸,err參數(shù)代表一個(gè)錯(cuò)誤對(duì)象爹谭,data為undefined。
這也是Node.js標(biāo)準(zhǔn)的回調(diào)函數(shù):第一個(gè)參數(shù)代表錯(cuò)誤信息榛搔,第二個(gè)參數(shù)代表結(jié)果诺凡。后面我們還會(huì)經(jīng)常編寫這種回調(diào)函數(shù)。
由于err是否為null就是判斷是否出錯(cuò)的標(biāo)志践惑,所以通常的判斷邏輯總是:
if(err) {// 出錯(cuò)了}else{// 正常}
如果我們要讀取的文件不是文本文件腹泌,而是二進(jìn)制文件,怎么辦童本?
下面的例子演示了如何讀取一個(gè)圖片文件:
'use strict';varfs =require('fs');fs.readFile('sample.png',function(err, data){if(err) {console.log(err);? }else{console.log(data);console.log(data.length +' bytes');? }});
當(dāng)讀取二進(jìn)制文件時(shí)真屯,不傳入文件編碼時(shí),回調(diào)函數(shù)的data參數(shù)將返回一個(gè)Buffer對(duì)象穷娱。在Node.js中绑蔫,Buffer對(duì)象就是一個(gè)包含零個(gè)或任意個(gè)字節(jié)的數(shù)組(注意和Array不同)。
Buffer對(duì)象可以和String作轉(zhuǎn)換泵额,例如配深,把一個(gè)Buffer對(duì)象轉(zhuǎn)換成String:
// Buffer -> Stringvartext = data.toString('utf-8');console.log(text);
或者把一個(gè)String轉(zhuǎn)換成Buffer:
// String -> Buffervarbuf =newBuffer(text,'utf-8');console.log(buf);
同步讀文件
除了標(biāo)準(zhǔn)的異步讀取模式外,fs也提供相應(yīng)的同步讀取函數(shù)嫁盲。同步讀取的函數(shù)和異步函數(shù)相比篓叶,多了一個(gè)Sync后綴,并且不接收回調(diào)函數(shù)羞秤,函數(shù)直接返回結(jié)果缸托。
用fs模塊同步讀取一個(gè)文本文件的代碼如下:
'use strict';varfs =require('fs');vardata = fs.readFileSync('sample.txt','utf-8');console.log(data);
可見,原異步調(diào)用的回調(diào)函數(shù)的data被函數(shù)直接返回瘾蛋,函數(shù)名需要改為readFileSync俐镐,其它參數(shù)不變。
如果同步讀取文件發(fā)生錯(cuò)誤哺哼,則需要用try...catch捕獲該錯(cuò)誤:
try{vardata = fs.readFileSync('sample.txt','utf-8');console.log(data);}catch(err) {// 出錯(cuò)了}
寫文件
將數(shù)據(jù)寫入文件是通過fs.writeFile()實(shí)現(xiàn)的:
'use strict';varfs =require('fs');vardata ='Hello, Node.js';fs.writeFile('output.txt', data,function(err){if(err) {console.log(err);? }else{console.log('ok.');? }});
writeFile()的參數(shù)依次為文件名佩抹、數(shù)據(jù)和回調(diào)函數(shù)。如果傳入的數(shù)據(jù)是String取董,默認(rèn)按UTF-8編碼寫入文本文件棍苹,如果傳入的參數(shù)是Buffer,則寫入的是二進(jìn)制文件茵汰∈嗬铮回調(diào)函數(shù)由于只關(guān)心成功與否,因此只需要一個(gè)err參數(shù)。
和readFile()類似栏豺,writeFile()也有一個(gè)同步方法梭灿,叫writeFileSync():
'use strict';varfs =require('fs');vardata ='Hello, Node.js';fs.writeFileSync('output.txt', data);
stat
如果我們要獲取文件大小,創(chuàng)建時(shí)間等信息冰悠,可以使用fs.stat()堡妒,它返回一個(gè)Stat對(duì)象,能告訴我們文件或目錄的詳細(xì)信息:
'use strict';varfs =require('fs');fs.stat('sample.txt',function(err, stat){if(err) {console.log(err);? }else{// 是否是文件:console.log('isFile: '+ stat.isFile());// 是否是目錄:console.log('isDirectory: '+ stat.isDirectory());if(stat.isFile()) {// 文件大小:console.log('size: '+ stat.size);// 創(chuàng)建時(shí)間, Date對(duì)象:console.log('birth time: '+ stat.birthtime);// 修改時(shí)間, Date對(duì)象:console.log('modified time: '+ stat.mtime);? ? }? }});
運(yùn)行結(jié)果如下:
isFile: true
isDirectory: false
size: 181
birth time: Fri Dec 11 2015 09:43:41 GMT+0800 (CST)
modified time: Fri Dec 11 2015 12:09:00 GMT+0800 (CST)
stat()也有一個(gè)對(duì)應(yīng)的同步函數(shù)statSync()溉卓,請(qǐng)?jiān)囍膶懮鲜霎惒酱a為同步代碼皮迟。
異步還是同步
在fs模塊中,提供同步方法是為了方便使用桑寨。那我們到底是應(yīng)該用異步方法還是同步方法呢伏尼?
由于Node環(huán)境執(zhí)行的JavaScript代碼是服務(wù)器端代碼,所以尉尾,絕大部分需要在服務(wù)器運(yùn)行期反復(fù)執(zhí)行業(yè)務(wù)邏輯的代碼爆阶,必須使用異步代碼,否則沙咏,同步代碼在執(zhí)行時(shí)期辨图,服務(wù)器將停止響應(yīng),因?yàn)镴avaScript只有一個(gè)執(zhí)行線程肢藐。
服務(wù)器啟動(dòng)時(shí)如果需要讀取配置文件故河,或者結(jié)束時(shí)需要寫入到狀態(tài)文件時(shí),可以使用同步代碼吆豹,因?yàn)檫@些代碼只在啟動(dòng)和結(jié)束時(shí)執(zhí)行一次鱼的,不影響服務(wù)器正常運(yùn)行時(shí)的異步執(zhí)行。
stream
stream是Node.js提供的又一個(gè)僅在服務(wù)區(qū)端可用的模塊痘煤,目的是支持“流”這種數(shù)據(jù)結(jié)構(gòu)凑阶。
什么是流?流是一種抽象的數(shù)據(jù)結(jié)構(gòu)衷快。想象水流宙橱,當(dāng)在水管中流動(dòng)時(shí),就可以從某個(gè)地方(例如自來水廠)源源不斷地到達(dá)另一個(gè)地方(比如你家的洗手池)。
我們也可以把數(shù)據(jù)看成是數(shù)據(jù)流,比如你敲鍵盤的時(shí)候芍秆,就可以把每個(gè)字符依次連起來最欠,看成字符流。這個(gè)流是從鍵盤輸入到應(yīng)用程序积担,實(shí)際上它還對(duì)應(yīng)著一個(gè)名字:標(biāo)準(zhǔn)輸入流(stdin)陨晶。
如果應(yīng)用程序把字符一個(gè)一個(gè)輸出到顯示器上,這也可以看成是一個(gè)流先誉,這個(gè)流也有名字:標(biāo)準(zhǔn)輸出流(stdout)湿刽。流的特點(diǎn)是數(shù)據(jù)是有序的,而且必須依次讀取刃滓,或者依次寫入咧虎,不能像Array那樣隨機(jī)定位卓缰。
nodejs-stream
有些流用來讀取數(shù)據(jù)砰诵,比如從文件讀取數(shù)據(jù)時(shí)征唬,可以打開一個(gè)文件流,然后從文件流中不斷地讀取數(shù)據(jù)茁彭。有些流用來寫入數(shù)據(jù)鳍鸵,比如向文件寫入數(shù)據(jù)時(shí),只需要把數(shù)據(jù)不斷地往文件流中寫進(jìn)去就可以了尉间。
在Node.js中偿乖,流也是一個(gè)對(duì)象,我們只需要響應(yīng)流的事件就可以了:data事件表示流的數(shù)據(jù)已經(jīng)可以讀取了哲嘲,end事件表示這個(gè)流已經(jīng)到末尾了贪薪,沒有數(shù)據(jù)可以讀取了,error事件表示出錯(cuò)了眠副。
下面是一個(gè)從文件流讀取文本內(nèi)容的示例:
'use strict';varfs =require('fs');// 打開一個(gè)流:varrs = fs.createReadStream('sample.txt','utf-8');rs.on('data',function(chunk){console.log('DATA:')console.log(chunk);});rs.on('end',function(){console.log('END');});rs.on('error',function(err){console.log('ERROR: '+ err);});
要注意画切,data事件可能會(huì)有多次,每次傳遞的chunk是流的一部分?jǐn)?shù)據(jù)囱怕。
要以流的形式寫入文件霍弹,只需要不斷調(diào)用write()方法,最后以end()結(jié)束:
'use strict';varfs =require('fs');varws1 = fs.createWriteStream('output1.txt','utf-8');ws1.write('使用Stream寫入文本數(shù)據(jù)...\n');ws1.write('END.');ws1.end();varws2 = fs.createWriteStream('output2.txt');ws2.write(newBuffer('使用Stream寫入二進(jìn)制數(shù)據(jù)...\n','utf-8'));ws2.write(newBuffer('END.','utf-8'));ws2.end();
所有可以讀取數(shù)據(jù)的流都繼承自stream.Readable娃弓,所有可以寫入的流都繼承自stream.Writable典格。
pipe
就像可以把兩個(gè)水管串成一個(gè)更長(zhǎng)的水管一樣,兩個(gè)流也可以串起來台丛。一個(gè)Readable流和一個(gè)Writable流串起來后耍缴,所有的數(shù)據(jù)自動(dòng)從Readable
流進(jìn)入Writable流,這種操作叫pipe。
在Node.js中防嗡,Readable流有一個(gè)pipe()方法变汪,就是用來干這件事的。
讓我們用pipe()把一個(gè)文件流和另一個(gè)文件流串起來蚁趁,這樣源文件的所有數(shù)據(jù)就自動(dòng)寫入到目標(biāo)文件里了裙盾,所以,這實(shí)際上是一個(gè)復(fù)制文件的程序:
'use strict';varfs =require('fs');varrs = fs.createReadStream('sample.txt');varws = fs.createWriteStream('copied.txt');rs.pipe(ws);
默認(rèn)情況下他嫡,當(dāng)Readable流的數(shù)據(jù)讀取完畢闷煤,end事件觸發(fā)后,將自動(dòng)關(guān)閉Writable流涮瞻。如果我們不希望自動(dòng)關(guān)閉Writable流鲤拿,需要傳入?yún)?shù):
readable.pipe(writable, {end:false});
http
Node.js開發(fā)的目的就是為了用JavaScript編寫Web服務(wù)器程序。因?yàn)镴avaScript實(shí)際上已經(jīng)統(tǒng)治了瀏覽器端的腳本署咽,其優(yōu)勢(shì)就是有世界上數(shù)量最多的前端開發(fā)人員近顷。如果已經(jīng)掌握了JavaScript前端開發(fā),再學(xué)習(xí)一下如何將JavaScript應(yīng)用在后端開發(fā)宁否,就是名副其實(shí)的全棧了窒升。
HTTP協(xié)議
要理解Web服務(wù)器程序的工作原理,首先慕匠,我們要對(duì)HTTP協(xié)議有基本的了解饱须。如果你對(duì)HTTP協(xié)議不太熟悉,先看一看HTTP協(xié)議簡(jiǎn)介台谊。
HTTP服務(wù)器
要開發(fā)HTTP服務(wù)器程序蓉媳,從頭處理TCP連接,解析HTTP是不現(xiàn)實(shí)的锅铅。這些工作實(shí)際上已經(jīng)由Node.js自帶的http模塊完成了酪呻。應(yīng)用程序并不直接和HTTP協(xié)議打交道,而是操作http模塊提供的request和response對(duì)象盐须。
request對(duì)象封裝了HTTP請(qǐng)求玩荠,我們調(diào)用request對(duì)象的屬性和方法就可以拿到所有HTTP請(qǐng)求的信息;
response對(duì)象封裝了HTTP響應(yīng)贼邓,我們操作response對(duì)象的方法阶冈,就可以把HTTP響應(yīng)返回給瀏覽器。
用Node.js實(shí)現(xiàn)一個(gè)HTTP服務(wù)器程序非常簡(jiǎn)單塑径。我們來實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的Web程序hello.js女坑,它對(duì)于所有請(qǐng)求,都返回Hello world!:
'use strict';// 導(dǎo)入http模塊:varhttp =require('http');// 創(chuàng)建http server晓勇,并傳入回調(diào)函數(shù):varserver = http.createServer(function(request, response){// 回調(diào)函數(shù)接收request和response對(duì)象,// 獲得HTTP請(qǐng)求的method和url:console.log(request.method +': '+ request.url);// 將HTTP響應(yīng)200寫入response, 同時(shí)設(shè)置Content-Type: text/html:response.writeHead(200, {'Content-Type':'text/html'});// 將HTTP響應(yīng)的HTML內(nèi)容寫入response:response.end('
Hello world!
');});// 讓服務(wù)器監(jiān)聽8080端口:server.listen(8080);console.log('Server is running at http://127.0.0.1:8080/');
在命令提示符下運(yùn)行該程序堂飞,可以看到以下輸出:
$ node hello.js Server is running at http://127.0.0.1:8080/
不要關(guān)閉命令提示符,直接打開瀏覽器輸入http://localhost:8080绑咱,即可看到服務(wù)器響應(yīng)的內(nèi)容:
http-hello-sample
同時(shí)绰筛,在命令提示符窗口,可以看到程序打印的請(qǐng)求信息:
GET: /GET: /favicon.ico
這就是我們編寫的第一個(gè)HTTP服務(wù)器程序描融!
文件服務(wù)器
讓我們繼續(xù)擴(kuò)展一下上面的Web程序铝噩。我們可以設(shè)定一個(gè)目錄,然后讓W(xué)eb程序變成一個(gè)文件服務(wù)器窿克。要實(shí)現(xiàn)這一點(diǎn)骏庸,我們只需要解析request.url中的路徑,然后在本地找到對(duì)應(yīng)的文件年叮,把文件內(nèi)容發(fā)送出去就可以了具被。
解析URL需要用到Node.js提供的url模塊,它使用起來非常簡(jiǎn)單只损,通過parse()將一個(gè)字符串解析為一個(gè)Url對(duì)象:
'use strict';varurl =require('url');console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));
結(jié)果如下:
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/path/to/file',
path: '/path/to/file?query=string',
href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash'
}
處理本地文件目錄需要使用Node.js提供的path模塊一姿,它可以方便地構(gòu)造目錄:
'use strict';varpath =require('path');// 解析當(dāng)前目錄:varworkDir = path.resolve('.');// '/Users/michael'// 組合完整的文件路徑:當(dāng)前目錄+'pub'+'index.html':varfilePath = path.join(workDir,'pub','index.html');// '/Users/michael/pub/index.html'
使用path模塊可以正確處理操作系統(tǒng)相關(guān)的文件路徑。在Windows系統(tǒng)下跃惫,返回的路徑類似于C:\Users\michael\static\index.html叮叹,這樣,我們就不關(guān)心怎么拼接路徑了爆存。
最后蛉顽,我們實(shí)現(xiàn)一個(gè)文件服務(wù)器file_server.js:
'use strict';varfs =require('fs'),? ? ? url =require('url'),? ? ? path =require('path'),? ? ? http =require('http');// 從命令行參數(shù)獲取root目錄,默認(rèn)是當(dāng)前目錄:varroot = path.resolve(process.argv[2] ||'.');console.log('Static root dir: '+ root);// 創(chuàng)建服務(wù)器:varserver = http.createServer(function(request, response){// 獲得URL的path先较,類似 '/css/bootstrap.css':varpathname = url.parse(request.url).pathname;// 獲得對(duì)應(yīng)的本地文件路徑携冤,類似 '/srv/www/css/bootstrap.css':varfilepath = path.join(root, pathname);// 獲取文件狀態(tài):fs.stat(filepath,function(err, stats){if(!err && stats.isFile()) {// 沒有出錯(cuò)并且文件存在:console.log('200 '+ request.url);// 發(fā)送200響應(yīng):response.writeHead(200);// 將文件流導(dǎo)向response:fs.createReadStream(filepath).pipe(response);? ? }else{// 出錯(cuò)了或者文件不存在:console.log('404 '+ request.url);// 發(fā)送404響應(yīng):response.writeHead(404);? ? ? response.end('404 Not Found');? ? }? });});server.listen(8080);console.log('Server is running at http://127.0.0.1:8080/');
沒有必要手動(dòng)讀取文件內(nèi)容。由于response對(duì)象本身是一個(gè)Writable Stream闲勺,直接用pipe()方法就實(shí)現(xiàn)了自動(dòng)讀取文件內(nèi)容并輸出到HTTP響應(yīng)噪叙。
在命令行運(yùn)行node file_server.js /path/to/dir,把/path/to/dir改成你本地的一個(gè)有效的目錄霉翔,然后在瀏覽器中輸入
http://localhost:8080/index.html
http-index-page
只要當(dāng)前目錄下存在文件index.html睁蕾,服務(wù)器就可以把文件內(nèi)容發(fā)送給瀏覽器。觀察控制臺(tái)輸出:
200 /index.html
200 /css/uikit.min.css
200 /js/jquery.min.js
200 /fonts/fontawesome-webfont.woff2
第一個(gè)請(qǐng)求是瀏覽器請(qǐng)求index.html頁面债朵,后續(xù)請(qǐng)求是瀏覽器解析HTML后發(fā)送的其它資源請(qǐng)求子眶。
crypto
crypto模塊的目的是為了提供通用的加密和哈希算法。用純JavaScript代碼實(shí)現(xiàn)這些功能不是不可能序芦,但速度會(huì)非常慢臭杰。Nodejs用C/C++實(shí)現(xiàn)這些算法后,通過cypto這個(gè)模塊暴露為JavaScript接口谚中,這樣用起來方便渴杆,運(yùn)行速度也快寥枝。
MD5和SHA1
MD5是一種常用的哈希算法,用于給任意數(shù)據(jù)一個(gè)“簽名”磁奖。這個(gè)簽名通常用一個(gè)十六進(jìn)制的字符串表示:
constcrypto =require('crypto');consthash = crypto.createHash('md5');// 可任意多次調(diào)用update():hash.update('Hello, world!');hash.update('Hello, nodejs!');console.log(hash.digest('hex'));// 7e1977739c748beac0c0fd14fd26a544
update()方法默認(rèn)字符串編碼為UTF-8囊拜,也可以傳入Buffer。
如果要計(jì)算SHA1比搭,只需要把'md5'改成'sha1'冠跷,就可以得到SHA1的結(jié)果
1f32b9c9932c02227819a4151feed43e131aca40
還可以使用更安全的sha256和sha512。
Hmac
Hmac算法也是一種哈希算法身诺,它可以利用MD5或SHA1等哈希算法蜜托。不同的是,Hmac還需要一個(gè)密鑰:
constcrypto =require('crypto');consthmac = crypto.createHmac('sha256','secret-key');hmac.update('Hello, world!');hmac.update('Hello, nodejs!');console.log(hmac.digest('hex'));// 80f7e22570...
只要密鑰發(fā)生了變化霉赡,那么同樣的輸入數(shù)據(jù)也會(huì)得到不同的簽名橄务,因此,可以把Hmac理解為用隨機(jī)數(shù)“增強(qiáng)”的哈希算法穴亏。
AES
AES是一種常用的對(duì)稱加密算法仪糖,加解密都用同一個(gè)密鑰。crypto模塊提供了AES支持迫肖,但是需要自己封裝好函數(shù)锅劝,便于使用:
constcrypto =require('crypto');functionaesEncrypt(data, key){constcipher = crypto.createCipher('aes192', key);varcrypted = cipher.update(data,'utf8','hex');? crypted += cipher.final('hex');returncrypted;}functionaesDecrypt(data, key){constdecipher = crypto.createDecipher('aes192', key);vardecrypted = decipher.update(encrypted,'hex','utf8');? decrypted += decipher.final('utf8');returndecrypted;}vardata ='Hello, this is a secret message!';varkey ='Password!';varencrypted = aesEncrypt(data, key);vardecrypted = aesDecrypt(encrypted, key);console.log('Plain text: '+ data);console.log('Encrypted text: '+ encrypted);console.log('Decrypted text: '+ decrypted);
運(yùn)行結(jié)果如下:
Plain text: Hello, this is a secret message!
Encrypted text: 8a944d97bdabc157a5b7a40cb180e7...
Decrypted text: Hello, this is a secret message!
可以看出,加密后的字符串通過解密又得到了原始內(nèi)容蟆湖。
注意到AES有很多不同的算法故爵,如aes192,aes-128-ecb隅津,aes-256-cbc等诬垂,AES除了密鑰外還可以指定IV(Initial Vector),不同的系統(tǒng)只要IV不同伦仍,用相同的密鑰加密相同的數(shù)據(jù)得到的加密結(jié)果也是不同的结窘。
加密結(jié)果通常有兩種表示方法:hex和base64,這些功能Nodejs全部都支持充蓝,但是在應(yīng)用中要注意隧枫,如果加解密雙方一方用Nodejs,另一方用Java谓苟、PHP等其它語言官脓,需要仔細(xì)測(cè)試。如果無法正確解密涝焙,要確認(rèn)雙方是否遵循同樣的AES算法卑笨,字符串密鑰和IV是否相同,加密后的數(shù)據(jù)是否統(tǒng)一為hex或base64格式仑撞。
Diffie-Hellman
DH算法是一種密鑰交換協(xié)議赤兴,它可以讓雙方在不泄漏密鑰的情況下協(xié)商出一個(gè)密鑰來妖滔。DH算法基于數(shù)學(xué)原理,比如小明和小紅想要協(xié)商一個(gè)密鑰桶良,可以這么做:
小明先選一個(gè)素?cái)?shù)和一個(gè)底數(shù)座舍,例如,素?cái)?shù)p=23艺普,底數(shù)g=5(底數(shù)可以任選)簸州,再選擇一個(gè)秘密整數(shù)a=6鉴竭,計(jì)算A=g^a mod p=8歧譬,然后大聲告訴小紅:p=23,g=5搏存,A=8瑰步;
小紅收到小明發(fā)來的p,g璧眠,A后缩焦,也選一個(gè)秘密整數(shù)b=15,然后計(jì)算B=g^b mod p=19责静,并大聲告訴小明:B=19袁滥;
小明自己計(jì)算出s=B^a mod p=2,小紅也自己計(jì)算出s=A^b mod p=2
灾螃,因此题翻,最終協(xié)商的密鑰s為2。
在這個(gè)過程中腰鬼,密鑰2并不是小明告訴小紅的嵌赠,也不是小紅告訴小明的,而是雙方協(xié)商計(jì)算出來的熄赡。第三方只能知道p=23姜挺,g=5,A=8彼硫,B=19炊豪,由于不知道雙方選的秘密整數(shù)a=6和b=15,因此無法計(jì)算出密鑰2拧篮。
用crypto模塊實(shí)現(xiàn)DH算法如下:
constcrypto =require('crypto');// xiaoming's keys:varming = crypto.createDiffieHellman(512);varming_keys = ming.generateKeys();varprime = ming.getPrime();vargenerator = ming.getGenerator();console.log('Prime: '+ prime.toString('hex'));console.log('Generator: '+ generator.toString('hex'));// xiaohong's keys:varhong = crypto.createDiffieHellman(prime, generator);varhong_keys = hong.generateKeys();// exchange and generate secret:varming_secret = ming.computeSecret(hong_keys);varhong_secret = hong.computeSecret(ming_keys);// print secret:console.log('Secret of Xiao Ming: '+ ming_secret.toString('hex'));console.log('Secret of Xiao Hong: '+ hong_secret.toString('hex'));
運(yùn)行后溜在,可以得到如下輸出:
$ node dh.js
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be
注意每次輸出都不一樣,因?yàn)樗財(cái)?shù)的選擇是隨機(jī)的