JavaScript 編程精解 中文第三版 二10、Node.js

來源: ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目

原文:Node.jsjavascript

譯者:飛龍html

協議:CC BY-NC-SA 4.0java

自豪地採用谷歌翻譯node

部分參考了《JavaScript 編程精解(第 2 版)》git

A student asked 'The programmers of old used only simple machines and no programming languages, yet they made beautiful programs. Why do we use complicated machines and programming languages?'. Fu-Tzu replied 'The builders of old used only sticks and clay, yet they made beautiful huts.'github

Master Yuan-Ma,《The Book of Programming》正則表達式

到目前爲止,咱們已經使用了 JavaScript 語言,並將其運用於單一的瀏覽器環境中。本章和下一章將會大體介紹 Node.js,該程序可讓讀者將你的 JavaScirpt 技能運用於瀏覽器以外。讀者能夠運用 Node.js 構建應用程序,實現簡單的命令行工具和複雜動態 HTTP 服務器。apache

這些章節旨在告訴你創建 Node.js 的主要概念,並向你提供信息,使你能夠採用 Nodejs 編寫一些實用程序。它們並非這個平臺的完整的介紹。npm

若是你想要運行本章中的代碼,須要安裝 Node.js 10 或更高版本。 爲此,請訪問 nodejs.org,並按照用於你的操做系統的安裝說明進行操做。 你也能夠在那裏找到 Node.js 的更多文檔。編程

背景

編寫經過網絡通訊的系統時,一個更困難的問題是管理輸入輸出,即向/從網絡和硬盤讀寫數據。處處移動數據會耗費時間,而調度這些任務的技巧會使得系統在相應用戶或網絡請求時產生巨大的性能差別。

在這樣的程序中,異步編程一般是有幫助的。 它容許程序同時向/從多個設備發送和接收數據,而無需複雜的線程管理和同步。

Node最初是爲了使異步編程簡單方便而設計的。 JavaScript 很好地適應了像 Node 這樣的系統。 它是少數幾種沒有內置輸入和輸出方式的編程語言之一。 所以,JavaScript 能夠適應 Node 的至關古怪的輸入和輸出方法,而不會產生兩個不一致的接口。 在 2009 年設計 Node 時,人們已經在瀏覽器中進行基於回調的編程,因此該語言的社區用於異步編程風格。

Node 命令

在系統中安裝完 Node.js 後,Node.js 會提供一個名爲node的程序,該程序用於執行 JavaScript 文件。假設你有一個文件 hello.js,該文件會包含如下代碼。

let message = "Hello world";
console.log(message);

讀者能夠仿照下面這種方式經過命令行執行程序。

$ node hello.js
Hello world

Node 中的console.log方法與瀏覽器中所作的相似,都用於打印文本片斷。但在 Node 中,該方法不會將文本顯示在瀏覽器的 JavaScript 控制檯中,而顯示在標準輸出流中。從命令行運行node時,這意味着你會在終端中看到記錄的值。

若你執行node時不附帶任何參數,node會給出提示符,讀者能夠輸入 JavaScript 代碼並當即看到執行結果。

$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$

process綁定相似於console綁定,是 Node 中的全局綁定。該綁定提供了多種方式來監聽並操做當前程序。該綁定中的exit方法能夠結束進程並賦予一個退出狀態碼,告知啓動node的程序(在本例中時命令行 Shell),當前程序是成功完成(代碼爲 0),仍是遇到了錯誤(其餘代碼)。

讀者能夠讀取process.argv來獲取傳遞給腳本的命令行參數,該綁定是一個字符串數組。請注意該數組包括了node命令和腳本名稱,所以實際的參數從索引 2 處開始。若showargv.js只包含一條console.log(process.argv)語句,你能夠這樣執行該腳本。

$ node showargv.js one --and two
["node", "/tmp/showargv.js", "one", "--and", "two"]

全部標準 JavaScript 全局綁定,好比ArrayMath以及JSON也都存在於 Node 環境中。而與瀏覽器相關的功能,好比documentalert則不存在。

