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到今天仍然有人在使用!)
沒有競爭就沒有發(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ù)。對于高性能,異步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)勢?
最大的優(yōu)勢是借助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ā)者對Joyent公司的策略不滿,于2014年從Node.js項(xiàng)目fork出了io.js項(xiàng)目,決定單獨(dú)發(fā)展,但兩者實(shí)際上是兼容的。
然而中國有句古話,叫做“分久必合,合久必分”。分家后沒多久,Joyent公司表示要和解,于是,io.js項(xiàng)目又決定回歸Node.js。
具體做法是將來io.js將首先添加新的特性,如果大家測試用得爽,就把新特性加入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)下載對應(yīng)平臺(tái)的安裝程序,網(wǎng)速慢的童鞋請移步國內(nèi)鏡像。
在Windows上安裝時(shí)務(wù)必選擇全部組件,包括勾選Add to Path
。
安裝完成后,在Windows環(huán)境下,請打開命令提示符,然后輸入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)境下,請打開終端,然后輸入node -v
,你應(yīng)該看到如下輸出:
$ node -vv6.2.0
如果版本號(hào)不是v6.2.x
,說明Node.js版本不對,后面章節(jié)的代碼不保證能正常運(yùn)行,請重新安裝最新版本。
npm
在正式開始Node.js學(xué)習(xí)之前,我們先認(rèn)識(shí)一下npm
。
npm
是什么東東?npm
其實(shí)是Node.js的包管理工具(package manager)。
為啥我們需要一個(gè)包管理工具呢?因?yàn)槲覀冊贜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í)候順帶裝好了。我們在命令提示符或者終端輸入npm -v
,應(yīng)該看到類似的輸出:
C:\>npm -v3.8.9
如果直接輸入npm
,你會(huì)看到類似下面的輸出:
C:\> npmUsage: npm <command>where <command> 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ì)彈出提示框:
一個(gè)是Notepad++,免費(fèi)使用,有中文界面:
請注意,用哪個(gè)都行,但是絕對不能用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ū)分命令行模式和Node交互模式。看到類似C:\>
是在Windows提供的命令行模式:
在命令行模式下,可以執(zhí)行node進(jìn)入Node交互式環(huán)境,也可以執(zhí)行node hello.js
運(yùn)行一個(gè).js
文件。看到>
是在Node交互式環(huán)境下:
在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ì)越來越長,越來越不容易維護(hù)。
為了編寫可維護(hù)的代碼,我們把很多函數(shù)分組,分別放到不同的文件里,這樣,每個(gè)文件包含的代碼就相對較少,很多編程語言都采用這種組織代碼的方式。在Node環(huán)境中,一個(gè).js
文件就稱之為一個(gè)模塊(module)。
使用模塊有什么好處?
最大的好處是大大提高了代碼的可維護(hù)性。其次,編寫代碼不必從零開始。當(dāng)一個(gè)模塊編寫完畢,就可以被其他地方引用。我們在編寫程序的時(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';
var s = 'Hello';
function greet(name) {
? console.log(s + ', ' + name + '!');
}
module.exports = greet;
函數(shù)greet()
是我們在hello
模塊中定義的,你可能注意到最后一行是一個(gè)奇怪的賦值語句,它的意思是,把函數(shù)greet
作為模塊的輸出暴露出去,這樣其他模塊就可以使用greet
函數(shù)了。
問題是其他模塊怎么使用hello
模塊的這個(gè)greet
函數(shù)呢?我們再編寫一個(gè)main.js
文件,調(diào)用hello
模塊的greet
函數(shù):
'use strict';
// 引入hello模塊:
var greet = require('./hello');
var s = 'Michael';
greet(s); // Hello, Michael!
注意到引入hello
模塊用Node提供的require
函數(shù):
var greet = require('./hello');
引入的模塊作為變量保存在greet
變量中,那greet
變量到底是什么東西?其實(shí)變量greet
就是在hello.js
中我們用module.exports = greet;
輸出的greet
函數(shù)。所以,main.js
就成功地引用了hello.js
模塊中定義的greet()
函數(shù),接下來就可以直接使用它了。
在使用require()
引入模塊的時(shí)候,請注意模塊的相對路徑。因?yàn)?code>main.js
和hello.js
位于同一個(gè)目錄,所以我們用了當(dāng)前目錄.:
var greet = require('./hello'); // 不要忘了寫相對目錄!
如果只寫模塊名:
var greet = 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ò)誤,你要檢查:
- 模塊名是否寫對了
- 模塊文件是否存在
- 相對路徑是否寫對了
CommonJS規(guī)范
這種模塊加載機(jī)制被稱為CommonJS規(guī)范。在這個(gè)規(guī)范下,每個(gè).js
文件都是一個(gè)模塊,它們內(nèi)部各自使用的變量名和函數(shù)名都互不沖突,例如,hello.js
和main.js
都申明了全局變量var s = 'xxx'
,但互不影響。
一個(gè)模塊想要對外暴露變量(函數(shù)也是變量),可以用module.exports = variable;
,一個(gè)模塊要引用其他模塊暴露的變量,用
var ref = require('module_name');
就拿到了引用模塊的變量。
要在模塊中對外輸出變量,用:module.exports = variable;
輸出的變量可以是任意對象、函數(shù)、數(shù)組等等。要引入其他模塊輸出的對象,用:var foo = require('other_module');
引入的對象具體是什么,取決于引入模塊輸出的對象。
深入了解模塊原理
如果你想詳細(xì)地了解CommonJS的模塊實(shí)現(xiàn)原理,請繼續(xù)往下閱讀
當(dāng)我們編寫JavaScript代碼時(shí),我們可以申明全局變量:
var s = 'global';
在瀏覽器中,大量使用全局變量可不好。如果你在a.js
中使用了全局變量s
,那么,在b.js
中也使用全局變量s
,將造成沖突,b.js
中對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)部的局部變量。
請注意我們編寫的hello.js
代碼是這樣的:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js加載了hello.js
后,它可以把代碼包裝一下,變成這樣執(zhí)行:
(function () {
? // 讀取的hello.js代碼:
? var s = 'Hello';
? var name = '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è)對象module:
// 準(zhǔn)備module對象:
var module = {
? id: 'hello',
? exports: {}
};
var load = function (module) {
? // 讀取的hello.js代碼:
? function greet(name) {
? ? console.log('Hello, ' + name + '!');
? }
? module.exports = greet;
? // hello.js代碼結(jié)束
? return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
可見,變量module
是Node在加載js文件前準(zhǔn)備的一個(gè)變量,并將其傳入加載函數(shù),我們在hello.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找到對應(yīng)的module
,把這個(gè)module
的exports
變量返回,這樣,另一個(gè)模塊就順利拿到了模塊的輸出:
var greet = require('./hello');
以上是Node實(shí)現(xiàn)JavaScript模塊的一個(gè)簡單的原理介紹。
module.exports vs exports
很多時(shí)候,你會(huì)看到,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:
方法一:對module.exports
賦值:
// hello.js
function hello() {
? console.log('Hello, world!');
}
function greet(name) {
? console.log('Hello, ' + name + '!');
}
function hello() {
? console.log('Hello, world!');
}
module.exports = { hello: hello, greet: greet};
方法二:直接使用exports:
// hello.js
function hello() {
? console.log('Hello, world!');
}
function greet(name) {
? console.log('Hello, ' + name + '!');
}
function hello() {
? console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
但是你不可以直接對exports
賦值:
// 代碼可以執(zhí)行,但是模塊并沒有輸出任何變量:
exports = {
? hello: hello,
? greet: greet
};
如果你對上面的寫法感到十分困惑,不要著急,我們來分析Node的加載機(jī)制:
首先,Node會(huì)把整個(gè)待加載的hello.js
文件放入一個(gè)包裝函數(shù)load
中執(zhí)行。在執(zhí)行這個(gè)load()
函數(shù)前,Node準(zhǔn)備好了module
變量:
var module = {
? id: 'hello',
? exports: {}
};
load()
函數(shù)最終返回module.exports
:
var load = function (exports, module) {
? // hello.js的文件內(nèi)容 ...
? // load函數(shù)返回:
? return module.exports;
};
var exported = load(module.exports, module);
也就是說,默認(rèn)情況下,Node準(zhǔn)備的exports
變量和module.exports
變量實(shí)際上是同一個(gè)變量,并且初始化為空對象{}
,于是,我們可以寫:
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è)空對象{}
,這樣你可以直接往里面加?xùn)|西。
但是,如果我們要輸出的是一個(gè)函數(shù)或數(shù)組,那么,只能給module.exports
賦值:
module.exports = function () {
? return 'foo';
};
給exports
賦值是無效的,因?yàn)橘x值后,module.exports
仍然是空對象{}
。
結(jié)論:
如果要輸出一個(gè)鍵值對象{}
,可以利用exports
這個(gè)已存在的空對象{}
,并繼續(xù)在上面添加新的鍵值;
如果要輸出一個(gè)函數(shù)或數(shù)組,必須直接對module.exports
對象賦值。
所以我們可以得出結(jié)論:直接對module.exports
賦值,可以應(yīng)對任何情況:
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ò)請求,讀寫文件,處理二進(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è)全局對象,在瀏覽器中,叫window
對象。而在Node.js環(huán)境中,也有唯一的全局對象,但不叫window
,而叫global
,這個(gè)對象的屬性和方法也和瀏覽器環(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è)對象,它代表當(dāng)前Node.js進(jìn)程。通過process
對象可以拿到許多有用信息:
> 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
對象來處理。如果我們響應(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í)行幾十毫秒~幾秒不等:
var data = getJSONSync('http://example.com/ajax');
同步操作的好處是代碼簡單,缺點(diǎn)是程序?qū)⒌却齀O操作,在等待時(shí)間內(nèi),無法響應(yīng)其它任何事件。而異步讀取不用等待IO操作,但代碼較麻煩。
異步讀文件
按照J(rèn)avaScript的標(biāo)準(zhǔn),異步讀取一個(gè)文本文件的代碼如下:
'use strict';
var fs = require('fs');
fs.readFile('sample.txt', 'utf-8', function (err, data) {
? if (err) {
? ? console.log(err);
? } else {
? ? console.log(data);
? }
});
請注意,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ò)誤對象,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';
var fs = 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
對象。在Node.js中,Buffer
對象就是一個(gè)包含零個(gè)或任意個(gè)字節(jié)的數(shù)組(注意和Array不同)。
Buffer
對象可以和String作轉(zhuǎn)換,例如,把一個(gè)Buffer
對象轉(zhuǎn)換成String:
// Buffer -> String
var text = data.toString('utf-8');
console.log(text);
或者把一個(gè)String
轉(zhuǎn)換成Buffer
:
// String -> Buffer
var buf = new Buffer(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';
var fs = require('fs');
var data = 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 {
? var data = fs.readFileSync('sample.txt', 'utf-8');
? console.log(data);
} catch (err) {
? // 出錯(cuò)了
}
寫文件
將數(shù)據(jù)寫入文件是通過fs.writeFile()
實(shí)現(xiàn)的:
'use strict';
var fs = require('fs');
var data = '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';
var fs = require('fs');
var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);
stat
如果我們要獲取文件大小,創(chuàng)建時(shí)間等信息,可以使用fs.stat()
,它返回一個(gè)Stat
對象,能告訴我們文件或目錄的詳細(xì)信息:
'use strict';
var fs = 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對象:
? ? ? console.log('birth time: ' + stat.birthtime);
? ? ? // 修改時(shí)間, Date對象:
? ? ? 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è)對應(yīng)的同步函數(shù)statSync()
,請?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í)際上它還對應(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ī)定位。
有些流用來讀取數(shù)據(jù),比如從文件讀取數(shù)據(jù)時(shí),可以打開一個(gè)文件流,然后從文件流中不斷地讀取數(shù)據(jù)。有些流用來寫入數(shù)據(jù),比如向文件寫入數(shù)據(jù)時(shí),只需要把數(shù)據(jù)不斷地往文件流中寫進(jìn)去就可以了。
在Node.js中,流也是一個(gè)對象,我們只需要響應(yīng)流的事件就可以了:data
事件表示流的數(shù)據(jù)已經(jīng)可以讀取了,end
事件表示這個(gè)流已經(jīng)到末尾了,沒有數(shù)據(jù)可以讀取了,error
事件表示出錯(cuò)了。
下面是一個(gè)從文件流讀取文本內(nèi)容的示例:
'use strict';
var fs = require('fs');
// 打開一個(gè)流:
var rs = 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';
var fs = require('fs');
var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream寫入文本數(shù)據(jù)...\n');
ws1.write('END.');
ws1.end();
var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream寫入二進(jìn)制數(shù)據(jù)...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();
所有可以讀取數(shù)據(jù)的流都繼承自stream.Readable
,所有可以寫入的流都繼承自stream.Writable
。
pipe
就像可以把兩個(gè)水管串成一個(gè)更長的水管一樣,兩個(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';
var fs = require('fs');
var rs = fs.createReadStream('sample.txt');
var ws = 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ù)量最多的前端開發(fā)人員。如果已經(jīng)掌握了JavaScript前端開發(fā),再學(xué)習(xí)一下如何將JavaScript應(yīng)用在后端開發(fā),就是名副其實(shí)的全棧了。
HTTP協(xié)議
要理解Web服務(wù)器程序的工作原理,首先,我們要對HTTP協(xié)議有基本的了解。如果你對HTTP協(xié)議不太熟悉,先看一看HTTP協(xié)議簡介。
HTTP服務(wù)器
要開發(fā)HTTP服務(wù)器程序,從頭處理TCP
連接,解析HTTP是不現(xiàn)實(shí)的。這些工作實(shí)際上已經(jīng)由Node.js自帶的http
模塊完成了。應(yīng)用程序并不直接和HTTP協(xié)議打交道,而是操作http
模塊提供的request
和response
對象。
request
對象封裝了HTTP請求,我們調(diào)用request
對象的屬性和方法就可以拿到所有HTTP請求的信息;
response
對象封裝了HTTP響應(yīng),我們操作response
對象的方法,就可以把HTTP響應(yīng)返回給瀏覽器。
用Node.js實(shí)現(xiàn)一個(gè)HTTP服務(wù)器程序非常簡單。我們來實(shí)現(xiàn)一個(gè)最簡單的Web程序hello.js
,它對于所有請求,都返回Hello world!
:
'use strict';
// 導(dǎo)入http模塊:
var http = require('http');
// 創(chuàng)建http server,并傳入回調(diào)函數(shù):
var server = http.createServer(function (request, response) {
? // 回調(diào)函數(shù)接收request和response對象,
? // 獲得HTTP請求的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('<h1>Hello world!</h1>');
});
// 讓服務(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)容:
同時(shí),在命令提示符窗口,可以看到程序打印的請求信息:
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
中的路徑,然后在本地找到對應(yīng)的文件,把文件內(nèi)容發(fā)送出去就可以了。
解析URL需要用到Node.js提供的url
模塊,它使用起來非常簡單,通過parse()
將一個(gè)字符串解析為一個(gè)Url
對象:
'use strict';
var url =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';
var path = require('path');
// 解析當(dāng)前目錄:
var workDir = path.resolve('.');
// '/Users/michael'
// 組合完整的文件路徑:當(dāng)前目錄+'pub'+'index.html':
var filePath = 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';
var fs = require('fs'),
? ? ? url = require('url'),
? ? ? path = require('path'),
? ? ? http = require('http');
// 從命令行參數(shù)獲取root目錄,默認(rèn)是當(dāng)前目錄:
var root = path.resolve(process.argv[2] || '.');
console.log('Static root dir: ' + root);
// 創(chuàng)建服務(wù)器:
var server = http.createServer(function (request, response) {
? // 獲得URL的path,類似 '/css/bootstrap.css':
? var pathname = url.parse(request.url).pathname;
? // 獲得對應(yīng)的本地文件路徑,類似 '/srv/www/css/bootstrap.css':
? var filepath = 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
對象本身是一個(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
只要當(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è)請求是瀏覽器請求index.html
頁面,后續(xù)請求是瀏覽器解析HTML后發(fā)送的其它資源請求。
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)制的字符串表示:
const crypto = require('crypto');
const hash = 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è)密鑰:
const crypto = require('crypto');
const hmac = 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是一種常用的對稱加密算法,加解密都用同一個(gè)密鑰。crypto
模塊提供了AES支持,但是需要自己封裝好函數(shù),便于使用:
const crypto = require('crypto');
function aesEncrypt(data, key) {
? const cipher = crypto.createCipher('aes192', key);
? var crypted = cipher.update(data, 'utf8', 'hex');
? crypted += cipher.final('hex');
? return crypted;
}
function aesDecrypt(data, key) {
? const decipher = crypto.createDecipher('aes192', key);
? var decrypted = decipher.update(encrypted, 'hex', 'utf8');
? decrypted += decipher.final('utf8');
? return decrypted;
}
var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = 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ì)測試。如果無法正確解密,要確認(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算法如下:
const crypto = require('crypto');
// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();
var prime = ming.getPrime();
var generator = ming.getGenerator();
console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));
// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();
// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_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ī)的。
證書
crypto模塊也可以處理數(shù)字證書。數(shù)字證書通常用在SSL連接,也就是Web的https連接。一般情況下,https連接只需要處理服務(wù)器端的單向認(rèn)證,如無特殊需求(例如自己作為Root給客戶發(fā)認(rèn)證證書),建議用反向代理服務(wù)器如Nginx等Web服務(wù)器去處理證書。