摘要:在這樣的程序中,異步編程通常是有幫助的。最初是為了使異步編程簡(jiǎn)單方便而設(shè)計(jì)的。在年設(shè)計(jì)時(shí),人們已經(jīng)在瀏覽器中進(jìn)行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風(fēng)格。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文: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 語言,并將其運(yùn)用于單一的瀏覽器環(huán)境中。本章和下一章將會(huì)大致介紹 Node.js,該程序可以讓讀者將你的 JavaScirpt 技能運(yùn)用于瀏覽器之外。讀者可以運(yùn)用 Node.js 構(gòu)建應(yīng)用程序,實(shí)現(xiàn)簡(jiǎn)單的命令行工具和復(fù)雜動(dòng)態(tài) HTTP 服務(wù)器。
這些章節(jié)旨在告訴你建立 Node.js 的主要概念,并向你提供信息,使你可以采用 Nodejs 編寫一些實(shí)用程序。它們并不是這個(gè)平臺(tái)的完整的介紹。
如果你想要運(yùn)行本章中的代碼,需要安裝 Node.js 10 或更高版本。 為此,請(qǐng)?jiān)L問 nodejs.org,并按照用于你的操作系統(tǒng)的安裝說明進(jìn)行操作。 你也可以在那里找到 Node.js 的更多文檔。
背景編寫通過網(wǎng)絡(luò)通信的系統(tǒng)時(shí),一個(gè)更困難的問題是管理輸入輸出,即向/從網(wǎng)絡(luò)和硬盤讀寫數(shù)據(jù)。到處移動(dòng)數(shù)據(jù)會(huì)耗費(fèi)時(shí)間,而調(diào)度這些任務(wù)的技巧會(huì)使得系統(tǒng)在相應(yīng)用戶或網(wǎng)絡(luò)請(qǐng)求時(shí)產(chǎn)生巨大的性能差異。
在這樣的程序中,異步編程通常是有幫助的。 它允許程序同時(shí)向/從多個(gè)設(shè)備發(fā)送和接收數(shù)據(jù),而無需復(fù)雜的線程管理和同步。
Node最初是為了使異步編程簡(jiǎn)單方便而設(shè)計(jì)的。 JavaScript 很好地適應(yīng)了像 Node 這樣的系統(tǒng)。 它是少數(shù)幾種沒有內(nèi)置輸入和輸出方式的編程語言之一。 因此,JavaScript 可以適應(yīng) Node 的相當(dāng)古怪的輸入和輸出方法,而不會(huì)產(chǎn)生兩個(gè)不一致的接口。 在 2009 年設(shè)計(jì) Node 時(shí),人們已經(jīng)在瀏覽器中進(jìn)行基于回調(diào)的編程,所以該語言的社區(qū)用于異步編程風(fēng)格。
Node 命令在系統(tǒng)中安裝完 Node.js 后,Node.js 會(huì)提供一個(gè)名為node的程序,該程序用于執(zhí)行 JavaScript 文件。假設(shè)你有一個(gè)文件 hello.js,該文件會(huì)包含以下代碼。
let message = "Hello world"; console.log(message);
讀者可以仿照下面這種方式通過命令行執(zhí)行程序。
$ node hello.js Hello world
Node 中的console.log方法與瀏覽器中所做的類似,都用于打印文本片段。但在 Node 中,該方法不會(huì)將文本顯示在瀏覽器的 JavaScript 控制臺(tái)中,而顯示在標(biāo)準(zhǔn)輸出流中。從命令行運(yùn)行node時(shí),這意味著你會(huì)在終端中看到記錄的值。
若你執(zhí)行node時(shí)不附帶任何參數(shù),node會(huì)給出提示符,讀者可以輸入 JavaScript 代碼并立即看到執(zhí)行結(jié)果。
$ node > 1 + 1 2 > [-1, -2, -3].map(Math.abs) [1, 2, 3] > process.exit(0) $
process綁定類似于console綁定,是 Node 中的全局綁定。該綁定提供了多種方式來監(jiān)聽并操作當(dāng)前程序。該綁定中的exit方法可以結(jié)束進(jìn)程并賦予一個(gè)退出狀態(tài)碼,告知啟動(dòng)node的程序(在本例中時(shí)命令行 Shell),當(dāng)前程序是成功完成(代碼為 0),還是遇到了錯(cuò)誤(其他代碼)。
讀者可以讀取process.argv來獲取傳遞給腳本的命令行參數(shù),該綁定是一個(gè)字符串?dāng)?shù)組。請(qǐng)注意該數(shù)組包括了node命令和腳本名稱,因此實(shí)際的參數(shù)從索引 2 處開始。若showargv.js只包含一條console.log(process.argv)語句,你可以這樣執(zhí)行該腳本。
$ node showargv.js one --and two ["node", "/tmp/showargv.js", "one", "--and", "two"]
所有標(biāo)準(zhǔn) JavaScript 全局綁定,比如Array、Math以及JSON也都存在于 Node 環(huán)境中。而與瀏覽器相關(guān)的功能,比如document與alert則不存在。
模塊除了前文提到的一些綁定,比如console和process,Node 在全局作用域中添加了很少綁定。如果你需要訪問其他的內(nèi)建功能,可以通過system模塊獲取。
第十章中描述了基于require函數(shù)的 CommonJS 模塊系統(tǒng)。該系統(tǒng)是 Node 的內(nèi)建模塊,用于在程序中裝載任何東西,從內(nèi)建模塊,到下載的包,再到普通文件都可以。
調(diào)用require時(shí),Node 會(huì)將給定的字符串解析為可加載的實(shí)際文件。路徑名若以"/"、"./"或"../"開頭,則解析為相對(duì)于當(dāng)前模塊的路徑,其中"./"表示當(dāng)前路徑,"../"表示當(dāng)前路徑的上一級(jí)路徑,而"/"則表示文件系統(tǒng)根路徑。因此若你訪問從文件/tmp/robot/robot.js訪問"./graph",Node 會(huì)嘗試加載文件/tmp/robot/graph.js。
.js擴(kuò)展名可能會(huì)被忽略,如果這樣的文件存在,Node 會(huì)添加它。 如果所需的路徑指向一個(gè)目錄,則 Node 將嘗試加載該目錄中名為index.js的文件。
當(dāng)一個(gè)看起來不像是相對(duì)路徑或絕對(duì)路徑的字符串被賦給require時(shí),按照假設(shè),它引用了內(nèi)置模塊,或者安裝在node_modules目錄中模塊。 例如,require("fs")會(huì)向你提供 Node 內(nèi)置的文件系統(tǒng)模塊。 而require("robot")可能會(huì)嘗試加載node_modules/robot/中的庫。 安裝這種庫的一種常見方法是使用 NPM,我們稍后講講它。
我們來建立由兩個(gè)文件組成的小項(xiàng)目。 第一個(gè)稱為main.js,并定義了一個(gè)腳本,可以從命令行調(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中定義了一個(gè)庫,用于截取字符串,這個(gè)命令行工具,以及其他需要直接訪問字符串反轉(zhuǎn)函數(shù)的腳本,都可以調(diào)用該庫。
exports.reverse = function(string) { return Array.from(string).reverse().join(""); };
請(qǐng)記住,將屬性添加到exports,會(huì)將它們添加到模塊的接口。 由于 Node.js 將文件視為 CommonJS 模塊,因此main.js可以從reverse.js獲取導(dǎo)出的reverse函數(shù)。
我們可以看到我們的工具執(zhí)行結(jié)果如下所示。
$ node main.js JavaScript tpircSavaJ使用 NPM 安裝
第十章中介紹的 NPM,是一個(gè) JavaScript 模塊的在線倉庫,其中大部分模塊是專門為 Node 編寫的。當(dāng)你在計(jì)算機(jī)上安裝 Node 時(shí),你就會(huì)獲得一個(gè)名為npm的程序,提供了訪問該倉庫的簡(jiǎn)易界面。
它的主要用途是下載包。 我們?cè)诘谑轮锌吹搅?b>ini包。 我們可以使用 NPM 在我們的計(jì)算機(jī)上獲取并安裝該包。
$ 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" }
運(yùn)行npm install后,NPM 將創(chuàng)建一個(gè)名為node_modules的目錄。 該目錄內(nèi)有一個(gè)包含庫的ini目錄。 你可以打開它并查看代碼。 當(dāng)我們調(diào)用require("ini")時(shí),加載這個(gè)庫,我們可以調(diào)用它的parse屬性來解析配置文件。
默認(rèn)情況下,NPM 在當(dāng)前目錄下安裝包,而不是在中央位置。 如果你習(xí)慣于其他包管理器,這可能看起來很不尋常,但它具有優(yōu)勢(shì) - 它使每個(gè)應(yīng)用程序完全控制它所安裝的包,并且使其在刪除應(yīng)用程序時(shí),更易于管理版本和清理。
包文件在npm install例子中,你可以看到package.json文件不存在的警告。 建議為每個(gè)項(xiàng)目創(chuàng)建一個(gè)文件,手動(dòng)或通過運(yùn)行npm init。 它包含該項(xiàng)目的一些信息,例如其名稱和版本,并列出其依賴項(xiàng)。
來自第七章的機(jī)器人模擬,在第十章中模塊化,它可能有一個(gè)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" }
當(dāng)你運(yùn)行npm install而沒有指定安裝包時(shí),NPM 將安裝package.json中列出的依賴項(xiàng)。 當(dāng)你安裝一個(gè)沒有列為依賴項(xiàng)的特定包時(shí),NPM會(huì)將它添加到package.json中。
版本package.json文件列出了程序自己的版本和它的依賴的版本。 版本是一種方式,用于處理包的多帶帶演變。為使用某個(gè)時(shí)候的包而編寫的代碼,可能不能使用包的更高版本。
NPM 要求其包遵循名為語義版本控制(semantic versioning)的綱要,它編碼了版本號(hào)中的哪些版本是兼容的(不破壞就接口)。 語義版本由三個(gè)數(shù)字組成,用點(diǎn)分隔,例如2.3.0。 每次添加新功能時(shí),中間數(shù)字都必須遞增。 每當(dāng)破壞兼容性時(shí),使用該包的現(xiàn)有代碼可能不適用于新版本,因此必須增加第一個(gè)數(shù)字。
package.json中的依賴項(xiàng)版本號(hào)前面的脫字符(^),表示可以安裝兼容給定編號(hào)的任何版本。 例如"^2.3.0"意味著任何大于等于2.3.0且小于3.0.0的版本都是允許的。
npm命令也用于發(fā)布新的包或包的新版本。 如果你在一個(gè)包含package.json文件的目錄中執(zhí)行npm publish,它將一個(gè)包發(fā)布到注冊(cè)處,帶有 JSON 文件中列出的名稱和版本。 任何人都可以將包發(fā)布到 NPM - 但只能用新名稱,因?yàn)槿魏稳丝梢愿卢F(xiàn)有的包,會(huì)有點(diǎn)恐怖。
由于npm程序是與開放系統(tǒng)(包注冊(cè)處)進(jìn)行對(duì)話的軟件,因此它沒有什么獨(dú)特之處。 另一個(gè)程序yarn,可以從 NPM 注冊(cè)處中安裝,使用一種不同的接口和安裝策略,與npm具有相同的作用。
本書不會(huì)深入探討 NPM 的使用細(xì)節(jié)。 請(qǐng)參閱npmjs.org來獲取更多文檔和搜索包的方法。
文件系統(tǒng)模塊在Node中最常用的內(nèi)建模塊就是fs(表示 filesystem,文件系統(tǒng))模塊。該模塊提供了處理文件和目錄的函數(shù)。
例如,有個(gè)函數(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的第二個(gè)參數(shù)表示字符編碼,用于將文件解碼成字符串。將文本編碼成二進(jìn)制數(shù)據(jù)有許多方式,但大多數(shù)現(xiàn)代系統(tǒng)使用 UTF-8,因此除非有特殊原因確信文件使用了別的編碼,否則讀取文件時(shí)使用"utf-8"是一種較為安全的方式。若你不傳遞任何編碼,Node 會(huì)認(rèn)為你需要解析二進(jìn)制數(shù)據(jù),因此會(huì)返回一個(gè)Buffer對(duì)象而非字符串。該對(duì)象類似于數(shù)組,每個(gè)元素是文件中字節(jié)(8 位的數(shù)據(jù)塊)對(duì)應(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]); });
有一個(gè)名為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."); });
這里我們不需要制定編碼,因?yàn)槿绻覀冋{(diào)用writeFile時(shí)傳遞的是字符串而非Buffer對(duì)象,則writeFile會(huì)使用默認(rèn)編碼(即 UTF-8)來輸出文本。
fs模塊也包含了其他實(shí)用函數(shù),其中readdir函數(shù)用于將目錄中的文件以字符串?dāng)?shù)組的方式返回,stat函數(shù)用于獲取文件信息,rename函數(shù)用于重命名文件,unlink用于刪除文件等。
而且其中大多數(shù)都將回調(diào)作為最后一個(gè)參數(shù),它們會(huì)以錯(cuò)誤(第一個(gè)參數(shù))或成功結(jié)果(第二個(gè)參數(shù))來調(diào)用。 我們?cè)诘谑徽轮锌吹剑@種編程風(fēng)格存在缺點(diǎn) - 最大的缺點(diǎn)是,錯(cuò)誤處理變得冗長(zhǎng)且容易出錯(cuò)。
相關(guān)細(xì)節(jié)請(qǐng)參見http://nodejs.org/中的文檔。
雖然Promise已經(jīng)成為 JavaScript 的一部分,但是,將它們與 Node.js 的集成的工作仍然還在進(jìn)行中。 從 v10 開始,標(biāo)準(zhǔn)庫中有一個(gè)名為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));
有時(shí)候你不需要異步,而是需要阻塞。 fs中的許多函數(shù)也有同步的變體,它們的名稱相同,末尾加上Sync。 例如,readFile的同步版本稱為readFileSync。
const {readFileSync} = require("fs"); console.log("The file contains:", readFileSync("file.txt", "utf8"));
請(qǐng)注意,在執(zhí)行這樣的同步操作時(shí),程序完全停止。 如果它應(yīng)該響應(yīng)用戶或網(wǎng)絡(luò)中的其他計(jì)算機(jī),那么可在同步操作中可能會(huì)產(chǎn)生令人討厭的延遲。
HTTP 模塊另一個(gè)主要模塊名為"http"。該模塊提供了執(zhí)行 HTTP 服務(wù)和產(chǎn)生 HTTP 請(qǐng)求的函數(shù)。
啟動(dòng)一個(gè) HTTP 服務(wù)器只需要以下代碼。
const {createServer} = require("http"); let server = createServer((request, response) => { response.writeHead(200, {"Content-Type": "text/html"}); response.write(`Hello!
You asked for
`); response.end(); }); server.listen(8000);${request.url}
若你在自己的機(jī)器上執(zhí)行該腳本,你可以打開網(wǎng)頁瀏覽器,并訪問 http://localhost:8000/hello,就會(huì)向你的服務(wù)器發(fā)出一個(gè)請(qǐng)求。服務(wù)器會(huì)響應(yīng)一個(gè)簡(jiǎn)單的 HTML 頁面。
每次客戶端嘗試連接服務(wù)器時(shí),服務(wù)器都會(huì)調(diào)用傳遞給createServer函數(shù)的參數(shù)。request和response綁定都是對(duì)象,分別表示輸入數(shù)據(jù)和輸出數(shù)據(jù)。request包含請(qǐng)求信息,例如該對(duì)象的url屬性表示請(qǐng)求的 URL。
因此,當(dāng)你在瀏覽器中打開該頁面時(shí),它會(huì)向你自己的計(jì)算機(jī)發(fā)送請(qǐng)求。 這會(huì)導(dǎo)致服務(wù)器函數(shù)運(yùn)行并返回一個(gè)響應(yīng),你可以在瀏覽器中看到該響應(yīng)。
你需要調(diào)用response對(duì)象的方法以將一些數(shù)據(jù)發(fā)回客戶端。第一個(gè)函數(shù)調(diào)用(writeHead)會(huì)輸出響應(yīng)頭(參見第十七章)。你需要向該函數(shù)傳遞狀態(tài)碼(本例中 200 表示成功)和一個(gè)對(duì)象,該對(duì)象包含協(xié)議頭信息的值。該示例設(shè)置了"Content-Type"頭,通知客戶端我們將發(fā)送一個(gè) HTML 文檔。
接下來使用response.write來發(fā)送響應(yīng)體(文檔自身)。若你想一段一段地發(fā)送相應(yīng)信息,可以多次調(diào)用該方法,例如將數(shù)據(jù)發(fā)送到客戶端。最后調(diào)用response.end發(fā)送相應(yīng)結(jié)束信號(hào)。
調(diào)用server.listen會(huì)使服務(wù)器在 8000 端口上開始等待請(qǐng)求。這就是你需要連接localhost:8000和服務(wù)器通信,而不是localhost(這樣將會(huì)使用默認(rèn)端口,即 80)的原因。
當(dāng)你運(yùn)行這個(gè)腳本時(shí),這個(gè)進(jìn)程就在那里等著。 當(dāng)一個(gè)腳本正在監(jiān)聽事件時(shí) - 這里是網(wǎng)絡(luò)連接 - Node 不會(huì)在到達(dá)腳本末尾時(shí)自動(dòng)退出。為了關(guān)閉它,請(qǐng)按Ctrl-C。
一個(gè)真實(shí)的 Web 服務(wù)器需要做的事情比示例多得多。其差別在于我們需要根據(jù)請(qǐng)求的方法(method屬性),來判斷客戶端嘗試執(zhí)行的動(dòng)作,并根據(jù)請(qǐng)求的 URL 來找出動(dòng)作處理的資源。本章隨后會(huì)介紹更高級(jí)的服務(wù)器。
我們可以使用http模塊的request函數(shù)來充當(dāng)一個(gè) 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ù)的第一個(gè)參數(shù)是請(qǐng)求配置,告知 Node 需要訪問的服務(wù)器、服務(wù)器請(qǐng)求地址、使用的方法等信息。第二個(gè)參數(shù)是響應(yīng)開始時(shí)的回調(diào)。該回調(diào)會(huì)接受一個(gè)參數(shù),用于檢查相應(yīng)信息,例如獲取狀態(tài)碼。
和在服務(wù)器中看到的response對(duì)象一樣,request返回的對(duì)象允許我們使用write方法多次發(fā)送數(shù)據(jù),并使用end方法結(jié)束發(fā)送。本例中并沒有使用write方法,因?yàn)?GET 請(qǐng)求的請(qǐng)求正文中無法包含數(shù)據(jù)。
https模塊中有類似的request函數(shù),可以用來向https: URL 發(fā)送請(qǐng)求。
但是使用 Node 的原始功能發(fā)送請(qǐng)求相當(dāng)麻煩。 NPM 上有更多方便的包裝包。 例如,node-fetch提供了我們從瀏覽器得知的,基于Promise的fetch接口。
流我們?cè)?HTTP 中看過兩個(gè)可寫流的例子,即服務(wù)器可以向response對(duì)象中寫入數(shù)據(jù),而request返回的請(qǐng)求對(duì)象也可以寫入數(shù)據(jù)。
可寫流是 Node 中廣泛使用的概念。這種對(duì)象擁有write方法,你可以傳遞字符串或Buffer對(duì)象,來向流寫入一些數(shù)據(jù)。它們end方法用于關(guān)閉流,并且還可以接受一個(gè)可選值,在流關(guān)閉之前將其寫入流。 這兩個(gè)方法也可以接受回調(diào)作為附加參數(shù),當(dāng)寫入或關(guān)閉完成時(shí)它們將被調(diào)用。
我們也可以使用fs模塊的createWriteStream,建立一個(gè)指向本地文件的輸出流。你可以調(diào)用該方法返回的結(jié)果對(duì)象的write方法,每次向文件中寫入一段數(shù)據(jù),而不是像writeFile那樣一次性寫入所有數(shù)據(jù)。
可讀流則略為復(fù)雜。傳遞給 HTTP 服務(wù)器回調(diào)的request綁定,以及傳遞給 HTTP 客戶端回調(diào)的response對(duì)象都是可讀流(服務(wù)器讀取請(qǐng)求并寫入響應(yīng),而客戶端則先寫入請(qǐng)求,然后讀取響應(yīng))。讀取流需要使用事件處理器,而不是方法。
Node 中發(fā)出的事件都有一個(gè)on方法,類似瀏覽器中的addEventListener方法。該方法接受一個(gè)事件名和一個(gè)函數(shù),并將函數(shù)注冊(cè)到事件上,接下來每當(dāng)指定事件發(fā)生時(shí),都會(huì)調(diào)用注冊(cè)的函數(shù)。
可讀流有data事件和end事件。data事件在每次數(shù)據(jù)到來時(shí)觸發(fā),end事件在流結(jié)束時(shí)觸發(fā)。該模型適用于“流”數(shù)據(jù),這類數(shù)據(jù)可以立即處理,即使整個(gè)文檔的數(shù)據(jù)沒有到位。我們可以使用createReadStream函數(shù)創(chuàng)建一個(gè)可讀流,來讀取本地文件。
這段代碼創(chuàng)建了一個(gè)服務(wù)器并讀取請(qǐng)求正文,然后將讀取到的數(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值是一個(gè)二進(jìn)制Buffer對(duì)象,我們可以使用它的toString方法,通過將其解碼為 UTF-8 編碼的字符,來將其轉(zhuǎn)換為字符串。
下面的一段代碼,和上面的服務(wù)(將字母轉(zhuǎn)換成大寫)一起運(yùn)行時(shí),它會(huì)向服務(wù)器發(fā)送一個(gè)請(qǐng)求并輸出獲取到的響應(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(進(jìn)程的標(biāo)準(zhǔn)輸出流,是一個(gè)可寫流)中寫入數(shù)據(jù),而不使用console.log,因?yàn)?b>console.log函數(shù)會(huì)在輸出的每段文本后加上額外的換行符,在這里不太合適。
文件服務(wù)器讓我們結(jié)合新學(xué)習(xí)的 HTTP 服務(wù)器和文件系統(tǒng)的知識(shí),并建立起兩者之間的橋梁:使用 HTTP 服務(wù)允許客戶遠(yuǎn)程訪問文件系統(tǒng)。這個(gè)服務(wù)有許多用處,它允許網(wǎng)絡(luò)應(yīng)用程序存儲(chǔ)并共享數(shù)據(jù)或使得一組人可以共享訪問一批文件。
當(dāng)我們將文件當(dāng)作 HTTP 資源時(shí),可以將 HTTP 的 GET、PUT 和 DELETE 方法分別看成讀取、寫入和刪除文件。我們將請(qǐng)求中的路徑解釋成請(qǐng)求指向的文件路徑。
我們可能不希望共享整個(gè)文件系統(tǒng),因此我們將這些路徑解釋成以服務(wù)器工作路徑(即啟動(dòng)服務(wù)器的路徑)為起點(diǎn)的相對(duì)路徑。若從/home/marijn/public(或 Windows 下的C:Usersmarijnpublic)啟動(dòng)服務(wù)器,那么對(duì)/file.txt的請(qǐng)求應(yīng)該指向/home/marijn/public/file.txt(或C:Usersmarijnpublicfile.txt)。
我們將一段段地構(gòu)建程序,使用名為methods的對(duì)象來存儲(chǔ)處理多種 HTTP 方法的函數(shù)。方法處理器是async函數(shù),它接受請(qǐng)求對(duì)象作為參數(shù)并返回一個(gè)Promise,解析為描述響應(yīng)的對(duì)象。
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.` }; }
這樣啟動(dòng)服務(wù)器之后,服務(wù)器永遠(yuǎn)只會(huì)產(chǎn)生 405 錯(cuò)誤響應(yīng),該代碼表示服務(wù)器拒絕處理特定的方法。
當(dāng)請(qǐng)求處理程序的Promise受到拒絕時(shí),catch調(diào)用會(huì)將錯(cuò)誤轉(zhuǎn)換為響應(yīng)對(duì)象(如果它還不是),以便服務(wù)器可以發(fā)回錯(cuò)誤響應(yīng),來通知客戶端它未能處理請(qǐng)求。
響應(yīng)描述的status字段可以省略,這種情況下,默認(rèn)為 200(OK)。 type屬性中的內(nèi)容類型也可以被省略,這種情況下,假定響應(yīng)為純文本。
當(dāng)body的值是可讀流時(shí),它將有pipe方法,用于將所有內(nèi)容從可讀流轉(zhuǎn)發(fā)到可寫流。 如果不是,則假定它是null(無正文),字符串或緩沖區(qū),并直接傳遞給響應(yīng)的end方法。
為了弄清哪個(gè)文件路徑對(duì)應(yīng)于請(qǐng)求URL,urlPath函數(shù)使用 Node 的url內(nèi)置模塊來解析 URL。 它接受路徑名,類似"/file.txt",將其解碼來去掉%20風(fēng)格的轉(zhuǎn)義代碼,并相對(duì)于程序的工作目錄來解析它。
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; }
只要你建立了一個(gè)接受網(wǎng)絡(luò)請(qǐng)求的程序,就必須開始關(guān)注安全問題。 在這種情況下,如果我們不小心,很可能會(huì)意外地將整個(gè)文件系統(tǒng)暴露給網(wǎng)絡(luò)。
文件路徑在 Node 中是字符串。 為了將這樣的字符串映射為實(shí)際的文件,需要大量有意義的解釋。 例如,路徑可能包含"../"來引用父目錄。 因此,一個(gè)顯而易見的問題來源是像/../ secret_file這樣的路徑請(qǐng)求。
為了避免這種問題,urlPath使用path模塊中的resolve函數(shù)來解析相對(duì)路徑。 然后驗(yàn)證結(jié)果位于工作目錄下面。 process.cwd函數(shù)(其中cwd代表“當(dāng)前工作目錄”)可用于查找此工作目錄。 當(dāng)路徑不起始于基本目錄時(shí),該函數(shù)將使用 HTTP 狀態(tài)碼來拋出錯(cuò)誤響應(yīng)對(duì)象,該狀態(tài)碼表明禁止訪問資源。
我們需要?jiǎng)?chuàng)建GET方法,在讀取目錄時(shí)返回文件列表,在讀取普通文件時(shí)返回文件內(nèi)容。
一個(gè)棘手的問題是我們返回文件內(nèi)容時(shí)添加的Content-Type頭應(yīng)該是什么類型。因?yàn)檫@些文件可以是任何內(nèi)容,我們的服務(wù)器無法簡(jiǎn)單地對(duì)所有文件返回相同的內(nèi)容類型。但 NPM 可以幫助我們完成該任務(wù)。mime包(以text/plain這種方式表示的內(nèi)容類型,名為 MIME 類型)可以獲取大量文件擴(kuò)展名的正確類型。
以下npm命令在服務(wù)器腳本所在的目錄中,安裝mime的特定版本。
$ npm install mime@2.2.0
當(dāng)請(qǐng)求文件不存在時(shí),應(yīng)該返回的正確 HTTP 狀態(tài)碼是 404。我們使用stat函數(shù),來找出特定文件是否存在以及是否是一個(gè)目錄。
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)}; } };
因?yàn)?b>stat訪問磁盤需要耗費(fèi)一些時(shí)間,因此該函數(shù)是異步的。由于我們使用Promise而不是回調(diào)風(fēng)格,因此必須從fs/promises而不是fs導(dǎo)入。
當(dāng)文件不存在時(shí),stat會(huì)拋出一個(gè)錯(cuò)誤對(duì)象,code屬性為"ENOENT"。 這些有些模糊的,受 Unix 啟發(fā)的代碼,是你識(shí)別 Node 中的錯(cuò)誤類型的方式。
由stat返回的stats對(duì)象告訴了我們文件的一系列信息,比如文件大小(size屬性)和修改日期(mtime屬性)。這里我們想知道的是,該文件是一個(gè)目錄還是普通文件,isDirectory方法可以告訴我們答案。
我們使用readdir來讀取目錄中的文件列表,并將其返回給客戶端。對(duì)于普通文件,我們使用createReadStream創(chuàng)建一個(gè)可讀流,并將其傳遞給respond對(duì)象,同時(shí)使用mime模塊根據(jù)文件名獲取內(nèi)容類型并傳遞給respond。
處理DELETE請(qǐng)求的代碼就稍顯簡(jiǎn)單了。
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}; };
當(dāng) HTTP 響應(yīng)不包含任何數(shù)據(jù)時(shí),狀態(tài)碼 204(“No Content”,無內(nèi)容)可用于表明這一點(diǎn)。 由于刪除的響應(yīng)不需要傳輸任何信息,除了操作是否成功之外,在這里返回是明智的。
你可能想知道,為什么試圖刪除不存在的文件會(huì)返回成功狀態(tài)代碼,而不是錯(cuò)誤。 當(dāng)被刪除的文件不存在時(shí),可以說該請(qǐng)求的目標(biāo)已經(jīng)完成。 HTTP 標(biāo)準(zhǔn)鼓勵(lì)我們使請(qǐng)求是冪等(idempotent)的,這意味著,多次發(fā)送相同請(qǐng)求的結(jié)果,會(huì)與一次相同。 從某種意義上說,如果你試圖刪除已經(jīng)消失的東西,那么你試圖去做的效果已經(jīng)實(shí)現(xiàn) - 東西已經(jīng)不存在了。
下面是PUT請(qǐng)求的處理器。
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}; };
我們不需要檢查文件是否存在,如果存在,只需覆蓋即可。我們?cè)俅问褂?b>pipe來將可讀流中的數(shù)據(jù)移動(dòng)到可寫流中,在本例中是將請(qǐng)求的數(shù)據(jù)移動(dòng)到文件中。但是由于pipe沒有為返回Promise而編寫,所以我們必須編寫包裝器pipeStream,它從調(diào)用pipe的結(jié)果中創(chuàng)建一個(gè)Promise。
當(dāng)打開文件createWriteStream時(shí)出現(xiàn)問題時(shí)仍然會(huì)返回一個(gè)流,但是這個(gè)流會(huì)觸發(fā)"error"事件。 例如,如果網(wǎng)絡(luò)出現(xiàn)故障,請(qǐng)求的輸出流也可能失敗。 所以我們連接兩個(gè)流的"error"事件來拒絕Promise。 當(dāng)pipe完成時(shí),它會(huì)關(guān)閉輸出流,從而導(dǎo)致觸發(fā)"finish"事件。 這是我們可以成功解析Promise的地方(不返回任何內(nèi)容)。
完整的服務(wù)器腳本請(qǐng)見eloquentjavascript.net/code/file_server.js。讀者可以下載該腳本,并且在安裝依賴項(xiàng)之后,使用 Node 啟動(dòng)你自己的文件服務(wù)器。當(dāng)然你可以修改并擴(kuò)展該腳本,來完成本章的習(xí)題或進(jìn)行實(shí)驗(yàn)。
命令行工具curl在類 Unix 系統(tǒng)(比如 Mac 或者 Linux)中得到廣泛使用,可用于產(chǎn)生 HTTP 請(qǐng)求。接下來的會(huì)話用于簡(jiǎn)單測(cè)試我們的服務(wù)器。這里需要注意,-x用于設(shè)置請(qǐng)求方法,-d用于包含請(qǐng)求正文。
$ 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一開始不存在,因此第一請(qǐng)求失敗。而PUT請(qǐng)求則創(chuàng)建文件,因此我們看到下一個(gè)請(qǐng)求可以成功獲取該文件。在使用DELETE請(qǐng)求刪除該文件后,第三次GET請(qǐng)求再次找不到該文件。
本章小結(jié)Node 是一個(gè)不錯(cuò)的小型系統(tǒng),可讓我們?cè)诜菫g覽器環(huán)境下運(yùn)行 JavaScript。Node 最初的設(shè)計(jì)意圖是完成網(wǎng)絡(luò)任務(wù),扮演網(wǎng)絡(luò)中的節(jié)點(diǎn)。但同時(shí)也能用來執(zhí)行任何腳本任務(wù),如果你覺得編寫 JavaScript 代碼是一件愜意的事情,那么使用 Node 來自動(dòng)完成每天的任務(wù)是非常不錯(cuò)的。
NPM 為你所能想到的功能(當(dāng)然還有相當(dāng)多你想不到的)提供了包,你可以通過使用npm程序,獲取并安裝這些包。Node 也附帶了許多內(nèi)建模塊,包括fs模塊(處理文件系統(tǒng))、http模塊(執(zhí)行 HTTP 服務(wù)器并生成 HTTP 請(qǐng)求)。
Node 中的所有輸入輸出都是異步的,除非你明確使用函數(shù)的同步變體,比如readFileSync。當(dāng)調(diào)用異步函數(shù)時(shí),使用者提供回調(diào),并且 Node 會(huì)在準(zhǔn)備好的時(shí)候,使用錯(cuò)誤值和結(jié)果(如果有的話)調(diào)用它們。
習(xí)題 搜索工具在 Unix 系統(tǒng)上,有一個(gè)名為grep的命令行工具,可以用來在文件中快速搜索正則表達(dá)式。
編寫一個(gè)可以從命令行運(yùn)行的 Node 腳本,其行為類似grep。 它將其第一個(gè)命令行參數(shù)視為正則表達(dá)式,并將任何其他參數(shù)視為要搜索的文件。 它應(yīng)該輸出內(nèi)容與正則表達(dá)式匹配的,任何文件的名稱。
當(dāng)它有效時(shí),將其擴(kuò)展,以便當(dāng)其中一個(gè)參數(shù)是目錄時(shí),它將搜索該目錄及其子目錄中的所有文件。
按照你認(rèn)為合適的方式,使用異步或同步文件系統(tǒng)函數(shù)。 配置一些東西,以便同時(shí)請(qǐng)求多個(gè)異步操作可能會(huì)加快速度,但不是很大,因?yàn)榇蠖鄶?shù)文件系統(tǒng)一次只能讀取一個(gè)東西。
目錄創(chuàng)建盡管我們的文件服務(wù)器中的DELETE方法可以刪除目錄(使用rmdir),但服務(wù)器目前不提供任何方法來創(chuàng)建目錄。
添加對(duì)MKCOL方法(“make column”)的支持,它應(yīng)該通過調(diào)用fs模塊的mkdir創(chuàng)建一個(gè)目錄。 MKCOL并不是廣泛使用的 HTTP 方法,但是它在 WebDAV 標(biāo)準(zhǔn)中有相同的用途,這個(gè)標(biāo)準(zhǔn)在 HTTP 之上規(guī)定了一組適用于創(chuàng)建文檔的約定。
你可以使用實(shí)現(xiàn)DELETE方法的函數(shù),作為MKCOL方法的藍(lán)圖。 當(dāng)找不到文件時(shí),嘗試用mkdir創(chuàng)建一個(gè)目錄。 當(dāng)路徑中存在目錄時(shí),可以返回 204 響應(yīng),以便目錄創(chuàng)建請(qǐng)求是冪等的。 如果這里存在非目錄文件,則返回錯(cuò)誤代碼。 代碼 400(“Bad Request”,請(qǐng)求無效)是適當(dāng)?shù)摹?/p> 網(wǎng)絡(luò)上的公共空間
由于文件服務(wù)器提供了任何類型的文件服務(wù),甚至只要包含正確的Content-Type協(xié)議頭,你可以使用其提供網(wǎng)站服務(wù)。由于該服務(wù)允許每個(gè)人刪除或替換文件,因此這是一類非常有趣的網(wǎng)站:任何人只要使用正確的 HTTP 請(qǐng)求,都可以修改、改進(jìn)并破壞文件。但這仍然是一個(gè)網(wǎng)站。
請(qǐng)編寫一個(gè)基礎(chǔ)的 HTML 頁面,包含一個(gè)簡(jiǎn)單的 JavaScript 文件。將該文件放在文件服務(wù)器的數(shù)據(jù)目錄下,并在你的瀏覽器中打開這些文件。
接下來,作為進(jìn)階練習(xí)或是周末作業(yè),將你迄今為止在本書中學(xué)習(xí)到的內(nèi)容整合起來,構(gòu)建一個(gè)對(duì)用戶友好的界面,在網(wǎng)站內(nèi)部修改網(wǎng)站。
使用 HTML 表單編輯組成網(wǎng)站的文件內(nèi)容,允許用戶使用 HTTP 請(qǐng)求在服務(wù)器上更新它們,如第十八章所述。
剛開始的時(shí)候,該頁面僅允許用戶編輯單個(gè)文件,然后進(jìn)行修改,允許選擇想要編輯的文件。向文件服務(wù)器發(fā)送請(qǐng)求時(shí),若URL是一個(gè)目錄,服務(wù)器會(huì)返回該目錄下的文件列表,你可以利用該特性實(shí)現(xiàn)你的網(wǎng)頁。
不要直接編輯文件服務(wù)器開放的代碼,如果你犯了什么錯(cuò)誤,很有可能就破壞了你的代碼。相反,將你的代碼保存在公共訪問目錄之外,測(cè)試時(shí)再將其拷貝到公共目錄中。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/105072.html
摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版技能分享會(huì)是一個(gè)活動(dòng),其中興趣相同的人聚在一起,針對(duì)他們所知的事情進(jìn)行小型非正式的展示。所有接口均以路徑為中心。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: Skill-Sharing Website 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...
摘要:為了運(yùn)行包裹的程序,可以將這些值應(yīng)用于它們。在瀏覽器中,輸出出現(xiàn)在控制臺(tái)中。在英文版頁面上運(yùn)行示例或自己的代碼時(shí),會(huì)在示例之后顯示輸出,而不是在瀏覽器的控制臺(tái)中顯示。這被稱為條件執(zhí)行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Program Structure 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...
摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲(chǔ)在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...
摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯編寫易于刪除,而不是易于擴(kuò)展的代碼。模塊之間的關(guān)系稱為依賴關(guān)系。用于連接模塊的最廣泛的方法稱為模塊。模塊的主要概念是稱為的函數(shù)。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Modules 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 編寫易于刪除,而不是易于擴(kuò)...
摘要:在本例中,使用屬性指定鏈接的目標(biāo),其中表示超文本鏈接。您應(yīng)該認(rèn)為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒有實(shí)際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
閱讀 2130·2021-11-18 10:07
閱讀 3507·2021-09-04 16:48
閱讀 3214·2019-08-30 15:53
閱讀 1235·2019-08-30 12:55
閱讀 2453·2019-08-29 15:08
閱讀 3149·2019-08-29 15:04
閱讀 2879·2019-08-29 14:21
閱讀 2906·2019-08-29 11:21