參考連接:http://nqdeng.github.io/7-days-nodejs/#1.1 NodeJS基礎 什麼是NodeJS JS是腳本語言,腳本語言都需要一個解析器才能運行。對於寫在HTML頁面里的JS,瀏覽器充當瞭解析器的角色。而對於需要獨立運行的JS,NodeJS就是一個解析器。 每一 ...
參考連接:http://nqdeng.github.io/7-days-nodejs/#1.1
NodeJS基礎
什麼是NodeJS
JS是腳本語言,腳本語言都需要一個解析器才能運行。對於寫在HTML頁面里的JS,瀏覽器充當瞭解析器的角色。而對於需要獨立運行的JS,NodeJS就是一個解析器。
每一種解析器都是一個運行環境,不但允許JS定義各種數據結構,進行各種計算,還允許JS使用運行環境提供的內置對象和方法做一些事情。例如運行在瀏覽器中的JS的用途是操作DOM,瀏覽器就提供了
document
之類的內置對象。而運行在NodeJS中的JS的用途是操作磁碟文件或搭建HTTP伺服器,NodeJS就相應提供了fs
、http
等內置對象。有啥用處
儘管存在一聽說可以直接運行JS文件就覺得很酷的同學,但大多數同學在接觸新東西時首先關心的是有啥用處,以及能帶來啥價值。
NodeJS的作者說,他創造NodeJS的目的是為了實現高性能Web伺服器,他首先看重的是事件機制和非同步IO模型的優越性,而不是JS。但是他需要選擇一種編程語言實現他的想法,這種編程語言不能自帶IO功能,並且需要能良好支持事件機制。JS沒有自帶IO功能,天生就用於處理瀏覽器中的DOM事件,並且擁有一大群程式員,因此就成為了天然的選擇。
如他所願,NodeJS在服務端活躍起來,出現了大批基於NodeJS的Web服務。而另一方面,NodeJS讓前端眾如獲神器,終於可以讓自己的能力覆蓋範圍跳出瀏覽器視窗,更大批的前端工具如雨後春筍。
因此,對於前端而言,雖然不是人人都要拿NodeJS寫一個伺服器程式,但簡單可至使用命令交互模式調試JS代碼片段,複雜可至編寫工具提升工作效率。
NodeJS生態圈正欣欣向榮。
如何安裝
安裝程式
NodeJS提供了一些安裝程式,都可以在nodejs.org這裡下載並安裝。
Windows系統下,選擇和系統版本匹配的
.msi
尾碼的安裝文件。Mac OS X系統下,選擇.pkg
尾碼的安裝文件。編譯安裝
Linux系統下沒有現成的安裝程式可用,雖然一些發行版可以使用
apt-get
之類的方式安裝,但不一定能安裝到最新版。因此Linux系統下一般使用以下方式編譯方式安裝NodeJS。
確保系統下g++版本在4.6以上,python版本在2.6以上。
從nodejs.org下載
tar.gz
尾碼的NodeJS最新版源代碼包並解壓到某個位置。進入解壓到的目錄,使用以下命令編譯和安裝。
$ ./configure $ make $ sudo make install
如何運行
打開終端,鍵入
node
進入命令交互模式,可以輸入一條代碼語句後立即執行並顯示結果,例如:$ node > console.log('Hello World!'); Hello World!
如果要運行一大段代碼的話,可以先寫一個JS文件再運行。例如有以下
hello.js
。function hello() { console.log('Hello World!'); } hello();
寫好後在終端下鍵入
node hello.js
運行,結果如下:$ node hello.js Hello World!
許可權問題
在Linux系統下,使用NodeJS監聽80或443埠提供HTTP(S)服務時需要root許可權,有兩種方式可以做到。
一種方式是使用
sudo
命令運行NodeJS。例如通過以下命令運行的server.js
中有許可權使用80和443埠。一般推薦這種方式,可以保證僅為有需要的JS腳本提供root許可權。$ sudo node server.js
另一種方式是使用
chmod +s
命令讓NodeJS總是以root許可權運行,具體做法如下。因為這種方式讓任何JS腳本都有了root許可權,不太安全,因此在需要很考慮安全的系統下不推薦使用。$ sudo chown root /usr/local/bin/node $ sudo chmod +s /usr/local/bin/node
模塊
編寫稍大一點的程式時一般都會將代碼模塊化。在NodeJS中,一般將代碼合理拆分到不同的JS文件中,每一個文件就是一個模塊,而文件路徑就是模塊名。
在編寫每個模塊時,都有
require
、exports
、module
三個預先定義好的變數可供使用。require
require
函數用於在當前模塊中載入和使用別的模塊,傳入一個模塊名,返回一個模塊導出對象。模塊名可使用相對路徑(以./
開頭),或者是絕對路徑(以/
或C:
之類的盤符開頭)。另外,模塊名中的.js
擴展名可以省略。以下是一個例子。var foo1 = require('./foo'); var foo2 = require('./foo.js'); var foo3 = require('/home/user/foo'); var foo4 = require('/home/user/foo.js'); // foo1至foo4中保存的是同一個模塊的導出對象。
另外,可以使用以下方式載入和使用一個JSON文件。
var data = require('./data.json');
exports
exports
對象是當前模塊的導出對象,用於導出模塊公有方法和屬性。別的模塊通過require
函數使用當前模塊時得到的就是當前模塊的exports
對象。以下例子中導出了一個公有方法。exports.hello = function () { console.log('Hello World!'); };
module
通過
module
對象可以訪問到當前模塊的一些相關信息,但最多的用途是替換當前模塊的導出對象。例如模塊導出對象預設是一個普通對象,如果想改成一個函數的話,可以使用以下方式。module.exports = function () { console.log('Hello World!'); };
以上代碼中,模塊預設導出對象被替換為一個函數。
模塊初始化
一個模塊中的JS代碼僅在模塊第一次被使用時執行一次,併在執行過程中初始化模塊的導出對象。之後,緩存起來的導出對象被重覆利用。
主模塊
通過命令行參數傳遞給NodeJS以啟動程式的模塊被稱為主模塊。主模塊負責調度組成整個程式的其它模塊完成工作。例如通過以下命令啟動程式時,
main.js
就是主模塊。$ node main.js
完整示例
例如有以下目錄。
- /home/user/hello/ - util/ counter.js main.js
其中
counter.js
內容如下:var i = 0; function count() { return ++i; } exports.count = count;
該模塊內部定義了一個私有變數
i
,併在exports
對象導出了一個公有方法count
。主模塊
main.js
內容如下:var counter1 = require('./util/counter'); var counter2 = require('./util/counter'); console.log(counter1.count()); console.log(counter2.count()); console.log(counter2.count());
運行該程式的結果如下:
$ node main.js 1 2 3
可以看到,
counter.js
並沒有因為被require了兩次而初始化兩次。二進位模塊
雖然一般我們使用JS編寫模塊,但NodeJS也支持使用C/C++編寫二進位模塊。編譯好的二進位模塊除了文件擴展名是
.node
外,和JS模塊的使用方式相同。雖然二進位模塊能使用操作系統提供的所有功能,擁有無限的潛能,但對於前端同學而言編寫過於困難,並且難以跨平臺使用,因此不在本教程的覆蓋範圍內。小結
本章介紹了有關NodeJS的基本概念和使用方法,總結起來有以下知識點:
NodeJS是一個JS腳本解析器,任何操作系統下安裝NodeJS本質上做的事情都是把NodeJS執行程式複製到一個目錄,然後保證這個目錄在系統PATH環境變數下,以便終端下可以使用
node
命令。終端下直接輸入
node
命令可進入命令交互模式,很適合用來測試一些JS代碼片段,比如正則表達式。NodeJS使用CMD模塊系統,主模塊作為程式入口點,所有模塊在執行過程中只初始化一次。
除非JS模塊不能滿足需求,否則不要輕易使用二進位模塊,否則你的用戶會叫苦連天。
代碼的組織和部署
有經驗的C程式員在編寫一個新程式時首先從make文件寫起。同樣的,使用NodeJS編寫程式前,為了有個良好的開端,首先需要準備好代碼的目錄結構和部署方式,就如同修房子要先搭腳手架。本章將介紹與之相關的各種知識。
模塊路徑解析規則
我們已經知道,
require
函數支持斜杠(/
)或盤符(C:
)開頭的絕對路徑,也支持./
開頭的相對路徑。但這兩種路徑在模塊之間建立了強耦合關係,一旦某個模塊文件的存放位置需要變更,使用該模塊的其它模塊的代碼也需要跟著調整,變得牽一發動全身。因此,require
函數支持第三種形式的路徑,寫法類似於foo/bar
,並依次按照以下規則解析路徑,直到找到模塊位置。
內置模塊
如果傳遞給
require
函數的是NodeJS內置模塊名稱,不做路徑解析,直接返回內部模塊的導出對象,例如require('fs')
。node_modules目錄
NodeJS定義了一個特殊的
node_modules
目錄用於存放模塊。例如某個模塊的絕對路徑是/home/user/hello.js
,在該模塊中使用require('foo/bar')
方式載入模塊時,則NodeJS依次嘗試使用以下路徑。/home/user/node_modules/foo/bar /home/node_modules/foo/bar /node_modules/foo/bar
NODE_PATH環境變數
與PATH環境變數類似,NodeJS允許通過NODE_PATH環境變數來指定額外的模塊搜索路徑。NODE_PATH環境變數中包含一到多個目錄路徑,路徑之間在Linux下使用
:
分隔,在Windows下使用;
分隔。例如定義了以下NODE_PATH環境變數:NODE_PATH=/home/user/lib:/home/lib
當使用
require('foo/bar')
的方式載入模塊時,則NodeJS依次嘗試以下路徑。/home/user/lib/foo/bar /home/lib/foo/bar
包(package)
我們已經知道了JS模塊的基本單位是單個JS文件,但複雜些的模塊往往由多個子模塊組成。為了便於管理和使用,我們可以把由多個子模塊組成的大模塊稱做
包
,並把所有子模塊放在同一個目錄里。在組成一個包的所有子模塊中,需要有一個入口模塊,入口模塊的導出對象被作為包的導出對象。例如有以下目錄結構。
- /home/user/lib/ - cat/ head.js body.js main.js
其中
cat
目錄定義了一個包,其中包含了3個子模塊。main.js
作為入口模塊,其內容如下:var head = require('./head'); var body = require('./body'); exports.create = function (name) { return { name: name, head: head.create(), body: body.create() }; };
在其它模塊里使用包的時候,需要載入包的入口模塊。接著上例,使用
require('/home/user/lib/cat/main')
能達到目的,但是入口模塊名稱出現在路徑里看上去不是個好主意。因此我們需要做點額外的工作,讓包使用起來更像是單個模塊。index.js
當模塊的文件名是
index.js
,載入模塊時可以使用模塊所在目錄的路徑代替模塊文件路徑,因此接著上例,以下兩條語句等價。var cat = require('/home/user/lib/cat'); var cat = require('/home/user/lib/cat/index');
這樣處理後,就只需要把包目錄路徑傳遞給
require
函數,感覺上整個目錄被當作單個模塊使用,更有整體感。package.json
如果想自定義入口模塊的文件名和存放位置,就需要在包目錄下包含一個
package.json
文件,併在其中指定入口模塊的路徑。上例中的cat
模塊可以重構如下。- /home/user/lib/ - cat/ + doc/ - lib/ head.js body.js main.js + tests/ package.json
其中
package.json
內容如下。{ "name": "cat", "main": "./lib/main.js" }
如此一來,就同樣可以使用
require('/home/user/lib/cat')
的方式載入模塊。NodeJS會根據包目錄下的package.json
找到入口模塊所在位置。命令行程式
使用NodeJS編寫的東西,要麼是一個包,要麼是一個命令行程式,而前者最終也會用於開發後者。因此我們在部署代碼時需要一些技巧,讓用戶覺得自己是在使用一個命令行程式。
例如我們用NodeJS寫了個程式,可以把命令行參數原樣列印出來。該程式很簡單,在主模塊內實現了所有功能。並且寫好後,我們把該程式部署在
/home/user/bin/node-echo.js
這個位置。為了在任何目錄下都能運行該程式,我們需要使用以下終端命令。$ node /home/user/bin/node-echo.js Hello World Hello World
這種使用方式看起來不怎麼像是一個命令行程式,下邊的才是我們期望的方式。
$ node-echo Hello World
Linux
在Linux系統下,我們可以把JS文件當作shell腳本來運行,從而達到上述目的,具體步驟如下:
在shell腳本中,可以通過
#!
註釋來指定當前腳本使用的解析器。所以我們首先在node-echo.js
文件頂部增加以下一行註釋,表明當前腳本使用NodeJS解析。#! /usr/bin/env node
NodeJS會忽略掉位於JS模塊首行的
#!
註釋,不必擔心這行註釋是非法語句。然後,我們使用以下命令賦予
node-echo.js
文件執行許可權。$ chmod +x /home/user/bin/node-echo.js
最後,我們在PATH環境變數中指定的某個目錄下,例如在
/usr/local/bin
下邊創建一個軟鏈文件,文件名與我們希望使用的終端命令同名,命令如下:$ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo
這樣處理後,我們就可以在任何目錄下使用
node-echo
命令了。Windows
在Windows系統下的做法完全不同,我們得靠
.cmd
文件來解決問題。假設node-echo.js
存放在C:\Users\user\bin
目錄,並且該目錄已經添加到PATH環境變數里了。接下來需要在該目錄下新建一個名為node-echo.cmd
的文件,文件內容如下:@node "C:\User\user\bin\node-echo.js" %*
這樣處理後,我們就可以在任何目錄下使用
node-echo
命令了。工程目錄
瞭解了以上知識後,現在我們可以來完整地規劃一個工程目錄了。以編寫一個命令行程式為例,一般我們會同時提供命令行模式和API模式兩種使用方式,並且我們會藉助三方包來編寫代碼。除了代碼外,一個完整的程式也應該有自己的文檔和測試用例。因此,一個標準的工程目錄都看起來像下邊這樣。
- /home/user/workspace/node-echo/ # 工程目錄 - bin/ # 存放命令行相關代碼 node-echo + doc/ # 存放文檔 - lib/ # 存放API相關代碼 echo.js - node_modules/ # 存放三方包 + argv/ + tests/ # 存放測試用例 package.json # 元數據文件 README.md # 說明文件
其中部分文件內容如下:
/* 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" }
以上例子中分類存放了不同類型的文件,並通過
node_moudles
目錄直接使用三方包名載入模塊。此外,定義了package.json
之後,node-echo
目錄也可被當作一個包來使用。NPM
NPM是隨同NodeJS一起安裝的包管理工具,能解決NodeJS代碼部署上的很多問題,常見的使用場景有以下幾種:
允許用戶從NPM伺服器下載別人編寫的三方包到本地使用。
允許用戶從NPM伺服器下載並安裝別人編寫的命令行程式到本地使用。
允許用戶將自己編寫的包或命令行程式上傳到NPM伺服器供別人使用。
可以看到,NPM建立了一個NodeJS生態圈,NodeJS開發者和用戶可以在裡邊互通有無。以下分別介紹這三種場景下怎樣使用NPM。
下載三方包
需要使用三方包時,首先得知道有哪些包可用。雖然npmjs.org提供了個搜索框可以根據包名來搜索,但如果連想使用的三方包的名字都不確定的話,就請百度一下吧。知道了包名後,比如上邊例子中的
argv
,就可以在工程目錄下打開終端,使用以下命令來下載三方包。$ npm install argv ... [email protected] node_modules\argv
下載好之後,
argv
包就放在了工程目錄下的node_modules
目錄中,因此在代碼中只需要通過require('argv')
的方式就好,無需指定三方包路徑。以上命令預設下載最新版三方包,如果想要下載指定版本的話,可以在包名後邊加上
@<version>
,例如通過以下命令可下載0.0.1版的argv
。$ npm install [email protected] ... [email protected] node_modules\argv
如果使用到的三方包比較多,在終端下一個包一條命令地安裝未免太人肉了。因此NPM對
package.json
的欄位做了擴展,允許在其中申明三方包依賴。因此,上邊例子中的package.json
可以改寫如下:{ "name": "node-echo", "main": "./lib/echo.js", "dependencies": { "argv": "0.0.2" } }
這樣處理後,在工程目錄下就可以使用
npm install
命令批量安裝三方包了。更重要的是,當以後node-echo
也上傳到了NPM伺服器,別人下載這個包時,NPM會根據包中申明的三方包依賴自動下載進一步依賴的三方包。例如,使用npm install node-echo
命令時,NPM會自動創建以下目錄結構。- project/ - node_modules/ - node-echo/ - node_modules/ + argv/ ... ...
如此一來,用戶只需關心自己直接使用的三方包,不需要自己去解決所有包的依賴關係。
安裝命令行程式
從NPM服務上下載安裝一個命令行程式的方法與三方包類似。例如上例中的
node-echo
提供了命令行使用方式,只要node-echo
自己配置好了相關的package.json
欄位,對於用戶而言,只需要使用以下命令安裝程式。$ npm install node-echo -g
參數中的
-g
表示全局安裝,因此node-echo
會預設安裝到以下位置,並且NPM會自動創建好Linux系統下需要的軟鏈文件或Windows系統下需要的.cmd
文件。- /usr/local/ # Linux系統下 - lib/node_modules/ + node-echo/ ... - bin/ node-echo ... ... - %APPDATA%\npm\ # Windows系統下 - node_modules\ + node-echo\ ... node-echo.cmd ...
發佈代碼
第一次使用NPM發佈代碼前需要註冊一個賬號。終端下運行
npm adduser
,之後按照提示做即可。賬號搞定後,接著我們需要編輯package.json
文件,加入NPM必需的欄位。接著上邊node-echo
的例子,package.json
里必要的欄位如下。{ "name": "node-echo", # 包名,在NPM伺服器上須要保持唯一 "version": "1.0.0", # 當前版本號 "dependencies": { # 三方包依賴,需要指定包名和版本號 "argv": "0.0.2" }, "main": "./lib/echo.js", # 入口模塊位置 "bin" : { "node-echo": "./bin/node-echo" # 命令行程式名和主模塊位置 } }
之後,我們就可以在
package.json
所在目錄下運行npm publish
發佈代碼了。版本號
使用NPM下載和發佈代碼時都會接觸到版本號。NPM使用語義版本號來管理代碼,這裡簡單介紹一下。
語義版本號分為
X.Y.Z
三位,分別代表主版本號、次版本號和補丁版本號。當代碼變更時,版本號按以下原則更新。+ 如果只是修複bug,需要更新Z位。 + 如果是新增了功能,但是向下相容,需要更新Y位。 + 如果有大變動,向下不相容,需要更新X位。
版本號有了這個保證後,在申明三方包依賴時,除了可依賴於一個固定版本號外,還可依賴於某個範圍的版本號。例如
"argv": "0.0.x"
表示依賴於0.0.x
系列的最新版argv
。NPM支持的所有版本號範圍指定方式可以查看官方文檔。靈機一點
除了本章介紹的部分外,NPM還提供了很多功能,
package.json
里也有很多其它有用的欄位。除了可以在npmjs.org/doc/查看官方文檔外,這裡再介紹一些NPM常用命令。
NPM提供了很多命令,例如
install
和publish
,使用npm help
可查看所有命令。使用
npm help <command>
可查看某條命令的詳細幫助,例如npm help install
。在
package.json
所在目錄下使用npm install . -g
可先在本地安裝當前命令行程式,可用於發佈前的本地測試。使用
npm update <package>
可以把當前目錄下node_modules
子目錄裡邊的對應模塊更新至最新版本。使用
npm update <package> -g
可以把全局安裝的對應命令行程式更新至最新版。使用
npm cache clear
可以清空NPM本地緩存,用於對付使用相同版本號發佈新版本代碼的人。使用
npm unpublish <package>@<version>
可以撤銷發佈自己發佈過的某個版本代碼。小結
本章介紹了使用NodeJS編寫代碼前需要做的準備工作,總結起來有以下幾點:
編寫代碼前先規劃好目錄結構,才能做到有條不紊。
稍大些的程式可以將代碼拆分為多個模塊管理,更大些的程式可以使用包來組織模塊。
合理使用
node_modules
和NODE_PATH
來解耦包的使用方式和物理路徑。使用NPM加入NodeJS生態圈互通有無。
想到了心儀的包名時請提前在NPM上搶註。
文件操作
讓前端覺得如獲神器的不是NodeJS能做網路編程,而是NodeJS能夠操作文件。小至文件查找,大至代碼編譯,幾乎沒有一個前端工具不操作文件。換個角度講,幾乎也只需要一些數據處理邏輯,再加上一些文件操作,就能夠編寫出大多數前端工具。本章將介紹與之相關的NodeJS內置模塊。
開門紅
NodeJS提供了基本的文件操作API,但是像文件拷貝這種高級功能就沒有提供,因此我們先拿文件拷貝程式練手。與
copy
命令類似,我們的程式需要能接受源文件路徑與目標文件路徑兩個參數。小文件拷貝
我們使用NodeJS內置的
fs
模塊簡單實現這個程式如下。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
從源路徑讀取文件內容,並使用fs.writeFileSync
將文件內容寫入目標路徑。豆知識:
process
是一個全局變數,可通過process.argv
獲得命令行參數。由於argv[0]
固定等於NodeJS執行程式的絕對路徑,argv[1]
固定等於主模塊的絕對路徑,因此第一個命令行參數從argv[2]
這個位置開始。大文件拷貝
上邊的程式拷貝一些小文件沒啥問題,但這種一次性把所有文件內容都讀取到記憶體中後再一次性寫入磁碟的方式不適合拷貝大文件,記憶體會爆倉。對於大文件,我們只能讀一點寫一點,直到完成拷貝。因此上邊的程式需要改造如下。
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
創建了一個源文件的只讀數據流,並使用fs.createWriteStream
創建了一個目標文件的只寫數據流,並且用pipe
方法把兩個數據流連接了起來。連接起來後發生的事情,說得抽象點的話,水順著水管從一個桶流到了另一個桶。API走馬觀花
我們先大致看看NodeJS提供了哪些和文件操作有關的API。這裡並不逐一介紹每個API的使用方法,官方文檔已經做得很好了。
Buffer(數據塊)
JS語言自身只有字元串數據類型,沒有二進位數據類型,因此NodeJS提供了一個與
String
對等的全局構造函數Buffer
來提供對二進位數據的操作。除了可以讀取文件得到Buffer
的實例外,還能夠直接構造,例如:var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
Buffer
與字元串類似,除了可以用.length
屬性得到位元組長度外,還可以用[index]
方式讀取指定位置的位元組,例如:bin[0]; // => 0x68;
Buffer
與字元串能夠互相轉化,例如可以使用指定編碼將二進位數據轉化為字元串:var str = bin.toString('utf-8'); // => "hello"
或者反過來,將字元串轉換為指定編碼下的二進位數據:
var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>
Buffer
與字元串有一個重要區別。字元串是只讀的,並且對字元串的任何修改得到的都是一個新字元串,原字元串保持不變。至於Buffer
,更像是可以做指針操作的C語言數組。例如,可以用[index]
方式直接修改某個位置的位元組。bin[0] = 0x48;
而
.slice
方法也不是返回一個新的Buffer
,而更像是返回了指向原Buffer
中間的某個位置的指針,如下所示。[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ] ^ ^ | | bin bin.slice(2)
因此對
.slice
方法返回的Buffer
的修改會作用於原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
,得首先創建一個新的Buffer
,並通過.copy
方法把原Buffer
中的數據複製過去。這個類似於申請一塊新的記憶體,並把已有記憶體中的數據複製過去。以下是一個例子。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的數據處理能力從字元串擴展到了任意二進位數據。Stream(數據流)
當記憶體中無法一次裝下需要處理的數據時,或者一邊讀取一邊處理更加高效時,我們就需要用到數據流。NodeJS中通過各種
Stream
來提供對數據流的操作。以上邊的大文件拷貝程式為例,我們可以為數據來源創建一個只讀數據流,示例如下:
var rs = fs.createReadStream(pathname); rs.on('data', function (chunk) { doSomething(chunk); }); rs.on('end', function () { cleanUp(); });
豆知識:
Stream
基於事件機制工作,所有Stream
的實例都繼承於NodeJS提供的EventEmitter。上邊的代碼中
data
事件會源源不斷地被觸發,不管doSomething
函數是否處理得過來。代碼可以繼續做如下改造,以解決這個問題。var rs = fs.createReadStream(src); rs.on('data', function (chunk) { rs.pause(); doSomething(chunk, function () { rs.resume(); }); }); rs.on('end', function () { cleanUp(); });
以上代碼給
doSomething
函數加上了回調,因此我們可以在處理數據前暫停數據讀取,併在處理數據後繼續讀取數據。此外,我們也可以為數據目標創建一個只寫數據流,示例如下:
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
換成了往只寫數據流里寫入數據後,以上代碼看起來就像是一個文件拷貝程式了。但是以上代碼存在上邊提到的問題,如果寫入速度跟不上讀取速度的話,只寫數據流內部的緩存會爆倉。我們可以根據.write
方法的返回值來判斷傳入的數據是寫入目標了,還是臨時放在了緩存了,並根據drain
事件來判斷什麼時候只寫數據流已經將緩存中的數據寫入目標,可以傳入下一個待寫數據了。因此代碼可以改造如下: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(); });
以上代碼實現了數據從只讀數據流到只寫數據流的搬運,並包括了防爆倉控制。因為這種使用場景很多,例如上邊的大文件拷貝程式,NodeJS直接提供了
.pipe
方法來做這件事情,其內部實現方式與上邊的代碼類似。File System(文件系統)
NodeJS通過
fs
內置模塊提供對文件的操作。fs
模塊提供的API基本上可以分為以下三類:
文件屬性讀寫。
其中常用的有
fs.stat
、fs.chmod
、fs.chown
等等。文件內容讀寫。
其中常用的有
fs.readFile
、fs.readdir
、fs.writeFile
、fs.mkdir
等等。底層文件操作。
其中常用的有
fs.open
、fs.read
、fs.write
、fs.close
等等。NodeJS最精華的非同步IO模型在
fs
模塊里有著充分的體現,例如上邊提到的這些API都通過回調函數傳遞結果。以fs.readFile
為例:fs.readFile(pathname, function (err, data) { if (err) { // Deal with error. } else { // Deal with data. } });
如上邊代碼所示,基本上所有
fs
模塊API的回調參數都有兩個。第一個參數在有錯誤發生時等於異常對象,第二個參數始終用於返回API方法執行結果。此外,
fs
模塊的所有非同步API都有對應的同步版本,用於無法使用非同步操作時,或者同步操作更方便時的情況。同步API除了方法名的末尾多了一個Sync
之外,異常對象與執行結果的傳遞方式也有相應變化。同樣以fs.readFileSync
為例:try { var data = fs.readFileSync(pathname); // Deal with data. } catch (err) { // Deal with error. }
fs
模塊提供的API很多,這裡不一一介紹,需要時請自行查閱官方文檔。Path(路徑)
操作文件時難免不與文件路徑打交道。NodeJS提供了
path
內置模塊來簡化路徑相關操作,並提升代碼可讀性。以下分別介紹幾個常用的API。
path.normalize
將傳入的路徑轉換為標準路徑,具體講的話,除瞭解析路徑中的
.
與..
外,還能去掉多餘的斜杠。如果有程式需要使用路徑作為某些數據的索引,但又允許用戶隨意輸入路徑時,就需要使用該方法保證路徑的唯一性。以下是一個例子: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 }
坑出沒註意: 標準化之後的路徑里的斜杠在Windows系統下是
\
,而在Linux系統下是/
。如果想保證任何系統下都使用/
作為路徑分隔符的話,需要用.replace(/\\/g, '/')
再替換一下標準路徑。path.join
將傳入的多個路徑拼接為標準路徑。該方法可避免手工拼接路徑字元串的繁瑣,並且能在不同系統下正確使用相應的路徑分隔符。以下是一個例子:
path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
path.extname
當我們需要根據不同文件擴展名做不同操作時,該方法就顯得很好用。以下是一個例子:
path.extname('foo/bar.js'); // => ".js"
path
模塊提供的其餘方法也不多,稍微看一下官方文檔就能全部掌握。遍歷目錄
遍歷目錄是操作文件時的一個常見需求。比如寫一個程式,需要找到並處理指定目錄下的所有JS文件時,就需要遍歷整個目錄。
遞歸演算法
遍歷目錄時一般使用遞歸演算法,否則就難以編寫出簡潔的代碼。遞歸演算法與數學歸納法類似,通過不斷縮小問題的規模來解決問題。以下示例說明瞭這種方法。
function factorial(n) { if (n === 1) { return 1; } else { return n * factorial(n - 1); } }
上邊的函數用於計算N的階乘(N!)。可以看到,當N大於1時,問題簡化為計算N乘以N-1的階乘。當N等於1時,問題達到最小規模,不需要再簡化,因此直接返回1。
陷阱: 使用遞歸演算法編寫的代碼雖然簡潔,但由於每遞歸一次就產生一次函數調用,在需要優先考慮性能時,需要把遞歸演算法轉換為迴圈演算法,以減少函數調用次數。
遍歷演算法
目錄是一個樹狀結構,在遍歷時一般使用深度優先+先序遍歷演算法。深度優先,意味著到達一個節點後,首先接著遍歷子節點而不是鄰居節點。先序遍歷,意味著首次到達了某節點就算遍歷完成,而不是最後一次返回某節點才算數。因此使用這種遍歷方式時,下邊這棵樹的遍歷順序是
A > B > D > E > C > F
。A / \ B C / \ \ D E F
同步遍歷
瞭解了必要的演算法後,我們可以簡單地實現以下目錄遍歷函數。
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); } }); }
可以看到,該函數以某個目錄作為遍歷的起點。遇到一個子目錄時,就先接著遍歷子目錄。遇到一個文件時,就把文件的絕對路徑傳給回調函數。回調函數拿到文件路徑後,就可以做各種判斷和處理。因此假設有以下目錄:
- /home/user/ - foo/ x.js - bar/ y.js z.css
使用以下代碼遍歷該目錄時,得到的輸入如下。
travel('/home/user', function (pathname) { console.log(pathname); }); ------------------------ /home/user/foo/x.js /home/user/bar/y.js /home/user/z.css
非同步遍歷
如果讀取目錄或讀取文件狀態時使用的是非同步API,目錄遍歷函數實現起來會有些複雜,但原理完全相同。
travel
函數的非同步版本如下。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)); }); }
這裡不詳細介紹非同步遍歷函數的編寫技巧,在後續章節中會詳細介紹這個。總之我們可以看到非同步編程還是蠻複雜的。
文本編碼
使用NodeJS編寫前端工具時,操作得最多的是文本文件,因此也就涉及到了文件編碼的處理問題。我們常用的文本編碼有
UTF8
和GBK
兩種,並且UTF8
文件還可能帶有BOM。在讀取不同編碼的文本文件時,需要將文件內容轉換為JS使用的UTF8
編碼字元串後才能正常處理。BOM的移除
BOM用於標記一個文本文件使用Unicode編碼,其本身是一個Unicode字元("\uFEFF"),位於文本文件頭部。在不同的Unicode編碼下,BOM字元對應的二進位位元組如下:
Bytes Encoding ---------------------------- FE FF UTF16BE FF FE UTF16LE EF BB BF UTF8
因此,我們可以根據文本文件頭幾個位元組等於啥來判斷文件是否包含BOM,以及使用哪種Unicode編碼。但是,BOM字元雖然起到了標記文件編碼的作用,其本身卻不屬於文件內容的一部分,如果讀取文本文件時不去掉BOM,在某些使用場景下就會有問題。例如我們把幾個JS文件合併成一個文件後,如果文件中間含有BOM字元,就會導致瀏覽器JS語法錯誤。因此,使用NodeJS讀取文本文件時,一般需要去掉BOM。例如,以下代碼實現了識別和去除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轉UTF8
NodeJS支持在讀取文本文件時,或者在
Buffer
轉換為字元串時指定文本編碼,但遺憾的是,GBK編碼不在NodeJS自身支持範圍內。因此,一般我們藉助iconv-lite
這個三方包來轉換編碼。使用NPM下載該包後,我們可以按下邊方式編寫一個讀取GBK文本文件的函數。var iconv = require('iconv-lite'); function readGBKText(pathname) { var bin = fs.readFileSync(pathname); return iconv.decode(bin, 'gbk'); }
單位元組編碼
有時候,我們無法預知需要讀取的文件採用哪種編碼,因此也就無法指定正確的編碼。比如我們要處理的某些CSS文件中,有的用GBK編碼,有的用UTF8編碼。雖然可以一定程度可以根據文件的位元組內容猜測出文本編碼,但這裡要介紹的是有些局限,但是要簡單得多的一種技術。
首先我們知道,如果一個文本文件只包含英文字元,比如
Hello World
,那無論用GBK編碼或是UTF8編碼讀取這個文件都是沒問題的。這是因為在這些編碼下,ASCII0~128範圍內字元都使用相同的單位元組編碼。反過來講,即使一個文本文件中有中文等字元,如果我們需要處理的字元僅在ASCII0~128範圍內,比如除了註釋和字元串以外的JS代碼,我們就可以統一使用單位元組編碼來讀取文件,不用關心文件的實際編碼是GBK還是UTF8。以下示例說明瞭這種方法。
1. GBK編碼源文件內容: var foo = '中文'; 2. 對應位元組: 76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B 3. 使用單位元組編碼讀取後得到的內容: var foo = '{亂碼}{亂碼}{亂碼}{亂碼}'; 4. 替換內容: var bar = '{亂碼}{亂碼}{亂碼}{亂碼}'; 5. 使用單位元組編碼保存後對應位元組: 76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B 6. 使用GBK編碼讀取後得到內容: var bar = '中文';
這裡的訣竅在於,不管大於0xEF的單個位元組在單位元組編碼下被解析成什麼亂碼字元,使用同樣的單位元組編碼保存這些亂碼字元時,背後對應的位元組保持不變。
NodeJS中自帶了一種
binary
編碼可以用來實現這個方法,因此在下例中,我們使用這種編碼來演示上例對應的代碼該怎麼寫。function replace(pathname) { var str = fs.readFileSync(pathname, 'binary'); str = str.replace('foo', 'bar'); fs.writeFileSync(pathname, str, 'binary'); }
小結
本章介紹了使用NodeJS操作文件時需要的API以及一些技巧,總結起來有以下幾點:
學好文件操作,編寫各種程式都不怕。
如果不是很在意性能,
fs
模塊的同步API能讓生活更加美好。需要對文件讀寫做到位元組級別的精細控制時,請使用
fs
模塊的文件底層操作API。不要使用拼接字元串的方式來處理路徑,使用
path
模塊。掌握好目錄遍歷和文件編碼處理技巧,很實用。
網路操作
不瞭解網路編程的程式員不是好前端,而NodeJS恰好提供了一扇瞭解網路編程的視窗。通過NodeJS,除了可以編寫一些服務端程式來協助前端開發和測試外,還能夠學習一些HTTP協議與Socket協議的相關知識,這些知識在優化前端性能和排查前端故障時說不定能派上用場。本章將介紹與之相關的NodeJS內置模塊。
開門紅
NodeJS本來的用途是編寫高性能Web伺服器。我們首先在這裡重覆一下官方文檔里的例子,使用NodeJS內置的
http
模塊簡單實現一個HTTP伺服器。var http = require('http'); http.createServer(function (request, response) { response.writeHead(200, { 'Content-Type': 'text-plain' }); response.end('Hello World\n'); }).listen(8124);
以上程式創建了一個HTTP伺服器並監聽
8124
埠,打開瀏覽器訪問該埠http://127.0.0.1:8124/
就能夠看到效果。豆知識: 在Linux系統下,監聽1024以下埠需要root許可權。因此,如果想監聽80或443埠的話,需要使用
sudo
命令啟動程式。API走馬觀花
我們先大致看看NodeJS提供了哪些和網路操作有關的API。這裡並不逐一介紹每個API的使用方法,官方文檔已經做得很好了。
HTTP
'http'模塊提供兩種使用方式:
作為服務端使用時,創建一個HTTP伺服器,監聽HTTP客戶端請求並返迴響應。
作為客戶端使用時,發起一個HTTP客戶端請求,獲取服務端響應。
首先我們來看看服務端模式下如何工作。如開門紅中的例子所示,首先需要使用
.createServer
方法創建一個伺服器,然後調用.listen
方法監聽埠。之後,每當來了一個客戶端請求,創建伺服器時傳入的回調函數就被調用一次。可以看出,這是一種事件機制。HTTP請求本質上是一個數據流,由請求頭(headers)和請求體(body)組成。例如以下是一個完整的HTTP請求數據內容。
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
可以看到,空行之上是請求頭,之下是請求體。HTTP請求在發送給伺服器時,可以認為是按照從頭到尾的順序一個位元組一個位元組地以數據流方式發送的。而
http
模塊創建的HTTP伺服器在接收到完整的請求頭後,就會調用回調函數。在回調函數中,除了可以使用request
對象訪問請求頭數據外,還能把request
對象當作一個只讀數據流來訪問請求體數據。以下是一個例子。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響應本質上也是一個數據流,同樣由響應頭(headers)和響應體(body)組成。例如以下是一個完整的HTTP請求數據內容。
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
在回調函數中,除了可以使用
response
對象來寫入響應頭數據外,還能把response
對象當作一個只寫數據流來寫入響應體數據。例如在以下例子中,服務端原樣將客戶端請求的請求體數據返回給客戶端。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);
接下來我們看看客戶端模式下如何工作。為了發起一個客戶端HTTP請求,我們需要指定目標伺服器的位置併發送請求頭和請求體,以下示例演示了具體做法。
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
方法創建了一個客戶端,並指定請求目標和請求頭數據。之後,就可以把request
對象當作一個只寫數據流來寫入請求體數據和結束請求。另外,由於HTTP請求中GET
請求是最常見的一種,並且不需要請求體,因此http
模塊也提供了以下便捷API。http.get('http://www.example.com/', function (response) {});
當客戶端發送請求並接收到完整的服務端響應頭時,就會調用回調函數。在回調函數中,除了可以使用
response
對象訪問響應頭數據外,還能把response
對象當作一個只讀數據流來訪問響應體數據。以下是一個例子。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
https
模塊與http
模塊極為類似,區別在於https
模塊需要額外處理SSL證書。在服務端模式下,創建一個HTTPS伺服器的示例如下。
var options = { key: fs.readFileSync('./ssl/default.key'), cert: fs.readFileSync('./ssl/default.cer') }; var server = https.createServer(options, function (request, response) { // ... });
可以看到,與創建HTTP伺服器相比,多了一個
options
對象,通過key
和cert
欄位指定了HTTPS伺服器使用的私鑰和公鑰。另外,NodeJS支持SNI技術,可以根據HTTPS客戶端請求使用的功能變數名稱動態使用不同的證書,因此同一個HTTPS伺服器可以使用多個功能變數名稱提供服務。接著上例,可以使用以下方法為HTTPS伺服器添加多組證書。
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') });
在客戶端模式下,發起一個HTTPS客戶端請求與
http
模塊幾乎相同,示例如下。var options = { hostname: 'www.example.com', port: 443, path: '/', method: 'GET' }; var request = https.request(options, function (response) {}); request.end();
但如果目標伺服器使用的SSL證書是自製的,不是從頒發機構購買的,預設情況下
https
模塊會拒絕連接,提示說有證書安全問題。在options
裡加入rejectUnauthorized: false
欄位可以禁用對證書有效性的檢查,從而允許https
模塊請求開發環境下使用自製證書的HTTPS伺服器。URL
處理HTTP請求時
url
模塊使用率超高,因為該模塊允許解析URL、生成URL,以及拼接URL。首先我們來看看一個完整的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
方法來將一個URL字元串轉換為URL對象,示例如下。url.parse('http://user:[email protected]: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:[email protected]:8080/p/a/t/h?query=string#hash' } */
傳給
.parse
方法的不一定要是一個完整的URL,例如在HTTP伺服器回調函數中,request.url
不包含協議頭和功能變數名稱,但同樣可以用.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
方法還支持第二個和第三個布爾類型可選參數。第二個參數等於true
時,該方法返回的URL對象中,query
欄位不再是一個字元串,而是一個經過querystring
模塊轉換後的參數對象。第三個參數等於true
時,該方法可以正確解析不帶協議頭的URL,例如//www.example.com/foo/bar
。反過來,
format
方法允許將一個URL對象轉換為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
querystring
模塊用於實現URL參數字元串與參數對象的互相轉換,示例如下。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
zlib
模塊提供了數據壓縮和解壓的功能。當我們處理HTTP請求和響應時,可能需要用到這個模塊。首先我們看一個使用
zlib
模塊壓縮HTTP響應體數據的例子。這個例子中,判斷了客戶端是否支持gzip,併在支持的情況下使用zlib
模塊返回gzip之後的響應體數據。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);
接著我們看一個使用
zlib
模塊解壓HTTP響應體數據的例子。這個例子中,判斷了服務端響應是否使用gzip壓縮,併在壓縮的情況下使用zlib
模塊解壓響應體數據。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
net
模塊可用於創建Socket伺服器或Socket客戶端。由於Socket在前端領域的使用範圍還不是很廣,這裡先不涉及到WebSocket的介紹,僅僅簡單演示一下如何從Socket層面來實現HTTP請求和響應。首先我們來看一個使用Socket搭建一個很不嚴謹的HTTP伺服器的例子。這個HTTP伺服器不管收到啥請求,都固定返回相同的響應。
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);
接著我們來看一個使用Socket發起HTTP客戶端請求的例子。這個例子中,Socket客戶端在建立連接後發送了一個HTTP GET請求,並通過
data
事件監聽函數來獲取伺服器響應。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(); });
靈機一點
使用NodeJS操作網路,特別是操作HTTP請求和響應時會遇到一些驚喜,這裡對一些常見問題做解答。
問: 為什麼通過
headers
對象訪問到的HTTP請求頭或響應頭欄位不是駝峰的?答: 從規範上講,HTTP請求頭和響應頭欄位都應該是駝峰的。但現實是殘酷的,不是每個HTTP服務端或客戶端程式都嚴格遵循規範,所以NodeJS在處理從別的客戶端或服務端收到的頭欄位時,都統一地轉換為了小寫字母格式,以便開發者能使用統一的方式來訪問頭欄位,例如
headers['content-length']
。問: 為什麼
http
模塊創建的HTTP伺服器返回的響應是chunked
傳輸方式的?答: 因為預設情況下,使用
.writeHead
方法寫入響應頭後,允許使用.write
方法寫入任意長度的響應體數據,並使用.end
方法結束一個響應。由於響應體數據長度不確定,因此NodeJS自動在響應頭裡添加了Transfer-Encoding: chunked
欄位,並採用chunked
傳輸方式。但是當響應體數據長度確定時,可使用.writeHead
方法在響應頭裡加上Content-Length
欄位,這樣做之後NodeJS就不會自動添加Transfer-Encoding
欄位和使用chunked
傳輸方式。問: 為什麼使用
http
模塊發起HTTP客戶端請求時,有時候會發生socket hang up
錯誤?答: 發起客戶端HTTP請求前需要先創建一個客戶端。
http
模塊提供了一個全局客戶端http.globalAgent
,可以讓我們使用.request
或.get
方法時不用手動創建客戶端。但是全局客戶端預設只允許5個併發Socket連接,當某一個時刻HTTP客戶端請求創建過多,超過這個數字時,就會發生socket hang up
錯誤。解決方法也很簡單,通過http.globalAgent.maxSocket