七天學(xué)會(huì)NodeJS

先保存起來(lái)免得地址失效
https://nqdeng.github.io/7-days-nodejs/#6

NodeJS基礎(chǔ)

什么是NodeJS

JS是腳本語(yǔ)言幔崖,腳本語(yǔ)言都需要一個(gè)解析器才能運(yùn)行食店。對(duì)于寫(xiě)在HTML頁(yè)面里的JS,瀏覽器充當(dāng)了解析器的角色赏寇。而對(duì)于需要獨(dú)立運(yùn)行的JS吉嫩,NodeJS就是一個(gè)解析器。

每一種解析器都是一個(gè)運(yùn)行環(huán)境嗅定,不但允許JS定義各種數(shù)據(jù)結(jié)構(gòu)自娩,進(jìn)行各種計(jì)算,還允許JS使用運(yùn)行環(huán)境提供的內(nèi)置對(duì)象和方法做一些事情渠退。例如運(yùn)行在瀏覽器中的JS的用途是操作DOM忙迁,瀏覽器就提供了document之類(lèi)的內(nèi)置對(duì)象。而運(yùn)行在NodeJS中的JS的用途是操作磁盤(pán)文件或搭建HTTP服務(wù)器碎乃,NodeJS就相應(yīng)提供了fs姊扔、http等內(nèi)置對(duì)象。

有啥用處

盡管存在一聽(tīng)說(shuō)可以直接運(yùn)行JS文件就覺(jué)得很酷的同學(xué)梅誓,但大多數(shù)同學(xué)在接觸新東西時(shí)首先關(guān)心的是有啥用處旱眯,以及能帶來(lái)啥價(jià)值晨川。

NodeJS的作者說(shuō),他創(chuàng)造NodeJS的目的是為了實(shí)現(xiàn)高性能Web服務(wù)器删豺,他首先看重的是事件機(jī)制和異步IO模型的優(yōu)越性共虑,而不是JS。但是他需要選擇一種編程語(yǔ)言實(shí)現(xiàn)他的想法呀页,這種編程語(yǔ)言不能自帶IO功能妈拌,并且需要能良好支持事件機(jī)制。JS沒(méi)有自帶IO功能蓬蝶,天生就用于處理瀏覽器中的DOM事件尘分,并且擁有一大群程序員,因此就成為了天然的選擇丸氛。

如他所愿培愁,NodeJS在服務(wù)端活躍起來(lái),出現(xiàn)了大批基于NodeJS的Web服務(wù)缓窜。而另一方面定续,NodeJS讓前端眾如獲神器,終于可以讓自己的能力覆蓋范圍跳出瀏覽器窗口禾锤,更大批的前端工具如雨后春筍私股。

因此,對(duì)于前端而言恩掷,雖然不是人人都要拿NodeJS寫(xiě)一個(gè)服務(wù)器程序倡鲸,但簡(jiǎn)單可至使用命令交互模式調(diào)試JS代碼片段,復(fù)雜可至編寫(xiě)工具提升工作效率黄娘。

NodeJS生態(tài)圈正欣欣向榮峭状。

如何安裝

安裝程序

NodeJS提供了一些安裝程序,都可以在nodejs.org這里下載并安裝逼争。

Windows系統(tǒng)下优床,選擇和系統(tǒng)版本匹配的.msi后綴的安裝文件。Mac OS X系統(tǒng)下氮凝,選擇.pkg后綴的安裝文件羔巢。

編譯安裝

Linux系統(tǒng)下沒(méi)有現(xiàn)成的安裝程序可用望忆,雖然一些發(fā)行版可以使用apt-get之類(lèi)的方式安裝罩阵,但不一定能安裝到最新版。因此Linux系統(tǒng)下一般使用以下方式編譯方式安裝NodeJS启摄。

  1. 確保系統(tǒng)下g++版本在4.6以上稿壁,python版本在2.6以上。

  2. nodejs.org下載tar.gz后綴的NodeJS最新版源代碼包并解壓到某個(gè)位置歉备。

  3. 進(jìn)入解壓到的目錄傅是,使用以下命令編譯和安裝。

     $ ./configure
     $ make
     $ sudo make install
    

如何運(yùn)行

打開(kāi)終端,鍵入node進(jìn)入命令交互模式喧笔,可以輸入一條代碼語(yǔ)句后立即執(zhí)行并顯示結(jié)果帽驯,例如:

$ node
> console.log('Hello World!');
Hello World!

如果要運(yùn)行一大段代碼的話,可以先寫(xiě)一個(gè)JS文件再運(yùn)行书闸。例如有以下hello.js尼变。

function hello() {
    console.log('Hello World!');
}
hello();

寫(xiě)好后在終端下鍵入node hello.js運(yùn)行,結(jié)果如下:

$ node hello.js
Hello World!

權(quán)限問(wèn)題

在Linux系統(tǒng)下浆劲,使用NodeJS監(jiān)聽(tīng)80或443端口提供HTTP(S)服務(wù)時(shí)需要root權(quán)限嫌术,有兩種方式可以做到。

一種方式是使用sudo命令運(yùn)行NodeJS牌借。例如通過(guò)以下命令運(yùn)行的server.js中有權(quán)限使用80和443端口度气。一般推薦這種方式,可以保證僅為有需要的JS腳本提供root權(quán)限膨报。

$ sudo node server.js

另一種方式是使用chmod +s命令讓NodeJS總是以root權(quán)限運(yùn)行磷籍,具體做法如下。因?yàn)檫@種方式讓任何JS腳本都有了root權(quán)限丙躏,不太安全择示,因此在需要很考慮安全的系統(tǒng)下不推薦使用。

$ sudo chown root /usr/local/bin/node
$ sudo chmod +s /usr/local/bin/node

模塊

編寫(xiě)稍大一點(diǎn)的程序時(shí)一般都會(huì)將代碼模塊化晒旅。在NodeJS中栅盲,一般將代碼合理拆分到不同的JS文件中,每一個(gè)文件就是一個(gè)模塊废恋,而文件路徑就是模塊名谈秫。

在編寫(xiě)每個(gè)模塊時(shí),都有require鱼鼓、exports拟烫、module三個(gè)預(yù)先定義好的變量可供使用。

require

require函數(shù)用于在當(dāng)前模塊中加載和使用別的模塊迄本,傳入一個(gè)模塊名硕淑,返回一個(gè)模塊導(dǎo)出對(duì)象。模塊名可使用相對(duì)路徑(以./開(kāi)頭)嘉赎,或者是絕對(duì)路徑(以/C:之類(lèi)的盤(pán)符開(kāi)頭)置媳。另外,模塊名中的.js擴(kuò)展名可以省略公条。以下是一個(gè)例子拇囊。

var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');

// foo1至foo4中保存的是同一個(gè)模塊的導(dǎo)出對(duì)象。

另外靶橱,可以使用以下方式加載和使用一個(gè)JSON文件寥袭。

var data = require('./data.json');

exports

exports對(duì)象是當(dāng)前模塊的導(dǎo)出對(duì)象路捧,用于導(dǎo)出模塊公有方法和屬性。別的模塊通過(guò)require函數(shù)使用當(dāng)前模塊時(shí)得到的就是當(dāng)前模塊的exports對(duì)象传黄。以下例子中導(dǎo)出了一個(gè)公有方法杰扫。

exports.hello = function () {
    console.log('Hello World!');
};

module

通過(guò)module對(duì)象可以訪問(wèn)到當(dāng)前模塊的一些相關(guān)信息,但最多的用途是替換當(dāng)前模塊的導(dǎo)出對(duì)象膘掰。例如模塊導(dǎo)出對(duì)象默認(rèn)是一個(gè)普通對(duì)象涉波,如果想改成一個(gè)函數(shù)的話,可以使用以下方式炭序。

module.exports = function () {
    console.log('Hello World!');
};

以上代碼中啤覆,模塊默認(rèn)導(dǎo)出對(duì)象被替換為一個(gè)函數(shù)。

模塊初始化

一個(gè)模塊中的JS代碼僅在模塊第一次被使用時(shí)執(zhí)行一次惭聂,并在執(zhí)行過(guò)程中初始化模塊的導(dǎo)出對(duì)象窗声。之后,緩存起來(lái)的導(dǎo)出對(duì)象被重復(fù)利用辜纲。

主模塊

通過(guò)命令行參數(shù)傳遞給NodeJS以啟動(dòng)程序的模塊被稱(chēng)為主模塊笨觅。主模塊負(fù)責(zé)調(diào)度組成整個(gè)程序的其它模塊完成工作。例如通過(guò)以下命令啟動(dòng)程序時(shí)耕腾,main.js就是主模塊见剩。

$ node main.js

完整示例

例如有以下目錄。

- /home/user/hello/
    - util/
        counter.js
    main.js

其中counter.js內(nèi)容如下:

var i = 0;

function count() {
    return ++i;
}

exports.count = count;

該模塊內(nèi)部定義了一個(gè)私有變量i,并在exports對(duì)象導(dǎo)出了一個(gè)公有方法count

主模塊main.js內(nèi)容如下:

var counter1 = require('./util/counter');
var    counter2 = require('./util/counter');

console.log(counter1.count());
console.log(counter2.count());
console.log(counter2.count());

運(yùn)行該程序的結(jié)果如下:

$ node main.js
1
2
3

可以看到盏筐,counter.js并沒(méi)有因?yàn)楸籸equire了兩次而初始化兩次。

二進(jìn)制模塊

雖然一般我們使用JS編寫(xiě)模塊羹呵,但NodeJS也支持使用C/C++編寫(xiě)二進(jìn)制模塊。編譯好的二進(jìn)制模塊除了文件擴(kuò)展名是.node外疗琉,和JS模塊的使用方式相同冈欢。雖然二進(jìn)制模塊能使用操作系統(tǒng)提供的所有功能,擁有無(wú)限的潛能盈简,但對(duì)于前端同學(xué)而言編寫(xiě)過(guò)于困難凑耻,并且難以跨平臺(tái)使用,因此不在本教程的覆蓋范圍內(nèi)柠贤。

小結(jié)

本章介紹了有關(guān)NodeJS的基本概念和使用方法香浩,總結(jié)起來(lái)有以下知識(shí)點(diǎn):

  • NodeJS是一個(gè)JS腳本解析器,任何操作系統(tǒng)下安裝NodeJS本質(zhì)上做的事情都是把NodeJS執(zhí)行程序復(fù)制到一個(gè)目錄种吸,然后保證這個(gè)目錄在系統(tǒng)PATH環(huán)境變量下弃衍,以便終端下可以使用node命令呀非。

  • 終端下直接輸入node命令可進(jìn)入命令交互模式坚俗,很適合用來(lái)測(cè)試一些JS代碼片段镜盯,比如正則表達(dá)式。

  • NodeJS使用CMD模塊系統(tǒng)猖败,主模塊作為程序入口點(diǎn)速缆,所有模塊在執(zhí)行過(guò)程中只初始化一次。

  • 除非JS模塊不能滿足需求恩闻,否則不要輕易使用二進(jìn)制模塊艺糜,否則你的用戶會(huì)叫苦連天。

代碼的組織和部署

有經(jīng)驗(yàn)的C程序員在編寫(xiě)一個(gè)新程序時(shí)首先從make文件寫(xiě)起幢尚。同樣的破停,使用NodeJS編寫(xiě)程序前,為了有個(gè)良好的開(kāi)端尉剩,首先需要準(zhǔn)備好代碼的目錄結(jié)構(gòu)和部署方式真慢,就如同修房子要先搭腳手架。本章將介紹與之相關(guān)的各種知識(shí)理茎。

模塊路徑解析規(guī)則

我們已經(jīng)知道黑界,require函數(shù)支持斜杠(/)或盤(pán)符(C:)開(kāi)頭的絕對(duì)路徑,也支持./開(kāi)頭的相對(duì)路徑皂林。但這兩種路徑在模塊之間建立了強(qiáng)耦合關(guān)系朗鸠,一旦某個(gè)模塊文件的存放位置需要變更,使用該模塊的其它模塊的代碼也需要跟著調(diào)整础倍,變得牽一發(fā)動(dòng)全身烛占。因此,require函數(shù)支持第三種形式的路徑沟启,寫(xiě)法類(lèi)似于foo/bar扰楼,并依次按照以下規(guī)則解析路徑,直到找到模塊位置美浦。

  1. 內(nèi)置模塊

    如果傳遞給require函數(shù)的是NodeJS內(nèi)置模塊名稱(chēng)弦赖,不做路徑解析,直接返回內(nèi)部模塊的導(dǎo)出對(duì)象浦辨,例如require('fs')蹬竖。

  2. node_modules目錄

    NodeJS定義了一個(gè)特殊的node_modules目錄用于存放模塊。例如某個(gè)模塊的絕對(duì)路徑是/home/user/hello.js流酬,在該模塊中使用require('foo/bar')方式加載模塊時(shí)币厕,則NodeJS依次嘗試使用以下路徑。

     /home/user/node_modules/foo/bar
     /home/node_modules/foo/bar
     /node_modules/foo/bar
    
  3. NODE_PATH環(huán)境變量

    與PATH環(huán)境變量類(lèi)似芽腾,NodeJS允許通過(guò)NODE_PATH環(huán)境變量來(lái)指定額外的模塊搜索路徑旦装。NODE_PATH環(huán)境變量中包含一到多個(gè)目錄路徑,路徑之間在Linux下使用:分隔摊滔,在Windows下使用;分隔阴绢。例如定義了以下NODE_PATH環(huán)境變量:

     NODE_PATH=/home/user/lib:/home/lib
    

    當(dāng)使用require('foo/bar')的方式加載模塊時(shí)店乐,則NodeJS依次嘗試以下路徑。

     /home/user/lib/foo/bar
     /home/lib/foo/bar
    

包(package)

我們已經(jīng)知道了JS模塊的基本單位是單個(gè)JS文件呻袭,但復(fù)雜些的模塊往往由多個(gè)子模塊組成眨八。為了便于管理和使用,我們可以把由多個(gè)子模塊組成的大模塊稱(chēng)做左电,并把所有子模塊放在同一個(gè)目錄里廉侧。

在組成一個(gè)包的所有子模塊中,需要有一個(gè)入口模塊篓足,入口模塊的導(dǎo)出對(duì)象被作為包的導(dǎo)出對(duì)象段誊。例如有以下目錄結(jié)構(gòu)。

- /home/user/lib/
    - cat/
        head.js
        body.js
        main.js

其中cat目錄定義了一個(gè)包栈拖,其中包含了3個(gè)子模塊枕扫。main.js作為入口模塊,其內(nèi)容如下:

var head = require('./head');
var body = require('./body');

exports.create = function (name) {
    return {
        name: name,
        head: head.create(),
        body: body.create()
    };
};

在其它模塊里使用包的時(shí)候辱魁,需要加載包的入口模塊烟瞧。接著上例,使用require('/home/user/lib/cat/main')能達(dá)到目的染簇,但是入口模塊名稱(chēng)出現(xiàn)在路徑里看上去不是個(gè)好主意参滴。因此我們需要做點(diǎn)額外的工作,讓包使用起來(lái)更像是單個(gè)模塊锻弓。

index.js

當(dāng)模塊的文件名是index.js砾赔,加載模塊時(shí)可以使用模塊所在目錄的路徑代替模塊文件路徑,因此接著上例青灼,以下兩條語(yǔ)句等價(jià)暴心。

var cat = require('/home/user/lib/cat');
var cat = require('/home/user/lib/cat/index');

