自 Node.js 公諸于世的那一刻,就伴隨著贊揚(yáng)和批評(píng)的聲音。這個(gè)爭(zhēng)論仍在持續(xù),而且并不會(huì)很快消失。而我們常常忽略掉這些爭(zhēng)論產(chǎn)生的原因,每種編程語(yǔ)言和平臺(tái)都是因某些問(wèn)題而受到批評(píng),而這些問(wèn)題的產(chǎn)生,是取決于我們?nèi)绾问褂眠@個(gè)平臺(tái)。不管有多難才能寫出安全的 Node.js 代碼,或有多容易寫出高并發(fā)的代碼,該平臺(tái)已經(jīng)有相當(dāng)長(zhǎng)一段時(shí)間,并已被用來(lái)建立一個(gè)數(shù)量龐大、穩(wěn)健和成熟的 web 服務(wù)器。這些 web 服務(wù)器伸縮性強(qiáng),并且它們通過(guò)在 Internet 上穩(wěn)定的運(yùn)行時(shí)間,證明自己的穩(wěn)定性。 然而,像其它平臺(tái)一樣,Node.js 容易因開(kāi)發(fā)者問(wèn)題而受到批評(píng)。一些錯(cuò)誤會(huì)降低性能,而其它一些問(wèn)題會(huì)讓 Node.js 直接崩潰。在這篇文章里,我們將會(huì)聊一聊關(guān)于 Node.js 新手的 10 個(gè)常犯錯(cuò)誤,并讓他們知道如何避免這些錯(cuò)誤,從而成為一名 Node.js 高手。 錯(cuò)誤 #1:阻塞事件循環(huán) JavaScript 在 Node.js (就像在瀏覽器一樣) 提供單線程執(zhí)行環(huán)境。這意味著你的程序不能同時(shí)執(zhí)行兩部分代碼,但能通過(guò) I/O 綁定異步回調(diào)函數(shù)實(shí)現(xiàn)并發(fā)。例如:一個(gè)來(lái)自Node.js 的請(qǐng)求是到數(shù)據(jù)庫(kù)引擎獲取一些文檔,在這同時(shí)允許 Node.js 專注于應(yīng)用程序其它部分:
然而,具有計(jì)算密集型代碼的 Node.js 實(shí)例被數(shù)以萬(wàn)計(jì)客戶端同時(shí)連接執(zhí)行時(shí),會(huì)導(dǎo)致阻塞事件循環(huán),并使所有客戶端處于等待響應(yīng)狀態(tài)。計(jì)算密集型代碼,包括嘗試給一個(gè)龐大數(shù)組進(jìn)行排序操作和運(yùn)行一個(gè)格外長(zhǎng)的循環(huán)等。例如:
基于小 “users” 數(shù)組執(zhí)行 “sortUserByAge” 函數(shù),可能沒(méi)什么問(wèn)題,當(dāng)基于龐大數(shù)組時(shí),會(huì)嚴(yán)重影響整體性能。如果在不得不這樣操作的情況下,你必須確保程序除了等待事件循環(huán)而別無(wú)他事(例如,用 Node.js 建立命令行工具的一部分,整個(gè)東西同步運(yùn)行是沒(méi)問(wèn)題的),然后這可能沒(méi)問(wèn)題。然而,在 Node.js 服務(wù)器實(shí)例嘗試同時(shí)服務(wù)成千上萬(wàn)個(gè)用戶的情況下,這將是一個(gè)毀滅性的問(wèn)題。 如果用戶數(shù)組是從數(shù)據(jù)庫(kù)檢索出來(lái)的,有個(gè)解決辦法是,先在數(shù)據(jù)庫(kù)中排序,然后再直接檢索。如果因需要計(jì)算龐大的金融交易歷史數(shù)據(jù)總和,而造成阻塞事件循環(huán),這可以創(chuàng)建額外的worker / queue 來(lái)避免阻塞事件循環(huán)。 正如你所看到的,這沒(méi)有新技術(shù)來(lái)解決這類 Node.js 問(wèn)題,而每種情況都需要單獨(dú)處理。而基本解決思路是:不要讓 Node.js 實(shí)例的主線程執(zhí)行 CPU 密集型工作 – 客戶端同時(shí)鏈接時(shí)。 錯(cuò)誤 #2:調(diào)用回調(diào)函數(shù)多于一次 JavaScript 一直都是依賴于回調(diào)函數(shù)。在瀏覽器中,處理事件是通過(guò)調(diào)用函數(shù)(通常是匿名的),這個(gè)動(dòng)作如同回調(diào)函數(shù)。Node.js 在引進(jìn) promises 之前,回調(diào)函數(shù)是異步元素用來(lái)互相連接對(duì)方的唯一方式 。現(xiàn)在回調(diào)函數(shù)仍被使用,并且包開(kāi)發(fā)者仍然圍繞著回調(diào)函數(shù)設(shè)計(jì) APIs。一個(gè)關(guān)于使用回調(diào)函數(shù)的常見(jiàn) Node.js 問(wèn)題是:不止一次調(diào)用。通常情況下,一個(gè)包提供一個(gè)函數(shù)去異步處理一些東西,設(shè)計(jì)出來(lái)是期待有一個(gè)函數(shù)作為最后一個(gè)參數(shù),當(dāng)異步任務(wù)完成時(shí)就會(huì)被調(diào)用:
注意每次調(diào)用 “done” 都有一個(gè)返回語(yǔ)句(return),而最后一個(gè) “done” 則可省略返回語(yǔ)句。這是因?yàn)檎{(diào)用回調(diào)函數(shù)后,并不會(huì)自動(dòng)結(jié)束當(dāng)前執(zhí)行函數(shù)。如果第一個(gè) “return” 注釋掉,然后給這個(gè)函數(shù)傳進(jìn)一個(gè)非字符串密碼,導(dǎo)致 “computeHash” 仍然會(huì)被調(diào)用。這取決于 “computeHash” 如何處理這樣一種情況,“done” 可能會(huì)調(diào)用多次。任何一個(gè)人在別處使用這個(gè)函數(shù)可能會(huì)變得措手不及,因?yàn)樗鼈儌鬟M(jìn)的該回調(diào)函數(shù)被多次調(diào)用。 只要小心就可以避免這個(gè) Node.js 錯(cuò)誤。而一些 Node.js 開(kāi)發(fā)者養(yǎng)成一個(gè)習(xí)慣是:在每個(gè)回調(diào)函數(shù)調(diào)用前添加一個(gè) return 關(guān)鍵字。
對(duì)于許多異步函數(shù),它的返回值幾乎是無(wú)意義的,所以該方法能讓你很好地避免這個(gè)問(wèn)題。 錯(cuò)誤 #3:函數(shù)嵌套過(guò)深 函數(shù)嵌套過(guò)深,時(shí)常被稱為“回調(diào)函數(shù)地獄”,但這并不是 Node.js 自身問(wèn)題。然而,這會(huì)導(dǎo)致一個(gè)問(wèn)題:代碼很快失去控制。
任務(wù)有多復(fù)雜,代碼就有多糟糕。以這種方式嵌套回調(diào)函數(shù),我們很容易就會(huì)碰到問(wèn)題而崩潰,并且難以閱讀和維護(hù)代碼。一種替代方式是以函數(shù)聲明這些任務(wù),然后將它們連接起來(lái)。盡管,有一種最干凈的方法之一 (有爭(zhēng)議的)是使用 Node.js 工具包,它專門處理異步 JavaScript 模式,例如 Async.js :
類似于 “async.waterfall”,Async.js 提供了很多其它函數(shù)來(lái)解決不同的異步模式。為了簡(jiǎn)潔,我們?cè)谶@里使用一個(gè)較為簡(jiǎn)單的案例,但實(shí)際情況往往更糟。 錯(cuò)誤 #4:期望回調(diào)函數(shù)以同步方式運(yùn)行 異步程序的回調(diào)函數(shù)并不是 JavaScript 和 Node.js 獨(dú)有的,但它們是造成回調(diào)函數(shù)流行的原因。而對(duì)于其它編程語(yǔ)言,我們潛意識(shí)地認(rèn)為執(zhí)行順序是一步接一步的,如兩個(gè)語(yǔ)句將會(huì)執(zhí)行完第一句再執(zhí)行第二句,除非這兩個(gè)語(yǔ)句間有一個(gè)明確的跳轉(zhuǎn)語(yǔ)句。盡管那樣,它們經(jīng)常局限于條件語(yǔ)句、循環(huán)語(yǔ)句和函數(shù)調(diào)用。 然而,在 JavaScript 中,回調(diào)某個(gè)特定函數(shù)可能并不會(huì)立刻運(yùn)行,而是等到任務(wù)完成后才運(yùn)行。下面例子就是直到?jīng)]有任何任務(wù),當(dāng)前函數(shù)才運(yùn)行:
你會(huì)注意到,調(diào)用 “testTimeout” 函數(shù)會(huì)首先打印 “Begin”,然后打印 “Waiting..”,緊接大約一秒后才打印 “Done!”。 任何一個(gè)需要在回調(diào)函數(shù)被觸發(fā)后執(zhí)行的東西,都要把它放在回調(diào)函數(shù)內(nèi)。 錯(cuò)誤 #5:用“exports”,而不是“module.exports” Node.js 將每個(gè)文件視為一個(gè)孤立的小模塊。如果你的包(package)含有兩個(gè)文件,或許是 “a.js” 和 “b.js”。因?yàn)?“b.js” 要獲取 “a.js” 的功能,所以 “a.js” 必須通過(guò)為 exports 對(duì)象添加屬性來(lái)導(dǎo)出它。
當(dāng)這樣操作后,任何引入 “a.js” 模塊的文件將會(huì)得到一個(gè)帶有屬性方法 “verifyPassword” 的對(duì)象:
然而,如果我們想直接導(dǎo)出這個(gè)函數(shù),而不是作為某個(gè)對(duì)象的屬性呢?我們能通過(guò)覆蓋 exports 對(duì)象來(lái)達(dá)到這個(gè)目的,但我們不能將它視為一個(gè)全局變量:
注意,我們是如何將 “exports” 作為 module 對(duì)象的一個(gè)屬性。在這里知道 “module.exports” 和 “exports” 之間區(qū)別是非常重要的,并且這經(jīng)常會(huì)導(dǎo)致 Node.js 開(kāi)發(fā)新手們產(chǎn)生挫敗感。 錯(cuò)誤 #6:在回調(diào)函數(shù)內(nèi)拋出錯(cuò)誤 JavaScript 有個(gè)“異常”概念。異常處理與大多數(shù)傳統(tǒng)語(yǔ)言的語(yǔ)法類似,例如 Java 和 C++,JavaScript 能在 try-catch 塊內(nèi) “拋出(throw)” 和 捕捉(catch)異常:
然而,如果你把 try-catch 放在異步函數(shù)內(nèi),它會(huì)出乎你意料,它并不會(huì)執(zhí)行。例如,如果你想保護(hù)一段含有很多異步活動(dòng)的代碼,而且這段代碼包含在一個(gè) try-catch 塊內(nèi),而結(jié)果是:它不一定會(huì)運(yùn)行。
如果回調(diào)函數(shù) “db.User.get” 異步觸發(fā)了,雖然作用域里包含的 try-catch 塊離開(kāi)了上下文,仍然能捕捉那些在回調(diào)函數(shù)的拋出的錯(cuò)誤。 這就是 Node.js 中如何處理錯(cuò)誤的另外一種方式。另外,有必要遵循所有回調(diào)函數(shù)的參數(shù)(err, …)模式,所有回調(diào)函數(shù)的第一個(gè)參數(shù)期待是一個(gè)錯(cuò)誤對(duì)象。 錯(cuò)誤 #7:認(rèn)為數(shù)字是整型 數(shù)字在 JavaScript 中都是浮點(diǎn)型,JS 沒(méi)有整型。你可能不能預(yù)料到這將是一個(gè)問(wèn)題,因?yàn)閿?shù)大到超出浮點(diǎn)型范圍的情況并不常見(jiàn)。
不幸的是,在 JavaScript 中,這種關(guān)于數(shù)字的怪異情況遠(yuǎn)不止于此。盡管數(shù)字都是浮點(diǎn)型,對(duì)于下面的表達(dá)式,操作符對(duì)于整型也能正常運(yùn)行:
然而,不像算術(shù)運(yùn)算符那樣,位操作符和位移操作符只能操作后 32 位,如同 “整型” 數(shù)。例如,嘗試位移 “Math.pow(2,53)” 1 位,會(huì)得到結(jié)果 0。嘗試與 1 進(jìn)行按位或運(yùn)算,得到結(jié)果 1。
你可能很少需要處理很大的數(shù),但如果你真的要處理的話,有很多大整型庫(kù)能對(duì)大型精度數(shù)完成重要的數(shù)學(xué)運(yùn)算,如 node-bigint。 錯(cuò)誤 #8:忽略了 Streaming(流) API 的優(yōu)勢(shì) 大家都說(shuō)想建立一個(gè)小型代理服務(wù)器,它能響應(yīng)從其它服務(wù)器獲取內(nèi)容的請(qǐng)求。作為一個(gè)案例,我們將建立一個(gè)供應(yīng) Gravatar 圖像的小型 Web 服務(wù)器:
在這個(gè)特殊例子中有一個(gè) Node.js 問(wèn)題,我們從 Gravatar 獲取圖像,將它讀進(jìn)緩存區(qū),然后響應(yīng)請(qǐng)求。這不是一個(gè)多么糟糕的問(wèn)題,因?yàn)?Gravatar 返回的圖像并不是很大。然而,想象一下,如果我們代理的內(nèi)容大小有成千上萬(wàn)兆。那就有一個(gè)更好的方法了:
這里,我們獲取圖像,并簡(jiǎn)單地通過(guò)管道響應(yīng)給客戶端。絕不需要我們?cè)陧憫?yīng)之前,將全部?jī)?nèi)容讀取到緩沖區(qū)。 錯(cuò)誤 #9:把 Console.log 用于調(diào)試目的 在 Node.js 中,“console.log” 允許你向控制臺(tái)打印幾乎所有東西。傳遞一個(gè)對(duì)象給它,它會(huì)以 JavaScript 對(duì)象字面量的方式打印出來(lái)。它接受任意多個(gè)參數(shù),并以空格作為分隔符打印它們。有許多個(gè)理由讓開(kāi)發(fā)者很想用這個(gè)來(lái)調(diào)試(debug)自己的代碼;然而,我強(qiáng)烈建議你避免在真正程序里使用 “console.log” 。你應(yīng)該避免在全部代碼里使用 “console.log” 進(jìn)行調(diào)試(debug),當(dāng)不需要它們的時(shí)候,應(yīng)注釋掉它們。相反,使用專門為調(diào)試建立的庫(kù),如:debug。 當(dāng)你開(kāi)始編寫應(yīng)用程序時(shí),這些庫(kù)能方便地啟動(dòng)和禁用某行調(diào)試(debug)功能。例如,通過(guò)不設(shè)置 DEBUG 環(huán)境變量,能夠防止所有調(diào)試行被打印到終端。使用它很簡(jiǎn)單:
為了啟動(dòng)調(diào)試行,將環(huán)境變量 DEBUG 設(shè)置為 “app” 或 “*”,就能簡(jiǎn)單地運(yùn)行這些代碼了:
錯(cuò)誤 #10:不使用管理程序 不管你的 Node.js 代碼運(yùn)行在生產(chǎn)環(huán)境還是本地開(kāi)發(fā)環(huán)境,一個(gè)監(jiān)控管理程序能很好地管理你的程序,所以它是一個(gè)非常有用并值得擁有的東西。開(kāi)發(fā)者設(shè)計(jì)和實(shí)現(xiàn)現(xiàn)代應(yīng)用時(shí)常常推薦的一個(gè)最佳實(shí)踐是:快速失敗,快速迭代。 如果發(fā)生一個(gè)意料之外的錯(cuò)誤,不要試圖去處理它,而是讓你的程序崩潰,并有個(gè)監(jiān)控者在幾秒后重啟它。管理程序的好處不止是重啟崩潰的程序。這個(gè)工具允許你重啟崩潰的程序的同時(shí),也允許文件發(fā)生改變時(shí)重啟程序。這讓開(kāi)發(fā) Node.js 程序變成一段更愉快的體驗(yàn)。 有很多 Node.js 可用的管理程序。例如:
所有這些工具各有優(yōu)劣。一些有利于在同一個(gè)機(jī)器里處理多個(gè)應(yīng)用程序,而其它擅長(zhǎng)于日志管理。然而,如果你想開(kāi)始使用這些程序,它們都是很好的選擇。 總結(jié) 正如你所知道的那樣,一些 Node.js 問(wèn)題能對(duì)你的程序造成毀滅性打擊。而一些則會(huì)在你嘗試完成最簡(jiǎn)單的東西時(shí),讓你產(chǎn)生挫敗感。盡管 Node.js 的開(kāi)發(fā)門檻較低,但它仍然有很容易搞混的地方。從其它編程語(yǔ)言轉(zhuǎn)過(guò)來(lái)學(xué)習(xí) Node.js 開(kāi)發(fā)者可能會(huì)遇到這些問(wèn)題,但這些錯(cuò)誤在 Node.js 新手中也是十分常見(jiàn)的。幸運(yùn)的是,它們很容易避免。我希望這個(gè)簡(jiǎn)短指導(dǎo)能幫助 Node.js 新手寫出更優(yōu)秀的代碼,并為我們開(kāi)發(fā)出穩(wěn)定高效的軟件。 |
|