node.js

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ì)彈出提示框:

hello.js

一個(gè)是Notepad++,免費(fèi)使用,有中文界面:

notepad-hello.js

請注意,用哪個(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提供的命令行模式:

run-node-hello

在命令行模式下,可以執(zhí)行node進(jìn)入Node交互式環(huán)境,也可以執(zhí)行node hello.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ì)越來越長,越來越不容易維護(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è)moduleexports變量返回,這樣,另一個(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ù)為nulldata參數(shù)為讀取到的String。當(dāng)讀取發(fā)生錯(cuò)誤時(shí),err參數(shù)代表一個(gè)錯(cuò)誤對象,dataundefined

這也是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ī)定位。

nodejs-stream

有些流用來讀取數(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模塊提供的requestresponse對象。

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)容:

http-hello-sample

同時(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
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è)請求是瀏覽器請求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有很多不同的算法,如aes192aes-128-ecbaes-256-cbc等,AES除了密鑰外還可以指定IV(Initial Vector),不同的系統(tǒng)只要IV不同,用相同的密鑰加密相同的數(shù)據(jù)得到的加密結(jié)果也是不同的。

加密結(jié)果通常有兩種表示方法:hexbase64,這些功能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=23g=5A=8B=19,由于不知道雙方選的秘密整數(shù)a=6b=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ù)器去處理證書。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • topics: 1.The Node.js philosophy 2.The reactor pattern 3....
    宮若石閱讀 1,126評(píng)論 0 1
  • Node.js Stream(流) Stream 是一個(gè)抽象接口,Node 中有很多對象實(shí)現(xiàn)了這個(gè)接口。例如,對h...
    FTOLsXD閱讀 618評(píng)論 0 2
  • Node.js是目前非常火熱的技術(shù),但是它的誕生經(jīng)歷卻很奇特。 眾所周知,在Netscape設(shè)計(jì)出JavaScri...
    w_zhuan閱讀 3,639評(píng)論 2 41
  • 個(gè)人入門學(xué)習(xí)用筆記、不過多作為參考依據(jù)。如有錯(cuò)誤歡迎斧正 目錄 簡書好像不支持錨點(diǎn)、復(fù)制搜索(反正也是寫給我自己看...
    kirito_song閱讀 2,495評(píng)論 1 37
  • 早上去圖書館的時(shí)候,路上來了一輛車,停在了一個(gè)過道前,把路堵得死死的。從車的駕駛座上下來一個(gè)中年男子,中年男子攔著...
    Bingo愛吃飯閱讀 368評(píng)論 0 0