本文是“NodeJS 最佳實踐”教程,分上下兩個部分。
上篇
人們總是問我們關於Node.js的最佳實戰和技巧,所以打算通過這篇文章解釋清楚,並總結一下我們在RisingStack公司寫程式碼的經驗。
Node.js最佳實戰中的一部分是編碼規範,另一部分則是處理開發流程。
編碼規範
回撥慣例
模組應該公開一個錯誤優先(error-first)的回撥介面。
就像下面這樣:
1 2 3 4 5 6 7 8 |
module.exports = function (dragonName, callback) { // 這裡做一些處理工作 var dragon = createDragon(dragonName); // 注意, callback第一個引數是 error // 這裡傳入null // 如果出錯則傳入錯誤資訊 return callback(null, dragon); } |
確保在回撥中檢查錯誤資訊
要更好地弄明白為什麼必須這樣做,先想辦法建立一個會掛掉的例子,然後修復它。
1 2 3 4 5 6 7 8 |
// 這個例子是會掛掉的, 我們很快會修復 :) var fs = require('fs'); function readJSON(filePath, callback){ fs.readFile(filePath, function(err, data) { callback(JSON.parse(data)); }); } readJSON('./package.json', function (err, pkg) { ... } |
首要問題是 readJSON
函式,在執行過程中出現了錯誤,而這個函式卻沒有做任何錯誤檢查。你務必要先做錯誤檢查。
改進方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 這個例子還是會掛掉 , 很快會修復! function readJSON(filePath, callback) { fs.readFile(filePath, function(err, data) { // 這裡我們先判斷是否有錯誤發生 if (err) { // 出現錯誤,將錯誤傳入回撥函式 // 記住: 錯誤優先(error-first) 回撥 callback(err); } // 如果沒有錯誤則傳入null和JSON callback(null, JSON.parse(data)); }); } |
將回撥函式返回
上面的例子還是存在一個錯誤,就是如果錯誤發生了,if
中的表示式不會停止執行,而是繼續執行下去。這會導致很多未知的錯誤。長話短說,務必通過回撥函式返回。
1 2 3 4 5 6 7 8 9 10 |
// 這個例子仍舊會掛掉, 馬上修復! function readJSON(filePath, callback) { fs.readFile(filePath, function(err, data) { if (err) { return callback(err); } return callback(null, JSON.parse(data)); }); } |
僅在同步程式碼中使用try-catch
幾乎完美了!但還有一件事,我們必須要小心 JSON.parse
。呼叫JSON.parse
時,如果傳入的字串無法解析成JSON
格式,會丟擲異常。
由於JSON.parse是同步發生的,我們可以用try-catch包裝起來。請注意,你只能對同步程式碼塊做此操作,對回撥函式是不起作用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 這個例子終於可以正常工作啦 :) function readJSON(filePath, callback) { fs.readFile(filePath, function(err, data) { var parsedJson; // 處理錯誤 if (err) { return callback(err); } // 解析JSON try { parsedJson = JSON.parse(data); } catch (exception){ return callback(exception); } // 一切工作正常 return callback(null, parsedJson); }); } |
儘量避免 this 和 new 關鍵字
由於Node涉及了大量的回撥操作,並且重度使用高階函式控制流程,因此在Node中繫結一個具體的上下文並不總是行之有效。使用函數語言程式設計風格能避免不少麻煩。
當然,在某些情況下原型(prototype)可能更高效,不過只要可能,還是儘量避免它們。
建立微模組
用unix的方式:
開發者構建一個程式時應該將其分成很多簡單的模組,各部分由定義良好的介面整合,所以問題是區域性的,並且能通過替換程式部件的方式在將來的版中加入新特性。
不要建立怪獸般的程式碼,保持簡潔,一個模組就只做一件事,但是要做到極致。
使用良好的非同步模式
使用async非同步處理模組。
錯誤處理
錯誤可以分為兩部分,操作錯誤和程式設計錯誤。
操作錯誤
精心編寫的應用程式中也一樣會出現操作錯誤。因為這些不是 bug ,而是由於作業系統或遠端服務導致的,例如:
- 請求超時
- 系統記憶體不足
- 遠端連線失敗
處理操作錯誤
根據不同執行錯誤的型別,你可以採用下面的方式處理:
- 嘗試解決錯誤——如果檔案丟失,你可以提前建立一個。
- 當處理網路通訊時,可以重試操作。
- 把問題告訴客戶,表示有些功能不能正常工作——可以用於處理使用者輸入。
- 如果錯誤無法在當前條件下解決,終止程式,例如應用程式無法讀取它的配置檔案。
還有,上述的所有處理方式都應該記錄日誌。
程式設計錯誤
程式設計錯誤都算是bug。下面所列的幾條你應該避免,例如:
- 呼叫非同步函式時沒有回撥。
- 不能讀取未定義(undefined)的屬性
處理程式設計錯誤
如果錯誤屬於bug,立刻終止程式,你並不知道應用當前的執行狀態。當錯誤發生時,程式控制系統應該會重啟應用程式,例如:supervisord 或者 monit。
工作流技巧
使用 npm init 建立新專案
init 命令可以幫助你建立應用程式的 package.json 配置檔案。檔案設定了一些預設配置,之後可以修改。
建立一個優秀的專案應該這樣開始:
1 2 3 |
mkdir my-awesome-new-project cd my-awesome-new-project npm init |
指定開始和測試指令碼。
在你的 package.son 檔案中,你可以在 scripts 部分中設定指令碼。npm init 預設會建立兩個,start 和 test 指令碼。可以通過 npm start 和 npm test 命令執行。
還有,作為加分項:你可以在這裡加入自定義指令碼,通過 npm run-script <SCRIPT_NAME> 來執行。
注意,NPM 會通過設定 $PATH 來掃描 node_modules/.bin 下的所有可執行指令碼。這樣可以避免安裝全域性的 NPM 模組。
環境變數
生產部署和演示部署都應該由環境變數來實現。最主流的實現方式是同時在生產和演示中設定 NODE_ENV變數。
根據你設定的環境變數,你可以使用 nconf 模組來載入配置資訊。
當然,你也可以在你的Node.js 應用中使用其它環境變數設定 process.env,這是一個包含了使用者環境的物件。
不要重新發明輪子
務必優先尋找現成的解決方案。NPM 的庫超級多,涵蓋了你平時需要的大部分功能。
使用風格指南
所有的程式碼都保持統一風格有助於理解大型程式碼庫。其中應該包含縮排、變數命名、最佳實踐以及其他方面。
如果想看一個實際的例子,請檢視 RisingStack 編寫的 Node.js 風格指南。
後記
我希望這篇文章對你編寫Node.js有所幫助,並且解決一些令你頭疼的難題。下篇文章會繼續探討操作技巧和最佳實踐。
你可以從 持續部署Node.js 應用 閱讀部署相關的技巧。
還有,我們將於 11月21日舉辦名為 One-Shot Budapest 的 Node大會(NodeConf)。關注PayPal、NPM、Strongloop分享的經驗。希望到時能看到你。
下篇
你應該還記得我們上篇的Node.js 最佳實踐。這篇文章裡我們繼續討論更多的最佳實踐,這些實踐有助於你成為更棒的Node.js開發者。
保持風格一致
當在一個較大的團隊裡開發JavaScript 應用程式時,建立一個所有人都遵守的風格指南非常重要。如果你想找靈感,我會推薦你去閱讀 RisingStack 的 Node.js 風格指南。
但是這只是起步——當你設定好標準,所有團隊成員都必須遵守風格指南。這是 JSCS 誕生的原因。
JSCS 是一個JavaScript 編碼風格檢查工具。將JSCS加入專案對你來說小菜一碟:
1 |
npm install jscs --save-dev |
你需要做的下一步關鍵就是在 package.json
檔案中加入下面的程式碼來開啟它:
1 2 3 |
scripts: { "jscs": "jscs index.js" } |
當然,你也可以加入多個檔案、目錄檢查。但為什麼我們僅僅在 package.json
檔案中建立了一個自定義的指令碼呢?我們是以本地的方式安裝 jscs
的,所以在一個系統中可以有多個不同版本。這樣還能正常工作是因為NPM 執行時會將 node_modules/.bin
設定到 PATH
上。
你可以在 .jscsrc
檔案中定義驗證規則,或者使用預設規則。從這裡可以檢視可用的預設,通過 --preset=[PRESET_NAME]
來應用。
執行 JSHint、JSCS 規則
你的構建過程還應該包含 JSHint 和 JSCS,不過在開發者的電腦上執行 pre-commit checks 或許是個不錯的主意。
要實現這個很簡單,你可以使用 pre-commit
NPM 庫:
1 |
npm install --save-dev pre-commit |
然後在 package.json 檔案中作如下配置:
1 2 3 4 |
pre-commit": [ "jshint", "jscs" ], |
注意,pre-commit 將會掃描 package.json中script裡的所有指令碼。開啟以後,每次提交時都會自動進行檢查。
用JS替換JSON做配置
我們看到大量的專案都是使用JSON檔案做配置的。這是目前最普遍的做法,JS配置檔案則能夠提供更大的靈活度。所以我們推薦你使用 config.js 檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var url =require('url'); var config = module.exports = {}; var redisToGoConfig; config.server = { host: '0.0.0.0', port: process.env.PORT || 3000 }; // look, a comment in the config file! // would be tricky in a JSON ;) config.redis = { host: 'localhost', port: 6379, options: { } }; if (process.env.REDISTOGO_URL) { redisToGoConfig = url.parse(process.env.REDISTOGO_URL); config.redis.port = redisToGoConfig.port; config.redis.host = redisToGoConfig.hostname; config.redis.options.auth_pass = redisToGoConfig.auth.split(':')[1]; } |
使用 NODE_PATH
你是否曾經碰到過下面這種情況?
1 2 3 4 5 |
var myModule = require('../../../../lib/myModule'); myModule.doSomething(function (err) { }); |
當你的專案結構變得錯綜複雜,模組依賴會非常麻煩。要解決這個問題有兩個辦法:
- 把你的模組軟連結到node_modules目錄下。
- 使用 NODE_PATH。
在RisingStack我們使用 NODE_PATH的方式,因為將所有相關檔案軟連結到 node_modules目錄需要大量額外的工作,並且在很多作業系統下都不適用。
設定 NODE_PATH
假設你的專案結構是這樣的:
我們可以使用 指向 lib 目錄的NODE_PATH,而不是使用相對路徑。在我們的package.json 的 start script部分,我們使用NODE_PATH設定並且用npm start 執行專案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var Car = require('model/Car'); console.log('I am a Car!'); { "name": "node_path", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "NODE_PATH=lib node index.js" }, "author": "", "license": "ISC" } |
依賴注入
依賴注入是一種軟體設計模式,是指將一到多個依賴(或服務)注入或通過引用的方式引入到需要依賴的物件。
依賴注入在測試中非常有用。使用這個模式你可以輕鬆模擬模組間的依賴關係。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function userModel (options) { var db; if (!options.db) { throw new Error('Options.db is required'); } db = options.db; return { create: function (done) { db.query('INSERT ...', done); } } } module.exports = userModel; |
1 2 3 4 5 6 7 8 9 10 |
var db = require('db'); // do some init here, or connect db.init(); var userModel = require('User')({ db: db }); userModel.create(function (err, user) { }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var test = require('tape'); var userModel = require('User'); test('it creates a user with id', function (t) { var user = { id: 1 }; var fakeDb = { query: function (done) { done(null, user); } } userModel({ db: fakeDb }).create(function (err, user) { t.equal(user.id, 1, 'User id should match'); t.end(); }) }); |
上面的例子中我們有兩個不通過的 db。在 index.js 檔案中是“真實的” db 模組,而第二段程式碼中我們只是簡單地建立了一個模擬的db模組。
這樣我們在測試時就可以輕鬆地將模擬的依賴引入模組。
開發應用時需要一臂之力?
RisingStack 提供了 JavaScript 開發和諮詢服務——如果你需要幫助請聯絡我們!