這樣處理后,就只需要把包目錄路徑傳遞給require函數(shù)杂拨,感覺(jué)上整個(gè)目錄被當(dāng)作單個(gè)模塊使用专普,更有整體感。

package.json

如果想自定義入口模塊的文件名和存放位置弹沽,就需要在包目錄下包含一個(gè)package.json文件檀夹,并在其中指定入口模塊的路徑。上例中的cat模塊可以重構(gòu)如下策橘。

- /home/user/lib/
    - cat/
        + doc/
        - lib/
            head.js
            body.js
            main.js
        + tests/
        package.json

其中package.json內(nèi)容如下炸渡。

{
    "name": "cat",
    "main": "./lib/main.js"
}

如此一來(lái),就同樣可以使用require('/home/user/lib/cat')的方式加載模塊丽已。NodeJS會(huì)根據(jù)包目錄下的package.json找到入口模塊所在位置蚌堵。

命令行程序

使用NodeJS編寫(xiě)的東西,要么是一個(gè)包,要么是一個(gè)命令行程序吼畏,而前者最終也會(huì)用于開(kāi)發(fā)后者督赤。因此我們?cè)诓渴鸫a時(shí)需要一些技巧,讓用戶覺(jué)得自己是在使用一個(gè)命令行程序宫仗。

例如我們用NodeJS寫(xiě)了個(gè)程序,可以把命令行參數(shù)原樣打印出來(lái)旁仿。該程序很簡(jiǎn)單藕夫,在主模塊內(nèi)實(shí)現(xiàn)了所有功能。并且寫(xiě)好后枯冈,我們把該程序部署在/home/user/bin/node-echo.js這個(gè)位置毅贮。為了在任何目錄下都能運(yùn)行該程序,我們需要使用以下終端命令尘奏。

$ node /home/user/bin/node-echo.js Hello World
Hello World

這種使用方式看起來(lái)不怎么像是一個(gè)命令行程序滩褥,下邊的才是我們期望的方式。

$ node-echo Hello World

Linux

在Linux系統(tǒng)下炫加,我們可以把JS文件當(dāng)作shell腳本來(lái)運(yùn)行瑰煎,從而達(dá)到上述目的,具體步驟如下:

  1. 在shell腳本中俗孝,可以通過(guò)#!注釋來(lái)指定當(dāng)前腳本使用的解析器酒甸。所以我們首先在node-echo.js文件頂部增加以下一行注釋?zhuān)砻鳟?dāng)前腳本使用NodeJS解析。

     #! /usr/bin/env node
    

    NodeJS會(huì)忽略掉位于JS模塊首行的#!注釋?zhuān)槐負(fù)?dān)心這行注釋是非法語(yǔ)句赋铝。

  2. 然后插勤,我們使用以下命令賦予node-echo.js文件執(zhí)行權(quán)限。

     $ chmod +x /home/user/bin/node-echo.js
    
  3. 最后革骨,我們?cè)赑ATH環(huán)境變量中指定的某個(gè)目錄下农尖,例如在/usr/local/bin下邊創(chuàng)建一個(gè)軟鏈文件,文件名與我們希望使用的終端命令同名良哲,命令如下:

     $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo
    

這樣處理后盛卡,我們就可以在任何目錄下使用node-echo命令了。

Windows

在Windows系統(tǒng)下的做法完全不同筑凫,我們得靠.cmd文件來(lái)解決問(wèn)題窟扑。假設(shè)node-echo.js存放在C:\Users\user\bin目錄,并且該目錄已經(jīng)添加到PATH環(huán)境變量里了漏健。接下來(lái)需要在該目錄下新建一個(gè)名為node-echo.cmd的文件嚎货,文件內(nèi)容如下:

@node "C:\User\user\bin\node-echo.js" %*

這樣處理后,我們就可以在任何目錄下使用node-echo命令了蔫浆。

工程目錄

了解了以上知識(shí)后殖属,現(xiàn)在我們可以來(lái)完整地規(guī)劃一個(gè)工程目錄了。以編寫(xiě)一個(gè)命令行程序?yàn)槔呤ⅲ话阄覀儠?huì)同時(shí)提供命令行模式和API模式兩種使用方式洗显,并且我們會(huì)借助三方包來(lái)編寫(xiě)代碼外潜。除了代碼外,一個(gè)完整的程序也應(yīng)該有自己的文檔和測(cè)試用例挠唆。因此处窥,一個(gè)標(biāo)準(zhǔn)的工程目錄都看起來(lái)像下邊這樣。

- /home/user/workspace/node-echo/   # 工程目錄
    - bin/                          # 存放命令行相關(guān)代碼
        node-echo
    + doc/                          # 存放文檔
    - lib/                          # 存放API相關(guān)代碼
        echo.js
    - node_modules/                 # 存放三方包
        + argv/
    + tests/                        # 存放測(cè)試用例
    package.json                    # 元數(shù)據(jù)文件
    README.md                       # 說(shuō)明文件

其中部分文件內(nèi)容如下:

/* bin/node-echo */
var argv = require('argv'),
    echo = require('../lib/echo');
console.log(echo(argv.join(' ')));

/* lib/echo.js */
module.exports = function (message) {
    return message;
};

/* package.json */
{
    "name": "node-echo",
    "main": "./lib/echo.js"
}

以上例子中分類(lèi)存放了不同類(lèi)型的文件玄组,并通過(guò)node_moudles目錄直接使用三方包名加載模塊滔驾。此外,定義了package.json之后俄讹,node-echo目錄也可被當(dāng)作一個(gè)包來(lái)使用哆致。

NPM

NPM是隨同NodeJS一起安裝的包管理工具,能解決NodeJS代碼部署上的很多問(wèn)題患膛,常見(jiàn)的使用場(chǎng)景有以下幾種:

  • 允許用戶從NPM服務(wù)器下載別人編寫(xiě)的三方包到本地使用摊阀。

  • 允許用戶從NPM服務(wù)器下載并安裝別人編寫(xiě)的命令行程序到本地使用。

  • 允許用戶將自己編寫(xiě)的包或命令行程序上傳到NPM服務(wù)器供別人使用踪蹬。

可以看到胞此,NPM建立了一個(gè)NodeJS生態(tài)圈,NodeJS開(kāi)發(fā)者和用戶可以在里邊互通有無(wú)跃捣。以下分別介紹這三種場(chǎng)景下怎樣使用NPM豌鹤。

下載三方包

需要使用三方包時(shí),首先得知道有哪些包可用枝缔。雖然npmjs.org提供了個(gè)搜索框可以根據(jù)包名來(lái)搜索布疙,但如果連想使用的三方包的名字都不確定的話,就請(qǐng)百度一下吧愿卸。知道了包名后灵临,比如上邊例子中的argv,就可以在工程目錄下打開(kāi)終端趴荸,使用以下命令來(lái)下載三方包儒溉。

$ npm install argv
...
argv@0.0.2 node_modules\argv

下載好之后,argv包就放在了工程目錄下的node_modules目錄中发钝,因此在代碼中只需要通過(guò)require('argv')的方式就好顿涣,無(wú)需指定三方包路徑。

以上命令默認(rèn)下載最新版三方包酝豪,如果想要下載指定版本的話涛碑,可以在包名后邊加上@<version>,例如通過(guò)以下命令可下載0.0.1版的argv孵淘。

$ npm install argv@0.0.1
...
argv@0.0.1 node_modules\argv

如果使用到的三方包比較多蒲障,在終端下一個(gè)包一條命令地安裝未免太人肉了。因此NPM對(duì)package.json的字段做了擴(kuò)展,允許在其中申明三方包依賴(lài)揉阎。因此庄撮,上邊例子中的package.json可以改寫(xiě)如下:

{
    "name": "node-echo",
    "main": "./lib/echo.js",
    "dependencies": {
        "argv": "0.0.2"
    }
}

這樣處理后,在工程目錄下就可以使用npm install命令批量安裝三方包了毙籽。更重要的是洞斯,當(dāng)以后node-echo也上傳到了NPM服務(wù)器,別人下載這個(gè)包時(shí)坑赡,NPM會(huì)根據(jù)包中申明的三方包依賴(lài)自動(dòng)下載進(jìn)一步依賴(lài)的三方包烙如。例如,使用npm install node-echo命令時(shí)垮衷,NPM會(huì)自動(dòng)創(chuàng)建以下目錄結(jié)構(gòu)厅翔。

- project/
    - node_modules/
        - node-echo/
            - node_modules/
                + argv/
            ...
    ...

如此一來(lái)乖坠,用戶只需關(guān)心自己直接使用的三方包搀突,不需要自己去解決所有包的依賴(lài)關(guān)系。

安裝命令行程序

從NPM服務(wù)上下載安裝一個(gè)命令行程序的方法與三方包類(lèi)似熊泵。例如上例中的node-echo提供了命令行使用方式仰迁,只要node-echo自己配置好了相關(guān)的package.json字段,對(duì)于用戶而言顽分,只需要使用以下命令安裝程序徐许。

$ npm install node-echo -g

參數(shù)中的-g表示全局安裝,因此node-echo會(huì)默認(rèn)安裝到以下位置卒蘸,并且NPM會(huì)自動(dòng)創(chuàng)建好Linux系統(tǒng)下需要的軟鏈文件或Windows系統(tǒng)下需要的.cmd文件雌隅。

- /usr/local/               # Linux系統(tǒng)下
    - lib/node_modules/
        + node-echo/
        ...
    - bin/
        node-echo
        ...
    ...

- %APPDATA%\npm\            # Windows系統(tǒng)下
    - node_modules\
        + node-echo\
        ...
    node-echo.cmd
    ...

發(fā)布代碼

第一次使用NPM發(fā)布代碼前需要注冊(cè)一個(gè)賬號(hào)。終端下運(yùn)行npm adduser缸沃,之后按照提示做即可恰起。賬號(hào)搞定后,接著我們需要編輯package.json文件趾牧,加入NPM必需的字段检盼。接著上邊node-echo的例子,package.json里必要的字段如下翘单。

{
    "name": "node-echo",           # 包名吨枉,在NPM服務(wù)器上須要保持唯一
    "version": "1.0.0",            # 當(dāng)前版本號(hào)
    "dependencies": {              # 三方包依賴(lài),需要指定包名和版本號(hào)
        "argv": "0.0.2"
      },
    "main": "./lib/echo.js",       # 入口模塊位置
    "bin" : {
        "node-echo": "./bin/node-echo"      # 命令行程序名和主模塊位置
    }
}

之后哄芜,我們就可以在package.json所在目錄下運(yùn)行npm publish發(fā)布代碼了貌亭。

版本號(hào)

使用NPM下載和發(fā)布代碼時(shí)都會(huì)接觸到版本號(hào)。NPM使用語(yǔ)義版本號(hào)來(lái)管理代碼认臊,這里簡(jiǎn)單介紹一下属提。

語(yǔ)義版本號(hào)分為X.Y.Z三位,分別代表主版本號(hào)、次版本號(hào)和補(bǔ)丁版本號(hào)冤议。當(dāng)代碼變更時(shí)斟薇,版本號(hào)按以下原則更新。

+ 如果只是修復(fù)bug恕酸,需要更新Z位堪滨。

+ 如果是新增了功能,但是向下兼容蕊温,需要更新Y位袱箱。

+ 如果有大變動(dòng),向下不兼容义矛,需要更新X位发笔。

版本號(hào)有了這個(gè)保證后,在申明三方包依賴(lài)時(shí)凉翻,除了可依賴(lài)于一個(gè)固定版本號(hào)外了讨,還可依賴(lài)于某個(gè)范圍的版本號(hào)。例如"argv": "0.0.x"表示依賴(lài)于0.0.x系列的最新版argv制轰。NPM支持的所有版本號(hào)范圍指定方式可以查看官方文檔瑞侮。

靈機(jī)一點(diǎn)

除了本章介紹的部分外搬味,NPM還提供了很多功能,package.json里也有很多其它有用的字段。除了可以在npmjs.org/doc/查看官方文檔外阎抒,這里再介紹一些NPM常用命令藕漱。

  • NPM提供了很多命令辜窑,例如installpublish躺盛,使用npm help可查看所有命令。

  • 使用npm help <command>可查看某條命令的詳細(xì)幫助彩库,例如npm help install肤无。

  • package.json所在目錄下使用npm install . -g可先在本地安裝當(dāng)前命令行程序,可用于發(fā)布前的本地測(cè)試侧巨。

  • 使用npm update <package>可以把當(dāng)前目錄下node_modules子目錄里邊的對(duì)應(yīng)模塊更新至最新版本舅锄。

  • 使用npm update <package> -g可以把全局安裝的對(duì)應(yīng)命令行程序更新至最新版。

  • 使用npm cache clear可以清空NPM本地緩存司忱,用于對(duì)付使用相同版本號(hào)發(fā)布新版本代碼的人皇忿。

  • 使用npm unpublish <package>@<version>可以撤銷(xiāo)發(fā)布自己發(fā)布過(guò)的某個(gè)版本代碼。

小結(jié)

本章介紹了使用NodeJS編寫(xiě)代碼前需要做的準(zhǔn)備工作坦仍,總結(jié)起來(lái)有以下幾點(diǎn):

  • 編寫(xiě)代碼前先規(guī)劃好目錄結(jié)構(gòu)鳍烁,才能做到有條不紊。

  • 稍大些的程序可以將代碼拆分為多個(gè)模塊管理繁扎,更大些的程序可以使用包來(lái)組織模塊幔荒。

  • 合理使用node_modulesNODE_PATH來(lái)解耦包的使用方式和物理路徑糊闽。

  • 使用NPM加入NodeJS生態(tài)圈互通有無(wú)。

  • 想到了心儀的包名時(shí)請(qǐng)?zhí)崆霸贜PM上搶注爹梁。

文件操作

讓前端覺(jué)得如獲神器的不是NodeJS能做網(wǎng)絡(luò)編程右犹,而是NodeJS能夠操作文件。小至文件查找姚垃,大至代碼編譯念链,幾乎沒(méi)有一個(gè)前端工具不操作文件。換個(gè)角度講积糯,幾乎也只需要一些數(shù)據(jù)處理邏輯掂墓,再加上一些文件操作,就能夠編寫(xiě)出大多數(shù)前端工具看成。本章將介紹與之相關(guān)的NodeJS內(nèi)置模塊君编。

開(kāi)門(mén)紅

NodeJS提供了基本的文件操作API,但是像文件拷貝這種高級(jí)功能就沒(méi)有提供川慌,因此我們先拿文件拷貝程序練手吃嘿。與copy命令類(lèi)似,我們的程序需要能接受源文件路徑與目標(biāo)文件路徑兩個(gè)參數(shù)窘游。

小文件拷貝

我們使用NodeJS內(nèi)置的fs模塊簡(jiǎn)單實(shí)現(xiàn)這個(gè)程序如下唠椭。

var fs = require('fs');

