打開 express.js 的正確姿勢

現在這個年月再來討論 express 其實并不討喜,nodejs 已經不是什么新技術了。雖然 nodejs 還不太成熟,雖然 express 的原作者也已經放棄了 nodejs,雖然nodejs 的包管理機制,callback hell 等問題被人吐槽無數,但還是阻止不了廣大的熱血青年涌向 nodejs 的懷抱。nodejs 實在是太誘人了,它為后端工程師提供了一種前所未有的"面向回調"的語言. 同時也為廣大的前端開發者敞開了一扇金燦燦的大門,一扇通往服務器端開發的大門,然而這并沒什么卵用。

前后端程序要解決的問題實際上是截然不同的,如果你認為一個熟悉前端的工程師能夠使用 nodejs 輕松駕馭后端的程序,那我只能說你想多了。 前端程序是運行在瀏覽器里的,要解決的最主要的問題是:用戶體驗,而主要困難是代碼對不同瀏覽器的兼容性。而后端程序運行在操作系統上,要解決的主要問題是:功能實現、性能、穩定性、擴展性···。 如果你分別找一份前端和后端的代碼比較一下,你會發現雖然都是 js,但是除了語法相似外就沒什么相似的地方了。

好了,廢話不多說了,我們來看看 express.js。

我們來談談哲學

express.js 中需要開發者編寫的主要是什么 (你是干什么的)?

先來看一下官方的示例:

var express = require('express');
var app = express();

app.get('/', function(req, res){
  res.send('hello world');
});

app.listen(3000);

express中我們最主要的任務是實現 app.get(), app.post() 中的 callback 部分,express 里叫 route handler, 也就是通常我們說的controller.

怎么開始對 request 的處理 (從哪兒來)?

官方文檔里告訴你要這么寫

app.get('/', function(req, res){
  res.send('hello world');
});

app.get 的第二個參數是一個 callback,他接受兩個參數,一個是 request,一個是 response。

但是如果你認真看文檔,你會發現,app.get定義其實是這樣子滴:app.get(path, callback [, callback ...]), 看見沒?可以有多個 callback!那怎么用呢?

// app.get 中的 callback 其實可以接受第三個參數--另一個 callback
// 這個 callback 指向app.get 參數中下一個 route handler(這很 nodejs)

app.get('/', function(req, res, next){
  //blabla, 業務邏輯1
  //blabla, 業務邏輯1
  //blabla, 業務邏輯1
  next(); // 調用下面這個 handler
          //  |
          //  |
          // \|/
}, function(req, res, next){
  //blabla, 業務邏輯2
  //blabla, 業務邏輯2
  //blabla, 業務邏輯2
  next(); // 調用下面這個 handler
          //  |
          //  |
          // \|/
}, function(req, res){
  // 最后一個 handler 中無需調用 next
  // 構建 response
  res.send('hello world');
});

大部分情況下next()不需要參數, 但是其實它是可以接受參數的, 這里有兩種情況:

  • next('route') 可以跳過后面的 route handler.
  • next(err)同樣會跳過后續的 route handler, 但是與next('route')不同的是, 他會觸發錯誤處理的 handler( 后面會提到).

如何結束對 request 的處理(到哪兒去)?

看上去很簡單的問題不是嗎?

通常你會使用 res.end(), 高級點的會使用 res.send(), res.json(), 甚至是 res.links(), res.format() 等等. 但是當你調用這些函數之后, 處理過程就結束了嗎? 其實沒有. 你會發現 res.end() 后面的代碼依然會被執行.

那么, 如何才能主動的結束 route handler 的執行呢? 答案就是上面提到的 next() 函數, 但是如果 next() 后面還有代碼怎么辦? 所以, 比較通用的方法是:

.....
return next(err);
// 后續的代碼以及后續的 route handler 都不會被執行.
.....

錯誤處理

這里首先推薦官方關于錯誤處理的指導(英文, 中文)

express 中可以定義全局的 error handler, 方法如下:

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
  // next(err);
});

error handler 同樣可以有多個, 與 route handler 類似的通過 next() 串聯, 不同的是: 需要帶上 err 參數.

全局 error handler 為工程師提供了極大的方便, 但是實際項目中經常發揮不了作用, 原因只有一個: 對錯誤處理的漠視

這里摘一個實際項目中的例子:

exports.client=function(callBack){
    pool.acquire(function(err,db){                          // err 被無情的忽略
        var timer=setTimeout(function(){
            pool.release(db);
        },20000);
        db.checkDisconnect=timer;
        callBack(db);
    });
};

exports.collection=function(db,collectionName,callBack){
    
   db.collection(collectionName,{safe:true},function(err,collection){
       if(err) {                                                       // 沒有將 err 向上傳遞.
           console.log("DBClient-collection-err: " + err);
       }
      callBack(collection);                                // 應該改為callBack(err, collection);
   });
};

get:function(id,callBack){
    var self=this;
    dbClient.client(function(db){
        dbClient.collection(db,self.DBTable,function(collection){
            collection.findOne({'id':id},function(err, doc){
                if(err) {                                             // 沒有將 err 向上傳遞.
                   console.log("DBClient-findOne-err: " + err);
                }
                if(doc){
                    delete doc._id; 
                }
                
                callBack(doc);                            // 應該改為callBack(err, doc);
                
                dbClient.close(db);
            });
        });
    });
}


exports.add=function(req,res){
    var cookie=req.cookies;
    var userid=cookie.userid;

    // 這里完全忽略了 dao.findOne 中有可能出現的 err !!!
    // 一旦 err 發生, 后續對 user 的引用就會發生異常, 而這個時候看到的 err 的位置有可能和實際的 err 位置相距十萬八千里. 使錯誤難以定位.
    // 一旦 err 發生, 第一次引用 user 之前的代碼, 通通都是無用功, 有可能對程序性能造成影響.

    dao.findOne({id:userid},config.dbUser,function(user){
        //.....
        //.....
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容