国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

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

qqlcbb / 1407人閱讀

摘要:在這樣的程序中,異步編程通常是有幫助的。最初是為了使異步編程簡單方便而設(shè)計的。在年設(shè)計時,人們已經(jīng)在瀏覽器中進行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風格。

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

原文:Node.js

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

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

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."

Master Yuan-Ma,《The Book of Programming》

到目前為止,我們已經(jīng)使用了 JavaScript 語言,并將其運用于單一的瀏覽器環(huán)境中。本章和下一章將會大致介紹 Node.js,該程序可以讓讀者將你的 JavaScirpt 技能運用于瀏覽器之外。讀者可以運用 Node.js 構(gòu)建應(yīng)用程序,實現(xiàn)簡單的命令行工具和復(fù)雜動態(tài) HTTP 服務(wù)器。

這些章節(jié)旨在告訴你建立 Node.js 的主要概念,并向你提供信息,使你可以采用 Nodejs 編寫一些實用程序。它們并不是這個平臺的完整的介紹。

如果你想要運行本章中的代碼,需要安裝 Node.js 10 或更高版本。 為此,請訪問 nodejs.org,并按照用于你的操作系統(tǒng)的安裝說明進行操作。 你也可以在那里找到 Node.js 的更多文檔。

背景

編寫通過網(wǎng)絡(luò)通信的系統(tǒng)時,一個更困難的問題是管理輸入輸出,即向/從網(wǎng)絡(luò)和硬盤讀寫數(shù)據(jù)。到處移動數(shù)據(jù)會耗費時間,而調(diào)度這些任務(wù)的技巧會使得系統(tǒng)在相應(yīng)用戶或網(wǎng)絡(luò)請求時產(chǎn)生巨大的性能差異。

在這樣的程序中,異步編程通常是有幫助的。 它允許程序同時向/從多個設(shè)備發(fā)送和接收數(shù)據(jù),而無需復(fù)雜的線程管理和同步。

Node最初是為了使異步編程簡單方便而設(shè)計的。 JavaScript 很好地適應(yīng)了像 Node 這樣的系統(tǒng)。 它是少數(shù)幾種沒有內(nèi)置輸入和輸出方式的編程語言之一。 因此,JavaScript 可以適應(yīng) Node 的相當古怪的輸入和輸出方法,而不會產(chǎn)生兩個不一致的接口。 在 2009 年設(shè)計 Node 時,人們已經(jīng)在瀏覽器中進行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風格。

Node 命令

在系統(tǒng)中安裝完 Node.js 后,Node.js 會提供一個名為node的程序,該程序用于執(zhí)行 JavaScript 文件。假設(shè)你有一個文件 hello.js,該文件會包含以下代碼。

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

讀者可以仿照下面這種方式通過命令行執(zhí)行程序。

$ node hello.js
Hello world

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

若你執(zhí)行node時不附帶任何參數(shù),node會給出提示符,讀者可以輸入 JavaScript 代碼并立即看到執(zhí)行結(jié)果。

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

process綁定類似于console綁定,是 Node 中的全局綁定。該綁定提供了多種方式來監(jiān)聽并操作當前程序。該綁定中的exit方法可以結(jié)束進程并賦予一個退出狀態(tài)碼,告知啟動node的程序(在本例中時命令行 Shell),當前程序是成功完成(代碼為 0),還是遇到了錯誤(其他代碼)。

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

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

所有標準 JavaScript 全局綁定,比如ArrayMath以及JSON也都存在于 Node 環(huán)境中。而與瀏覽器相關(guān)的功能,比如documentalert則不存在。

模塊

除了前文提到的一些綁定,比如consoleprocess,Node 在全局作用域中添加了很少綁定。如果你需要訪問其他的內(nèi)建功能,可以通過system模塊獲取。

第十章中描述了基于require函數(shù)的 CommonJS 模塊系統(tǒng)。該系統(tǒng)是 Node 的內(nèi)建模塊,用于在程序中裝載任何東西,從內(nèi)建模塊,到下載的包,再到普通文件都可以。

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

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

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