function copy(src, dst) {
    fs.writeFileSync(dst, fs.readFileSync(src));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

以上程序使用fs.readFileSync從源路徑讀取文件內(nèi)容跳纳,并使用fs.writeFileSync將文件內(nèi)容寫(xiě)入目標(biāo)路徑忍饰。

豆知識(shí): process是一個(gè)全局變量,可通過(guò)process.argv獲得命令行參數(shù)寺庄。由于argv[0]固定等于NodeJS執(zhí)行程序的絕對(duì)路徑艾蓝,argv[1]固定等于主模塊的絕對(duì)路徑,因此第一個(gè)命令行參數(shù)從argv[2]這個(gè)位置開(kāi)始斗塘。

大文件拷貝

上邊的程序拷貝一些小文件沒(méi)啥問(wèn)題赢织,但這種一次性把所有文件內(nèi)容都讀取到內(nèi)存中后再一次性寫(xiě)入磁盤(pán)的方式不適合拷貝大文件,內(nèi)存會(huì)爆倉(cāng)馍盟。對(duì)于大文件于置,我們只能讀一點(diǎn)寫(xiě)一點(diǎn),直到完成拷貝贞岭。因此上邊的程序需要改造如下八毯。

var fs = require('fs');

function copy(src, dst) {
    fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

以上程序使用fs.createReadStream創(chuàng)建了一個(gè)源文件的只讀數(shù)據(jù)流,并使用fs.createWriteStream創(chuàng)建了一個(gè)目標(biāo)文件的只寫(xiě)數(shù)據(jù)流瞄桨,并且用pipe方法把兩個(gè)數(shù)據(jù)流連接了起來(lái)话速。連接起來(lái)后發(fā)生的事情,說(shuō)得抽象點(diǎn)的話芯侥,水順著水管從一個(gè)桶流到了另一個(gè)桶泊交。

API走馬觀花

我們先大致看看NodeJS提供了哪些和文件操作有關(guān)的API乳讥。這里并不逐一介紹每個(gè)API的使用方法,官方文檔已經(jīng)做得很好了廓俭。

Buffer(數(shù)據(jù)塊)

**官方文檔: **http://nodejs.org/api/buffer.html

JS語(yǔ)言自身只有字符串?dāng)?shù)據(jù)類(lèi)型云石,沒(méi)有二進(jìn)制數(shù)據(jù)類(lèi)型,因此NodeJS提供了一個(gè)與String對(duì)等的全局構(gòu)造函數(shù)Buffer來(lái)提供對(duì)二進(jìn)制數(shù)據(jù)的操作研乒。除了可以讀取文件得到Buffer的實(shí)例外留晚,還能夠直接構(gòu)造,例如:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

Buffer與字符串類(lèi)似告嘲,除了可以用.length屬性得到字節(jié)長(zhǎng)度外错维,還可以用[index]方式讀取指定位置的字節(jié),例如:

bin[0]; // => 0x68;

Buffer與字符串能夠互相轉(zhuǎn)化橄唬,例如可以使用指定編碼將二進(jìn)制數(shù)據(jù)轉(zhuǎn)化為字符串:

var str = bin.toString('utf-8'); // => "hello"

或者反過(guò)來(lái)赋焕,將字符串轉(zhuǎn)換為指定編碼下的二進(jìn)制數(shù)據(jù):

var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>

Buffer與字符串有一個(gè)重要區(qū)別。字符串是只讀的仰楚,并且對(duì)字符串的任何修改得到的都是一個(gè)新字符串隆判,原字符串保持不變。至于Buffer僧界,更像是可以做指針操作的C語(yǔ)言數(shù)組侨嘀。例如,可以用[index]方式直接修改某個(gè)位置的字節(jié)捂襟。

bin[0] = 0x48;

.slice方法也不是返回一個(gè)新的Buffer咬腕,而更像是返回了指向原Buffer中間的某個(gè)位置的指針,如下所示葬荷。

[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
    ^           ^
    |           |
   bin     bin.slice(2)

因此對(duì).slice方法返回的Buffer的修改會(huì)作用于原Buffer涨共,例如:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var sub = bin.slice(2);

sub[0] = 0x65;
console.log(bin); // => <Buffer 68 65 65 6c 6f>

也因此,如果想要拷貝一份Buffer宠漩,得首先創(chuàng)建一個(gè)新的Buffer举反,并通過(guò).copy方法把原Buffer中的數(shù)據(jù)復(fù)制過(guò)去。這個(gè)類(lèi)似于申請(qǐng)一塊新的內(nèi)存扒吁,并把已有內(nèi)存中的數(shù)據(jù)復(fù)制過(guò)去火鼻。以下是一個(gè)例子。

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var dup = new Buffer(bin.length);

bin.copy(dup);
dup[0] = 0x48;
console.log(bin); // => <Buffer 68 65 6c 6c 6f>
console.log(dup); // => <Buffer 48 65 65 6c 6f>

總之雕崩,Buffer將JS的數(shù)據(jù)處理能力從字符串?dāng)U展到了任意二進(jìn)制數(shù)據(jù)魁索。

Stream(數(shù)據(jù)流)

**官方文檔: **http://nodejs.org/api/stream.html

當(dāng)內(nèi)存中無(wú)法一次裝下需要處理的數(shù)據(jù)時(shí),或者一邊讀取一邊處理更加高效時(shí)晨逝,我們就需要用到數(shù)據(jù)流蛾默。NodeJS中通過(guò)各種Stream來(lái)提供對(duì)數(shù)據(jù)流的操作。

以上邊的大文件拷貝程序?yàn)槔矫玻覀兛梢詾閿?shù)據(jù)來(lái)源創(chuàng)建一個(gè)只讀數(shù)據(jù)流支鸡,示例如下:

var rs = fs.createReadStream(pathname);

rs.on('data', function (chunk) {
    doSomething(chunk);
});

rs.on('end', function () {
    cleanUp();
});

豆知識(shí): Stream基于事件機(jī)制工作冬念,所有Stream的實(shí)例都繼承于NodeJS提供的EventEmitter

上邊的代碼中data事件會(huì)源源不斷地被觸發(fā)牧挣,不管doSomething函數(shù)是否處理得過(guò)來(lái)急前。代碼可以繼續(xù)做如下改造,以解決這個(gè)問(wèn)題瀑构。

var rs = fs.createReadStream(src);

rs.on('data', function (chunk) {
    rs.pause();
    doSomething(chunk, function () {
        rs.resume();
    });
});

rs.on('end', function () {
    cleanUp();
});

以上代碼給doSomething函數(shù)加上了回調(diào)裆针,因此我們可以在處理數(shù)據(jù)前暫停數(shù)據(jù)讀取,并在處理數(shù)據(jù)后繼續(xù)讀取數(shù)據(jù)寺晌。

此外世吨,我們也可以為數(shù)據(jù)目標(biāo)創(chuàng)建一個(gè)只寫(xiě)數(shù)據(jù)流,示例如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on('data', function (chunk) {
    ws.write(chunk);
});

rs.on('end', function () {
    ws.end();
});

我們把doSomething換成了往只寫(xiě)數(shù)據(jù)流里寫(xiě)入數(shù)據(jù)后呻征,以上代碼看起來(lái)就像是一個(gè)文件拷貝程序了耘婚。但是以上代碼存在上邊提到的問(wèn)題,如果寫(xiě)入速度跟不上讀取速度的話陆赋,只寫(xiě)數(shù)據(jù)流內(nèi)部的緩存會(huì)爆倉(cāng)沐祷。我們可以根據(jù).write方法的返回值來(lái)判斷傳入的數(shù)據(jù)是寫(xiě)入目標(biāo)了,還是臨時(shí)放在了緩存了攒岛,并根據(jù)drain事件來(lái)判斷什么時(shí)候只寫(xiě)數(shù)據(jù)流已經(jīng)將緩存中的數(shù)據(jù)寫(xiě)入目標(biāo)赖临,可以傳入下一個(gè)待寫(xiě)數(shù)據(jù)了。因此代碼可以改造如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on('data', function (chunk) {
    if (ws.write(chunk) === false) {
        rs.pause();
    }
});

rs.on('end', function () {
    ws.end();
});

ws.on('drain', function () {
    rs.resume();
});

以上代碼實(shí)現(xiàn)了數(shù)據(jù)從只讀數(shù)據(jù)流到只寫(xiě)數(shù)據(jù)流的搬運(yùn)灾锯,并包括了防爆倉(cāng)控制兢榨。因?yàn)檫@種使用場(chǎng)景很多,例如上邊的大文件拷貝程序挠进,NodeJS直接提供了.pipe方法來(lái)做這件事情色乾,其內(nèi)部實(shí)現(xiàn)方式與上邊的代碼類(lèi)似誊册。

File System(文件系統(tǒng))

**官方文檔: **http://nodejs.org/api/fs.html

NodeJS通過(guò)fs內(nèi)置模塊提供對(duì)文件的操作领突。fs模塊提供的API基本上可以分為以下三類(lèi):

  • 文件屬性讀寫(xiě)。

    其中常用的有fs.stat案怯、fs.chmod君旦、fs.chown等等。

  • 文件內(nèi)容讀寫(xiě)嘲碱。

    其中常用的有fs.readFile金砍、fs.readdirfs.writeFile麦锯、fs.mkdir等等恕稠。

  • 底層文件操作。

    其中常用的有fs.open扶欣、fs.read鹅巍、fs.write千扶、fs.close等等。

NodeJS最精華的異步IO模型在fs模塊里有著充分的體現(xiàn)骆捧,例如上邊提到的這些API都通過(guò)回調(diào)函數(shù)傳遞結(jié)果澎羞。以fs.readFile為例:

fs.readFile(pathname, function (err, data) {
    if (err) {
        // Deal with error.
    } else {
        // Deal with data.
    }
});

如上邊代碼所示,基本上所有fs模塊API的回調(diào)參數(shù)都有兩個(gè)敛苇。第一個(gè)參數(shù)在有錯(cuò)誤發(fā)生時(shí)等于異常對(duì)象妆绞,第二個(gè)參數(shù)始終用于返回API方法執(zhí)行結(jié)果。

此外枫攀,fs模塊的所有異步API都有對(duì)應(yīng)的同步版本括饶,用于無(wú)法使用異步操作時(shí),或者同步操作更方便時(shí)的情況来涨。同步API除了方法名的末尾多了一個(gè)Sync之外巷帝,異常對(duì)象與執(zhí)行結(jié)果的傳遞方式也有相應(yīng)變化。同樣以fs.readFileSync為例:

try {
    var data = fs.readFileSync(pathname);
    // Deal with data.
} catch (err) {
    // Deal with error.
}

fs模塊提供的API很多扫夜,這里不一一介紹楞泼,需要時(shí)請(qǐng)自行查閱官方文檔。

Path(路徑)

**官方文檔: **http://nodejs.org/api/path.html

操作文件時(shí)難免不與文件路徑打交道笤闯。NodeJS提供了path內(nèi)置模塊來(lái)簡(jiǎn)化路徑相關(guān)操作堕阔,并提升代碼可讀性。以下分別介紹幾個(gè)常用的API颗味。

  • path.normalize

    將傳入的路徑轉(zhuǎn)換為標(biāo)準(zhǔn)路徑超陆,具體講的話,除了解析路徑中的...外浦马,還能去掉多余的斜杠时呀。如果有程序需要使用路徑作為某些數(shù)據(jù)的索引,但又允許用戶隨意輸入路徑時(shí)晶默,就需要使用該方法保證路徑的唯一性谨娜。以下是一個(gè)例子:

      var cache = {};
    
      function store(key, value) {
          cache[path.normalize(key)] = value;
      }
    
      store('foo/bar', 1);
      store('foo//baz//../bar', 2);
      console.log(cache);  // => { "foo/bar": 2 }
    

    **坑出沒(méi)注意: **標(biāo)準(zhǔn)化之后的路徑里的斜杠在Windows系統(tǒng)下是\,而在Linux系統(tǒng)下是/磺陡。如果想保證任何系統(tǒng)下都使用/作為路徑分隔符的話趴梢,需要用.replace(/\\/g, '/')再替換一下標(biāo)準(zhǔn)路徑。

  • path.join

    將傳入的多個(gè)路徑拼接為標(biāo)準(zhǔn)路徑币他。該方法可避免手工拼接路徑字符串的繁瑣坞靶,并且能在不同系統(tǒng)下正確使用相應(yīng)的路徑分隔符。以下是一個(gè)例子:

      path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
    
  • path.extname

    當(dāng)我們需要根據(jù)不同文件擴(kuò)展名做不同操作時(shí)蝴悉,該方法就顯得很好用彰阴。以下是一個(gè)例子:

      path.extname('foo/bar.js'); // => ".js"
    

path模塊提供的其余方法也不多,稍微看一下官方文檔就能全部掌握拍冠。

遍歷目錄

遍歷目錄是操作文件時(shí)的一個(gè)常見(jiàn)需求尿这。比如寫(xiě)一個(gè)程序廉丽,需要找到并處理指定目錄下的所有JS文件時(shí),就需要遍歷整個(gè)目錄妻味。

遞歸算法

遍歷目錄時(shí)一般使用遞歸算法正压,否則就難以編寫(xiě)出簡(jiǎn)潔的代碼。遞歸算法與數(shù)學(xué)歸納法類(lèi)似责球,通過(guò)不斷縮小問(wèn)題的規(guī)模來(lái)解決問(wèn)題焦履。以下示例說(shuō)明了這種方法。

