Node.js Stream(流)
Stream 是一個(gè)抽象接口悼瘾,Node 中有很多對(duì)象實(shí)現(xiàn)了這個(gè)接口捆憎。例如握恳,對(duì)http 服務(wù)器發(fā)起請(qǐng)求的request 對(duì)象就是一個(gè) Stream挖帘,還有stdout(標(biāo)準(zhǔn)輸出)完丽。
Node.js,Stream 有四種流類型:
<li>Readable - 可讀操作拇舀。
<li>Writable - 可寫操作逻族。
<li>Duplex - 可讀可寫操作.
<li>Transform - 操作被寫入數(shù)據(jù),然后讀出結(jié)果骄崩。
所有的 Stream 對(duì)象都是 EventEmitter 的實(shí)例聘鳞。常用的事件有:
<li>data - 當(dāng)有數(shù)據(jù)可讀時(shí)觸發(fā)。
<li>end - 沒有更多的數(shù)據(jù)可讀時(shí)觸發(fā)要拂。
<li>error - 在接收和寫入過程中發(fā)生錯(cuò)誤時(shí)觸發(fā)抠璃。
<li>finish - 所有數(shù)據(jù)已被寫入到底層系統(tǒng)時(shí)觸發(fā)。
從流中讀取數(shù)據(jù)
創(chuàng)建 input.txt 文件脱惰,內(nèi)容如下:
hello world!
創(chuàng)建 main.js 文件, 代碼如下:
var fs = require("fs");
var data = '';
// 創(chuàng)建可讀流
var readerStream = fs.createReadStream('input.txt');
// 設(shè)置編碼為 utf8鸡典。
readerStream.setEncoding('UTF8');
// 處理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
data += chunk;
});
readerStream.on('end',function(){
console.log(data);
});
readerStream.on('error', function(err){
console.log(err.stack);
});
console.log("程序執(zhí)行完畢");
寫入流
創(chuàng)建 main.js 文件, 代碼如下:
var fs = require('fs');
var data = "hello world!!!"
// 創(chuàng)建一個(gè)可以寫入的流,寫入到文件 output.txt 中
var writerStream = fs.createWriteStream('output.txt');
// 使用 utf8 編碼寫入數(shù)據(jù)
writerStream.write(data,'UTF8');
// 標(biāo)記文件末尾
writerStream.end();
// 處理流事件 --> finish, end, and error
writerStream.on('finish',function{
console.log('寫入完成!');
});
writerStream.on('error',function(err){
console.log(err.stack);
});
console.log('程序執(zhí)行完畢');
以上程序會(huì)將 data 變量的數(shù)據(jù)寫入到 output.txt 文件中枪芒。代碼執(zhí)行結(jié)果如下:
查看 output.txt 文件的內(nèi)容:
管道流
管道提供了一個(gè)輸出流到輸入流的機(jī)制彻况。通常我們用于從一個(gè)流中獲取數(shù)據(jù)并將數(shù)據(jù)傳遞到另外一個(gè)流中。
如上面的圖片所示舅踪,我們把文件比作裝水的桶纽甘,而水就是文件里的內(nèi)容,我們用一根管子(pipe)連接兩個(gè)桶使得水從一個(gè)桶流入另一個(gè)桶抽碌,這樣就慢慢的實(shí)現(xiàn)了大文件的復(fù)制過程悍赢。
以下實(shí)例我們通過讀取一個(gè)文件內(nèi)容并將內(nèi)容寫入到另外一個(gè)文件中决瞳。
設(shè)置 input.txt 文件內(nèi)容如下:
hello world
管道流操作實(shí)例
創(chuàng)建 main.js 文件, 代碼如下:
var fs = require('fs');
//創(chuàng)建一個(gè)輸入流
var readerStream = fs.createReadStream('input.txt');
//創(chuàng)建一個(gè)輸出流
var writerStream = fs.createWriteStream('output.txt');
// 管道讀寫操作
// 讀取 input.txt 文件內(nèi)容,并將內(nèi)容寫入到 output.txt 文件中
readerStream.pipe(writerStream);
console.log('程序執(zhí)行完畢');
代碼執(zhí)行結(jié)果如下:
查看 output.txt 文件的內(nèi)容:
鏈?zhǔn)搅?/h1>
鏈?zhǔn)绞峭ㄟ^連接輸出流到另外一個(gè)流并創(chuàng)建多個(gè)對(duì)個(gè)流操作鏈的機(jī)制左权。鏈?zhǔn)搅饕话阌糜诠艿啦僮鳌?br>
接下來我們就是用管道和鏈?zhǔn)絹韷嚎s和解壓文件皮胡。
創(chuàng)建 compress.js 文件, 代碼如下:
var fs = require("fs");
var zlib = require('zlib');
// 壓縮 input.txt 文件為 input.txt.gz
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
console.log("文件壓縮完成。");
代碼執(zhí)行結(jié)果如下:
執(zhí)行完以上操作后赏迟,我們可以看到當(dāng)前目錄下生成了 input.txt 的壓縮文件 input.txt.gz屡贺。
接下來,讓我們來解壓該文件锌杀,創(chuàng)建 decompress.js 文件甩栈,代碼如下:
var fs = require('fs');
var zip = require('zlib');
fs.createReadStream('input.txt.gz').pipe(zip.createGunzip()).pipe(fs.createWriteStream('input.txt'));
console.log('文件解壓完成');
代碼執(zhí)行結(jié)果如下:
Node.js模塊系統(tǒng)
為了讓Node.js的文件可以相互調(diào)用,Node.js提供了一個(gè)簡單的模塊系統(tǒng)糕再。
模塊是Node.js 應(yīng)用程序的基本組成部分量没,文件和模塊是一一對(duì)應(yīng)的。換言之突想,一個(gè) Node.js 文件就是一個(gè)模塊殴蹄,這個(gè)文件可能是JavaScript 代碼、JSON 或者編譯過的C/C++ 擴(kuò)展猾担。
創(chuàng)建模塊
在 Node.js 中袭灯,創(chuàng)建一個(gè)模塊非常簡單,如下我們創(chuàng)建一個(gè) 'main.js' 文件垒探,代碼如下:
var hello = require('./hello');
hello.world();
以上實(shí)例中妓蛮,代碼 require('./hello') 引入了當(dāng)前目錄下的hello.js文件(./ 為當(dāng)前目錄,node.js默認(rèn)后綴為js)圾叼。
Node.js 提供了exports 和 require 兩個(gè)對(duì)象蛤克,其中 exports 是模塊公開的接口,require 用于從外部獲取一個(gè)模塊的接口夷蚊,即所獲取模塊的 exports 對(duì)象构挤。
接下來我們就來創(chuàng)建hello.js文件,代碼如下:
exports.world = function() {
console.log('Hello World');
}
在以上示例中惕鼓,hello.js 通過 exports 對(duì)象把 world 作為模塊的訪問接口筋现,在 main.js 中通過 require('./hello') 加載這個(gè)模塊,然后就可以直接訪 問 hello.js 中 exports 對(duì)象的成員函數(shù)了箱歧。
有時(shí)候我們只是想把一個(gè)對(duì)象封裝到模塊中矾飞,格式如下:
module.exports = function() {
// ...
}
例如:
//hello.js
var Hello = function() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;
這樣就可以直接獲得這個(gè)對(duì)象了:
//main.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('HB');
hello.sayHello();
模塊接口的唯一變化是使用 module.exports = Hello 代替了exports.world = function(){}。 在外部引用該模塊時(shí)呀邢,其接口對(duì)象就是要輸出的 Hello 對(duì)象本身洒沦,而不是原先的 exports。
服務(wù)端的模塊放在哪里
也許你已經(jīng)注意到价淌,我們已經(jīng)在代碼中使用了模塊了申眼。像這樣:
var http = require("http");
...
http.createServer(...);
Node.js中自帶了一個(gè)叫做"http"的模塊瞒津,我們?cè)谖覀兊拇a中請(qǐng)求它并把返回值賦給一個(gè)本地變量。
這把我們的本地變量變成了一個(gè)擁有所有 http 模塊所提供的公共方法的對(duì)象括尸。
Node.js 的 require方法中的文件查找策略如下:
由于Node.js中存在4類模塊(原生模塊和3種文件模塊)巷蚪,盡管require方法極其簡單,但是內(nèi)部的加載卻是十分復(fù)雜的濒翻,其加載優(yōu)先級(jí)也各自不同屁柏。如下圖所示:
從文件模塊緩存中加載
盡管原生模塊與文件模塊的優(yōu)先級(jí)不同,但是都不會(huì)優(yōu)先于從文件模塊的緩存中加載已經(jīng)存在的模塊肴焊。
從原生模塊加載
原生模塊的優(yōu)先級(jí)僅次于文件模塊緩存的優(yōu)先級(jí)前联。require方法在解析文件名之后功戚,優(yōu)先檢查模塊是否在原生模塊列表中娶眷。以http模塊為例,盡管在目錄下存在一個(gè)http/http.js/http.node/http.json文件啸臀,require("http")都不會(huì)從這些文件中加載届宠,而是從原生模塊中加載。
原生模塊也有一個(gè)緩存區(qū)乘粒,同樣也是優(yōu)先從緩存區(qū)加載豌注。如果緩存區(qū)沒有被加載過,則調(diào)用原生模塊的加載方式進(jìn)行加載和執(zhí)行灯萍。
從文件加載
當(dāng)文件模塊緩存中不存在轧铁,而且不是原生模塊的時(shí)候,Node.js會(huì)解析require方法傳入的參數(shù)旦棉,并從文件系統(tǒng)中加載實(shí)際的文件齿风,加載過程中的包裝和編譯細(xì)節(jié)在前一節(jié)中已經(jīng)介紹過,這里我們將詳細(xì)描述查找文件模塊的過程绑洛,其中救斑,也有一些細(xì)節(jié)值得知曉。
require方法接受以下幾種參數(shù)的傳遞:
<li>http真屯、fs脸候、path等,原生模塊绑蔫。
<li>./mod或../mod运沦,相對(duì)路徑的文件模塊。
<li>/pathtomodule/mod配深,絕對(duì)路徑的文件模塊携添。
<li>mod,非原生模塊的文件模塊凉馆。
在路徑 Y 下執(zhí)行 require(X) 語句執(zhí)行順序:
1. 如果 X 是內(nèi)置模塊
a. 返回內(nèi)置模塊
b. 停止執(zhí)行
2. 如果 X 以 '/' 開頭
a. 設(shè)置 Y 為文件根路徑
3. 如果 X 以 './' 或 '/' or '../' 開頭
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. 拋出異常 "not found"
LOAD_AS_FILE(X)
1. 如果 X 是一個(gè)文件, 將 X 作為 JavaScript 文本載入并停止執(zhí)行薪寓。
2. 如果 X.js 是一個(gè)文件, 將 X.js 作為 JavaScript 文本載入并停止執(zhí)行亡资。
3. 如果 X.json 是一個(gè)文件, 解析 X.json 為 JavaScript 對(duì)象并停止執(zhí)行。
4. 如果 X.node 是一個(gè)文件, 將 X.node 作為二進(jìn)制插件載入并停止執(zhí)行向叉。
LOAD_INDEX(X)
1. 如果 X/index.js 是一個(gè)文件, 將 X/index.js 作為 JavaScript 文本載入并停止執(zhí)行锥腻。
2. 如果 X/index.json 是一個(gè)文件, 解析 X/index.json 為 JavaScript 對(duì)象并停止執(zhí)行。
3. 如果 X/index.node 是一個(gè)文件, 將 X/index.node 作為二進(jìn)制插件載入并停止執(zhí)行母谎。
LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json 是一個(gè)文件,
a. 解析 X/package.json, 并查找 "main" 字段瘦黑。
b. let M = X + (json main 字段)
c. LOAD_AS_FILE(M)
d. LOAD_INDEX(M)
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS
Node.js 函數(shù)
在JavaScript中,一個(gè)函數(shù)可以作為另一個(gè)函數(shù)的參數(shù)奇唤。我們可以先定義一個(gè)函數(shù)幸斥,然后傳遞,也可以在傳遞參數(shù)的地方直接定義函數(shù)咬扇。
Node.js中函數(shù)的使用與Javascript類似甲葬,舉例來說,你可以這樣做:
function say(word) {
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
以上代碼中懈贺,我們把 say 函數(shù)作為execute函數(shù)的第一個(gè)變量進(jìn)行了傳遞经窖。這里返回的不是 say 的返回值,而是 say 本身梭灿!
這樣一來画侣, say 就變成了execute 中的本地變量 someFunction ,execute可以通過調(diào)用 someFunction() (帶括號(hào)的形式)來使用 say 函數(shù)堡妒。
當(dāng)然配乱,因?yàn)?say 有一個(gè)變量, execute 在調(diào)用 someFunction 時(shí)可以傳遞這樣一個(gè)變量皮迟。
匿名函數(shù)
我們可以把一個(gè)函數(shù)作為變量傳遞搬泥。但是我們不一定要繞這個(gè)"先定義,再傳遞"的圈子万栅,我們可以直接在另一個(gè)函數(shù)的括號(hào)中定義和傳遞這個(gè)函數(shù):
function execute(someFunction,value){
someFunction(value);
}
execute(function(world){console.log(world)},'hello');
我們?cè)?execute 接受第一個(gè)參數(shù)的地方直接定義了我們準(zhǔn)備傳遞給 execute 的函數(shù)佑钾。
用這種方式,我們甚至不用給這個(gè)函數(shù)起名字烦粒,這也是為什么它被叫做匿名函數(shù) 休溶。
函數(shù)傳遞是如何讓HTTP服務(wù)器工作的
帶著這些知識(shí),我們?cè)賮砜纯次覀兒喖s而不簡單的HTTP服務(wù)器:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
現(xiàn)在它看上去應(yīng)該清晰了很多:我們向 createServer 函數(shù)傳遞了一個(gè)匿名函數(shù)扰她。
用這樣的代碼也可以達(dá)到同樣的目的:
var http = require("http");
function onRequest(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
Node.js 路由
我們要為路由提供請(qǐng)求的URL和其他需要的GET及POST參數(shù)兽掰,隨后路由需要根據(jù)這些數(shù)據(jù)來執(zhí)行相應(yīng)的代碼。
因此徒役,我們需要查看HTTP請(qǐng)求孽尽,從中提取出請(qǐng)求的URL以及GET/POST參數(shù)。這一功能應(yīng)當(dāng)屬于路由還是服務(wù)器(甚至作為一個(gè)模塊自身的功能)確實(shí)值得探討忧勿,但這里暫定其為我們的HTTP服務(wù)器的功能杉女。
我們需要的所有數(shù)據(jù)都會(huì)包含在request對(duì)象中瞻讽,該對(duì)象作為onRequest()回調(diào)函數(shù)的第一個(gè)參數(shù)傳遞。但是為了解析這些數(shù)據(jù)熏挎,我們需要額外的Node.JS模塊速勇,它們分別是url和querystring模塊。
url.parse(string).query
|
url.parse(string).pathname |
| |
| |
------ -------------------
http://localhost:8888/start?foo=bar&hello=world
--- -----
| |
| |
querystring.parse(queryString)["foo"] |
|
querystring.parse(queryString)["hello"]
當(dāng)然我們也可以用querystring模塊來解析POST請(qǐng)求體中的參數(shù).
現(xiàn)在我們來給onRequest()函數(shù)加上一些邏輯坎拐,用來找出瀏覽器請(qǐng)求的URL路徑:
var http = require("http");
var url = require("url");
var start = function() {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
好了烦磁,我們的應(yīng)用現(xiàn)在可以通過請(qǐng)求的URL路徑來區(qū)別不同請(qǐng)求了--這使我們得以使用路由(還未完成)來將請(qǐng)求以URL路徑為基準(zhǔn)映射到處理程序上。
在我們所要構(gòu)建的應(yīng)用中哼勇,這意味著來自/start和/upload的請(qǐng)求可以使用不同的代碼來處理都伪。稍后我們將看到這些內(nèi)容是如何整合到一起的。
現(xiàn)在我們可以來編寫路由了积担,建立一個(gè)名為 router.js 的文件陨晶,添加以下內(nèi)容:
function route(pathname) {
console.log("About to route a request for " + pathname);
}
exports.route = route;
如你所見,這段代碼什么也沒干磅轻,不過對(duì)于現(xiàn)在來說這是應(yīng)該的珍逸。在添加更多的邏輯以前逐虚,我們先來看看如何把路由和服務(wù)器整合起來聋溜。
我們的服務(wù)器應(yīng)當(dāng)知道路由的存在并加以有效利用。我們當(dāng)然可以通過硬編碼的方式將這一依賴項(xiàng)綁定到服務(wù)器上叭爱,但是其它語言的編程經(jīng)驗(yàn)告訴我們這會(huì)是一件非常痛苦的事撮躁,因此我們將使用依賴注入的方式較松散地添加路由模塊。
首先买雾,我們來擴(kuò)展一下服務(wù)器的start()函數(shù)把曼,以便將路由函數(shù)作為參數(shù)傳遞過去,server.js 文件代碼如下
var http = require("http");
var url = require("url");
function start(route) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
同時(shí)漓穿,我們會(huì)相應(yīng)擴(kuò)展index.js嗤军,使得路由函數(shù)可以被注入到服務(wù)器中:
var server = require('./main');
var router = require('./route');
server.start(router.route);
現(xiàn)在啟動(dòng)應(yīng)用(node index.js,始終記得這個(gè)命令行)晃危,隨后請(qǐng)求一個(gè)URL叙赚,你將會(huì)看到應(yīng)用輸出相應(yīng)的信息,這表明我們的HTTP服務(wù)器已經(jīng)在使用路由模塊了僚饭,并會(huì)將請(qǐng)求的路徑傳遞給路由:
瀏覽器訪問 http://127.0.0.1:8888/震叮,輸出結(jié)果如下: