天天看點

Nodejs最佳實踐,空閑的時候翻翻

今天看到一篇很不錯的文章,雖然很長,但對于Nodejs開發人員來說很有必要收藏。

1. 項目結構實踐

✔ 1.1 元件式建構你的解決方案

TL;DR: 大型項目的最壞的隐患就是維護一個龐大的,含有幾百個依賴的代碼庫 - 當開發人員準備整合新的需求的時候,這樣一個龐然大物勢必減緩了開發效率。反之,把您的代碼拆分成元件,每一個元件有它自己的檔案夾和代碼庫,并且確定每一個元件小而簡單。檢視正确的項目結構的例子請通路下面的 ‘更多’ 連結。

否則: 當編寫新需求的開發人員逐漸意識到他所做改變的影響,并擔心會破壞其他的依賴子產品 - 部署會變得更慢,風險更大。當所有業務邏輯沒有被分開,這也會被認為很難擴充

✔ 1.2 分層設計元件,保持Express在特定的區域

TL;DR: 每一個元件都應該包含’層級’ - 一個專注的用于接入網絡,邏輯,資料的概念。這樣不僅獲得一個清晰的分離考量,而且使仿真和測試系統變得異常容易。盡管這是一個普通的模式,但接口開發者易于混淆層級關系,比如把網絡層的對象(Express req, res)傳給業務邏輯和資料層 - 這會令您的應用彼此依賴,并且隻能通過Express使用。

否則: 對于混淆了網絡層和其它層的應用,将不易于測試,執行CRON的任務,其它非-Express的調用者無法使用

✔ 1.3 封裝公共子產品成為NPM的包

TL;DR: 由大量代碼構成的一個大型應用中,貫徹全局的,比如日志,加密和其它類似的公共元件,應該進行封裝,并暴露成一個私有的NPM包。這将使其在更多的代碼庫和項目中被使用變成了可能。

否則: 您将不得不重造部署和依賴的輪子

✔ 1.4 分離 Express ‘app’ and ‘server’

TL;DR: 避免定義整個Express應用在一個單獨的大檔案裡, 這是一個不好的習慣 - 分離您的 ‘Express’ 定義至少在兩個檔案中: API聲明(app.js) 和 網絡相關(WWW)。對于更好的結構,是把你的API聲明放在元件中。

否則: 您的API将隻能通過HTTP的調用進行測試(慢,并且很難産生測試覆寫報告)。維護一個有着上百行代碼的檔案也不是一個令人開心的事情。

✔ 1.5 使用易于設定環境變量,安全和分級的配置

TL;DR: 一個完美無瑕的配置安裝應該確定 (a) 元素可以從檔案中,也可以從環境變量中讀取 (b) 密碼排除在送出的代碼之外 © 為了易于檢索,配置是分級的。僅有幾個包可以滿足這樣的條件,比如rc, nconf, config 和 convict。

否則: 不能滿足任意的配置要求将會使開發,運維團隊,或者兩者,易于陷入泥潭。

2. 錯誤處理最佳實踐

✔ 2.1 使用 Async-Await 和 promises 用于異步錯誤處理

TL;DR: 使用回調的方式處理異步錯誤可能是導緻災難的最快的方式(a.k.a the pyramid of doom)。對您的代碼來說,最好的禮物就是使用規範的promise庫或async-await來替代,這會使其像try-catch一樣更加簡潔,具有熟悉的代碼結構。

否則: Node.js回調特性, function(err, response), 是導緻不可維護代碼的一個必然的方式。究其原因,是由于混合了随意的錯誤處理代碼,臃腫的内嵌,蹩腳的代碼模式。

✔ 2.2 僅使用内建的錯誤對象

TL;DR: 很多人抛出異常使用字元串類型或一些自定義類型 - 這會導緻錯誤處理邏輯和子產品間的調用複雜化。是否您reject一個promise,抛出異常或發出(emit)錯誤 - 使用内建的錯誤對象将會增加設計一緻性,并防止資訊的丢失。

否則: 調用某些子產品,将不确定哪種錯誤類型會傳回 - 這将會使恰當的錯誤處理更加困難。更壞的情況是,使用特定的類型描述錯誤,會導緻重要的錯誤資訊缺失,比如stack trace!

✔ 2.3 區分運作錯誤和程式設計錯誤

TL;DR: 運作錯誤(例如, API接受到一個無效的輸入)指的是一些已知場景下的錯誤,這類錯誤的影響已經完全被了解,并能被考慮周全的處理掉。同時,程式設計錯誤(例如,嘗試讀取未定義的變量)指的是未知的編碼問題,影響到應用得當的重新開機。

否則: 當一個錯誤産生的時候,您總是得重新開機應用,但為什麼要讓 ~5000 個線上使用者不能通路,僅僅是因為一個細微的,可以預測的,運作時錯誤?相反的方案,也不完美 – 當未知的問題(程式問題)産生的時候,使應用依舊可以通路,可能導緻不可預測行為。區分兩者會使處理更有技巧,并在給定的上下文下給出一個平衡的對策。

✔ 2.4 集中處理錯誤,不要在Express中間件中處理錯誤

TL;DR: 錯誤處理邏輯,比如給管理者發送郵件,日志應該封裝在一個特定的,集中的對象當中,這樣當錯誤産生的時候,所有的終端(例如 Express中間件,cron任務,單元測試)都可以調用。