function factorial(n) {
    if (n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

上邊的函數(shù)用于計(jì)算N的階乘(N!)雏逾〖慰悖可以看到,當(dāng)N大于1時(shí)栖博,問(wèn)題簡(jiǎn)化為計(jì)算N乘以N-1的階乘屑宠。當(dāng)N等于1時(shí),問(wèn)題達(dá)到最小規(guī)模仇让,不需要再簡(jiǎn)化典奉,因此直接返回1。

陷阱: 使用遞歸算法編寫(xiě)的代碼雖然簡(jiǎn)潔丧叽,但由于每遞歸一次就產(chǎn)生一次函數(shù)調(diào)用卫玖,在需要優(yōu)先考慮性能時(shí),需要把遞歸算法轉(zhuǎn)換為循環(huán)算法踊淳,以減少函數(shù)調(diào)用次數(shù)假瞬。

遍歷算法

目錄是一個(gè)樹(shù)狀結(jié)構(gòu),在遍歷時(shí)一般使用深度優(yōu)先+先序遍歷算法迂尝。深度優(yōu)先脱茉,意味著到達(dá)一個(gè)節(jié)點(diǎn)后,首先接著遍歷子節(jié)點(diǎn)而不是鄰居節(jié)點(diǎn)垄开。先序遍歷琴许,意味著首次到達(dá)了某節(jié)點(diǎn)就算遍歷完成,而不是最后一次返回某節(jié)點(diǎn)才算數(shù)说榆。因此使用這種遍歷方式時(shí)虚吟,下邊這棵樹(shù)的遍歷順序是A > B > D > E > C > F

          A
         / \
        B   C
       / \   \
      D   E   F

同步遍歷

了解了必要的算法后签财,我們可以簡(jiǎn)單地實(shí)現(xiàn)以下目錄遍歷函數(shù)。

function travel(dir, callback) {
    fs.readdirSync(dir).forEach(function (file) {
        var pathname = path.join(dir, file);

        if (fs.statSync(pathname).isDirectory()) {
            travel(pathname, callback);
        } else {
            callback(pathname);
        }
    });
}

可以看到偏塞,該函數(shù)以某個(gè)目錄作為遍歷的起點(diǎn)唱蒸。遇到一個(gè)子目錄時(shí),就先接著遍歷子目錄灸叼。遇到一個(gè)文件時(shí)神汹,就把文件的絕對(duì)路徑傳給回調(diào)函數(shù)庆捺。回調(diào)函數(shù)拿到文件路徑后屁魏,就可以做各種判斷和處理滔以。因此假設(shè)有以下目錄:

- /home/user/
    - foo/
        x.js
    - bar/
        y.js
    z.css

使用以下代碼遍歷該目錄時(shí),得到的輸入如下氓拼。

travel('/home/user', function (pathname) {
    console.log(pathname);
});

------------------------
/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.css

異步遍歷

如果讀取目錄或讀取文件狀態(tài)時(shí)使用的是異步API你画,目錄遍歷函數(shù)實(shí)現(xiàn)起來(lái)會(huì)有些復(fù)雜,但原理完全相同桃漾。travel函數(shù)的異步版本如下坏匪。

function travel(dir, callback, finish) {
    fs.readdir(dir, function (err, files) {
        (function next(i) {
            if (i < files.length) {
                var pathname = path.join(dir, files[i]);

                fs.stat(pathname, function (err, stats) {
                    if (stats.isDirectory()) {
                        travel(pathname, callback, function () {
                            next(i + 1);
                        });
                    } else {
                        callback(pathname, function () {
                            next(i + 1);
                        });
                    }
                });
            } else {
                finish && finish();
            }
        }(0));
    });
}

這里不詳細(xì)介紹異步遍歷函數(shù)的編寫(xiě)技巧,在后續(xù)章節(jié)中會(huì)詳細(xì)介紹這個(gè)撬统∈首遥總之我們可以看到異步編程還是蠻復(fù)雜的。

文本編碼

使用NodeJS編寫(xiě)前端工具時(shí)恋追,操作得最多的是文本文件凭迹,因此也就涉及到了文件編碼的處理問(wèn)題。我們常用的文本編碼有UTF8GBK兩種苦囱,并且UTF8文件還可能帶有BOM蕊苗。在讀取不同編碼的文本文件時(shí),需要將文件內(nèi)容轉(zhuǎn)換為JS使用的UTF8編碼字符串后才能正常處理沿彭。

BOM的移除

BOM用于標(biāo)記一個(gè)文本文件使用Unicode編碼朽砰,其本身是一個(gè)Unicode字符("\uFEFF"),位于文本文件頭部喉刘。在不同的Unicode編碼下瞧柔,BOM字符對(duì)應(yīng)的二進(jìn)制字節(jié)如下:

    Bytes      Encoding
----------------------------
    FE FF       UTF16BE
    FF FE       UTF16LE
    EF BB BF    UTF8

因此,我們可以根據(jù)文本文件頭幾個(gè)字節(jié)等于啥來(lái)判斷文件是否包含BOM,以及使用哪種Unicode編碼隔箍。但是态坦,BOM字符雖然起到了標(biāo)記文件編碼的作用,其本身卻不屬于文件內(nèi)容的一部分哥蔚,如果讀取文本文件時(shí)不去掉BOM,在某些使用場(chǎng)景下就會(huì)有問(wèn)題蛛蒙。例如我們把幾個(gè)JS文件合并成一個(gè)文件后糙箍,如果文件中間含有BOM字符,就會(huì)導(dǎo)致瀏覽器JS語(yǔ)法錯(cuò)誤牵祟。因此深夯,使用NodeJS讀取文本文件時(shí),一般需要去掉BOM。例如咕晋,以下代碼實(shí)現(xiàn)了識(shí)別和去除UTF8 BOM的功能雹拄。

function readText(pathname) {
    var bin = fs.readFileSync(pathname);

    if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
        bin = bin.slice(3);
    }

    return bin.toString('utf-8');
}

GBK轉(zhuǎn)UTF8

NodeJS支持在讀取文本文件時(shí),或者在Buffer轉(zhuǎn)換為字符串時(shí)指定文本編碼掌呜,但遺憾的是滓玖,GBK編碼不在NodeJS自身支持范圍內(nèi)。因此质蕉,一般我們借助iconv-lite這個(gè)三方包來(lái)轉(zhuǎn)換編碼势篡。使用NPM下載該包后,我們可以按下邊方式編寫(xiě)一個(gè)讀取GBK文本文件的函數(shù)饰剥。

var iconv = require('iconv-lite');

function readGBKText(pathname) {
    var bin = fs.readFileSync(pathname);

    return iconv.decode(bin, 'gbk');
}

單字節(jié)編碼

有時(shí)候殊霞,我們無(wú)法預(yù)知需要讀取的文件采用哪種編碼,因此也就無(wú)法指定正確的編碼汰蓉。比如我們要處理的某些CSS文件中绷蹲,有的用GBK編碼,有的用UTF8編碼顾孽。雖然可以一定程度可以根據(jù)文件的字節(jié)內(nèi)容猜測(cè)出文本編碼祝钢,但這里要介紹的是有些局限,但是要簡(jiǎn)單得多的一種技術(shù)若厚。

首先我們知道拦英,如果一個(gè)文本文件只包含英文字符,比如Hello World测秸,那無(wú)論用GBK編碼或是UTF8編碼讀取這個(gè)文件都是沒(méi)問(wèn)題的疤估。這是因?yàn)樵谶@些編碼下,ASCII0~128范圍內(nèi)字符都使用相同的單字節(jié)編碼霎冯。

反過(guò)來(lái)講铃拇,即使一個(gè)文本文件中有中文等字符,如果我們需要處理的字符僅在ASCII0~128范圍內(nèi)沈撞,比如除了注釋和字符串以外的JS代碼慷荔,我們就可以統(tǒng)一使用單字節(jié)編碼來(lái)讀取文件,不用關(guān)心文件的實(shí)際編碼是GBK還是UTF8缠俺。以下示例說(shuō)明了這種方法显晶。

1\. GBK編碼源文件內(nèi)容:
    var foo = '中文';
2\. 對(duì)應(yīng)字節(jié):
    76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
3\. 使用單字節(jié)編碼讀取后得到的內(nèi)容:
    var foo = '{亂碼}{亂碼}{亂碼}{亂碼}';
4\. 替換內(nèi)容:
    var bar = '{亂碼}{亂碼}{亂碼}{亂碼}';
5\. 使用單字節(jié)編碼保存后對(duì)應(yīng)字節(jié):
    76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
6\. 使用GBK編碼讀取后得到內(nèi)容:
    var bar = '中文';

這里的訣竅在于,不管大于0xEF的單個(gè)字節(jié)在單字節(jié)編碼下被解析成什么亂碼字符壹士,使用同樣的單字節(jié)編碼保存這些亂碼字符時(shí)磷雇,背后對(duì)應(yīng)的字節(jié)保持不變。

NodeJS中自帶了一種binary編碼可以用來(lái)實(shí)現(xiàn)這個(gè)方法墓卦,因此在下例中倦春,我們使用這種編碼來(lái)演示上例對(duì)應(yīng)的代碼該怎么寫(xiě)。

function replace(pathname) {
    var str = fs.readFileSync(pathname, 'binary');
    str = str.replace('foo', 'bar');
    fs.writeFileSync(pathname, str, 'binary');
}

小結(jié)

本章介紹了使用NodeJS操作文件時(shí)需要的API以及一些技巧落剪,總結(jié)起來(lái)有以下幾點(diǎn):

  • 學(xué)好文件操作睁本,編寫(xiě)各種程序都不怕。

  • 如果不是很在意性能忠怖,fs模塊的同步API能讓生活更加美好呢堰。

  • 需要對(duì)文件讀寫(xiě)做到字節(jié)級(jí)別的精細(xì)控制時(shí),請(qǐng)使用fs模塊的文件底層操作API凡泣。

  • 不要使用拼接字符串的方式來(lái)處理路徑枉疼,使用path模塊。

  • 掌握好目錄遍歷和文件編碼處理技巧鞋拟,很實(shí)用骂维。

網(wǎng)絡(luò)操作

不了解網(wǎng)絡(luò)編程的程序員不是好前端,而NodeJS恰好提供了一扇了解網(wǎng)絡(luò)編程的窗口贺纲。通過(guò)NodeJS航闺,除了可以編寫(xiě)一些服務(wù)端程序來(lái)協(xié)助前端開(kāi)發(fā)和測(cè)試外,還能夠?qū)W習(xí)一些HTTP協(xié)議與Socket協(xié)議的相關(guān)知識(shí)猴誊,這些知識(shí)在優(yōu)化前端性能和排查前端故障時(shí)說(shuō)不定能派上用場(chǎng)潦刃。本章將介紹與之相關(guān)的NodeJS內(nèi)置模塊。

開(kāi)門(mén)紅

NodeJS本來(lái)的用途是編寫(xiě)高性能Web服務(wù)器懈叹。我們首先在這里重復(fù)一下官方文檔里的例子乖杠,使用NodeJS內(nèi)置的http模塊簡(jiǎn)單實(shí)現(xiàn)一個(gè)HTTP服務(wù)器。

var http = require('http');

http.createServer(function (request, response) {
    response.writeHead(200, { 'Content-Type': 'text-plain' });
    response.end('Hello World\n');
}).listen(8124);

以上程序創(chuàng)建了一個(gè)HTTP服務(wù)器并監(jiān)聽(tīng)8124端口澄成,打開(kāi)瀏覽器訪問(wèn)該端口http://127.0.0.1:8124/就能夠看到效果胧洒。

豆知識(shí): 在Linux系統(tǒng)下,監(jiān)聽(tīng)1024以下端口需要root權(quán)限墨状。因此卫漫,如果想監(jiān)聽(tīng)80或443端口的話,需要使用sudo命令啟動(dòng)程序歉胶。

API走馬觀花

我們先大致看看NodeJS提供了哪些和網(wǎng)絡(luò)操作有關(guān)的API汛兜。這里并不逐一介紹每個(gè)API的使用方法,官方文檔已經(jīng)做得很好了通今。

HTTP

**官方文檔: **http://nodejs.org/api/http.html

'http'模塊提供兩種使用方式:

  • 作為服務(wù)端使用時(shí)粥谬,創(chuàng)建一個(gè)HTTP服務(wù)器,監(jiān)聽(tīng)HTTP客戶端請(qǐng)求并返回響應(yīng)辫塌。

  • 作為客戶端使用時(shí)漏策,發(fā)起一個(gè)HTTP客戶端請(qǐng)求,獲取服務(wù)端響應(yīng)臼氨。

首先我們來(lái)看看服務(wù)端模式下如何工作掺喻。如開(kāi)門(mén)紅中的例子所示,首先需要使用.createServer方法創(chuàng)建一個(gè)服務(wù)器,然后調(diào)用.listen方法監(jiān)聽(tīng)端口感耙。之后褂乍,每當(dāng)來(lái)了一個(gè)客戶端請(qǐng)求,創(chuàng)建服務(wù)器時(shí)傳入的回調(diào)函數(shù)就被調(diào)用一次即硼√悠可以看出,這是一種事件機(jī)制只酥。

HTTP請(qǐng)求本質(zhì)上是一個(gè)數(shù)據(jù)流褥实,由請(qǐng)求頭(headers)和請(qǐng)求體(body)組成。例如以下是一個(gè)完整的HTTP請(qǐng)求數(shù)據(jù)內(nèi)容裂允。

POST / HTTP/1.1
User-Agent: curl/7.26.0
Host: localhost
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded

Hello World

可以看到损离,空行之上是請(qǐng)求頭,之下是請(qǐng)求體绝编。HTTP請(qǐng)求在發(fā)送給服務(wù)器時(shí)僻澎,可以認(rèn)為是按照從頭到尾的順序一個(gè)字節(jié)一個(gè)字節(jié)地以數(shù)據(jù)流方式發(fā)送的。而http模塊創(chuàng)建的HTTP服務(wù)器在接收到完整的請(qǐng)求頭后瓮增,就會(huì)調(diào)用回調(diào)函數(shù)怎棱。在回調(diào)函數(shù)中,除了可以使用request對(duì)象訪問(wèn)請(qǐng)求頭數(shù)據(jù)外绷跑,還能把request對(duì)象當(dāng)作一個(gè)只讀數(shù)據(jù)流來(lái)訪問(wèn)請(qǐng)求體數(shù)據(jù)拳恋。以下是一個(gè)例子。

http.createServer(function (request, response) {
    var body = [];

    console.log(request.method);
    console.log(request.headers);

    request.on('data', function (chunk) {
        body.push(chunk);
    });

    request.on('end', function () {
        body = Buffer.concat(body);
        console.log(body.toString());
    });
}).listen(80);

------------------------------------
POST
{ 'user-agent': 'curl/7.26.0',
  host: 'localhost',
  accept: '*/*',
  'content-length': '11',
  'content-type': 'application/x-www-form-urlencoded' }
Hello World

HTTP響應(yīng)本質(zhì)上也是一個(gè)數(shù)據(jù)流砸捏,同樣由響應(yīng)頭(headers)和響應(yīng)體(body)組成谬运。例如以下是一個(gè)完整的HTTP請(qǐng)求數(shù)據(jù)內(nèi)容。

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Tue, 05 Nov 2013 05:31:38 GMT
Connection: keep-alive

Hello World

在回調(diào)函數(shù)中垦藏,除了可以使用response對(duì)象來(lái)寫(xiě)入響應(yīng)頭數(shù)據(jù)外梆暖,還能把response對(duì)象當(dāng)作一個(gè)只寫(xiě)數(shù)據(jù)流來(lái)寫(xiě)入響應(yīng)體數(shù)據(jù)。例如在以下例子中掂骏,服務(wù)端原樣將客戶端請(qǐng)求的請(qǐng)求體數(shù)據(jù)返回給客戶端轰驳。

http.createServer(function (request, response) {
    response.writeHead(200, { 'Content-Type': 'text/plain' });

    request.on('data', function (chunk) {
        response.write(chunk);
    });

    request.on('end', function () {
        response.end();
    });
}).listen(80);

接下來(lái)我們看看客戶端模式下如何工作。為了發(fā)起一個(gè)客戶端HTTP請(qǐng)求弟灼,我們需要指定目標(biāo)服務(wù)器的位置并發(fā)送請(qǐng)求頭和請(qǐng)求體级解,以下示例演示了具體做法。

var options = {
        hostname: 'www.example.com',
        port: 80,
        path: '/upload',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };

var request = http.request(options, function (response) {});

request.write('Hello World');
request.end();

可以看到田绑,.request方法創(chuàng)建了一個(gè)客戶端勤哗,并指定請(qǐng)求目標(biāo)和請(qǐng)求頭數(shù)據(jù)。之后掩驱,就可以把request對(duì)象當(dāng)作一個(gè)只寫(xiě)數(shù)據(jù)流來(lái)寫(xiě)入請(qǐng)求體數(shù)據(jù)和結(jié)束請(qǐng)求芒划。另外冬竟,由于HTTP請(qǐng)求中GET請(qǐng)求是最常見(jiàn)的一種,并且不需要請(qǐng)求體民逼,因此http模塊也提供了以下便捷API泵殴。

http.get('http://www.example.com/', function (response) {});

當(dāng)客戶端發(fā)送請(qǐng)求并接收到完整的服務(wù)端響應(yīng)頭時(shí),就會(huì)調(diào)用回調(diào)函數(shù)缴挖。在回調(diào)函數(shù)中袋狞,除了可以使用response對(duì)象訪問(wèn)響應(yīng)頭數(shù)據(jù)外焚辅,還能把response對(duì)象當(dāng)作一個(gè)只讀數(shù)據(jù)流來(lái)訪問(wèn)響應(yīng)體數(shù)據(jù)映屋。以下是一個(gè)例子。

http.get('http://www.example.com/', function (response) {
    var body = [];

    console.log(response.statusCode);
    console.log(response.headers);

    response.on('data', function (chunk) {
        body.push(chunk);
    });

    response.on('end', function () {
        body = Buffer.concat(body);
        console.log(body.toString());
    });
});