我們來建立由兩個文件組成的小項目。 第一個稱為main.js,并定義了一個腳本,可以從命令行調(diào)用來反轉(zhuǎn)字符串。

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

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

console.log(reverse(argument));

文件reverse.js中定義了一個庫,用于截取字符串,這個命令行工具,以及其他需要直接訪問字符串反轉(zhuǎn)函數(shù)的腳本,都可以調(diào)用該庫。

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

請記住,將屬性添加到exports,會將它們添加到模塊的接口。 由于 Node.js 將文件視為 CommonJS 模塊,因此main.js可以從reverse.js獲取導(dǎo)出的reverse函數(shù)。

我們可以看到我們的工具執(zhí)行結(jié)果如下所示。

$ 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
y = 2");
{ x: "1", y: "2" }

運行npm install后,NPM 將創(chuàng)建一個名為node_modules的目錄。 該目錄內(nèi)有一個包含庫的ini目錄。 你可以打開它并查看代碼。 當我們調(diào)用require("ini")時,加載這個庫,我們可以調(diào)用它的parse屬性來解析配置文件。

默認情況下,NPM 在當前目錄下安裝包,而不是在中央位置。 如果你習(xí)慣于其他包管理器,這可能看起來很不尋常,但它具有優(yōu)勢 - 它使每個應(yīng)用程序完全控制它所安裝的包,并且使其在刪除應(yīng)用程序時,更易于管理版本和清理。

包文件

npm install例子中,你可以看到package.json文件不存在的警告。 建議為每個項目創(chuàng)建一個文件,手動或通過運行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)的綱要,它編碼了版本號中的哪些版本是兼容的(不破壞就接口)。 語義版本由三個數(shù)字組成,用點分隔,例如2.3.0。 每次添加新功能時,中間數(shù)字都必須遞增。 每當破壞兼容性時,使用該包的現(xiàn)有代碼可能不適用于新版本,因此必須增加第一個數(shù)字。

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

npm命令也用于發(fā)布新的包或包的新版本。 如果你在一個包含package.json文件的目錄中執(zhí)行npm publish,它將一個包發(fā)布到注冊處,帶有 JSON 文件中列出的名稱和版本。 任何人都可以將包發(fā)布到 NPM - 但只能用新名稱,因為任何人可以更新現(xiàn)有的包,會有點恐怖。

由于npm程序是與開放系統(tǒng)(包注冊處)進行對話的軟件,因此它沒有什么獨特之處。 另一個程序yarn,可以從 NPM 注冊處中安裝,使用一種不同的接口和安裝策略,與npm具有相同的作用。

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

文件系統(tǒng)模塊

在Node中最常用的內(nèi)建模塊就是fs(表示 filesystem,文件系統(tǒng))模塊。該模塊提供了處理文件和目錄的函數(shù)。

例如,有個函數(shù)名為readFile,該函數(shù)讀取文件并調(diào)用回調(diào),并將文件內(nèi)容傳遞給回調(diào)。

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

readFile的第二個參數(shù)表示字符編碼,用于將文件解碼成字符串。將文本編碼成二進制數(shù)據(jù)有許多方式,但大多數(shù)現(xiàn)代系統(tǒng)使用 UTF-8,因此除非有特殊原因確信文件使用了別的編碼,否則讀取文件時使用"utf-8"是一種較為安全的方式。若你不傳遞任何編碼,Node 會認為你需要解析二進制數(shù)據(jù),因此會返回一個Buffer對象而非字符串。該對象類似于數(shù)組,每個元素是文件中字節(jié)(8 位的數(shù)據(jù)塊)對應(yīng)的數(shù)字。

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的函數(shù)與其類似,用于將文件寫到磁盤上。

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.");
});

這里我們不需要制定編碼,因為如果我們調(diào)用writeFile時傳遞的是字符串而非Buffer對象,則writeFile會使用默認編碼(即 UTF-8)來輸出文本。

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

