Eloquent JavaScript 笔记 十九:Node.js

来源:互联网 发布:怎么上传图片到淘宝 编辑:程序博客网 时间:2024/06/05 14:41

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 + 12> [-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 argumentvar argument = process.argv[2];console.log(garble(argument));
运行:

$ node main.js JavaScriptOf{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.txtFile not found$ curl -X PUT -d hello http://localhost:8000/file.txt$ curl http://localhost:8000/file.txthello$ curl -X DELETE http://localhost:8000/file.txt$ curl http://localhost:8000/file.txtFile 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

这道题相当复杂,稍后再看。