------------------------------------
200
{ 'content-type': 'text/html',
  server: 'Apache',
  'content-length': '801',
  date: 'Tue, 05 Nov 2013 06:08:41 GMT',
  connection: 'keep-alive' }
<!DOCTYPE html>
...

HTTPS

**官方文檔: **http://nodejs.org/api/https.html

https模塊與http模塊極為類(lèi)似同蜻,區(qū)別在于https模塊需要額外處理SSL證書(shū)棚点。

在服務(wù)端模式下,創(chuàng)建一個(gè)HTTPS服務(wù)器的示例如下湾蔓。

var options = {
        key: fs.readFileSync('./ssl/default.key'),
        cert: fs.readFileSync('./ssl/default.cer')
    };

var server = https.createServer(options, function (request, response) {
        // ...
    });

可以看到瘫析,與創(chuàng)建HTTP服務(wù)器相比,多了一個(gè)options對(duì)象默责,通過(guò)keycert字段指定了HTTPS服務(wù)器使用的私鑰和公鑰贬循。

另外,NodeJS支持SNI技術(shù)桃序,可以根據(jù)HTTPS客戶端請(qǐng)求使用的域名動(dòng)態(tài)使用不同的證書(shū)杖虾,因此同一個(gè)HTTPS服務(wù)器可以使用多個(gè)域名提供服務(wù)。接著上例媒熊,可以使用以下方法為HTTPS服務(wù)器添加多組證書(shū)奇适。

server.addContext('foo.com', {
    key: fs.readFileSync('./ssl/foo.com.key'),
    cert: fs.readFileSync('./ssl/foo.com.cer')
});

server.addContext('bar.com', {
    key: fs.readFileSync('./ssl/bar.com.key'),
    cert: fs.readFileSync('./ssl/bar.com.cer')
});

在客戶端模式下,發(fā)起一個(gè)HTTPS客戶端請(qǐng)求與http模塊幾乎相同芦鳍,示例如下嚷往。

var options = {
        hostname: 'www.example.com',
        port: 443,
        path: '/',
        method: 'GET'
    };

var request = https.request(options, function (response) {});

request.end();

但如果目標(biāo)服務(wù)器使用的SSL證書(shū)是自制的,不是從頒發(fā)機(jī)構(gòu)購(gòu)買(mǎi)的柠衅,默認(rèn)情況下https模塊會(huì)拒絕連接皮仁,提示說(shuō)有證書(shū)安全問(wèn)題。在options里加入rejectUnauthorized: false字段可以禁用對(duì)證書(shū)有效性的檢查菲宴,從而允許https模塊請(qǐng)求開(kāi)發(fā)環(huán)境下使用自制證書(shū)的HTTPS服務(wù)器贷祈。

URL

**官方文檔: **http://nodejs.org/api/url.html

處理HTTP請(qǐng)求時(shí)url模塊使用率超高,因?yàn)樵撃K允許解析URL裙顽、生成URL付燥,以及拼接URL。首先我們來(lái)看看一個(gè)完整的URL的各組成部分愈犹。

                           href
 -----------------------------------------------------------------
                            host              path
                      --------------- ----------------------------
 http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash
 -----    ---------   --------   ---- -------- ------------- -----
protocol     auth     hostname   port pathname     search     hash
                                                ------------
                                                   query

我們可以使用.parse方法來(lái)將一個(gè)URL字符串轉(zhuǎn)換為URL對(duì)象键科,示例如下闻丑。

url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'http:',
  auth: 'user:pass',
  host: 'host.com:8080',
  port: '8080',
  hostname: 'host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' }
*/

傳給.parse方法的不一定要是一個(gè)完整的URL,例如在HTTP服務(wù)器回調(diào)函數(shù)中勋颖,request.url不包含協(xié)議頭和域名嗦嗡,但同樣可以用.parse方法解析。

http.createServer(function (request, response) {
    var tmp = request.url; // => "/foo/bar?a=b"
    url.parse(tmp);
    /* =>
    { protocol: null,
      slashes: null,
      auth: null,
      host: null,
      port: null,
      hostname: null,
      hash: null,
      search: '?a=b',
      query: 'a=b',
      pathname: '/foo/bar',
      path: '/foo/bar?a=b',
      href: '/foo/bar?a=b' }
    */
}).listen(80);

.parse方法還支持第二個(gè)和第三個(gè)布爾類(lèi)型可選參數(shù)饭玲。第二個(gè)參數(shù)等于true時(shí)侥祭,該方法返回的URL對(duì)象中,query字段不再是一個(gè)字符串茄厘,而是一個(gè)經(jīng)過(guò)querystring模塊轉(zhuǎn)換后的參數(shù)對(duì)象矮冬。第三個(gè)參數(shù)等于true時(shí),該方法可以正確解析不帶協(xié)議頭的URL次哈,例如//www.example.com/foo/bar胎署。

反過(guò)來(lái),format方法允許將一個(gè)URL對(duì)象轉(zhuǎn)換為URL字符串窑滞,示例如下琼牧。

url.format({
    protocol: 'http:',
    host: 'www.example.com',
    pathname: '/p/a/t/h',
    search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/

另外,.resolve方法可以用于拼接URL哀卫,示例如下巨坊。

url.resolve('http://www.example.com/foo/bar', '../baz');
/* =>
http://www.example.com/baz
*/

Query String

**官方文檔: **http://nodejs.org/api/querystring.html

querystring模塊用于實(shí)現(xiàn)URL參數(shù)字符串與參數(shù)對(duì)象的互相轉(zhuǎn)換,示例如下此改。

querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/

querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
/* =>
'foo=bar&baz=qux&baz=quux&corge='
*/

Zlib

**官方文檔: **http://nodejs.org/api/zlib.html

zlib模塊提供了數(shù)據(jù)壓縮和解壓的功能趾撵。當(dāng)我們處理HTTP請(qǐng)求和響應(yīng)時(shí),可能需要用到這個(gè)模塊带斑。

首先我們看一個(gè)使用zlib模塊壓縮HTTP響應(yīng)體數(shù)據(jù)的例子鼓寺。這個(gè)例子中,判斷了客戶端是否支持gzip勋磕,并在支持的情況下使用zlib模塊返回gzip之后的響應(yīng)體數(shù)據(jù)妈候。

http.createServer(function (request, response) {
    var i = 1024,
        data = '';

    while (i--) {
        data += '.';
    }

    if ((request.headers['accept-encoding'] || '').indexOf('gzip') !== -1) {
        zlib.gzip(data, function (err, data) {
            response.writeHead(200, {
                'Content-Type': 'text/plain',
                'Content-Encoding': 'gzip'
            });
            response.end(data);
        });
    } else {
        response.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        response.end(data);
    }
}).listen(80);

接著我們看一個(gè)使用zlib模塊解壓HTTP響應(yīng)體數(shù)據(jù)的例子。這個(gè)例子中挂滓,判斷了服務(wù)端響應(yīng)是否使用gzip壓縮苦银,并在壓縮的情況下使用zlib模塊解壓響應(yīng)體數(shù)據(jù)。

var options = {
        hostname: 'www.example.com',
        port: 80,
        path: '/',
        method: 'GET',
        headers: {
            'Accept-Encoding': 'gzip, deflate'
        }
    };

http.request(options, function (response) {
    var body = [];

    response.on('data', function (chunk) {
        body.push(chunk);
    });

    response.on('end', function () {
        body = Buffer.concat(body);

        if (response.headers['content-encoding'] === 'gzip') {
            zlib.gunzip(body, function (err, data) {
                console.log(data.toString());
            });
        } else {
            console.log(data.toString());
        }
    });
}).end();

Net

**官方文檔: **http://nodejs.org/api/net.html

net模塊可用于創(chuàng)建Socket服務(wù)器或Socket客戶端赶站。由于Socket在前端領(lǐng)域的使用范圍還不是很廣幔虏,這里先不涉及到WebSocket的介紹,僅僅簡(jiǎn)單演示一下如何從Socket層面來(lái)實(shí)現(xiàn)HTTP請(qǐng)求和響應(yīng)贝椿。

首先我們來(lái)看一個(gè)使用Socket搭建一個(gè)很不嚴(yán)謹(jǐn)?shù)腍TTP服務(wù)器的例子想括。這個(gè)HTTP服務(wù)器不管收到啥請(qǐng)求,都固定返回相同的響應(yīng)烙博。

net.createServer(function (conn) {
    conn.on('data', function (data) {
        conn.write([
            'HTTP/1.1 200 OK',
            'Content-Type: text/plain',
            'Content-Length: 11',
            '',
            'Hello World'
        ].join('\n'));
    });
}).listen(80);

接著我們來(lái)看一個(gè)使用Socket發(fā)起HTTP客戶端請(qǐng)求的例子瑟蜈。這個(gè)例子中烟逊,Socket客戶端在建立連接后發(fā)送了一個(gè)HTTP GET請(qǐng)求,并通過(guò)data事件監(jiān)聽(tīng)函數(shù)來(lái)獲取服務(wù)器響應(yīng)铺根。

var options = {
        port: 80,
        host: 'www.example.com'
    };

var client = net.connect(options, function () {
        client.write([
            'GET / HTTP/1.1',
            'User-Agent: curl/7.26.0',
            'Host: www.baidu.com',
            'Accept: */*',
            '',
            ''
        ].join('\n'));
    });

client.on('data', function (data) {
    console.log(data.toString());
    client.end();
});

靈機(jī)一點(diǎn)

使用NodeJS操作網(wǎng)絡(luò)宪躯,特別是操作HTTP請(qǐng)求和響應(yīng)時(shí)會(huì)遇到一些驚喜,這里對(duì)一些常見(jiàn)問(wèn)題做解答位迂。

  • 問(wèn): 為什么通過(guò)headers對(duì)象訪問(wèn)到的HTTP請(qǐng)求頭或響應(yīng)頭字段不是駝峰的访雪?

    答: 從規(guī)范上講,HTTP請(qǐng)求頭和響應(yīng)頭字段都應(yīng)該是駝峰的掂林。但現(xiàn)實(shí)是殘酷的臣缀,不是每個(gè)HTTP服務(wù)端或客戶端程序都嚴(yán)格遵循規(guī)范,所以NodeJS在處理從別的客戶端或服務(wù)端收到的頭字段時(shí)党饮,都統(tǒng)一地轉(zhuǎn)換為了小寫(xiě)字母格式肝陪,以便開(kāi)發(fā)者能使用統(tǒng)一的方式來(lái)訪問(wèn)頭字段,例如headers['content-length']刑顺。

  • 問(wèn): 為什么http模塊創(chuàng)建的HTTP服務(wù)器返回的響應(yīng)是chunked傳輸方式的?

    答: 因?yàn)槟J(rèn)情況下饲常,使用.writeHead方法寫(xiě)入響應(yīng)頭后蹲堂,允許使用.write方法寫(xiě)入任意長(zhǎng)度的響應(yīng)體數(shù)據(jù),并使用.end方法結(jié)束一個(gè)響應(yīng)贝淤。由于響應(yīng)體數(shù)據(jù)長(zhǎng)度不確定柒竞,因此NodeJS自動(dòng)在響應(yīng)頭里添加了Transfer-Encoding: chunked字段,并采用chunked傳輸方式播聪。但是當(dāng)響應(yīng)體數(shù)據(jù)長(zhǎng)度確定時(shí)朽基,可使用.writeHead方法在響應(yīng)頭里加上Content-Length字段,這樣做之后NodeJS就不會(huì)自動(dòng)添加Transfer-Encoding字段和使用chunked傳輸方式离陶。

  • 問(wèn): 為什么使用http模塊發(fā)起HTTP客戶端請(qǐng)求時(shí)稼虎,有時(shí)候會(huì)發(fā)生socket hang up錯(cuò)誤?

    答: 發(fā)起客戶端HTTP請(qǐng)求前需要先創(chuàng)建一個(gè)客戶端招刨。http模塊提供了一個(gè)全局客戶端http.globalAgent霎俩,可以讓我們使用.request.get方法時(shí)不用手動(dòng)創(chuàng)建客戶端。但是全局客戶端默認(rèn)只允許5個(gè)并發(fā)Socket連接沉眶,當(dāng)某一個(gè)時(shí)刻HTTP客戶端請(qǐng)求創(chuàng)建過(guò)多打却,超過(guò)這個(gè)數(shù)字時(shí),就會(huì)發(fā)生socket hang up錯(cuò)誤谎倔。解決方法也很簡(jiǎn)單柳击,通過(guò)http.globalAgent.maxSockets屬性把這個(gè)數(shù)字改大些即可。另外片习,https模塊遇到這個(gè)問(wèn)題時(shí)也一樣通過(guò)https.globalAgent.maxSockets屬性來(lái)處理捌肴。

小結(jié)

本章介紹了使用NodeJS操作網(wǎng)絡(luò)時(shí)需要的API以及一些坑回避技巧彤守,總結(jié)起來(lái)有以下幾點(diǎn):

  • httphttps模塊支持服務(wù)端模式和客戶端模式兩種使用方式。

  • requestresponse對(duì)象除了用于讀寫(xiě)頭數(shù)據(jù)外哭靖,都可以當(dāng)作數(shù)據(jù)流來(lái)操作具垫。

  • url.parse方法加上request.url屬性是處理HTTP請(qǐng)求時(shí)的固定搭配。

  • 使用zlib模塊可以減少使用HTTP協(xié)議時(shí)的數(shù)據(jù)傳輸量试幽。

  • 通過(guò)net模塊的Socket服務(wù)器與客戶端可對(duì)HTTP協(xié)議做底層操作筝蚕。

  • 小心踩坑。

進(jìn)程管理

NodeJS可以感知和控制自身進(jìn)程的運(yùn)行環(huán)境和狀態(tài)铺坞,也可以創(chuàng)建子進(jìn)程并與其協(xié)同工作起宽,這使得NodeJS可以把多個(gè)程序組合在一起共同完成某項(xiàng)工作,并在其中充當(dāng)膠水和調(diào)度器的作用济榨。本章除了介紹與之相關(guān)的NodeJS內(nèi)置模塊外坯沪,還會(huì)重點(diǎn)介紹典型的使用場(chǎng)景。

開(kāi)門(mén)紅