模塊

除了前文提到的一些綁定,好比consoleprocess,Node 在全局做用域中添加了不多綁定。若是你須要訪問其餘的內建功能,能夠經過system模塊獲取。

第十章中描述了基於require函數的 CommonJS 模塊系統。該系統是 Node 的內建模塊,用於在程序中裝載任何東西,從內建模塊,到下載的包,再到普通文件均可以。

調用require時,Node 會將給定的字符串解析爲可加載的實際文件。路徑名若以"/""./""../"開頭,則解析爲相對於當前模塊的路徑,其中"./"表示當前路徑,"../"表示當前路徑的上一級路徑,而"/"則表示文件系統根路徑。所以若你訪問從文件/tmp/robot/robot.js訪問"./graph",Node 會嘗試加載文件/tmp/robot/graph.js

.js擴展名可能會被忽略,若是這樣的文件存在,Node 會添加它。 若是所需的路徑指向一個目錄,則 Node 將嘗試加載該目錄中名爲index.js的文件。

當一個看起來不像是相對路徑或絕對路徑的字符串被賦給require時,按照假設,它引用了內置模塊,或者安裝在node_modules目錄中模塊。 例如,require("fs")會向你提供 Node 內置的文件系統模塊。 而require("robot")可能會嘗試加載node_modules/robot/中的庫。 安裝這種庫的一種常見方法是使用 NPM,咱們稍後講講它。

咱們來創建由兩個文件組成的小項目。 第一個稱爲main.js,並定義了一個腳本,能夠從命令行調用來反轉字符串。

const {reverse} = require("./reverse");

// Index 2 holds the first actual command-line argument
let argument = process.argv[2];

console.log(reverse(argument));

文件reverse.js中定義了一個庫,用於截取字符串,這個命令行工具,以及其餘須要直接訪問字符串反轉函數的腳本,均可以調用該庫。

exports.reverse = function(string) {
  return Array.from(string).reverse().join("");
};

請記住,將屬性添加到exports,會將它們添加到模塊的接口。 因爲 Node.js 將文件視爲 CommonJS 模塊,所以main.js能夠從reverse.js獲取導出的reverse函數。

咱們能夠看到咱們的工具執行結果以下所示。

$ node main.js JavaScript
tpircSavaJ

使用 NPM 安裝

第十章中介紹的 NPM,是一個 JavaScript 模塊的在線倉庫,其中大部分模塊是專門爲 Node 編寫的。當你在計算機上安裝 Node 時,你就會得到一個名爲npm的程序,提供了訪問該倉庫的簡易界面。

它的主要用途是下載包。 咱們在第十章中看到了ini包。 咱們可使用 NPM 在咱們的計算機上獲取並安裝該包。

$ npm install ini
npm WARN enoent ENOENT: no such file or directory,
         open '/tmp/package.json'
+ ini@1.3.5
added 1 package in 0.552s
$ node
> const {parse} = require("ini");
> parse("x = 1\ny = 2");
{ x: '1', y: '2' }

運行npm install後,NPM 將建立一個名爲node_modules的目錄。 該目錄內有一個包含庫的ini目錄。 你能夠打開它並查看代碼。 當咱們調用require("ini")時,加載這個庫,咱們能夠調用它的parse屬性來解析配置文件。

默認狀況下,NPM 在當前目錄下安裝包,而不是在中央位置。 若是你習慣於其餘包管理器,這可能看起來很不尋常,但它具備優點 - 它使每一個應用程序徹底控制它所安裝的包,而且使其在刪除應用程序時,更易於管理版本和清理。

包文件

npm install例子中,你能夠看到package.json文件不存在的警告。 建議爲每一個項目建立一個文件,手動或經過運行npm init。 它包含該項目的一些信息,例如其名稱和版本,並列出其依賴項。

來自第七章的機器人模擬,在第十章中模塊化,它可能有一個package.json文件,以下所示:

{
  "author": "Marijn Haverbeke",
  "name": "eloquent-javascript-robot",
  "description": "Simulation of a package-delivery robot",
  "version": "1.0.0",
  "main": "run.js",
  "dependencies": {
    "dijkstrajs": "^1.0.1",
    "random-item": "^1.0.0"
  },
  "license": "ISC"
}

當你運行npm install而沒有指定安裝包時,NPM 將安裝package.json中列出的依賴項。 當你安裝一個沒有列爲依賴項的特定包時,NPM會將它添加到package.json中。

版本

package.json文件列出了程序本身的版本和它的依賴的版本。 版本是一種方式,用於處理包的單獨演變。爲使用某個時候的包而編寫的代碼,可能不能使用包的更高版本。

NPM 要求其包遵循名爲語義版本控制(semantic versioning)的綱要,它編碼了版本號中的哪些版本是兼容的(不破壞就接口)。 語義版本由三個數字組成,用點分隔,例如2.3.0。 每次添加新功能時,中間數字都必須遞增。 每當破壞兼容性時,使用該包的現有代碼可能不適用於新版本,所以必須增長第一個數字。

package.json中的依賴項版本號前面的脫字符(^),表示能夠安裝兼容給定編號的任何版本。 例如"^2.3.0"意味着任何大於等於2.3.0且小於3.0.0的版本都是容許的。

npm命令也用於發佈新的包或包的新版本。 若是你在一個包含package.json文件的目錄中執行npm publish,它將一個包發佈到註冊處,帶有 JSON 文件中列出的名稱和版本。 任何人均可以將包發佈到 NPM - 但只能用新名稱,由於任何人能夠更新現有的包,會有點恐怖。

因爲npm程序是與開放系統(包註冊處)進行對話的軟件,所以它沒有什麼獨特之處。 另外一個程序yarn,能夠從 NPM 註冊處中安裝,使用一種不一樣的接口和安裝策略,與npm具備相同的做用。

本書不會深刻探討 NPM 的使用細節。 請參閱npmjs.org來獲取更多文檔和搜索包的方法。

文件系統模塊

在Node中最經常使用的內建模塊就是fs(表示 filesystem,文件系統)模塊。該模塊提供了處理文件和目錄的函數。

例如,有個函數名爲readFile,該函數讀取文件並調用回調,並將文件內容傳遞給回調。

let {readFile} = require("fs");
readFile("file.txt", "utf8", (error, text) => {
  if (error) throw error;
  console.log("The file contains:", text);
});

readFile的第二個參數表示字符編碼,用於將文件解碼成字符串。將文本編碼成二進制數據有許多方式,但大多數現代系統使用 UTF-8,所以除非有特殊緣由確信文件使用了別的編碼,不然讀取文件時使用"utf-8"是一種較爲安全的方式。若你不傳遞任何編碼,Node 會認爲你須要解析二進制數據,所以會返回一個Buffer對象而非字符串。該對象相似於數組,每一個元素是文件中字節(8 位的數據塊)對應的數字。

const {readFile} = require("fs");
readFile("file.txt", (error, buffer) => {
  if (error) throw error;
  console.log("The file contained", buffer.length, "bytes.",
              "The first byte is:", buffer[0]);
});

有一個名爲writeFile的函數與其相似,用於將文件寫到磁盤上。

const {writeFile} = require("fs");
writeFile("graffiti.txt", "Node was here", err => {
  if (err) console.log(`Failed to write file: ${err}`);
  else console.log("File written.");
});

這裏咱們不須要制定編碼,由於若是咱們調用writeFile時傳遞的是字符串而非Buffer對象,則writeFile會使用默認編碼(即 UTF-8)來輸出文本。

fs模塊也包含了其餘實用函數,其中readdir函數用於將目錄中的文件以字符串數組的方式返回,stat函數用於獲取文件信息,rename函數用於重命名文件,unlink用於刪除文件等。

並且其中大多數都將回調做爲最後一個參數,它們會以錯誤(第一個參數)或成功結果(第二個參數)來調用。 咱們在第十一章中看到,這種編程風格存在缺點 - 最大的缺點是,錯誤處理變得冗長且容易出錯。

相關細節請參見http://nodejs.org/中的文檔。

雖然Promise已經成爲 JavaScript 的一部分,可是,將它們與 Node.js 的集成的工做仍然還在進行中。 從 v10 開始,標準庫中有一個名爲fs/promises的包,它導出的函數與fs大部分相同,但使用Promise而不是回調。

const {readFile} = require("fs/promises");
readFile("file.txt", "utf8")
  .then(text => console.log("The file contains:", text));

有時候你不須要異步,而是須要阻塞。 fs中的許多函數也有同步的變體,它們的名稱相同,末尾加上Sync。 例如,readFile的同步版本稱爲readFileSync

const {readFileSync} = require("fs");
console.log("The file contains:",
            readFileSync("file.txt", "utf8"));

請注意,在執行這樣的同步操做時,程序徹底中止。 若是它應該響應用戶或網絡中的其餘計算機,那麼可在同步操做中可能會產生使人討厭的延遲。

HTTP 模塊

另外一個主要模塊名爲"http"。該模塊提供了執行 HTTP 服務和產生 HTTP 請求的函數。

啓動一個 HTTP 服務器只須要如下代碼。

const {createServer} = require("http");
let server = createServer((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);

若你在本身的機器上執行該腳本,你能夠打開網頁瀏覽器,並訪問 http://localhost:8000/hello,就會向你的服務器發出一個請求。服務器會響應一個簡單的 HTML 頁面。

每次客戶端嘗試鏈接服務器時,服務器都會調用傳遞給createServer函數的參數。requestresponse綁定都是對象,分別表示輸入數據和輸出數據。request包含請求信息,例如該對象的url屬性表示請求的 URL。

所以,當你在瀏覽器中打開該頁面時,它會向你本身的計算機發送請求。 這會致使服務器函數運行並返回一個響應,你能夠在瀏覽器中看到該響應。

你須要調用response對象的方法以將一些數據發回客戶端。第一個函數調用(writeHead)會輸出響應頭(參見第十七章)。你須要向該函數傳遞狀態碼(本例中 200 表示成功)和一個對象,該對象包含協議頭信息的值。該示例設置了"Content-Type"頭,通知客戶端咱們將發送一個 HTML 文檔。

接下來使用response.write來發送響應體(文檔自身)。若你想一段一段地發送相應信息,能夠屢次調用該方法,例如將數據發送到客戶端。最後調用response.end發送相應結束信號。

調用server.listen會使服務器在 8000 端口上開始等待請求。這就是你須要鏈接localhost:8000和服務器通訊,而不是localhost(這樣將會使用默認端口,即 80)的緣由。

當你運行這個腳本時,這個進程就在那裏等着。 當一個腳本正在監聽事件時 - 這裏是網絡鏈接 - Node 不會在到達腳本末尾時自動退出。爲了關閉它,請按Ctrl-C

一個真實的 Web 服務器須要作的事情比示例多得多。其差異在於咱們須要根據請求的方法(method屬性),來判斷客戶端嘗試執行的動做,並根據請求的 URL 來找出動做處理的資源。本章隨後會介紹更高級的服務器。

咱們可使用http模塊的request函數來充當一個 HTTP 客戶端。

const {request} = require("http");
let requestStream = request({
  hostname: "eloquentjavascript.net",
  path: "/20_node.html",
  method: "GET",
  headers: {Accept: "text/html"}
}, response => {
  console.log("Server responded with status code",
              response.statusCode);
});
requestStream.end();

request函數的第一個參數是請求配置,告知 Node 須要訪問的服務器、服務器請求地址、使用的方法等信息。第二個參數是響應開始時的回調。該回調會接受一個參數,用於檢查相應信息,例如獲取狀態碼。

和在服務器中看到的response對象同樣,request返回的對象容許咱們使用write方法屢次發送數據,並使用end方法結束髮送。本例中並無使用write方法,由於 GET 請求的請求正文中沒法包含數據。

https模塊中有相似的request函數,能夠用來向https: URL 發送請求。

可是使用 Node 的原始功能發送請求至關麻煩。 NPM 上有更多方便的包裝包。 例如,node-fetch提供了咱們從瀏覽器得知的,基於Promisefetch接口。

咱們在 HTTP 中看過兩個可寫流的例子,即服務器能夠向response對象中寫入數據,而request返回的請求對象也能夠寫入數據。

可寫流是 Node 中普遍使用的概念。這種對象擁有write方法,你能夠傳遞字符串或Buffer對象,來向流寫入一些數據。它們end方法用於關閉流,而且還能夠接受一個可選值,在流關閉以前將其寫入流。 這兩個方法也能夠接受回調做爲附加參數,當寫入或關閉完成時它們將被調用。

咱們也可使用fs模塊的createWriteStream,創建一個指向本地文件的輸出流。你能夠調用該方法返回的結果對象的write方法,每次向文件中寫入一段數據,而不是像writeFile那樣一次性寫入全部數據。

可讀流則略爲複雜。傳遞給 HTTP 服務器回調的request綁定,以及傳遞給 HTTP 客戶端回調的response對象都是可讀流(服務器讀取請求並寫入響應,而客戶端則先寫入請求,而後讀取響應)。讀取流須要使用事件處理器,而不是方法。

Node 中發出的事件都有一個on方法,相似瀏覽器中的addEventListener方法。該方法接受一個事件名和一個函數,並將函數註冊到事件上,接下來每當指定事件發生時,都會調用註冊的函數。

可讀流有data事件和end事件。data事件在每次數據到來時觸發,end事件在流結束時觸發。該模型適用於「流」數據,這類數據能夠當即處理,即便整個文檔的數據沒有到位。咱們可使用createReadStream函數建立一個可讀流,來讀取本地文件。

這段代碼建立了一個服務器並讀取請求正文,而後將讀取到的數據所有轉換成大寫,並使用流寫回客戶端。

const {createServer} = require("http");
createServer((request, response) => {
  response.writeHead(200, {"Content-Type": "text/plain"});
  request.on("data", chunk =>
    response.write(chunk.toString().toUpperCase()));
  request.on("end", () => response.end());
  });
}).listen(8000);

