Eloquent JavaScript 筆記 十九:Node.js
1. Background
可以略過。
2. Asynchronicity
講同步和非同步的基本原理,可以略過。
3. The Node Command
首先,訪問 nodejs.org 網站,安裝node.js。
3.1. 執行js檔案:
建立一個檔案 hello.js,檔案內容:
var message = "Hello world";
console.log(message);
在命令列下,執行:
$ node hello.js
輸出:
Hello world
在node.js 環境下,console.log() 輸出到 stdout。
3.2. 執行 node的CLI:
像console一樣,process也是一個全域性物件。用來控制當前CLI的當前程序。$ node > 1 + 1 2 > [-1, -2, -3].map(Math.abs) [1, 2, 3] > process.exit(0) $
3.3. 訪問命令列引數:
建立檔案 showarv.js,檔案內容:
console.log(process.argv);
沒看錯,就一行。 process.argv 是個陣列,包含了命令列傳入的引數。
$ node showargv.js one --and two
["node", "/home/marijn/showargv.js", "one", "--and", "two"]
3.4. 全域性變數
標準的JavaScript全域性變數在node.js中都可以訪問,例如:Array, Math, JSON 等。
Browser相關的全域性變數就不能訪問了,例如:document,alert等。
在Browser環境下的全域性物件 window,在node.js 中變成了 global。
4. Modules
node.js 環境下 built-in 的功能比較少,很多功能需要額外安裝module。
node.js 內建了 CommonJS module 系統。我們可以直接使用 require 包含modules。
4.1. require 引數:
1. require("/home/marijn/elife/run.js"); 絕對路徑
2. require("./run.js"); 當前目錄
3. require("../world/world.js"); 基於當前目錄的相對路徑
4. require("fs") 內建的module
5. require("elife") 安裝在 node_modules/elife/ 目錄下的module。 使用npm會把module安裝在 node_modules 目錄下。
4.2. 使用require引用當前目錄下的module
建立module檔案,garble.js:
module.exports = function(string) {
return string.split("").map(function(ch) {
return String.fromCharCode(ch.charCodeAt(0) + 5);
}).join("");
};
建立main.js, 引用garble.js:
var garble = require("./garble");
// Index 2 holds the first actual command-line argument
var argument = process.argv[2];
console.log(garble(argument));
執行:
$ node main.js JavaScript
Of{fXhwnuy
5. Installing with NPM
NPM - Node Package Manager
當安裝node.js時,同時也安裝了npm。
$ npm install figlet
$ node
> var figlet = require("figlet"); > figlet.text("Hello world!", function(error, data) { if (error) console.error(error); else console.log(data); }); _ _ _ _ _ _ _ | | | | ___| | | ___ __ _____ _ __| | __| | | | |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | | | _ | __/ | | (_) | \ V V / (_) | | | | (_| |_| |_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)執行npm install,會在當前目錄建立 node_modules 資料夾,下載的modules就儲存在這個資料夾中。
注意上面的 figlet.text() 函式,它是一個非同步函式,它需要訪問 figlet.text 檔案,搜尋每個字母對應的圖形。
I/O 操作通常是比較費時的,所以,都要做成非同步函式。它的第二個引數是個function,當I/O執行完之後被呼叫。
這是node.js 的通用模式,非同步 I/O 函式通常都是這個寫法。
我們也可以寫一個 package.json 檔案,在其中配置多個module,以及相互之間的依賴規則。當執行 npm install 時,它會自動搜尋此檔案。
npm 的詳細使用方法在 npmjs.org 。
6. The File System Module
6.1. 使用node.js 內建的 fs 模組讀取檔案:
var fs = require("fs");
fs.readFile("file.txt", "utf8", function(error, text) {
if (error)
throw error;
console.log("The file contained:", text);
});
readFile() 的第二個引數是檔案編碼,但三個引數是function,在I/O完成後被呼叫。
6.2. 讀取二進位制檔案:
var fs = require("fs");
fs.readFile("file.txt", function(error, buffer) {
if (error)
throw error;
console.log("The file contained", buffer.length, "bytes.",
"The first byte is:", buffer[0]);
});
不寫檔案編碼,就是按二進位制讀取,buffer是個陣列,按位元組儲存檔案內容。6.3. 寫入檔案:
var fs = require("fs");
fs.writeFile("graffiti.txt", "Node was here", function(err) {
if (err)
console.log("Failed to write file:", err);
else
console.log("File written.");
});
不指定檔案編碼,預設是utf8。
fs 模組還有好多方法。
6.4. 同步I/O
var fs = require("fs");
console.log(fs.readFileSync("file.txt", "utf8"));
7. The HTTP Module
使用內建的 http 模組可以構建完整的 HTTP Server。 (哈哈,相當於 nginx + PHP)
7.1. 建立 http server:
var http = require("http");
var server = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Hello!</h1><p>You asked for <code>" +
request.url + "</code></p>");
response.end();
});
server.listen(8000);
執行這個檔案會讓控制檯阻塞。
每來一個request請求都會呼叫一次 createServer()。
7.2. 建立 http client:
var http = require("http");
var req = {
hostname: "eloquentjavascript.net",
path: "/20_node.html",
method: "GET",
headers: {Accept: "text/html"}
};
var request = http.request(req, function(response) {
console.log("Server responded with status code",
response.statusCode);
});
request.end();
建立HTTPS連線,使用 https 模組,基本功能和http一樣。8. Streams
8.1. writable stream
7.1 中的response 和 7.2中的 request 都有個write() 方法,可以多次呼叫此方法傳送資料。這叫 writable stream。
6.3 中的writeFile() 方法不是stream,因為,呼叫一次就會把檔案清空,重新寫一遍。
fs 也有stream方法。使用fs.createWriteStream() 可以建立一個stream物件,在此物件上呼叫 write() 方法就可以像流那樣寫入了。
8.2. readable stream
server 端的request物件,和client端的response物件都是 readable stream。在event handler中,才能從stream中讀取資料。
有 “data" , "end" 事件。
fs.createReadStream() 建立檔案 readable stream。
8.3. on
類似於 addEventListener()
8.4. 例子:server
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
request.on("data", function(chunk) {
response.write(chunk.toString().toUpperCase());
});
request.on("end", function() {
response.end();
});
}).listen(8000);
這是一個web server,把客戶端傳送來的字串變成大寫,再發送回去。chunk 是二進位制buffer。
8.5. 例子:client
var http = require("http");
var request = http.request({
hostname: "localhost",
port: 8000,
method: "POST"
}, function(response) {
response.on("data", function(chunk) {
process.stdout.write(chunk.toString());
});
});
request.end("Hello server");
如果 8.4 的server正在執行,執行這個檔案會在控制檯輸入:HELLO SERVER
process.stdout() 也是一個 writable stream。
這裡不能使用 console.log() ,因為它會在每一次呼叫後面加換行符。
9. A Simple File Server
9.1. File Server 說明
構建一個HTTP server,使用者可以通過http request訪問server上的檔案系統。
GET 方法讀取檔案,PUT 方法寫入檔案,DELETE方法刪除檔案。
只能訪問server執行的當前目錄,不能訪問整個檔案系統。
9.2. server 骨架
var http = require("http"), fs = require("fs");
var methods = Object.create(null);
http.createServer(function(request, response) {
function respond(code, body, type) {
if (!type) type = "text/plain";
response.writeHead(code, {"Content-Type": type});
if (body && body.pipe)
body.pipe(response);
else
response.end(body);
}
if (request.method in methods)
methods[request.method](urlToPath(request.url),
respond, request);
else
respond(405, "Method " + request.method +
" not allowed.");
}).listen(8000);
說明:
1. methods 儲存檔案操作方法,屬性名是相應的http method(GET, PUT, DELETE),屬性值是對應的function。
2. 如果在methods中找不到相應的方法,則返回405.
3. pipe() 在readable stream和writable stream之間建立管道,自動把資料傳送過去。
9.3. urlToPath()
function urlToPath(url) {
var path = require("url").parse(url).pathname;
return "." + decodeURIComponent(path);
}
使用內建的url模組,把url轉換成 pathname。
9.4. Content-Type
server給client返回檔案時,需要知道檔案的型別。這需要用到mime模組,用npm安裝:
$ npm install mime
9.5. GET
methods.GET = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(404, "File not found");
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.readdir(path, function(error, files) {
if (error)
respond(500, error.toString());
else
respond(200, files.join("\n"));
});
else
respond(200, fs.createReadStream(path),
require("mime").lookup(path));
});
};
fs.stat() 讀取檔案狀態。fs.readdir() 讀取目錄下的檔案列表。這段程式碼挺直觀。9.6. DELETE
methods.DELETE = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(204);
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.rmdir(path, respondErrorOrNothing(respond));
else
fs.unlink(path, respondErrorOrNothing(respond));
});
};
刪除一個不存在的檔案,返回 204,為什麼呢? 2xx 代表成功,而不是error。
當一個檔案不存在,我們可以說DELETE請求已經被滿足了。而且,HTTP標準鼓勵我們,多次響應一個請求,最好返回相同的結果。
function respondErrorOrNothing(respond) {
return function(error) {
if (error)
respond(500, error.toString());
else
respond(204);
};
}
9.7. PUT
methods.PUT = function(path, respond, request) {
var outStream = fs.createWriteStream(path);
outStream.on("error", function(error) {
respond(500, error.toString());
});
outStream.on("finish", function() {
respond(204);
});
request.pipe(outStream);
};
這裡沒有檢查檔案是否存在。如果存在直接覆蓋。又一次用到了 pipe, 把request直接連線到 file stream上。
9.8. 執行
把上面實現的server執行起來,使用curl測試它的功能:
$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found
10. Error Handling
如果上面的file server執行中丟擲異常,會怎樣? 崩潰。 需要try ... catch 捕獲異常,try寫在哪裡呢? 所有的行為都是非同步的,我們需要寫好多的try,因為,每一個callback中都需要單獨捕獲異常,否則,異常會直接被拋到函式呼叫的棧頂。
寫那麼多的異常處理程式碼,本身就違背了 “異常” 的設計初衷。它的初衷是為了集中處理錯誤,避免錯誤處理程式碼層層巢狀。
很多node程式不怎麼處理異常,因為,從某種角度來講,出現異常就是出現了程式無法處理的錯誤,這時讓程式崩潰是正確的反應。
另一種辦法是使用Promise,它會捕獲所有異常,轉到錯誤分支。
看一個例子:
var Promise = require("promise");
var fs = require("fs");
var readFile = Promise.denodeify(fs.readFile);
readFile("file.txt", "utf8").then(function(content) {
console.log("The file contained: " + content);
}, function(error) {
console.log("Failed to read file: " + error);
});
Promise.denodeify() 把node函式Promise化 —— 還實現原來的功能,但返回一個Promise物件。
用這種方法重寫 file server 的GET方法:
methods.GET = function(path) {
return inspectPath(path).then(function(stats) {
if (!stats) // Does not exist
return {code: 404, body: "File not found"};
else if (stats.isDirectory())
return fsp.readdir(path).then(function(files) {
return {code: 200, body: files.join("\n")};
});
else
return {code: 200,
type: require("mime").lookup(path),
body: fs.createReadStream(path)};
});
};
function inspectPath(path) {
return fsp.stat(path).then(null, function(error) {
if (error.code == "ENOENT") return null;
else throw error;
});
}
11. Exercise: Content Negotiation, Again
用http.request() 實現第17章的習題一。
var http = require("http");
function readStreamAsString(stream, callback) {
var data = "";
stream.on("data", function(chunk) {
data += chunk.toString();
});
stream.on("end", function() {
callback(null, data);
});
stream.on("error", function(error) {
callback(error);
});
}
["text/plain", "text/html", "application/json"].forEach(function (type) {
var req = {
hostname: "eloquentjavascript.net",
path: "/author",
method: "GET",
headers: {"Accept": type}
};
var request = http.request(req, function (response) {
if (response.statusCode != 200) {
console.error("Request for " + type + " failed: " + response.statusMessage);
}
else {
readStreamAsString(response, function (error, data) {
if (error) throw error;
console.log("Type " + type + ": " + data);
});
}
});
request.end();
});
概念都明白了,輪到自己寫程式碼時,才發現快忘光了。 一定要開啟編輯器,不看答案,手敲一遍。
12. Exercise: Fixing a Leak
function urlToPath(url) {
var path = require("url").parse(url).pathname;
var decoded = decodeURIComponent(path);
return "." + decoded.replace(/(\/|\\)\.\.(\/|\\|$)/g, "/");
}
13. Exercise: Creating Directories
methods.MKCOL = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
fs.mkdir(path, respondErrorOrNothing(respond));
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
respond(204);
else
respond(400, "File exists");
});
};
14. Exercise: A Public Space on The Web
這道題相當複雜,稍後再看。