我們已經(jīng)知道了NodeJS自帶的fs模塊比較基礎(chǔ)擒滑,把一個(gè)目錄里的所有文件和子目錄都拷貝到另一個(gè)目錄里需要寫(xiě)不少代碼腐晾。另外我們也知道,終端下的cp命令比較好用丐一,一條cp -r source/* target命令就能搞定目錄拷貝藻糖。那我們首先看看如何使用NodeJS調(diào)用終端命令來(lái)簡(jiǎn)化目錄拷貝,示例代碼如下:

var child_process = require('child_process');
var util = require('util');

function copy(source, target, callback) {
    child_process.exec(
        util.format('cp -r %s/* %s', source, target), callback);
}

copy('a', 'b', function (err) {
    // ...
});

從以上代碼中可以看到库车,子進(jìn)程是異步運(yùn)行的巨柒,通過(guò)回調(diào)函數(shù)返回執(zhí)行結(jié)果帮辟。

API走馬觀花

我們先大致看看NodeJS提供了哪些和進(jìn)程管理有關(guān)的API糠悯。這里并不逐一介紹每個(gè)API的使用方法,官方文檔已經(jīng)做得很好了锣咒。

Process

**官方文檔: **http://nodejs.org/api/process.html

任何一個(gè)進(jìn)程都有啟動(dòng)進(jìn)程時(shí)使用的命令行參數(shù)珍坊,有標(biāo)準(zhǔn)輸入標(biāo)準(zhǔn)輸出牺勾,有運(yùn)行權(quán)限,有運(yùn)行環(huán)境和運(yùn)行狀態(tài)垫蛆。在NodeJS中禽最,可以通過(guò)process對(duì)象感知和控制NodeJS自身進(jìn)程的方方面面。另外需要注意的是袱饭,process不是內(nèi)置模塊川无,而是一個(gè)全局對(duì)象,因此在任何地方都可以直接使用虑乖。

Child Process

**官方文檔: **http://nodejs.org/api/child_process.html

使用child_process模塊可以創(chuàng)建和控制子進(jìn)程懦趋。該模塊提供的API中最核心的是.spawn,其余API都是針對(duì)特定使用場(chǎng)景對(duì)它的進(jìn)一步封裝疹味,算是一種語(yǔ)法糖仅叫。

Cluster

**官方文檔: **http://nodejs.org/api/cluster.html

cluster模塊是對(duì)child_process模塊的進(jìn)一步封裝帜篇,專(zhuān)用于解決單進(jìn)程N(yùn)odeJS Web服務(wù)器無(wú)法充分利用多核CPU的問(wèn)題。使用該模塊可以簡(jiǎn)化多進(jìn)程服務(wù)器程序的開(kāi)發(fā)诫咱,讓每個(gè)核上運(yùn)行一個(gè)工作進(jìn)程笙隙,并統(tǒng)一通過(guò)主進(jìn)程監(jiān)聽(tīng)端口和分發(fā)請(qǐng)求。

應(yīng)用場(chǎng)景

和進(jìn)程管理相關(guān)的API單獨(dú)介紹起來(lái)比較枯燥坎缭,因此這里從一些典型的應(yīng)用場(chǎng)景出發(fā)竟痰,分別介紹一些重要API的使用方法。

如何獲取命令行參數(shù)

在NodeJS中可以通過(guò)process.argv獲取命令行參數(shù)掏呼。但是比較意外的是坏快,node執(zhí)行程序路徑和主模塊文件路徑固定占據(jù)了argv[0]argv[1]兩個(gè)位置,而第一個(gè)命令行參數(shù)從argv[2]開(kāi)始憎夷。為了讓argv使用起來(lái)更加自然莽鸿,可以按照以下方式處理。

function main(argv) {
    // ...
}

main(process.argv.slice(2));

如何退出程序

通常一個(gè)程序做完所有事情后就正常退出了拾给,這時(shí)程序的退出狀態(tài)碼為0祥得。或者一個(gè)程序運(yùn)行時(shí)發(fā)生了異常后就掛了鸣戴,這時(shí)程序的退出狀態(tài)碼不等于0啃沪。如果我們?cè)诖a中捕獲了某個(gè)異常,但是覺(jué)得程序不應(yīng)該繼續(xù)運(yùn)行下去窄锅,需要立即退出,并且需要把退出狀態(tài)碼設(shè)置為指定數(shù)字缰雇,比如1入偷,就可以按照以下方式:

try {
    // ...
} catch (err) {
    // ...
    process.exit(1);
}

如何控制輸入輸出

NodeJS程序的標(biāo)準(zhǔn)輸入流(stdin)、一個(gè)標(biāo)準(zhǔn)輸出流(stdout)械哟、一個(gè)標(biāo)準(zhǔn)錯(cuò)誤流(stderr)分別對(duì)應(yīng)process.stdin疏之、process.stdoutprocess.stderr,第一個(gè)是只讀數(shù)據(jù)流暇咆,后邊兩個(gè)是只寫(xiě)數(shù)據(jù)流锋爪,對(duì)它們的操作按照對(duì)數(shù)據(jù)流的操作方式即可。例如爸业,console.log可以按照以下方式實(shí)現(xiàn)其骄。

function log() {
    process.stdout.write(
        util.format.apply(util, arguments) + '\n');
}

如何降權(quán)

在Linux系統(tǒng)下,我們知道需要使用root權(quán)限才能監(jiān)聽(tīng)1024以下端口扯旷。但是一旦完成端口監(jiān)聽(tīng)后拯爽,繼續(xù)讓程序運(yùn)行在root權(quán)限下存在安全隱患,因此最好能把權(quán)限降下來(lái)钧忽。以下是這樣一個(gè)例子毯炮。

http.createServer(callback).listen(80, function () {
    var env = process.env,
        uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
        gid = parseInt(env['SUDO_GID'] || process.getgid(), 10);

    process.setgid(gid);
    process.setuid(uid);
});

上例中有幾點(diǎn)需要注意:

  1. 如果是通過(guò)sudo獲取root權(quán)限的逼肯,運(yùn)行程序的用戶的UID和GID保存在環(huán)境變量SUDO_UIDSUDO_GID里邊。如果是通過(guò)chmod +s方式獲取root權(quán)限的桃煎,運(yùn)行程序的用戶的UID和GID可直接通過(guò)process.getuidprocess.getgid方法獲取篮幢。

  2. process.setuidprocess.setgid方法只接受number類(lèi)型的參數(shù)。

  3. 降權(quán)時(shí)必須先降GID再降UID为迈,否則順序反過(guò)來(lái)的話就沒(méi)權(quán)限更改程序的GID了三椿。

如何創(chuàng)建子進(jìn)程

以下是一個(gè)創(chuàng)建NodeJS子進(jìn)程的例子。

var child = child_process.spawn('node', [ 'xxx.js' ]);

child.stdout.on('data', function (data) {
    console.log('stdout: ' + data);
});

child.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});

child.on('close', function (code) {
    console.log('child process exited with code ' + code);
});

上例中使用了.spawn(exec, args, options)方法曲尸,該方法支持三個(gè)參數(shù)赋续。第一個(gè)參數(shù)是執(zhí)行文件路徑,可以是執(zhí)行文件的相對(duì)或絕對(duì)路徑另患,也可以是根據(jù)PATH環(huán)境變量能找到的執(zhí)行文件名纽乱。第二個(gè)參數(shù)中,數(shù)組中的每個(gè)成員都按順序?qū)?yīng)一個(gè)命令行參數(shù)昆箕。第三個(gè)參數(shù)可選鸦列,用于配置子進(jìn)程的執(zhí)行環(huán)境與行為。

另外鹏倘,上例中雖然通過(guò)子進(jìn)程對(duì)象的.stdout.stderr訪問(wèn)子進(jìn)程的輸出薯嗤,但通過(guò)options.stdio字段的不同配置,可以將子進(jìn)程的輸入輸出重定向到任何數(shù)據(jù)流上纤泵,或者讓子進(jìn)程共享父進(jìn)程的標(biāo)準(zhǔn)輸入輸出流骆姐,或者直接忽略子進(jìn)程的輸入輸出。

進(jìn)程間如何通訊

在Linux系統(tǒng)下捏题,進(jìn)程之間可以通過(guò)信號(hào)互相通信玻褪。以下是一個(gè)例子。

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);

child.kill('SIGTERM');

/* child.js */
process.on('SIGTERM', function () {
    cleanUp();
    process.exit(0);
});

在上例中公荧,父進(jìn)程通過(guò).kill方法向子進(jìn)程發(fā)送SIGTERM信號(hào)带射,子進(jìn)程監(jiān)聽(tīng)process對(duì)象的SIGTERM事件響應(yīng)信號(hào)。不要被.kill方法的名稱(chēng)迷惑了循狰,該方法本質(zhì)上是用來(lái)給進(jìn)程發(fā)送信號(hào)的窟社,進(jìn)程收到信號(hào)后具體要做啥,完全取決于信號(hào)的種類(lèi)和進(jìn)程自身的代碼绪钥。

另外灿里,如果父子進(jìn)程都是NodeJS進(jìn)程,就可以通過(guò)IPC(進(jìn)程間通訊)雙向傳遞數(shù)據(jù)昧识。以下是一個(gè)例子钠四。

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ]
    });

child.on('message', function (msg) {
    console.log(msg);
});

child.send({ hello: 'hello' });

/* child.js */
process.on('message', function (msg) {
    msg.hello = msg.hello.toUpperCase();
    process.send(msg);
});

可以看到,父進(jìn)程在創(chuàng)建子進(jìn)程時(shí),在options.stdio字段中通過(guò)ipc開(kāi)啟了一條IPC通道缀去,之后就可以監(jiān)聽(tīng)子進(jìn)程對(duì)象的message事件接收來(lái)自子進(jìn)程的消息侣灶,并通過(guò).send方法給子進(jìn)程發(fā)送消息。在子進(jìn)程這邊缕碎,可以在process對(duì)象上監(jiān)聽(tīng)message事件接收來(lái)自父進(jìn)程的消息褥影,并通過(guò).send方法向父進(jìn)程發(fā)送消息。數(shù)據(jù)在傳遞過(guò)程中咏雌,會(huì)先在發(fā)送端使用JSON.stringify方法序列化凡怎,再在接收端使用JSON.parse方法反序列化。

如何守護(hù)子進(jìn)程

守護(hù)進(jìn)程一般用于監(jiān)控工作進(jìn)程的運(yùn)行狀態(tài)赊抖,在工作進(jìn)程不正常退出時(shí)重啟工作進(jìn)程统倒,保障工作進(jìn)程不間斷運(yùn)行。以下是一種實(shí)現(xiàn)方式氛雪。

/* daemon.js */
function spawn(mainModule) {
    var worker = child_process.spawn('node', [ mainModule ]);

    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(mainModule);
        }
    });
}

spawn('worker.js');

可以看到房匆,工作進(jìn)程非正常退出時(shí),守護(hù)進(jìn)程立即重啟工作進(jìn)程报亩。

小結(jié)

本章介紹了使用NodeJS管理進(jìn)程時(shí)需要的API以及主要的應(yīng)用場(chǎng)景浴鸿,總結(jié)起來(lái)有以下幾點(diǎn):

  • 使用process對(duì)象管理自身。

  • 使用child_process模塊創(chuàng)建和管理子進(jìn)程弦追。

異步編程

NodeJS最大的賣(mài)點(diǎn)——事件機(jī)制和異步IO岳链,對(duì)開(kāi)發(fā)者并不是透明的。開(kāi)發(fā)者需要按異步方式編寫(xiě)代碼才用得上這個(gè)賣(mài)點(diǎn)劲件,而這一點(diǎn)也遭到了一些NodeJS反對(duì)者的抨擊掸哑。但不管怎樣,異步編程確實(shí)是NodeJS最大的特點(diǎn)零远,沒(méi)有掌握異步編程就不能說(shuō)是真正學(xué)會(huì)了NodeJS举户。本章將介紹與異步編程相關(guān)的各種知識(shí)。

回調(diào)

在代碼中遍烦,異步編程的直接體現(xiàn)就是回調(diào)。異步編程依托于回調(diào)來(lái)實(shí)現(xiàn)躺枕,但不能說(shuō)使用了回調(diào)后程序就異步化了服猪。我們首先可以看看以下代碼。

function heavyCompute(n, callback) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }

    callback(count);
}

heavyCompute(10000, function (count) {
    console.log(count);
});

console.log('hello');

-- Console ------------------------------
100000000
hello

可以看到拐云,以上代碼中的回調(diào)函數(shù)仍然先于后續(xù)代碼執(zhí)行罢猪。JS本身是單線程運(yùn)行的,不可能在一段代碼還未結(jié)束運(yùn)行時(shí)去運(yùn)行別的代碼叉瘩,因此也就不存在異步執(zhí)行的概念膳帕。

但是,如果某個(gè)函數(shù)做的事情是創(chuàng)建一個(gè)別的線程或進(jìn)程,并與JS主線程并行地做一些事情危彩,并在事情做完后通知JS主線程攒磨,那情況又不一樣了。我們接著看看以下代碼汤徽。

setTimeout(function () {
    console.log('world');
}, 1000);

console.log('hello');

-- Console ------------------------------
hello
world

這次可以看到娩缰,回調(diào)函數(shù)后于后續(xù)代碼執(zhí)行了。如同上邊所說(shuō)谒府,JS本身是單線程的拼坎,無(wú)法異步執(zhí)行,因此我們可以認(rèn)為setTimeout這類(lèi)JS規(guī)范之外的由運(yùn)行環(huán)境提供的特殊函數(shù)做的事情是創(chuàng)建一個(gè)平行線程后立即返回完疫,讓JS主進(jìn)程可以接著執(zhí)行后續(xù)代碼泰鸡,并在收到平行進(jìn)程的通知后再執(zhí)行回調(diào)函數(shù)。除了setTimeout壳鹤、setInterval這些常見(jiàn)的盛龄,這類(lèi)函數(shù)還包括NodeJS提供的諸如fs.readFile之類(lèi)的異步API。

另外器虾,我們?nèi)匀换氐絁S是單線程運(yùn)行的這個(gè)事實(shí)上讯嫂,這決定了JS在執(zhí)行完一段代碼之前無(wú)法執(zhí)行包括回調(diào)函數(shù)在內(nèi)的別的代碼。也就是說(shuō)兆沙,即使平行線程完成工作了欧芽,通知JS主線程執(zhí)行回調(diào)函數(shù)了,回調(diào)函數(shù)也要等到JS主線程空閑時(shí)才能開(kāi)始執(zhí)行葛圃。以下就是這么一個(gè)例子千扔。

function heavyCompute(n) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }
}

var t = new Date();

setTimeout(function () {
    console.log(new Date() - t);
}, 1000);

heavyCompute(50000);

-- Console ------------------------------
8520

可以看到,本來(lái)應(yīng)該在1秒后被調(diào)用的回調(diào)函數(shù)因?yàn)镴S主線程忙于運(yùn)行其它代碼库正,實(shí)際執(zhí)行時(shí)間被大幅延遲曲楚。

代碼設(shè)計(jì)模式

異步編程有很多特有的代碼設(shè)計(jì)模式,為了實(shí)現(xiàn)同樣的功能褥符,使用同步方式和異步方式編寫(xiě)的代碼會(huì)有很大差異龙誊。以下分別介紹一些常見(jiàn)的模式。

函數(shù)返回值

使用一個(gè)函數(shù)的輸出作為另一個(gè)函數(shù)的輸入是很常見(jiàn)的需求喷楣,在同步方式下一般按以下方式編寫(xiě)代碼:

var output = fn1(fn2('input'));
// Do something.

而在異步方式下趟大,由于函數(shù)執(zhí)行結(jié)果不是通過(guò)返回值,而是通過(guò)回調(diào)函數(shù)傳遞铣焊,因此一般按以下方式編寫(xiě)代碼:

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});

可以看到逊朽,這種方式就是一個(gè)回調(diào)函數(shù)套一個(gè)回調(diào)函多,套得太多了很容易寫(xiě)出>形狀的代碼曲伊。

遍歷數(shù)組

在遍歷數(shù)組時(shí)叽讳,使用某個(gè)函數(shù)依次對(duì)數(shù)據(jù)成員做一些處理也是常見(jiàn)的需求。如果函數(shù)是同步執(zhí)行的,一般就會(huì)寫(xiě)出以下代碼:

var len = arr.length,
    i = 0;

for (; i < len; ++i) {
    arr[i] = sync(arr[i]);
}

// All array items have processed.

如果函數(shù)是異步執(zhí)行的岛蚤,以上代碼就無(wú)法保證循環(huán)結(jié)束后所有數(shù)組成員都處理完畢了邑狸。如果數(shù)組成員必須一個(gè)接一個(gè)串行處理,則一般按照以下方式編寫(xiě)異步代碼:

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function () {
    // All array items have processed.
}));