而且其中大多數(shù)都將回調(diào)作為最后一個參數(shù),它們會以錯誤(第一個參數(shù))或成功結(jié)果(第二個參數(shù))來調(diào)用。 我們在第十一章中看到,這種編程風格存在缺點 - 最大的缺點是,錯誤處理變得冗長且容易出錯。

相關(guān)細節(jié)請參見http://nodejs.org/中的文檔。

雖然Promise已經(jīng)成為 JavaScript 的一部分,但是,將它們與 Node.js 的集成的工作仍然還在進行中。 從 v10 開始,標準庫中有一個名為fs/promises的包,它導(dǎo)出的函數(shù)與fs大部分相同,但使用Promise而不是回調(diào)。

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

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

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

請注意,在執(zhí)行這樣的同步操作時,程序完全停止。 如果它應(yīng)該響應(yīng)用戶或網(wǎng)絡(luò)中的其他計算機,那么可在同步操作中可能會產(chǎn)生令人討厭的延遲。

HTTP 模塊

另一個主要模塊名為"http"。該模塊提供了執(zhí)行 HTTP 服務(wù)和產(chǎn)生 HTTP 請求的函數(shù)。

啟動一個 HTTP 服務(wù)器只需要以下代碼。

const {createServer} = require("http");
let server = createServer((request, response) => {
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write(`
    

Hello!

You asked for ${request.url}

`); response.end(); }); server.listen(8000);

若你在自己的機器上執(zhí)行該腳本,你可以打開網(wǎng)頁瀏覽器,并訪問 http://localhost:8000/hello,就會向你的服務(wù)器發(fā)出一個請求。服務(wù)器會響應(yīng)一個簡單的 HTML 頁面。

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

因此,當你在瀏覽器中打開該頁面時,它會向你自己的計算機發(fā)送請求。 這會導(dǎo)致服務(wù)器函數(shù)運行并返回一個響應(yīng),你可以在瀏覽器中看到該響應(yīng)。

你需要調(diào)用response對象的方法以將一些數(shù)據(jù)發(fā)回客戶端。第一個函數(shù)調(diào)用(writeHead)會輸出響應(yīng)頭(參見第十七章)。你需要向該函數(shù)傳遞狀態(tài)碼(本例中 200 表示成功)和一個對象,該對象包含協(xié)議頭信息的值。該示例設(shè)置了"Content-Type"頭,通知客戶端我們將發(fā)送一個 HTML 文檔。

接下來使用response.write來發(fā)送響應(yīng)體(文檔自身)。若你想一段一段地發(fā)送相應(yīng)信息,可以多次調(diào)用該方法,例如將數(shù)據(jù)發(fā)送到客戶端。最后調(diào)用response.end發(fā)送相應(yīng)結(jié)束信號。

調(diào)用server.listen會使服務(wù)器在 8000 端口上開始等待請求。這就是你需要連接localhost:8000和服務(wù)器通信,而不是localhost(這樣將會使用默認端口,即 80)的原因。

當你運行這個腳本時,這個進程就在那里等著。 當一個腳本正在監(jiān)聽事件時 - 這里是網(wǎng)絡(luò)連接 - Node 不會在到達腳本末尾時自動退出。為了關(guān)閉它,請按Ctrl-C

一個真實的 Web 服務(wù)器需要做的事情比示例多得多。其差別在于我們需要根據(jù)請求的方法(method屬性),來判斷客戶端嘗試執(zhí)行的動作,并根據(jù)請求的 URL 來找出動作處理的資源。本章隨后會介紹更高級的服務(wù)器。

我們可以使用http模塊的request函數(shù)來充當一個 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函數(shù)的第一個參數(shù)是請求配置,告知 Node 需要訪問的服務(wù)器、服務(wù)器請求地址、使用的方法等信息。第二個參數(shù)是響應(yīng)開始時的回調(diào)。該回調(diào)會接受一個參數(shù),用于檢查相應(yīng)信息,例如獲取狀態(tài)碼。

和在服務(wù)器中看到的response對象一樣,request返回的對象允許我們使用write方法多次發(fā)送數(shù)據(jù),并使用end方法結(jié)束發(fā)送。本例中并沒有使用write方法,因為 GET 請求的請求正文中無法包含數(shù)據(jù)。