傳遞給data處理器的chunk值是一個二進制Buffer對象,咱們可使用它的toString方法,經過將其解碼爲 UTF-8 編碼的字符,來將其轉換爲字符串。

下面的一段代碼,和上面的服務(將字母轉換成大寫)一塊兒運行時,它會向服務器發送一個請求並輸出獲取到的響應數據:

const {request} = require("http");
request({
  hostname: "localhost",
  port: 8000,
  method: "POST"
}, response => {
  response.on("data", chunk =>
    process.stdout.write(chunk.toString()));
}).end("Hello server");
// → HELLO SERVER

該示例代碼向process.stdout(進程的標準輸出流,是一個可寫流)中寫入數據,而不使用console.log,由於console.log函數會在輸出的每段文本後加上額外的換行符,在這裏不太合適。

文件服務器

讓咱們結合新學習的 HTTP 服務器和文件系統的知識,並創建起二者之間的橋樑:使用 HTTP 服務容許客戶遠程訪問文件系統。這個服務有許多用處,它容許網絡應用程序存儲並共享數據或使得一組人能夠共享訪問一批文件。

當咱們將文件看成 HTTP 資源時,能夠將 HTTP 的 GET、PUT 和 DELETE 方法分別當作讀取、寫入和刪除文件。咱們將請求中的路徑解釋成請求指向的文件路徑。

咱們可能不但願共享整個文件系統,所以咱們將這些路徑解釋成以服務器工做路徑(即啓動服務器的路徑)爲起點的相對路徑。若從/home/marijn/public(或 Windows 下的C:\Users\marijn\public)啓動服務器,那麼對/file.txt的請求應該指向/home/marijn/public/file.txt(或C:\Users\marijn\public\file.txt)。

咱們將一段段地構建程序,使用名爲methods的對象來存儲處理多種 HTTP 方法的函數。方法處理器是async函數,它接受請求對象做爲參數並返回一個Promise,解析爲描述響應的對象。