可以看到灭美,以上代碼在異步函數(shù)執(zhí)行一次并返回執(zhí)行結(jié)果后才傳入下一個(gè)數(shù)組成員并開(kāi)始下一輪執(zhí)行推溃,直到所有數(shù)組成員處理完畢后,通過(guò)回調(diào)的方式觸發(fā)后續(xù)代碼的執(zhí)行届腐。

如果數(shù)組成員可以并行處理铁坎,但后續(xù)代碼仍然需要所有數(shù)組成員處理完畢后才能執(zhí)行的話,則異步代碼會(huì)調(diào)整成以下形式:

(function (i, len, count, callback) {
    for (; i < len; ++i) {
        (function (i) {
            async(arr[i], function (value) {
                arr[i] = value;
                if (++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function () {
    // All array items have processed.
}));

可以看到犁苏,與異步串行遍歷的版本相比硬萍,以上代碼并行處理所有數(shù)組成員,并通過(guò)計(jì)數(shù)器變量來(lái)判斷什么時(shí)候所有數(shù)組成員都處理完畢了围详。

異常處理

JS自身提供的異常捕獲和處理機(jī)制——try..catch..朴乖,只能用于同步執(zhí)行的代碼。以下是一個(gè)例子助赞。

function sync(fn) {
    return fn();
}

try {
    sync(null);
    // Do something.
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
Error: object is not a function

可以看到买羞,異常會(huì)沿著代碼執(zhí)行路徑一直冒泡,直到遇到第一個(gè)try語(yǔ)句時(shí)被捕獲住雹食。但由于異步函數(shù)會(huì)打斷代碼執(zhí)行路徑畜普,異步函數(shù)執(zhí)行過(guò)程中以及執(zhí)行之后產(chǎn)生的異常冒泡到執(zhí)行路徑被打斷的位置時(shí),如果一直沒(méi)有遇到try語(yǔ)句群叶,就作為一個(gè)全局異常拋出吃挑。以下是一個(gè)例子。

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        callback(fn());
    }, 0);
}

try {
    async(null, function (data) {
        // Do something.
    });
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
/home/user/test.js:4
        callback(fn());
                 ^
TypeError: object is not a function
    at null._onTimeout (/home/user/test.js:4:13)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因?yàn)榇a執(zhí)行路徑被打斷了街立,我們就需要在異常冒泡到斷點(diǎn)之前用try語(yǔ)句把異常捕獲住舶衬,并通過(guò)回調(diào)函數(shù)傳遞被捕獲的異常。于是我們可以像下邊這樣改造上邊的例子赎离。

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        try {
            callback(null, fn());
        } catch (err) {
            callback(err);
        }
    }, 0);
}

async(null, function (err, data) {
    if (err) {
        console.log('Error: %s', err.message);
    } else {
        // Do something.
    }
});

-- Console ------------------------------
Error: object is not a function

可以看到逛犹,異常再次被捕獲住了。在NodeJS中梁剔,幾乎所有異步API都按照以上方式設(shè)計(jì)圾浅,回調(diào)函數(shù)中第一個(gè)參數(shù)都是err。因此我們?cè)诰帉?xiě)自己的異步函數(shù)時(shí)憾朴,也可以按照這種方式來(lái)處理異常,與NodeJS的設(shè)計(jì)風(fēng)格保持一致喷鸽。

有了異常處理方式后众雷,我們接著可以想一想一般我們是怎么寫(xiě)代碼的。基本上砾省,我們的代碼都是做一些事情鸡岗,然后調(diào)用一個(gè)函數(shù),然后再做一些事情编兄,然后再調(diào)用一個(gè)函數(shù)轩性,如此循環(huán)。如果我們寫(xiě)的是同步代碼狠鸳,只需要在代碼入口點(diǎn)寫(xiě)一個(gè)try語(yǔ)句就能捕獲所有冒泡上來(lái)的異常揣苏,示例如下。

function main() {
    // Do something.
    syncA();
    // Do something.
    syncB();
    // Do something.
    syncC();
}

try {
    main();
} catch (err) {
    // Deal with exception.
}

但是件舵,如果我們寫(xiě)的是異步代碼卸察,就只有呵呵了。由于每次異步函數(shù)調(diào)用都會(huì)打斷代碼執(zhí)行路徑铅祸,只能通過(guò)回調(diào)函數(shù)來(lái)傳遞異常坑质,于是我們就需要在每個(gè)回調(diào)函數(shù)里判斷是否有異常發(fā)生,于是只用三次異步函數(shù)調(diào)用临梗,就會(huì)產(chǎn)生下邊這種代碼涡扼。

function main(callback) {
    // Do something.
    asyncA(function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null);
                        }
                    });
                }
            });
        }
    });
}

main(function (err) {
    if (err) {
        // Deal with exception.
    }
});

可以看到,回調(diào)函數(shù)已經(jīng)讓代碼變得復(fù)雜了盟庞,而異步方式下對(duì)異常的處理更加劇了代碼的復(fù)雜度吃沪。如果NodeJS的最大賣(mài)點(diǎn)最后變成這個(gè)樣子,那就沒(méi)人愿意用NodeJS了茫经,因此接下來(lái)會(huì)介紹NodeJS提供的一些解決方案巷波。

域(Domain)

**官方文檔: **http://nodejs.org/api/domain.html

NodeJS提供了domain模塊,可以簡(jiǎn)化異步代碼的異常處理卸伞。在介紹該模塊之前抹镊,我們需要首先理解“域”的概念。簡(jiǎn)單的講荤傲,一個(gè)域就是一個(gè)JS運(yùn)行環(huán)境垮耳,在一個(gè)運(yùn)行環(huán)境中,如果一個(gè)異常沒(méi)有被捕獲遂黍,將作為一個(gè)全局異常被拋出终佛。NodeJS通過(guò)process對(duì)象提供了捕獲全局異常的方法,示例代碼如下

process.on('uncaughtException', function (err) {
    console.log('Error: %s', err.message);
});

setTimeout(function (fn) {
    fn();
});

-- Console ------------------------------
Error: undefined is not a function

雖然全局異常有個(gè)地方可以捕獲了雾家,但是對(duì)于大多數(shù)異常铃彰,我們希望盡早捕獲,并根據(jù)結(jié)果決定代碼的執(zhí)行路徑芯咧。我們用以下HTTP服務(wù)器代碼作為例子:

function async(request, callback) {
    // Do something.
    asyncA(request, function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(request, function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(request, function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null, data);
                        }
                    });
                }
            });
        }
    });
}

http.createServer(function (request, response) {
    async(request, function (err, data) {
        if (err) {
            response.writeHead(500);
            response.end();
        } else {
            response.writeHead(200);
            response.end(data);
        }
    });
});

以上代碼將請(qǐng)求對(duì)象交給異步函數(shù)處理后牙捉,再根據(jù)處理結(jié)果返回響應(yīng)竹揍。這里采用了使用回調(diào)函數(shù)傳遞異常的方案,因此async函數(shù)內(nèi)部如果再多幾個(gè)異步函數(shù)調(diào)用的話邪铲,代碼就變成上邊這副鬼樣子了芬位。為了讓代碼好看點(diǎn),我們可以在每處理一個(gè)請(qǐng)求時(shí)带到,使用domain模塊創(chuàng)建一個(gè)子域(JS子運(yùn)行環(huán)境)昧碉。在子域內(nèi)運(yùn)行的代碼可以隨意拋出異常,而這些異忱咳牵可以通過(guò)子域?qū)ο蟮?code>error事件統(tǒng)一捕獲被饿。于是以上代碼可以做如下改造:

function async(request, callback) {
    // Do something.
    asyncA(request, function (data) {
        // Do something
        asyncB(request, function (data) {
            // Do something
            asyncC(request, function (data) {
                // Do something
                callback(data);
            });
        });
    });
}

http.createServer(function (request, response) {
    var d = domain.create();

    d.on('error', function () {
        response.writeHead(500);
        response.end();
    });

    d.run(function () {
        async(request, function (data) {
            response.writeHead(200);
            response.end(data);
        });
    });
});

可以看到,我們使用.create方法創(chuàng)建了一個(gè)子域?qū)ο笥浪浚⑼ㄟ^(guò).run方法進(jìn)入需要在子域中運(yùn)行的代碼的入口點(diǎn)锹漱。而位于子域中的異步函數(shù)回調(diào)函數(shù)由于不再需要捕獲異常,代碼一下子瘦身很多慕嚷。

陷阱

無(wú)論是通過(guò)process對(duì)象的uncaughtException事件捕獲到全局異常哥牍,還是通過(guò)子域?qū)ο蟮?code>error事件捕獲到了子域異常,在NodeJS官方文檔里都強(qiáng)烈建議處理完異常后立即重啟程序喝检,而不是讓程序繼續(xù)運(yùn)行嗅辣。按照官方文檔的說(shuō)法,發(fā)生異常后的程序處于一個(gè)不確定的運(yùn)行狀態(tài)挠说,如果不立即退出的話澡谭,程序可能會(huì)發(fā)生嚴(yán)重內(nèi)存泄漏,也可能表現(xiàn)得很奇怪损俭。

但這里需要澄清一些事實(shí)蛙奖。JS本身的throw..try..catch異常處理機(jī)制并不會(huì)導(dǎo)致內(nèi)存泄漏,也不會(huì)讓程序的執(zhí)行結(jié)果出乎意料杆兵,但NodeJS并不是存粹的JS雁仲。NodeJS里大量的API內(nèi)部是用C/C++實(shí)現(xiàn)的,因此NodeJS程序的運(yùn)行過(guò)程中琐脏,代碼執(zhí)行路徑穿梭于JS引擎內(nèi)部和外部攒砖,而JS的異常拋出機(jī)制可能會(huì)打斷正常的代碼執(zhí)行流程,導(dǎo)致C/C++部分的代碼表現(xiàn)異常日裙,進(jìn)而導(dǎo)致內(nèi)存泄漏等問(wèn)題吹艇。

因此,使用uncaughtExceptiondomain捕獲異常昂拂,代碼執(zhí)行路徑里涉及到了C/C++部分的代碼時(shí)受神,如果不能確定是否會(huì)導(dǎo)致內(nèi)存泄漏等問(wèn)題,最好在處理完異常后重啟程序比較妥當(dāng)格侯。而使用try語(yǔ)句捕獲異常時(shí)一般捕獲到的都是JS本身的異常路克,不用擔(dān)心上訴問(wèn)題樟结。

小結(jié)

本章介紹了JS異步編程相關(guān)的知識(shí),總結(jié)起來(lái)有以下幾點(diǎn):

  • 不掌握異步編程就不算學(xué)會(huì)NodeJS精算。

  • 異步編程依托于回調(diào)來(lái)實(shí)現(xiàn),而使用回調(diào)不一定就是異步編程碎连。

  • 異步編程下的函數(shù)間數(shù)據(jù)傳遞灰羽、數(shù)組遍歷和異常處理與同步編程有很大差別。

  • 使用domain模塊簡(jiǎn)化異步代碼的異常處理鱼辙,并小心陷阱廉嚼。

大示例

學(xué)習(xí)講究的是學(xué)以致用和融會(huì)貫通。至此我們已經(jīng)分別介紹了NodeJS的很多知識(shí)點(diǎn)倒戏,本章作為最后一章怠噪,將完整地介紹一個(gè)使用NodeJS開(kāi)發(fā)Web服務(wù)器的示例泛范。

需求

我們要開(kāi)發(fā)的是一個(gè)簡(jiǎn)單的靜態(tài)文件合并服務(wù)器苗缩,該服務(wù)器需要支持類(lèi)似以下格式的JS或CSS文件合并請(qǐng)求姿搜。

http://assets.example.com/foo/??bar.js,baz.js

在以上URL中捉蚤,??是一個(gè)分隔符漱受,之前是需要合并的多個(gè)文件的URL的公共部分赎败,之后是使用,分隔的差異部分姚建。因此服務(wù)器處理這個(gè)URL時(shí)止状,返回的是以下兩個(gè)文件按順序合并后的內(nèi)容淑趾。

/foo/bar.js
/foo/baz.js

另外阳仔,服務(wù)器也需要能支持類(lèi)似以下格式的普通的JS或CSS文件請(qǐng)求。

http://assets.example.com/foo/bar.js

以上就是整個(gè)需求扣泊。

第一次迭代

快速迭代是一種不錯(cuò)的開(kāi)發(fā)方式近范,因此我們?cè)诘谝淮蔚鷷r(shí)先實(shí)現(xiàn)服務(wù)器的基本功能。

設(shè)計(jì)

簡(jiǎn)單分析了需求之后延蟹,我們大致會(huì)得到以下的設(shè)計(jì)方案评矩。

           +---------+   +-----------+   +----------+
request -->|  parse  |-->|  combine  |-->|  output  |--> response
           +---------+   +-----------+   +----------+

也就是說(shuō),服務(wù)器會(huì)首先分析URL等孵,得到請(qǐng)求的文件的路徑和類(lèi)型(MIME)稚照。然后,服務(wù)器會(huì)讀取請(qǐng)求的文件俯萌,并按順序合并文件內(nèi)容果录。最后,服務(wù)器返回響應(yīng)咐熙,完成對(duì)一次請(qǐng)求的處理弱恒。

另外,服務(wù)器在讀取文件時(shí)需要有個(gè)根目錄棋恼,并且服務(wù)器監(jiān)聽(tīng)的HTTP端口最好也不要寫(xiě)死在代碼里返弹,因此服務(wù)器需要是可配置的锈玉。

實(shí)現(xiàn)

根據(jù)以上設(shè)計(jì),我們寫(xiě)出了第一版代碼如下义起。

var fs = require('fs'),
    path = require('path'),
    http = require('http');

var MIME = {
    '.css': 'text/css',
    '.js': 'application/javascript'
};

function combineFiles(pathnames, callback) {
    var output = [];

    (function next(i, len) {
        if (i < len) {
            fs.readFile(pathnames[i], function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    output.push(data);
                    next(i + 1, len);
                }
            });
        } else {
            callback(null, Buffer.concat(output));
        }
    }(0, pathnames.length));
}

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80;

    http.createServer(function (request, response) {
        var urlInfo = parseURL(root, request.url);

        combineFiles(urlInfo.pathnames, function (err, data) {
            if (err) {
                response.writeHead(404);
                response.end(err.message);
            } else {
                response.writeHead(200, {
                    'Content-Type': urlInfo.mime
                });
                response.end(data);
            }
        });
    }).listen(port);
}

function parseURL(root, url) {
    var base, pathnames, parts;

    if (url.indexOf('??') === -1) {
        url = url.replace('/', '/??');
    }

    parts = url.split('??');
    base = parts[0];
    pathnames = parts[1].split(',').map(function (value) {
        return path.join(root, base, value);
    });

    return {
        mime: MIME[path.extname(pathnames[0])] || 'text/plain',
        pathnames: pathnames
    };
}

main(process.argv.slice(2));

以上代碼完整實(shí)現(xiàn)了服務(wù)器所需的功能拉背,并且有以下幾點(diǎn)值得注意:

  1. 使用命令行參數(shù)傳遞JSON配置文件路徑,入口函數(shù)負(fù)責(zé)讀取配置并創(chuàng)建服務(wù)器默终。

  2. 入口函數(shù)完整描述了程序的運(yùn)行邏輯椅棺,其中解析URL和合并文件的具體實(shí)現(xiàn)封裝在其它兩個(gè)函數(shù)里。

  3. 解析URL時(shí)先將普通URL轉(zhuǎn)換為了文件合并URL齐蔽,使得兩種URL的處理方式可以一致两疚。

  4. 合并文件時(shí)使用異步API讀取文件,避免服務(wù)器因等待磁盤(pán)IO而發(fā)生阻塞含滴。

我們可以把以上代碼保存為server.js诱渤,之后就可以通過(guò)node server.js config.json命令啟動(dòng)程序,于是我們的第一版靜態(tài)文件合并服務(wù)器就順利完工了谈况。

另外勺美,以上代碼存在一個(gè)不那么明顯的邏輯缺陷。例如鸦做,使用以下URL請(qǐng)求服務(wù)器時(shí)會(huì)有驚喜励烦。

    http://assets.example.com/foo/bar.js,foo/baz.js

經(jīng)過(guò)分析之后我們會(huì)發(fā)現(xiàn)問(wèn)題出在/被自動(dòng)替換/??這個(gè)行為上,而這個(gè)問(wèn)題我們可以到第二次迭代時(shí)再解決泼诱。

第二次迭代

在第一次迭代之后坛掠,我們已經(jīng)有了一個(gè)可工作的版本,滿足了功能需求治筒。接下來(lái)我們需要從性能的角度出發(fā)屉栓,看看代碼還有哪些改進(jìn)余地。

設(shè)計(jì)

map方法換成for循環(huán)或許會(huì)更快一些耸袜,但第一版代碼最大的性能問(wèn)題存在于從讀取文件到輸出響應(yīng)的過(guò)程當(dāng)中友多。我們以處理/??a.js,b.js,c.js這個(gè)請(qǐng)求為例,看看整個(gè)處理過(guò)程中耗時(shí)在哪兒堤框。

 發(fā)送請(qǐng)求       等待服務(wù)端響應(yīng)         接收響應(yīng)
---------+----------------------+------------->
         --                                        解析請(qǐng)求
           ------                                  讀取a.js
                 ------                            讀取b.js
                       ------                      讀取c.js
                             --                    合并數(shù)據(jù)
                               --                  輸出響應(yīng)

可以看到域滥,第一版代碼依次把請(qǐng)求的文件讀取到內(nèi)存中之后,再合并數(shù)據(jù)和輸出響應(yīng)蜈抓。這會(huì)導(dǎo)致以下兩個(gè)問(wèn)題:

  1. 當(dāng)請(qǐng)求的文件比較多比較大時(shí)启绰,串行讀取文件會(huì)比較耗時(shí),從而拉長(zhǎng)了服務(wù)端響應(yīng)等待時(shí)間沟使。

  2. 由于每次響應(yīng)輸出的數(shù)據(jù)都需要先完整地緩存在內(nèi)存里委可,當(dāng)服務(wù)器請(qǐng)求并發(fā)數(shù)較大時(shí),會(huì)有較大的內(nèi)存開(kāi)銷(xiāo)腊嗡。

對(duì)于第一個(gè)問(wèn)題着倾,很容易想到把讀取文件的方式從串行改為并行拾酝。但是別這樣做,因?yàn)閷?duì)于機(jī)械磁盤(pán)而言卡者,因?yàn)橹挥幸粋€(gè)磁頭蒿囤,嘗試并行讀取文件只會(huì)造成磁頭頻繁抖動(dòng),反而降低IO效率崇决。而對(duì)于固態(tài)硬盤(pán)蟋软,雖然的確存在多個(gè)并行IO通道,但是對(duì)于服務(wù)器并行處理的多個(gè)請(qǐng)求而言嗽桩,硬盤(pán)已經(jīng)在做并行IO了,對(duì)單個(gè)請(qǐng)求采用并行IO無(wú)異于拆東墻補(bǔ)西墻凄敢。因此碌冶,正確的做法不是改用并行IO,而是一邊讀取文件一邊輸出響應(yīng)涝缝,把響應(yīng)輸出時(shí)機(jī)提前至讀取第一個(gè)文件的時(shí)刻扑庞。這樣調(diào)整后,整個(gè)請(qǐng)求處理過(guò)程變成下邊這樣拒逮。

發(fā)送請(qǐng)求 等待服務(wù)端響應(yīng) 接收響應(yīng)
---------+----+------------------------------->
         --                                        解析請(qǐng)求
           --                                      檢查文件是否存在
             --                                    輸出響應(yīng)頭
               ------                              讀取和輸出a.js
                     ------                        讀取和輸出b.js
                           ------                  讀取和輸出c.js

按上述方式解決第一個(gè)問(wèn)題后罐氨,因?yàn)榉?wù)器不需要完整地緩存每個(gè)請(qǐng)求的輸出數(shù)據(jù)了,第二個(gè)問(wèn)題也迎刃而解滩援。

實(shí)現(xiàn)

根據(jù)以上設(shè)計(jì)栅隐,第二版代碼按以下方式調(diào)整了部分函數(shù)。

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80;

    http.createServer(function (request, response) {
        var urlInfo = parseURL(root, request.url);

        validateFiles(urlInfo.pathnames, function (err, pathnames) {
            if (err) {
                response.writeHead(404);
                response.end(err.message);
            } else {
                response.writeHead(200, {
                    'Content-Type': urlInfo.mime
                });
                outputFiles(pathnames, response);
            }
        });
    }).listen(port);
}

function outputFiles(pathnames, writer) {
    (function next(i, len) {
        if (i < len) {
            var reader = fs.createReadStream(pathnames[i]);

            reader.pipe(writer, { end: false });
            reader.on('end', function() {
                next(i + 1, len);
            });
        } else {
            writer.end();
        }
    }(0, pathnames.length));
}

function validateFiles(pathnames, callback) {
    (function next(i, len) {
        if (i < len) {
            fs.stat(pathnames[i], function (err, stats) {
                if (err) {
                    callback(err);
                } else if (!stats.isFile()) {
                    callback(new Error());
                } else {
                    next(i + 1, len);
                }
            });
        } else {
            callback(null, pathnames);
        }
    }(0, pathnames.length));
}

可以看到玩徊,第二版代碼在檢查了請(qǐng)求的所有文件是否有效之后租悄,立即就輸出了響應(yīng)頭,并接著一邊按順序讀取文件一邊輸出響應(yīng)內(nèi)容恩袱。并且泣棋,在讀取文件時(shí),第二版代碼直接使用了只讀數(shù)據(jù)流來(lái)簡(jiǎn)化代碼畔塔。

第三次迭代

第二次迭代之后潭辈,服務(wù)器本身的功能和性能已經(jīng)得到了初步滿足。接下來(lái)我們需要從穩(wěn)定性的角度重新審視一下代碼澈吨,看看還需要做些什么把敢。

設(shè)計(jì)

從工程角度上講,沒(méi)有絕對(duì)可靠的系統(tǒng)棚辽。即使第二次迭代的代碼經(jīng)過(guò)反復(fù)檢查后能確保沒(méi)有bug技竟,也很難說(shuō)是否會(huì)因?yàn)镹odeJS本身,或者是操作系統(tǒng)本身屈藐,甚至是硬件本身導(dǎo)致我們的服務(wù)器程序在某一天掛掉榔组。因此一般生產(chǎn)環(huán)境下的服務(wù)器程序都配有一個(gè)守護(hù)進(jìn)程熙尉,在服務(wù)掛掉的時(shí)候立即重啟服務(wù)。一般守護(hù)進(jìn)程的代碼會(huì)遠(yuǎn)比服務(wù)進(jìn)程的代碼簡(jiǎn)單搓扯,從概率上可以保證守護(hù)進(jìn)程更難掛掉检痰。如果再做得嚴(yán)謹(jǐn)一些,甚至守護(hù)進(jìn)程自身可以在自己掛掉時(shí)重啟自己锨推,從而實(shí)現(xiàn)雙保險(xiǎn)铅歼。

因此在本次迭代時(shí),我們先利用NodeJS的進(jìn)程管理機(jī)制换可,將守護(hù)進(jìn)程作為父進(jìn)程椎椰,將服務(wù)器程序作為子進(jìn)程,并讓父進(jìn)程監(jiān)控子進(jìn)程的運(yùn)行狀態(tài)沾鳄,在其異常退出時(shí)重啟子進(jìn)程慨飘。

實(shí)現(xiàn)

根據(jù)以上設(shè)計(jì),我們編寫(xiě)了守護(hù)進(jìn)程需要的代碼译荞。

var cp = require('child_process');

var worker;

function spawn(server, config) {
    worker = cp.spawn('node', [ server, config ]);
    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(server, config);
        }
    });
}

function main(argv) {
    spawn('server.js', argv[0]);
    process.on('SIGTERM', function () {
        worker.kill();
        process.exit(0);
    });
}

main(process.argv.slice(2));

此外瓤的,服務(wù)器代碼本身的入口函數(shù)也要做以下調(diào)整。

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80,
        server;

    server = http.createServer(function (request, response) {
        ...
    }).listen(port);

    process.on('SIGTERM', function () {
        server.close(function () {
            process.exit(0);
        });
    });
}

我們可以把守護(hù)進(jìn)程的代碼保存為daemon.js吞歼,之后我們可以通過(guò)node daemon.js config.json啟動(dòng)服務(wù)圈膏,而守護(hù)進(jìn)程會(huì)進(jìn)一步啟動(dòng)和監(jiān)控服務(wù)器進(jìn)程。此外篙骡,為了能夠正常終止服務(wù)稽坤,我們讓守護(hù)進(jìn)程在接收到SIGTERM信號(hào)時(shí)終止服務(wù)器進(jìn)程。而在服務(wù)器進(jìn)程這一端医增,同樣在收到SIGTERM信號(hào)時(shí)先停掉HTTP服務(wù)再正常退出慎皱。至此,我們的服務(wù)器程序就靠譜很多了叶骨。

第四次迭代

在我們解決了服務(wù)器本身的功能茫多、性能和可靠性的問(wèn)題后,接著我們需要考慮一下代碼部署的問(wèn)題忽刽,以及服務(wù)器控制的問(wèn)題天揖。

設(shè)計(jì)

一般而言,程序在服務(wù)器上有一個(gè)固定的部署目錄跪帝,每次程序有更新后今膊,都重新發(fā)布到部署目錄里。而一旦完成部署后伞剑,一般也可以通過(guò)固定的服務(wù)控制腳本啟動(dòng)和停止服務(wù)斑唬。因此我們的服務(wù)器程序部署目錄可以做如下設(shè)計(jì)。

- deploy/
    - bin/
        startws.sh
        killws.sh
    + conf/
        config.json
    + lib/
        daemon.js
        server.js

在以上目錄結(jié)構(gòu)中,我們分類(lèi)存放了服務(wù)控制腳本恕刘、配置文件和服務(wù)器代碼缤谎。

實(shí)現(xiàn)

按以上目錄結(jié)構(gòu)分別存放對(duì)應(yīng)的文件之后,接下來(lái)我們看看控制腳本怎么寫(xiě)褐着。首先是start.sh坷澡。

#!/bin/sh
if [ ! -f "pid" ]
then
    node ../lib/daemon.js ../conf/config.json &
    echo $! > pid
fi

然后是killws.sh

#!/bin/sh
if [ -f "pid" ]
then
    kill $(tr -d '\r\n' < pid)
    rm pid
fi

于是這樣我們就有了一個(gè)簡(jiǎn)單的代碼部署目錄和服務(wù)控制腳本含蓉,我們的服務(wù)器程序就可以上線工作了频敛。

后續(xù)迭代

我們的服務(wù)器程序正式上線工作后,我們接下來(lái)或許會(huì)發(fā)現(xiàn)還有很多可以改進(jìn)的點(diǎn)馅扣。比如服務(wù)器程序在合并JS文件時(shí)可以自動(dòng)在JS文件之間插入一個(gè);來(lái)避免一些語(yǔ)法問(wèn)題斟赚,比如服務(wù)器程序需要提供日志來(lái)統(tǒng)計(jì)訪問(wèn)量,比如服務(wù)器程序需要能充分利用多核CPU差油,等等汁展。而此時(shí)的你,在學(xué)習(xí)了這么久NodeJS之后厌殉,應(yīng)該已經(jīng)知道該怎么做了。

小結(jié)

本章將之前零散介紹的知識(shí)點(diǎn)串了起來(lái)侈咕,完整地演示了一個(gè)使用NodeJS開(kāi)發(fā)程序的例子公罕,至此我們的課程就全部結(jié)束了。以下是對(duì)新誕生的NodeJSer的一些建議耀销。

  • 要熟悉官方API文檔楼眷。并不是說(shuō)要熟悉到能記住每個(gè)API的名稱(chēng)和用法,而是要熟悉NodeJS提供了哪些功能熊尉,一旦需要時(shí)知道查詢API文檔的哪塊地方罐柳。

  • 要先設(shè)計(jì)再實(shí)現(xiàn)。在開(kāi)發(fā)一個(gè)程序前首先要有一個(gè)全局的設(shè)計(jì)狰住,不一定要很周全张吉,但要足夠能寫(xiě)出一些代碼。

  • 要實(shí)現(xiàn)后再設(shè)計(jì)催植。在寫(xiě)了一些代碼肮蛹,有了一些具體的東西后,一定會(huì)發(fā)現(xiàn)一些之前忽略掉的細(xì)節(jié)创南。這時(shí)再反過(guò)來(lái)改進(jìn)之前的設(shè)計(jì)伦忠,為第二輪迭代做準(zhǔn)備。

  • 要充分利用三方包稿辙。NodeJS有一個(gè)龐大的生態(tài)圈昆码,在寫(xiě)代碼之前先看看有沒(méi)有現(xiàn)成的三方包能節(jié)省不少時(shí)間。

  • 不要迷信三方包。任何事情做過(guò)頭了就不好了赋咽,三方包也是一樣旧噪。三方包是一個(gè)黑盒,每多使用一個(gè)三方包冬耿,就為程序增加了一份潛在風(fēng)險(xiǎn)舌菜。并且三方包很難恰好只提供程序需要的功能,每多使用一個(gè)三方包亦镶,就讓程序更加臃腫一些日月。因此在決定使用某個(gè)三方包之前,最好三思而后行缤骨。


    image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末爱咬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绊起,更是在濱河造成了極大的恐慌精拟,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虱歪,死亡現(xiàn)場(chǎng)離奇詭異蜂绎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)笋鄙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)师枣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人萧落,你說(shuō)我怎么就攤上這事践美。” “怎么了找岖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵陨倡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我许布,道長(zhǎng)兴革,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任蜜唾,我火速辦了婚禮帖旨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灵妨。我一直安慰自己解阅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布泌霍。 她就那樣靜靜地躺著货抄,像睡著了一般述召。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蟹地,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天积暖,我揣著相機(jī)與錄音,去河邊找鬼怪与。 笑死夺刑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的分别。 我是一名探鬼主播遍愿,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼耘斩!你這毒婦竟也來(lái)了沼填?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤括授,失蹤者是張志新(化名)和其女友劉穎坞笙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體荚虚,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薛夜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了版述。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片却邓。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖院水,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情简十,我是刑警寧澤檬某,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站螟蝙,受9級(jí)特大地震影響恢恼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胰默,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一场斑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧牵署,春花似錦漏隐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春脖隶,著一層夾襖步出監(jiān)牢的瞬間扁耐,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工产阱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婉称,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓构蹬,卻偏偏與公主長(zhǎng)得像王暗,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子怎燥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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