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:
$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$
像console一样,process也是一个全局对象。用来控制当前CLI的当前进程。
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
运行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
这道题相当复杂,稍后再看。