const {createServer} = require("http");

const methods = Object.create(null);

createServer((request, response) => {
  let handler = methods[request.method] || notAllowed;
  handler(request)
    .catch(error => {
      if (error.status != null) return error;
      return {body: String(error), status: 500};
    })
    .then(({body, status = 200, type = "text/plain"}) => {
       response.writeHead(status, {"Content-Type": type});
       if (body && body.pipe) body.pipe(response);
       else response.end(body);
    });
}).listen(8000);

async function notAllowed(request) {
  return {
    status: 405,
    body: `Method ${request.method} not allowed.`
  };
}

這樣啓動服務器以後,服務器永遠只會產生 405 錯誤響應,該代碼表示服務器拒絕處理特定的方法。

當請求處理程序的Promise受到拒絕時,catch調用會將錯誤轉換爲響應對象(若是它還不是),以便服務器能夠發回錯誤響應,來通知客戶端它未能處理請求。

響應描述的status字段能夠省略,這種狀況下,默認爲 200(OK)。 type屬性中的內容類型也能夠被省略,這種狀況下,假定響應爲純文本。

body的值是可讀流時,它將有pipe方法,用於將全部內容從可讀流轉發到可寫流。 若是不是,則假定它是null(無正文),字符串或緩衝區,並直接傳遞給響應的end方法。

爲了弄清哪一個文件路徑對應於請求URL,urlPath函數使用 Node 的url內置模塊來解析 URL。 它接受路徑名,相似"/file.txt",將其解碼來去掉%20風格的轉義代碼,並相對於程序的工做目錄來解析它。

const {parse} = require("url");
const {resolve} = require("path");

const baseDirectory = process.cwd();

function urlPath(url) {
  let {pathname} = parse(url);
  let path = resolve(decodeURIComponent(pathname).slice(1));
  if (path != baseDirectory &&
      !path.startsWith(baseDirectory + "/")) {
    throw {status: 403, body: "Forbidden"};
  }
  return path;
}

只要你創建了一個接受網絡請求的程序,就必須開始關注安全問題。 在這種狀況下,若是咱們不當心,極可能會意外地將整個文件系統暴露給網絡。

文件路徑在 Node 中是字符串。 爲了將這樣的字符串映射爲實際的文件,須要大量有意義的解釋。 例如,路徑可能包含"../"來引用父目錄。 所以,一個顯而易見的問題來源是像/../ secret_file這樣的路徑請求。

爲了不這種問題,urlPath使用path模塊中的resolve函數來解析相對路徑。 而後驗證結果位於工做目錄下面。 process.cwd函數(其中cwd表明「當前工做目錄」)可用於查找此工做目錄。 當路徑不起始於基本目錄時,該函數將使用 HTTP 狀態碼來拋出錯誤響應對象,該狀態碼代表禁止訪問資源。

咱們須要建立GET方法,在讀取目錄時返回文件列表,在讀取普通文件時返回文件內容。

一個棘手的問題是咱們返回文件內容時添加的Content-Type頭應該是什麼類型。由於這些文件能夠是任何內容,咱們的服務器沒法簡單地對全部文件返回相同的內容類型。但 NPM 能夠幫助咱們完成該任務。mime包(以text/plain這種方式表示的內容類型,名爲 MIME 類型)能夠獲取大量文件擴展名的正確類型。

如下npm命令在服務器腳本所在的目錄中,安裝mime的特定版本。

$ npm install mime@2.2.0

當請求文件不存在時,應該返回的正確 HTTP 狀態碼是 404。咱們使用stat函數,來找出特定文件是否存在以及是不是一個目錄。

const {createReadStream} = require("fs");
const {stat, readdir} = require("fs/promises");
const mime = require("mime");

methods.GET = async function(request) {
  let path = urlPath(request.url);
  let stats;
  try {
    stats = await stat(path);
  } catch (error) {
    if (error.code != "ENOENT") throw error;
    else return {status: 404, body: "File not found"};
  }
  if (stats.isDirectory()) {
    return {body: (await readdir(path)).join("\n")};
  } else {
    return {body: createReadStream(path),
            type: mime.getType(path)};
  }
};

