1. 程式人生 > >Eloquent JavaScript 筆記 十九:Node.js

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:

$ 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

> 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

這道題相當複雜,稍後再看。