https模塊中有類似的request函數(shù),可以用來向https: URL 發(fā)送請求。

但是使用 Node 的原始功能發(fā)送請求相當麻煩。 NPM 上有更多方便的包裝包。 例如,node-fetch提供了我們從瀏覽器得知的,基于Promisefetch接口。

我們在 HTTP 中看過兩個可寫流的例子,即服務(wù)器可以向response對象中寫入數(shù)據(jù),而request返回的請求對象也可以寫入數(shù)據(jù)。

可寫流是 Node 中廣泛使用的概念。這種對象擁有write方法,你可以傳遞字符串或Buffer對象,來向流寫入一些數(shù)據(jù)。它們end方法用于關(guān)閉流,并且還可以接受一個可選值,在流關(guān)閉之前將其寫入流。 這兩個方法也可以接受回調(diào)作為附加參數(shù),當寫入或關(guān)閉完成時它們將被調(diào)用。

我們也可以使用fs模塊的createWriteStream,建立一個指向本地文件的輸出流。你可以調(diào)用該方法返回的結(jié)果對象的write方法,每次向文件中寫入一段數(shù)據(jù),而不是像writeFile那樣一次性寫入所有數(shù)據(jù)。

可讀流則略為復(fù)雜。傳遞給 HTTP 服務(wù)器回調(diào)的request綁定,以及傳遞給 HTTP 客戶端回調(diào)的response對象都是可讀流(服務(wù)器讀取請求并寫入響應(yīng),而客戶端則先寫入請求,然后讀取響應(yīng))。讀取流需要使用事件處理器,而不是方法。

Node 中發(fā)出的事件都有一個on方法,類似瀏覽器中的addEventListener方法。該方法接受一個事件名和一個函數(shù),并將函數(shù)注冊到事件上,接下來每當指定事件發(fā)生時,都會調(diào)用注冊的函數(shù)。

可讀流有data事件和end事件。data事件在每次數(shù)據(jù)到來時觸發(fā),end事件在流結(jié)束時觸發(fā)。該模型適用于“流”數(shù)據(jù),這類數(shù)據(jù)可以立即處理,即使整個文檔的數(shù)據(jù)沒有到位。我們可以使用createReadStream函數(shù)創(chuàng)建一個可讀流,來讀取本地文件。

這段代碼創(chuàng)建了一個服務(wù)器并讀取請求正文,然后將讀取到的數(shù)據(jù)全部轉(zhuǎn)換成大寫,并使用流寫回客戶端。

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 編碼的字符,來將其轉(zhuǎn)換為字符串。

下面的一段代碼,和上面的服務(wù)(將字母轉(zhuǎn)換成大寫)一起運行時,它會向服務(wù)器發(fā)送一個請求并輸出獲取到的響應(yīng)數(shù)據(jù):

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(進程的標準輸出流,是一個可寫流)中寫入數(shù)據(jù),而不使用console.log,因為console.log函數(shù)會在輸出的每段文本后加上額外的換行符,在這里不太合適。

文件服務(wù)器

讓我們結(jié)合新學(xué)習(xí)的 HTTP 服務(wù)器和文件系統(tǒng)的知識,并建立起兩者之間的橋梁:使用 HTTP 服務(wù)允許客戶遠程訪問文件系統(tǒng)。這個服務(wù)有許多用處,它允許網(wǎng)絡(luò)應(yīng)用程序存儲并共享數(shù)據(jù)或使得一組人可以共享訪問一批文件。

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

我們可能不希望共享整個文件系統(tǒng),因此我們將這些路徑解釋成以服務(wù)器工作路徑(即啟動服務(wù)器的路徑)為起點的相對路徑。若從/home/marijn/public(或 Windows 下的C:Usersmarijnpublic)啟動服務(wù)器,那么對/file.txt的請求應(yīng)該指向/home/marijn/public/file.txt(或C:Usersmarijnpublicfile.txt)。