由於stat訪問磁盤須要耗費一些時間,所以該函數是異步的。因爲咱們使用Promise而不是回調風格,所以必須從fs/promises而不是fs導入。

當文件不存在時,stat會拋出一個錯誤對象,code屬性爲'ENOENT'。 這些有些模糊的,受 Unix 啓發的代碼,是你識別 Node 中的錯誤類型的方式。

stat返回的stats對象告訴了咱們文件的一系列信息,好比文件大小(size屬性)和修改日期(mtime屬性)。這裏咱們想知道的是,該文件是一個目錄仍是普通文件,isDirectory方法能夠告訴咱們答案。

咱們使用readdir來讀取目錄中的文件列表,並將其返回給客戶端。對於普通文件,咱們使用createReadStream建立一個可讀流,並將其傳遞給respond對象,同時使用mime模塊根據文件名獲取內容類型並傳遞給respond

處理DELETE請求的代碼就稍顯簡單了。

const {rmdir, unlink} = require("fs/promises");

methods.DELETE = async function(request) {
  let path = urlPath(request.url);
  let stats;
  try {
    stats = await stat(path);
  } catch (error) {
    if (error.code != "ENOENT") throw error;
    else return {status: 204};
  }
  if (stats.isDirectory()) await rmdir(path);
  else await unlink(path);
  return {status: 204};
};

當 HTTP 響應不包含任何數據時,狀態碼 204(「No Content」,無內容)可用於代表這一點。 因爲刪除的響應不須要傳輸任何信息,除了操做是否成功以外,在這裏返回是明智的。

你可能想知道,爲何試圖刪除不存在的文件會返回成功狀態代碼,而不是錯誤。 當被刪除的文件不存在時,能夠說該請求的目標已經完成。 HTTP 標準鼓勵咱們使請求是冪等(idempotent)的,這意味着,屢次發送相同請求的結果,會與一次相同。 從某種意義上說,若是你試圖刪除已經消失的東西,那麼你試圖去作的效果已經實現 - 東西已經不存在了。

下面是PUT請求的處理器。

const {createWriteStream} = require("fs");

function pipeStream(from, to) {
  return new Promise((resolve, reject) => {
    from.on("error", reject);
    to.on("error", reject);
    to.on("finish", resolve);
    from.pipe(to);
  });
}

methods.PUT = async function(request) {
  let path = urlPath(request.url);
  await pipeStream(request, createWriteStream(path));
  return {status: 204};
};

咱們不須要檢查文件是否存在,若是存在,只需覆蓋便可。咱們再次使用pipe來將可讀流中的數據移動到可寫流中,在本例中是將請求的數據移動到文件中。可是因爲pipe沒有爲返回Promise而編寫,因此咱們必須編寫包裝器pipeStream,它從調用pipe的結果中建立一個Promise

當打開文件createWriteStream時出現問題時仍然會返回一個流,可是這個流會觸發'error'事件。 例如,若是網絡出現故障,請求的輸出流也可能失敗。 因此咱們鏈接兩個流的'error'事件來拒絕Promise。 當pipe完成時,它會關閉輸出流,從而致使觸發'finish'事件。 這是咱們能夠成功解析Promise的地方(不返回任何內容)。

完整的服務器腳本請見eloquentjavascript.net/code/file_server.js。讀者能夠下載該腳本,而且在安裝依賴項以後,使用 Node 啓動你本身的文件服務器。固然你能夠修改並擴展該腳本,來完成本章的習題或進行實驗。

命令行工具curl在類 Unix 系統(好比 Mac 或者 Linux)中獲得普遍使用,可用於產生 HTTP 請求。接下來的會話用於簡單測試咱們的服務器。這裏須要注意,-x用於設置請求方法,-d用於包含請求正文。

$ 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