否則: 錯誤處理的邏輯不放在一起将會導緻代碼重複和非常可能不恰當的錯誤處理。

✔ 2.5 對API錯誤使用Swagger文檔化

TL;DR: 讓你的API調用者知道哪種錯誤會傳回,這樣他們就能完全的處理這些錯誤,而不至于系統崩潰。Swagger,REST API的文檔架構,通常處理這類問題。

否則: 任何API的用戶端可能決定崩潰并重新開機,僅僅因為它收到一個不能處理的錯誤。注意:API的調用者可能是你(在微服務環境中非常典型)。

✔ 2.6 當一個特殊的情況産生,停掉服務是得體的

TL;DR: 當一個不确定錯誤産生(一個開發錯誤,最佳實踐條款#3) - 這就意味着對應用運轉健全的不确定。一個普通的實踐将是建議仔細地重新開機程序,并使用一些‘啟動器’工具,比如Forever和PM2。

否則: 當一個未知的異常被抛出,意味着某些對象包含錯誤的狀态(例如某個全局事件發生器由于某些内在的錯誤,不在産生事件),未來的請求可能失敗或者行為異常。

✔ 2.7 使用一個成熟的日志工具提高錯誤的可見性

TL;DR: 一系列成熟的日志工具,比如Winston,Bunyan和Log4J,會加速錯誤的發現和了解。忘記console.log吧。

否則: 浏覽console的log,和不通過查詢工具或者一個好的日志檢視器,手動浏覽繁瑣的文本檔案,會使你忙于工作到很晚。

✔ 2.8 使用你最喜歡的測試架構測試錯誤流

TL;DR: 無論專業的自動化測試或者簡單的手動開發測試 - 確定您的代碼不僅滿足正常的場景,而且處理并且傳回正确的錯誤。測試架構,比如Mocha & Chai可以非常容易的處理這些問題(在"Gist popup"中檢視代碼執行個體) 。

否則: 沒有測試,不管自動還是手動,您不可能依賴代碼去傳回正确的錯誤。而沒有可以了解的錯誤,那将毫無錯誤處理可言。

✔ 2.9 使用APM産品發現錯誤和當機時間

TL;DR: 監控和性能産品 (别名 APM) 先前一步的檢測您的代碼庫和API,這樣他們能自動的,像使用魔法一樣的強調錯誤,當機和您忽略的性能慢的部分。

否則: 您花了很多的力氣在測量API的性能和錯誤,但可能您從來沒有意識到真實場景下您最慢的代碼塊和他們對UX的影響。

✔ 2.10 捕獲未處理的promise rejections

TL;DR: 任何在promise中被抛出的異常将被收回和遺棄,除非開發者沒有忘記去明确的處理。即使您的代碼調用的是process.uncaughtException!解決這個問題可以注冊到事件process.unhandledRejection。

否則: 您的錯誤将被回收,無蹤迹可循。沒有什麼可以需要考慮。

✔ 2.11 快速查錯,驗證參數使用一個專門的庫

TL;DR: 這應該是您的Express最佳實踐中的一部分 – assert API輸入避免難以了解的漏洞,這類漏洞以後會非常難以追蹤。而驗證代碼通常是一件乏味的事情,除非使用一些非常炫酷的幫助庫比如Joi。

否則: 考慮這種情況 – 您的功能期望一個數字參數 “Discount” ,然而調用者忘記傳值,之後在您的代碼中檢查是否 Discount!=0 (允許的折扣值大于零),這樣它将允許使用者使用一個折扣。OMG,多麼不爽的一個漏洞。你能明白嗎?

3. 編碼風格實踐

✔ 3.1 使用ESLint

TL;DR: ESLint是檢查可能的代碼錯誤和修複代碼樣式的事實上的标準,不僅可以識别實際的間距問題, 而且還可以檢測嚴重的反模式代碼, 如開發人員在不分類的情況下抛出錯誤。盡管ESlint可以自動修複代碼樣式,但其他的工具比如prettier和beautify在格式化修複上功能強大,可以和Eslint結合起來使用。

否則: 開發人員将必須關注單調乏味的間距和線寬問題, 并且時間可能會浪費在過多考慮項目的代碼樣式。

✔ 3.2 Node.js特定的插件

TL;DR: 除了僅僅涉及 vanilla JS 的 ESLint 标準規則,添加 Node 相關的插件,比如eslint-plugin-node, eslint-plugin-mocha and eslint-plugin-node-security

否則: 許多錯誤的Node.js代碼模式可能在檢測下逃生。例如,開發人員可能需要某些檔案,把一個變量作為路徑名 (variableAsPath) ,這會導緻攻擊者可以執行任何JS腳本。Node.JS linters可以檢測這類模式,并及早預警。

✔ 3.3 在同一行開始一個代碼塊的大括号

TL;DR: 代碼塊的第一個大括号應該和聲明的起始保持在同一行中。

代碼示例

// 建議

function someFunction() {

// 代碼塊

}

// 避免

function someFunction()

{

// 代碼塊

}

否則: 不遵守這項最佳實踐可能導緻意外的結果,在Stackoverflow的文章中可以檢視到,如下:

✔ 3.4 不要忘記分号

TL;DR: 即使沒有獲得一緻的認同,但在每一個表達式後面放置分号還是值得推薦的。這将使您的代碼, 對于其他閱讀代碼的開發者來說,可讀性,明确性更強。

否則: 在前面的章節裡面已經提到,如果表達式的末尾沒有添加分号,JavaScript的解釋器會在自動添加一個,這可能會導緻一些意想不到的結果。

✔ 3.5 命名您的方法

TL;DR: 命名所有的方法,包含閉包和回調, 避免匿名方法。當剖析一個node應用的時候,這是特别有用的。命名所有的方法将會使您非常容易的了解記憶體快照中您正在檢視的内容。

否則: 使用一個核心dump(記憶體快照)調試線上問題,會是一項非常挑戰的事項,因為你注意到的嚴重記憶體洩漏問題極有可能産生于匿名的方法。

✔ 3.6 變量、常量、函數和類的命名約定

TL;DR: 當命名變量和方法的時候,使用 lowerCamelCase ,當命名類的時候,使用 UpperCamelCase (首字母大寫),對于常量,則 UPPERCASE 。這将幫助您輕松地區分普通變量/函數和需要執行個體化的類。使用描述性名稱,但使它們盡量簡短。

否則: JavaScript是世界上唯一一門不需要執行個體化,就可以直接調用構造函數(“Class”)的編碼語言。是以,類和函數的構造函數由采用UpperCamelCase開始區分。

代碼示例

// 使用UpperCamelCase命名類名

class SomeClassExample () {

// 常量使用const關鍵字,并使用lowerCamelCase命名
const config = {
  key: 'value'
};

// 變量和方法使用lowerCamelCase命名
let someVariableExample = 'value';
function doSomething() {

}
           

}

✔ 3.7 使用const優于let,廢棄var

TL;DR: 使用const意味着一旦一個變量被配置設定,它不能被重新配置設定。使用const将幫助您免于使用相同的變量用于不同的用途,并使你的代碼更清晰。如果一個變量需要被重新配置設定,以在一個循環為例,使用let聲明它。let的另一個重要方面是,使用let聲明的變量隻在定義它的塊作用域中可用。 var是函數作用域,不是塊級作用域,既然您有const和let讓您随意使用,那麼不應該在ES6中使用var。

否則: 當經常更改變量時,調試變得更麻煩了。

✔ 3.8 先require, 而不是在方法内部

TL;DR: 在每個檔案的起始位置,在任何函數的前面和外部require子產品。這種簡單的最佳實踐,不僅能幫助您輕松快速地在檔案頂部辨識出依賴關系,而且避免了一些潛在的問題。

否則: 在Node.js中,require 是同步運作的。如果從函數中調用它們,它可能會阻塞其他請求,在更關鍵的時間得到處理。另外,如果所require的子產品或它自己的任何依賴項抛出錯誤并使伺服器崩潰,最好盡快查明它,如果該子產品在函數中require的,則可能不是這樣的情況。

✔ 3.9 require 檔案夾,而不是檔案

TL;DR: 當在一個檔案夾中開發庫/子產品,放置一個檔案index.js暴露子產品的 内部,這樣每個消費者都會通過它。這将作為您子產品的一個接口,并使未來的變化簡單而不違反規則。

否則: 更改檔案内部結構或簽名可能會破壞與用戶端的接口。

代碼示例

// 建議

module.exports.SMSProvider = require(’./SMSProvider’);

module.exports.SMSNumberResolver = require(’./SMSNumberResolver’);

// 避免

module.exports.SMSProvider = require(’./SMSProvider/SMSProvider.js’);

module.exports.SMSNumberResolver = require(’./SMSNumberResolver/SMSNumberResolver.js’);

✔ 3.10 使用 === 操作符

TL;DR: 對比弱等于 ==,優先使用嚴格的全等于 === 。==将在它們轉換為普通類型後比較兩個變量。在 === 中沒有類型轉換,并且兩個變量必須是相同的類型。

否則: 與 == 操作符比較,不相等的變量可能會傳回true。

代碼示例

‘’ == ‘0’ // false

0 == ‘’ // true

0 == ‘0’ // true

false == ‘false’ // false

false == ‘0’ // true

false == undefined // false

false == null // false

null == undefined // true

’ \t\r\n ’ == 0 // true

如果使用===, 上面所有語句都将傳回 false。

✔ 3.11 使用 Async Await, 避免回調

TL;DR: Node 8 LTS現已全面支援異步等待。這是一種新的方式處理異步請求,取代回調和promise。Async-await是非阻塞的,它使異步代碼看起來像是同步的。您可以給你的代碼的最好的禮物是用async-await提供了一個更緊湊的,熟悉的,類似try catch的代碼文法。

否則: 使用回調的方式處理異步錯誤可能是陷入困境最快的方式 - 這種方式必須面對不停地檢測錯誤,處理别扭的代碼内嵌,難以推理編碼流。

✔ 3.12 使用 (=>) 箭頭函數

TL;DR: 盡管使用 async-await 和避免方法作為參數是被推薦的, 但當處理那些接受promise和回調的老的API的時候 - 箭頭函數使代碼結構更加緊湊,并保持了根方法上的語義上下文 (例如 ‘this’)。

否則: 更長的代碼(在ES5方法中)更易于産生缺陷,并讀起來很是笨重。

4. 測試和總體的品質實踐

✔ 4.1 至少,編寫API(元件)測試

TL;DR: 大多數項目隻是因為時間表太短而沒有進行任何自動化測試,或者測試項目失控而正被遺棄。是以,優先從API測試開始,這是最簡單的編寫和提供比單元測試更多覆寫率的事情(你甚至可能不需要編碼而進行API測試,像Postman。之後,如果您有更多的資源和時間,繼續使用進階測試類型,如單元測試、DB測試、性能測試等。

否則: 您可能需要花很長時間編寫單元測試,才發現隻有20%的系統覆寫率。

✔ 4.2 使用一個linter檢測代碼問題

TL;DR: 使用代碼linter檢查基本品質并及早檢測反模式。在任何測試之前運作它, 并将其添加為預送出的git鈎子, 以最小化審查和更正任何問題所需的時間。也可在Section 3中查閱編碼樣式實踐

否則: 您可能讓一些反模式和易受攻擊的代碼傳遞到您的生産環境中。

✔ 4.3 仔細挑選您的持續內建(CI)平台

TL;DR: 您的持續內建平台(cicd)将內建各種品質工具(如測試、lint),是以它應該是一個充滿活力的生态系統,包含各種插件。jenkins曾經是許多項目的預設選項,因為它有最大的社群,同時也是一個非常強大的平台,這樣的代價是要求一個陡峭的學習曲線。如今,使用SaaS工具,比如CircleCI及其他,安裝一套CI解決方案,相對是一件容易的事情。這些工具允許建構靈活的CI管道,而無需管理整個基礎設施。最終,這是一個魯棒性和速度之間的權衡 - 仔細選擇您支援的方案。

否則: 一旦您需要一些進階定制,選擇一些細分市場供應商可能會讓您停滞不前。另一方面,伴随着jenkins,可能會在基礎設施設定上浪費寶貴的時間。

✔ 4.4 經常檢查易受攻擊的依賴

TL;DR: 即使是那些最有名的依賴子產品,比如Express,也有已知的漏洞。使用社群和商業工具,比如 link npm audit ,內建在您的CI平台上,在每一次建構的時候都會被調用,這樣可以很容易地解決漏洞問題。

否則: 在沒有專用工具的情況下,使代碼清除漏洞,需要不斷地跟蹤有關新威脅的線上出版物,相當繁瑣。

✔ 4.5 測試标簽化

TL;DR: 不同的測試必須運作在不同的情景:quick smoke,IO-less,當開發者儲存或送出一個檔案,測試應該啟動;完整的端到端的測試通常運作在一個新的pull request被送出之後,等等。這可以通過對測試用例設定标簽,比如關鍵字像#cold #api #sanity,來完成。這樣您可以對您的測試集進行grep,調用需要的子集。例如,這就是您通過Mocha僅僅調用sanity測試集所需要做的:mocha --grep ‘sanity’。

否則: 運作所有的測試,包括執行資料庫查詢的幾十個測試,任何時候開發者進行小的改動都可能很慢,這使得開發者不願意運作測試。

✔ 4.6 檢查測試覆寫率,它有助于識别錯誤的測試模式

TL;DR: 代碼覆寫工具比如 Istanbul/NYC,很好用有3個原因:它是免費的(獲得這份報告不需要任何開銷),它有助于确定測試覆寫率降低的部分,以及最後但非最不重要的是它指出了測試中的不比對:通過檢視顔色标記的代碼覆寫報告您可以注意到,例如,從來不會被測到的代碼片段像catch語句(即測試隻是調用正确的路徑,而不調用應用程式發生錯誤時的行為)。如果覆寫率低于某個門檻值,則将其設定為失敗的建構。

否則: 當你的大部分代碼沒有被測試覆寫時,就不會有任何自動化的度量名額告訴你了。

✔ 4.7 檢查過期的依賴包

TL;DR: 使用您的首選工具 (例如 “npm outdated” or npm-check-updates 來檢測已安裝的過期依賴包, 将此檢查注入您的 CI 管道, 甚至在嚴重的情況下使建構失敗。例如, 當一個已安裝的依賴包滞後5個更新檔時 (例如:本地版本是1.3.1 的, 存儲庫版本是1.3.8 的), 或者它被其作者标記為已棄用, 可能會出現嚴重的情況 - 停掉這次建構并防止部署此版本。

否則: 您的生産環境将運作已被其作者明确标記為有風險的依賴包

✔ 4.8 對于e2e testing,使用docker-compose

TL;DR: 端對端(e2e)測試包含現場資料,由于它依賴于很多重型服務如資料庫,習慣被認為是CI過程中最薄弱的環節。Docker-compose通過制定類似生産的環境,并使用一個簡單的文本檔案和簡單的指令,輕松化解了這個問題。它為了e2e測試,允許制作所有相關服務,資料庫和隔離網絡。最後但并非最不重要的一點是,它可以保持一個無狀态環境,該環境在每個測試套件之前被調用,然後立即消失。

否則: 沒有docker-compose,團隊必須維護一個測試資料庫在每一個測試環境上,包含開發機器,保持所有資料同步,這樣測試結果不會因環境不同而不同。

5. 上線實踐

✔ 5.1. 監控!

TL;DR: 監控是一種在顧客之前發現問題的遊戲 – 顯然這應該被賦予前所未有的重要性。考慮從定義你必須遵循的基本度量标準開始(我的建議在裡面),到檢查附加的花哨特性并選擇解決所有問題的解決方案。市場已經淹沒其中。點選下面的 ‘The Gist’ ,了解解決方案的概述。

否則: 錯誤 === 失望的客戶. 非常簡單.

✔ 5.2. 使用智能日志增加透明度

TL;DR: 日志可以是調試語句的一個不能說話的倉庫,或者表述應用運作過程的一個漂亮儀表闆的驅動。從第1天計劃您的日志平台:如何收集、存儲和分析日志,以確定所需資訊(例如,錯誤率、通過服務和伺服器等完成整個事務)都能被提取出來。

否則: 您最終像是面對一個黑盒,不知道發生了什麼事情,然後你開始重新寫日志語句添加額外的資訊。

✔ 5.3. 委托可能的一切(例如:gzip,SSL)給反向代理

TL;DR: Node處理CPU密集型任務,如gzipping,SSL termination等,表現糟糕。相反,使用一個 ‘真正’ 的中間件服務像Nginx,HAProxy或者雲供應商的服務。

否則: 可憐的單線程Node将不幸地忙于處理網絡任務,而不是處理應用程式核心,性能會相應降低。

✔ 5.4. 鎖住依賴

TL;DR: 您的代碼必須在所有的環境中是相同的,但是令人驚訝的是,NPM預設情況下會讓依賴在不同環境下發生偏移 – 當在不同的環境中安裝包的時候,它試圖拿包的最新版本。克服這種問題可以利用NPM配置檔案, .npmrc,告訴每個環境儲存準确的(不是最新的)包的版本。另外,對于更精細的控制,使用NPM “shrinkwrap”。*更新:作為NPM5,依賴預設鎖定。新的包管理工具,Yarn,也預設鎖定。

否則: QA測試通過的代碼和準許的版本,在生産中表現不一緻。更糟糕的是,同一生産叢集中的不同伺服器可能運作不同的代碼。

✔ 5.5. 使用正确的工具保護程序正常運作

TL;DR: 程序必須繼續運作,并在失敗時重新啟動。對于簡單的情況下,“重新開機”工具如PM2可能足夠,但在今天的“Dockerized”世界 – 叢集管理工具也值得考慮

否則: 運作幾十個執行個體沒有明确的戰略和太多的工具(叢集管理,docker,PM2)可能導緻一個DevOps混亂

✔ 5.6. 利用CPU多核

TL;DR: 在基本形式上,node應用程式運作在單個CPU核心上,而其他都處于空閑狀态。複制node程序和利用多核,這是您的職責 – 對于中小應用,您可以使用Node Cluster和PM2. 對于一個大的應用,可以考慮使用一些Docker cluster(例如k8s,ECS)複制程序或基于Linux init system(例如systemd)的部署腳本

否則: 您的應用可能隻是使用了其可用資源中的25% (!),甚至更少。注意,一台典型的伺服器有4個或更多的CPU,預設的Node.js部署僅僅用了一個CPU(甚至使用PaaS服務,比如AWS beanstalk,也一樣)。

✔ 5.7. 建立一個“維護端點”

TL;DR: 在一個安全的API中暴露一組系統相關的資訊,比如記憶體使用情況和REPL等等。盡管這裡強烈建議依賴标準和作戰測試工具,但一些有價值的資訊和操作更容易使用代碼完成。

否則: 您會發現,您正在執行許多“診斷部署” – 将代碼發送到生産中,僅僅隻為了診斷目的提取一些資訊。

✔ 5.8. 使用APM産品發現錯誤和當機時間

TL;DR: 監控和性能的産品(即APM)先前一步地評估代碼庫和API,自動的超過傳統的監測,并測量在服務和層級上的整體使用者體驗。例如,一些APM産品可以突顯導緻最終使用者負載過慢的事務,同時指出根本原因。

否則: 你可能會花大力氣測量API性能和停機時間,也許你永遠不會知道,真實場景下哪個是你最慢的代碼部分,這些怎麼影響使用者體驗。

✔ 5.9. 使您的代碼保持生産環境就緒

TL;DR: 在意識中抱着最終上線的想法進行編碼,從第1天開始計劃上線。這聽起來有點模糊,是以我編寫了一些與生産維護密切相關的開發技巧(點選下面的要點)

否則: 一個世界冠軍級别的IT/運維人員也不能拯救一個編碼低劣的系統。

✔ 5.10. 測量和保護記憶體使用

TL;DR: Node.js和記憶體有引起争論的聯系:V8引擎對記憶體的使用有稍微的限制(1.4GB),在node的代碼裡面有記憶體洩漏的很多途徑 – 是以監視node的程序記憶體是必須的。在小應用程式中,你可以使用shell指令周期性地測量記憶體,但在中等規模的應用程式中,考慮把記憶體監控建成一個健壯的監控系統。

否則: 您的記憶體可能一天洩漏一百兆,就像曾發生在沃爾瑪的一樣。

✔ 5.11. Node外管理您的前端資源

TL;DR: 使用專門的中間件(nginx,S3,CDN)服務前端内容,這是因為在處理大量靜态檔案的時候,由于node的單線程模型,它的性能很受影響。

否則: 您的單個node線程将忙于傳輸成百上千的html/圖檔/angular/react檔案,而不是配置設定其所有的資源為了其擅長的任務 – 服務動态内容

✔ 5.12. 保持無狀态,幾乎每天都要停下伺服器

TL;DR: 在外部資料存儲上,存儲任意類型資料(例如使用者會話,緩存,上傳檔案)。考慮間隔地停掉您的伺服器或者使用 ‘serverless’ 平台(例如 AWS Lambda),這是一個明确的強化無狀态的行為。

否則: 某個伺服器上的故障将導緻應用程式當機,而不僅僅是停用故障機器。此外,由于依賴特定伺服器,伸縮彈性會變得更具挑戰性。

✔ 5.13. 使用自動檢測漏洞的工具

TL;DR: 即使是最有信譽的依賴項,比如Express,會有使系統處于危險境地的已知漏洞(随着時間推移)。通過使用社群的或者商業工具,不時的檢查漏洞和警告(本地或者Github上),這類問題很容易被抑制,有些問題甚至可以立即修補。

否則: 否則: 在沒有專用工具的情況下,使代碼清除漏洞,需要不斷地跟蹤有關新威脅的線上出版物。相當繁瑣。

✔ 5.14. 在每一個log語句中指明 ‘TransactionId’

TL;DR: 在每一個請求的每一條log入口,指明同一個辨別符,transaction-id: {某些值}。然後在檢查日志中的錯誤時,很容易總結出前後發生的事情。不幸的是,由于Node異步的天性自然,這是不容易辦到的,看下代碼裡面的例子

否則: 在沒有上下文的情況下檢視生産錯誤日志,這會使問題變得更加困難和緩慢去解決。

✔ 5.15. 設定NODE_ENV=production

TL;DR: 設定環境變量NODE_ENV為‘production’ 或者 ‘development’,這是一個是否激活上線優化的标志 - 很多NPM的包通過它來判斷目前的環境,據此優化生産環境代碼。

否則: 遺漏這個簡單的屬性可能大幅減弱性能。例如,在使用Express作為服務端渲染頁面的時候,如果未設定NODE_ENV,性能将會減慢大概三分之一!

✔ 5.16. 設計自動化、原子化和零停機時間部署

TL;DR: 研究表明,執行許多部署的團隊降低了嚴重上線問題的可能性。不需要危險的手動步驟和服務停機時間的快速和自動化部署大大改善了部署過程。你應該達到使用Docker結合CI工具,使他們成為簡化部署的行業标準。

否則: 長時間部署 -> 線上當機 & 和人相關的錯誤 -> 團隊部署時不自信 -> 更少的部署和需求

6. 安全最佳實踐

✔ 6.1. 擁護linter安全準則

TL;DR: 使用安全相關的linter插件,比如eslint-plugin-security,盡早捕獲安全隐患或者問題,最好在編碼階段。這能幫助察覺安全的問題,比如使用eval,調用子程序,或者根據字面含義(比如,使用者輸入)引入子產品等等。點選下面‘更多’獲得一個安全linter可以檢測到的代碼示例。

Otherwise: 在開發過程中, 可能一個直白的安全隐患, 成為生産環境中一個嚴重問題。此外, 項目可能沒有遵循一緻的安全規範, 而導緻引入漏洞, 或敏感資訊被送出到遠端倉庫中。

這裡是引用

✔ 6.2. 使用中間件限制并發請求

TL;DR: DOS攻擊非常流行而且相對容易處理。使用外部服務,比如cloud負載均衡, cloud防火牆, nginx, 或者(對于小的,不是那麼重要的app)一個速率限制中間件(比如express-rate-limit),來實作速率限制。

否則: 應用程式可能受到攻擊, 導緻拒絕服務, 在這種情況下, 真實使用者會遭受服務降級或不可用。

✔ 6.3 把機密資訊從配置檔案中抽離出來,或者使用包對其加密

TL;DR: 不要在配置檔案或源代碼中存儲純文字機密資訊。相反, 使用諸如Vault産品、Kubernetes/Docker Secrets或使用環境變量之類的安全管理系統。最後一個結果是, 存儲在源代碼管理中的機密資訊必須進行加密和管理 (滾動密鑰(rolling keys)、過期時間、稽核等)。使用pre-commit/push鈎子防止意外送出機密資訊。

否則: 源代碼管理, 即使對于私有倉庫, 也可能會被錯誤地公開, 此時所有的秘密資訊都會被公開。外部組織的源代碼管理的通路權限将無意中提供對相關系統 (資料庫、api、服務等) 的通路。

✔ 6.4. 使用 ORM/ODM 庫防止查詢注入漏洞

TL;DR: 要防止 SQL/NoSQL 注入和其他惡意攻擊, 請始終使用 ORM/ODM 或database庫來轉義資料或支援命名的或索引的參數化查詢, 并注意驗證使用者輸入的預期類型。不要隻使用JavaScript模闆字元串或字元串串聯将值插入到查詢語句中, 因為這會将應用程式置于廣泛的漏洞中。所有知名的Node.js資料通路庫(例如Sequelize, Knex, mongoose)包含對注入漏洞的内置包含措施。

否則: 未經驗證或未脫敏處理的使用者輸入,可能會導緻操作員在使用MongoDB進行NoSQL操作時進行注入, 而不使用适當的過濾系統或ORM很容易導緻SQL注入攻擊, 進而造成巨大的漏洞。

✔ 6.5. 通用安全最佳實際集合

TL;DR: 這些是與Node.js不直接相關的安全建議的集合-Node的實作與任何其他語言沒有太大的不同。單擊 “閱讀更多” 浏覽。

✔ 6.6. 調整 HTTP 響應頭以加強安全性

TL;DR: 應用程式應該使用安全的header來防止攻擊者使用常見的攻擊方式,諸如跨站點腳本(XSS)、點選劫持和其他惡意攻擊。可以使用子產品,比如 helmet輕松進行配置。

否則: 攻擊者可以對應用程式的使用者進行直接攻擊, 導緻巨大的安全漏洞

✔ 6.7. 經常自動檢查易受攻擊的依賴庫

TL;DR: 在npm的生态系統中, 一個項目有許多依賴是很常見的。在找到新的漏洞時, 應始終将依賴項保留在檢查中。使用工具,類似于npm audit 或者 snyk跟蹤、監視和修補易受攻擊的依賴項。将這些工具與 CI 設定內建, 以便在将其上線之前捕捉到易受攻擊的依賴庫。

否則: 攻擊者可以檢測到您的web架構并攻擊其所有已知的漏洞。

✔ 6.8. 避免使用Node.js的crypto庫處理密碼,使用Bcrypt

TL;DR: 密碼或機密資訊(API密鑰)應該使用安全的哈希+salt函數(如 “bcrypt”)來存儲, 因為性能和安全原因, 這應該是其JavaScript實作的首選。

否則: 在不使用安全功能的情況下,儲存的密碼或秘密資訊容易受到暴力破解和字典攻擊, 最終會導緻他們的洩露。

✔ 6.9. 轉義 HTML、JS 和 CSS 輸出

TL;DR: 發送給浏覽器的不受信任資料可能會被執行, 而不是顯示, 這通常被稱為跨站點腳本(XSS)攻擊。使用專用庫将資料顯式标記為不應執行的純文字内容(例如:編碼、轉義),可以減輕這種問題。

否則: 攻擊者可能會将惡意的JavaScript代碼存儲在您的DB中, 然後将其發送給可憐的用戶端。

✔ 6.10. 驗證傳入的JSON schemas

TL;DR: 驗證傳入請求的body payload,并確定其符合預期要求, 如果沒有, 則快速報錯。為了避免每個路由中繁瑣的驗證編碼, 您可以使用基于JSON的輕量級驗證架構,比如jsonschema or joi

否則: 您疏忽和寬松的方法大大增加了攻擊面, 并鼓勵攻擊者嘗試許多輸入, 直到他們找到一些組合, 使應用程式崩潰。

✔ 6.11. 支援黑名單的JWT

TL;DR: 當使用JSON Web Tokens(例如, 通過Passport.js), 預設情況下, 沒有任何機制可以從發出的令牌中撤消通路權限。一旦發現了一些惡意使用者活動, 隻要它們持有有效的标記, 就無法阻止他們通路系統。通過實作一個不受信任令牌的黑名單,并在每個請求上驗證,來減輕此問題。

否則: 過期或錯誤的令牌可能被第三方惡意使用,以通路應用程式,并模拟令牌的所有者。

✔ 6.12. 限制每個使用者允許的登入請求

TL;DR: 一類保護暴力破解的中間件,比如express-brute,應該被用在express的應用中,來防止暴力/字典攻擊;這類攻擊主要應用于一些敏感路由,比如/admin 或者 /login,基于某些請求屬性, 如使用者名, 或其他辨別符, 如正文參數等。

否則: 攻擊者可以發出無限制的密碼比對嘗試, 以擷取對應用程式中特權帳戶的通路權限。

✔ 6.13. 使用非root使用者運作Node.js

TL;DR: Node.js作為一個具有無限權限的root使用者運作,這是一種普遍的情景。例如,在Docker容器中,這是預設行為。建議建立一個非root使用者,并儲存到Docker鏡像中(下面給出了示例),或者通過調用帶有"-u username" 的容器來代表此使用者運作該程序。

否則: 在伺服器上運作腳本的攻擊者在本地計算機上獲得無限制的權利 (例如,改變iptable,引流到他的伺服器上)

✔ 6.14. 使用反向代理或中間件限制負載大小

TL;DR: 請求body有效載荷越大, Node.js的單線程就越難處理它。這是攻擊者在沒有大量請求(DOS/DDOS 攻擊)的情況下,就可以讓伺服器跪下的機會。在邊緣上(例如,防火牆,ELB)限制傳入請求的body大小,或者通過配置express body parser僅接收小的載荷,可以減輕這種問題。

否則: 您的應用程式将不得不處理大的請求, 無法處理它必須完成的其他重要工作, 進而導緻對DOS攻擊的性能影響和脆弱性。

✔ 6.15. 避免JavaScript的eval聲明

TL;DR: eval 是邪惡的, 因為它允許在運作時執行自定義的JavaScript代碼。這不僅是一個性能方面的問題, 而且也是一個重要的安全問題, 因為惡意的JavaScript代碼可能來源于使用者輸入。應該避免的另一種語言功能是 new Function 構造函數。setTimeout 和 setInterval 也不應該傳入動态JavaScript代碼。

否則: 惡意JavaScript代碼查找傳入 eval 或其他實時判斷的JavaScript函數的文本的方法, 并将獲得在該頁面上javascript權限的完全通路權。此漏洞通常表現為XSS攻擊。

✔ 6.16. 防止惡意RegEx讓Node.js的單線程過載執行

TL;DR: 正規表達式,在友善的同時,對JavaScript應用構成了真正的威脅,特别是Node.js平台。比對文本的使用者輸入需要大量的CPU周期來處理。在某種程度上,正則處理是效率低下的,比如驗證10個單詞的單個請求可能阻止整個event loop長達6秒,并讓CPU引火燒身。由于這個原因,偏向第三方的驗證包,比如validator.js,而不是采用正則,或者使用safe-regex來檢測有問題的正規表達式。

否則: 寫得不好的正規表達式可能容易受到正規表達式DoS攻擊的影響, 這将完全阻止event loop。例如,流行的moment包在2017年的11月,被發現使用了錯誤的RegEx用法而易受攻擊。

✔ 6.17. 使用變量避免子產品加載

TL;DR: 避免通過作為參數的路徑requiring/importing另一個檔案, 原因是它可能源自使用者輸入。此規則可擴充為通路一般檔案(即:fs.readFile())或使用來自使用者輸入的動态變量通路其他敏感資源。Eslint-plugin-security linter可以捕捉這樣的模式, 并盡早提前警告。

否則: 惡意使用者輸入可以找到用于獲得篡改檔案的參數, 例如, 檔案系統上以前上載的檔案, 或通路已有的系統檔案。

✔ 6.18. 在沙箱中運作不安全代碼

TL;DR: 當任務執行在運作時給出的外部代碼時(例如, 插件), 使用任何類型的沙盒執行環境保護主代碼,并隔離開主代碼和插件。這可以通過一個專用的過程來實作 (例如:cluster.fork()), 無伺服器環境或充當沙盒的專用npm包。

否則: 插件可以通過無限循環、記憶體超載和對敏感程序環境變量的通路等多種選項進行攻擊

✔ 6.19. 使用子程序時要格外小心

TL;DR: 盡可能地避免使用子程序,如果您仍然必須這樣做,驗證和清理輸入以減輕shell注入攻擊。更喜歡使用 “child_process”。execFile 的定義将隻執行具有一組屬性的單個指令, 并且不允許 shell 參數擴充。傾向于使用child_process.execFile,從定義上來說,它将僅僅執行具有一組屬性的單個指令,并且不允許shell參數擴充。

否則: 由于将惡意使用者輸入傳遞給未脫敏處理的系統指令, 直接地使用子程序可能導緻遠端指令執行或shell注入攻擊。

✔ 6.20. 隐藏用戶端的錯誤詳細資訊

TL;DR: 預設情況下, 內建的express錯誤處理程式隐藏錯誤詳細資訊。但是, 極有可能, 您實作自己的錯誤處理邏輯與自定義錯誤對象(被許多人認為是最佳做法)。如果這樣做, 請確定不将整個Error對象傳回到用戶端, 這可能包含一些敏感的應用程式詳細資訊。

否則: 敏感應用程式詳細資訊(如伺服器檔案路徑、使用中的第三方子產品和可能被攻擊者利用的應用程式的其他内部工作流)可能會從stack trace發現的資訊中洩露。

link 更多: 隐藏用戶端的錯誤詳細資訊

✔ 6.21. 對npm或Yarn,配置2FA

TL;DR: 開發鍊中的任何步驟都應使用MFA(多重身份驗證)進行保護, npm/Yarn對于那些能夠掌握某些開發人員密碼的攻擊者來說是一個很好的機會。使用開發人員憑據, 攻擊者可以向跨項目和服務廣泛安裝的庫中注入惡意代碼。甚至可能在網絡上公開釋出。在npm中啟用2因素身份驗證(2-factor-authentication), 攻擊者幾乎沒有機會改變您的軟體包代碼。

否則: Have you heard about the eslint developer who’s password was hijacked?

✔ 6.22. 修改session中間件設定

TL;DR: 每個web架構和技術都有其已知的弱點-告訴攻擊者我們使用的web架構對他們來說是很大的幫助。使用session中間件的預設設定, 可以以類似于X-Powered-Byheader的方式向子產品和架構特定的劫持攻擊公開您的應用。嘗試隐藏識别和揭露技術棧的任何内容(例如:Nonde.js, express)。

否則: 可以通過不安全的連接配接發送cookie, 攻擊者可能會使用會話辨別來辨別web應用程式的基礎架構以及特定于子產品的漏洞。

✔ 6.23. 通過顯式設定程序應崩潰的情況,以避免DOS攻擊

TL;DR: 當錯誤未被處理時, Node程序将崩潰。即使錯誤被捕獲并得到處理,許多最佳實踐甚至建議退出。例如, Express會在任何異步錯誤上崩潰 - 除非使用catch子句包裝路由。這将打開一個非常惬意的攻擊點, 攻擊者識别哪些輸入會導緻程序崩潰并重複發送相同的請求。沒有即時補救辦法, 但一些技術可以減輕苦楚: 每當程序因未處理的錯誤而崩潰,都會發出警報,驗證輸入并避免由于使用者輸入無效而導緻程序崩潰,并使用catch将所有路由處理包裝起來,并在請求中出現錯誤時, 考慮不要崩潰(與全局發生的情況相反)。

否則: 這隻是一個起到教育意義的假設: 給定許多Node.js應用程式, 如果我們嘗試傳遞一個空的JSON正文到所有POST請求 - 少數應用程式将崩潰。在這一點上, 我們可以隻是重複發送相同的請求, 就可以輕松地搞垮應用程式。

✔ 6.24. 避免不安全的重定向

TL;DR: 不驗證使用者輸入的重定向可使攻擊者啟動網絡釣魚詐騙,竊取使用者憑據,以及執行其他惡意操作。

否則: 當攻擊者發現你沒有校驗使用者提供的外部輸入時,他們會在論壇、社交媒體以和其他公共場合釋出他們精心制作的連結來誘使使用者點選,以此達到漏洞利用的目的。

繼續閱讀