我們將一段段地構(gòu)建程序,使用名為methods的對象來存儲處理多種 HTTP 方法的函數(shù)。方法處理器是async函數(shù),它接受請求對象作為參數(shù)并返回一個Promise,解析為描述響應(yīng)的對象。

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.`
  };
}

這樣啟動服務(wù)器之后,服務(wù)器永遠只會產(chǎn)生 405 錯誤響應(yīng),該代碼表示服務(wù)器拒絕處理特定的方法。

當請求處理程序的Promise受到拒絕時,catch調(diào)用會將錯誤轉(zhuǎn)換為響應(yīng)對象(如果它還不是),以便服務(wù)器可以發(fā)回錯誤響應(yīng),來通知客戶端它未能處理請求。

響應(yīng)描述的status字段可以省略,這種情況下,默認為 200(OK)。 type屬性中的內(nèi)容類型也可以被省略,這種情況下,假定響應(yīng)為純文本。

body的值是可讀流時,它將有pipe方法,用于將所有內(nèi)容從可讀流轉(zhuǎn)發(fā)到可寫流。 如果不是,則假定它是null(無正文),字符串或緩沖區(qū),并直接傳遞給響應(yīng)的end方法。

為了弄清哪個文件路徑對應(yīng)于請求URL,urlPath函數(shù)使用 Node 的url內(nèi)置模塊來解析 URL。 它接受路徑名,類似"/file.txt",將其解碼來去掉%20風格的轉(zhuǎn)義代碼,并相對于程序的工作目錄來解析它。

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;
}

只要你建立了一個接受網(wǎng)絡(luò)請求的程序,就必須開始關(guān)注安全問題。 在這種情況下,如果我們不小心,很可能會意外地將整個文件系統(tǒng)暴露給網(wǎng)絡(luò)。

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

為了避免這種問題,urlPath使用path模塊中的resolve函數(shù)來解析相對路徑。 然后驗證結(jié)果位于工作目錄下面。 process.cwd函數(shù)(其中cwd代表“當前工作目錄”)可用于查找此工作目錄。 當路徑不起始于基本目錄時,該函數(shù)將使用 HTTP 狀態(tài)碼來拋出錯誤響應(yīng)對象,該狀態(tài)碼表明禁止訪問資源。

我們需要創(chuàng)建GET方法,在讀取目錄時返回文件列表,在讀取普通文件時返回文件內(nèi)容。

一個棘手的問題是我們返回文件內(nèi)容時添加的Content-Type頭應(yīng)該是什么類型。因為這些文件可以是任何內(nèi)容,我們的服務(wù)器無法簡單地對所有文件返回相同的內(nèi)容類型。但 NPM 可以幫助我們完成該任務(wù)。mime包(以text/plain這種方式表示的內(nèi)容類型,名為 MIME 類型)可以獲取大量文件擴展名的正確類型。

以下npm命令在服務(wù)器腳本所在的目錄中,安裝mime的特定版本。

$ npm install mime@2.2.0

當請求文件不存在時,應(yīng)該返回的正確 HTTP 狀態(tài)碼是 404。我們使用stat函數(shù),來找出特定文件是否存在以及是否是一個目錄。

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("
")};
  } else {
    return {body: createReadStream(path),
            type: mime.getType(path)};
  }
};

因為stat訪問磁盤需要耗費一些時間,因此該函數(shù)是異步的。由于我們使用Promise而不是回調(diào)風格,因此必須從fs/promises而不是fs導(dǎo)入。

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

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

我們使用readdir來讀取目錄中的文件列表,并將其返回給客戶端。對于普通文件,我們使用createReadStream創(chuàng)建一個可讀流,并將其傳遞給respond對象,同時使用mime模塊根據(jù)文件名獲取內(nèi)容類型并傳遞給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 響應(yīng)不包含任何數(shù)據(jù)時,狀態(tài)碼 204(“No Content”,無內(nèi)容)可用于表明這一點。 由于刪除的響應(yīng)不需要傳輸任何信息,除了操作是否成功之外,在這里返回是明智的。

你可能想知道,為什么試圖刪除不存在的文件會返回成功狀態(tài)代碼,而不是錯誤。 當被刪除的文件不存在時,可以說該請求的目標已經(jīng)完成。 HTTP 標準鼓勵我們使請求是冪等(idempotent)的,這意味著,多次發(fā)送相同請求的結(jié)果,會與一次相同。 從某種意義上說,如果你試圖刪除已經(jīng)消失的東西,那么你試圖去做的效果已經(jīng)實現(xiàn) - 東西已經(jīng)不存在了。

下面是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來將可讀流中的數(shù)據(jù)移動到可寫流中,在本例中是將請求的數(shù)據(jù)移動到文件中。但是由于pipe沒有為返回Promise而編寫,所以我們必須編寫包裝器pipeStream,它從調(diào)用pipe的結(jié)果中創(chuàng)建一個Promise

當打開文件createWriteStream時出現(xiàn)問題時仍然會返回一個流,但是這個流會觸發(fā)"error"事件。 例如,如果網(wǎng)絡(luò)出現(xiàn)故障,請求的輸出流也可能失敗。 所以我們連接兩個流的"error"事件來拒絕Promise。 當pipe完成時,它會關(guān)閉輸出流,從而導(dǎo)致觸發(fā)"finish"事件。 這是我們可以成功解析Promise的地方(不返回任何內(nèi)容)。

完整的服務(wù)器腳本請見eloquentjavascript.net/code/file_server.js。讀者可以下載該腳本,并且在安裝依賴項之后,使用 Node 啟動你自己的文件服務(wù)器。當然你可以修改并擴展該腳本,來完成本章的習(xí)題或進行實驗。

命令行工具curl在類 Unix 系統(tǒng)(比如 Mac 或者 Linux)中得到廣泛使用,可用于產(chǎn)生 HTTP 請求。接下來的會話用于簡單測試我們的服務(wù)器。這里需要注意,-x用于設(shè)置請求方法,-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請求則創(chuàng)建文件,因此我們看到下一個請求可以成功獲取該文件。在使用DELETE請求刪除該文件后,第三次GET請求再次找不到該文件。

本章小結(jié)

Node 是一個不錯的小型系統(tǒng),可讓我們在非瀏覽器環(huán)境下運行 JavaScript。Node 最初的設(shè)計意圖是完成網(wǎng)絡(luò)任務(wù),扮演網(wǎng)絡(luò)中的節(jié)點。但同時也能用來執(zhí)行任何腳本任務(wù),如果你覺得編寫 JavaScript 代碼是一件愜意的事情,那么使用 Node 來自動完成每天的任務(wù)是非常不錯的。

NPM 為你所能想到的功能(當然還有相當多你想不到的)提供了包,你可以通過使用npm程序,獲取并安裝這些包。Node 也附帶了許多內(nèi)建模塊,包括fs模塊(處理文件系統(tǒng))、http模塊(執(zhí)行 HTTP 服務(wù)器并生成 HTTP 請求)。

Node 中的所有輸入輸出都是異步的,除非你明確使用函數(shù)的同步變體,比如readFileSync。當調(diào)用異步函數(shù)時,使用者提供回調(diào),并且 Node 會在準備好的時候,使用錯誤值和結(jié)果(如果有的話)調(diào)用它們。

習(xí)題 搜索工具

在 Unix 系統(tǒng)上,有一個名為grep的命令行工具,可以用來在文件中快速搜索正則表達式。

編寫一個可以從命令行運行的 Node 腳本,其行為類似grep。 它將其第一個命令行參數(shù)視為正則表達式,并將任何其他參數(shù)視為要搜索的文件。 它應(yīng)該輸出內(nèi)容與正則表達式匹配的,任何文件的名稱。

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

按照你認為合適的方式,使用異步或同步文件系統(tǒng)函數(shù)。 配置一些東西,以便同時請求多個異步操作可能會加快速度,但不是很大,因為大多數(shù)文件系統(tǒng)一次只能讀取一個東西。

目錄創(chuàng)建

盡管我們的文件服務(wù)器中的DELETE方法可以刪除目錄(使用rmdir),但服務(wù)器目前不提供任何方法來創(chuàng)建目錄。

添加對MKCOL方法(“make column”)的支持,它應(yīng)該通過調(diào)用fs模塊的mkdir創(chuàng)建一個目錄。 MKCOL并不是廣泛使用的 HTTP 方法,但是它在 WebDAV 標準中有相同的用途,這個標準在 HTTP 之上規(guī)定了一組適用于創(chuàng)建文檔的約定。

你可以使用實現(xiàn)DELETE方法的函數(shù),作為MKCOL方法的藍圖。 當找不到文件時,嘗試用mkdir創(chuàng)建一個目錄。 當路徑中存在目錄時,可以返回 204 響應(yīng),以便目錄創(chuàng)建請求是冪等的。 如果這里存在非目錄文件,則返回錯誤代碼。 代碼 400(“Bad Request”,請求無效)是適當?shù)摹?/p> 網(wǎng)絡(luò)上的公共空間

由于文件服務(wù)器提供了任何類型的文件服務(wù),甚至只要包含正確的Content-Type協(xié)議頭,你可以使用其提供網(wǎng)站服務(wù)。由于該服務(wù)允許每個人刪除或替換文件,因此這是一類非常有趣的網(wǎng)站:任何人只要使用正確的 HTTP 請求,都可以修改、改進并破壞文件。但這仍然是一個網(wǎng)站。

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

接下來,作為進階練習(xí)或是周末作業(yè),將你迄今為止在本書中學(xué)習(xí)到的內(nèi)容整合起來,構(gòu)建一個對用戶友好的界面,在網(wǎng)站內(nèi)部修改網(wǎng)站。

使用 HTML 表單編輯組成網(wǎng)站的文件內(nèi)容,允許用戶使用 HTTP 請求在服務(wù)器上更新它們,如第十八章所述。

剛開始的時候,該頁面僅允許用戶編輯單個文件,然后進行修改,允許選擇想要編輯的文件。向文件服務(wù)器發(fā)送請求時,若URL是一個目錄,服務(wù)器會返回該目錄下的文件列表,你可以利用該特性實現(xiàn)你的網(wǎng)頁。

不要直接編輯文件服務(wù)器開放的代碼,如果你犯了什么錯誤,很有可能就破壞了你的代碼。相反,將你的代碼保存在公共訪問目錄之外,測試時再將其拷貝到公共目錄中。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105072.html

相關(guān)文章

  • JavaScript 編程精解 中文三版 二十一、項目:技能分享網(wǎng)站

    摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會是一個活動,其中興趣相同的人聚在一起,針對他們所知的事情進行小型非正式的展示。所有接口均以路徑為中心。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: Skill-Sharing Website 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...

    remcarpediem 評論0 收藏0
  • JavaScript 編程精解 中文三版 二、程序結(jié)構(gòu)

    摘要:為了運行包裹的程序,可以將這些值應(yīng)用于它們。在瀏覽器中,輸出出現(xiàn)在控制臺中。在英文版頁面上運行示例或自己的代碼時,會在示例之后顯示輸出,而不是在瀏覽器的控制臺中顯示。這被稱為條件執(zhí)行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Program Structure 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...

    ThinkSNS 評論0 收藏0
  • JavaScript 編程精解 中文三版 零、前言

    摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...

    sanyang 評論0 收藏0
  • JavaScript 編程精解 中文三版 十、模塊

    摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯編寫易于刪除,而不是易于擴展的代碼。模塊之間的關(guān)系稱為依賴關(guān)系。用于連接模塊的最廣泛的方法稱為模塊。模塊的主要概念是稱為的函數(shù)。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Modules 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 編寫易于刪除,而不是易于擴...

    justjavac 評論0 收藏0
  • JavaScript 編程精解 中文三版 十三、瀏覽器中的 JavaScript

    摘要:在本例中,使用屬性指定鏈接的目標,其中表示超文本鏈接。您應(yīng)該認為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒有實際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...

    zhiwei 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<