因爲file.txt一開始不存在,所以第一請求失敗。而PUT請求則建立文件,所以咱們看到下一個請求能夠成功獲取該文件。在使用DELETE請求刪除該文件後,第三次GET請求再次找不到該文件。

本章小結

Node 是一個不錯的小型系統,可以讓咱們在非瀏覽器環境下運行 JavaScript。Node 最初的設計意圖是完成網絡任務,扮演網絡中的節點。但同時也能用來執行任何腳本任務,若是你以爲編寫 JavaScript 代碼是一件愜意的事情,那麼使用 Node 來自動完成天天的任務是很是不錯的。

NPM 爲你所能想到的功能(固然還有至關多你想不到的)提供了包,你能夠經過使用npm程序,獲取並安裝這些包。Node 也附帶了許多內建模塊,包括fs模塊(處理文件系統)、http模塊(執行 HTTP 服務器並生成 HTTP 請求)。

Node 中的全部輸入輸出都是異步的,除非你明確使用函數的同步變體,好比readFileSync。當調用異步函數時,使用者提供回調,而且 Node 會在準備好的時候,使用錯誤值和結果(若是有的話)調用它們。

習題

搜索工具

在 Unix 系統上,有一個名爲grep的命令行工具,能夠用來在文件中快速搜索正則表達式。

編寫一個能夠從命令行運行的 Node 腳本,其行爲相似grep。 它將其第一個命令行參數視爲正則表達式,並將任何其餘參數視爲要搜索的文件。 它應該輸出內容與正則表達式匹配的,任何文件的名稱。

當它有效時,將其擴展,以便當其中一個參數是目錄時,它將搜索該目錄及其子目錄中的全部文件。

按照你認爲合適的方式,使用異步或同步文件系統函數。 配置一些東西,以便同時請求多個異步操做可能會加快速度,但不是很大,由於大多數文件系統一次只能讀取一個東西。

目錄建立

儘管咱們的文件服務器中的DELETE方法能夠刪除目錄(使用rmdir),但服務器目前不提供任何方法來建立目錄。

添加對MKCOL方法(「make column」)的支持,它應該經過調用fs模塊的mkdir建立一個目錄。 MKCOL並非普遍使用的 HTTP 方法,可是它在 WebDAV 標準中有相同的用途,這個標準在 HTTP 之上規定了一組適用於建立文檔的約定。

你可使用實現DELETE方法的函數,做爲MKCOL方法的藍圖。 當找不到文件時,嘗試用mkdir建立一個目錄。 當路徑中存在目錄時,能夠返回 204 響應,以便目錄建立請求是冪等的。 若是這裏存在非目錄文件,則返回錯誤代碼。 代碼 400(「Bad Request」,請求無效)是適當的。

網絡上的公共空間

因爲文件服務器提供了任何類型的文件服務,甚至只要包含正確的Content-Type協議頭,你可使用其提供網站服務。因爲該服務容許每一個人刪除或替換文件,所以這是一類很是有趣的網站:任何人只要使用正確的 HTTP 請求,均可以修改、改進並破壞文件。但這仍然是一個網站。

請編寫一個基礎的 HTML 頁面,包含一個簡單的 JavaScript 文件。將該文件放在文件服務器的數據目錄下,並在你的瀏覽器中打開這些文件。

接下來,做爲進階練習或是週末做業,將你迄今爲止在本書中學習到的內容整合起來,構建一個對用戶友好的界面,在網站內部修改網站。

使用 HTML 表單編輯組成網站的文件內容,容許用戶使用 HTTP 請求在服務器上更新它們,如第十八章所述。

剛開始的時候,該頁面僅容許用戶編輯單個文件,而後進行修改,容許選擇想要編輯的文件。向文件服務器發送請求時,若URL是一個目錄,服務器會返回該目錄下的文件列表,你能夠利用該特性實現你的網頁。

不要直接編輯文件服務器開放的代碼,若是你犯了什麼錯誤,頗有可能就破壞了你的代碼。相反,將你的代碼保存在公共訪問目錄以外,測試時再將其拷貝到公共目錄中。

相關文章
相關標籤/搜索