1. 程式人生 > >淺談 Express 4.0 Router 模組

淺談 Express 4.0 Router 模組

Express 是目前 node 社群最主要的 Web 框架,前不久剛剛升級到了 4.0 版本。與 3.x 版本比,4.0 版本擁有一個全新設計的 Router 模組,開發者可以更方便的對 middleware 進行隔離與重用。

Express 3.x 時代的中介軟體 (middleware) 與控制器 (controller)

在 express 3.x 版本中,一個控制器往往不是業務邏輯的全部,中介軟體才是業務邏輯的大頭。例如一個處理使用者訂單的服務,往往驗證使用者許可權、讀寫資料庫等主要邏輯工作都在中介軟體中就完成了,而控制器所做的大部分工作就是拼資料給檢視 (view)。

一個標準的 URL 對映寫法如下:

app.js
1
2
3
4
5
// 這裡是一些中介軟體,express 的常見寫法
var A = require('./middlewares/A');
var B = require('./middlewares/B');
var C = require('./middlewares/C');
app.get('/books', A, B, C, require('./controllers/book').index);
controllers/book.js
1
2
3
4
5
6
7
exports.index = function (req, res) {
    var retA = req.A; // 中介軟體 A 的輸出結果
var retB = req.B; // 中介軟體 B 的輸出結果 var retC = req.C; // 中介軟體 C 的輸出結果 // ... 其餘程式邏輯 } // ...

控制器與中介軟體的相互依賴關係是完全隱式的,不能通過程式碼分析來得到任何的保證。中介軟體的插入與控制器的程式碼被分離在了 app.js與 controllers/book.js 兩個檔案中,不僅閱讀起來不直觀,修改起來也很容易出錯。假如有一天團隊的新同事修改了 book.js 去掉了中介軟體 C 的邏輯,但是忘記了修改 app.js(這是一個非常容易犯的錯誤),那麼 express 不會報任何錯誤,code review 也很難發現,因為這兩處程式碼離得實在是太遠了。

這種設計同時會導致測試非常困難,因為單獨 require 一個控制器是毫無意義的,因為控制器本身不可能獨立於中介軟體來執行。如果要測試控制器,要麼在單元測試程式碼在控制器前裡現場裝配中介軟體,要麼就 mock 請求通過中介軟體後的資料。無論哪種做法,都需要單元測試完全瞭解中介軟體與控制器的業務邏輯才可能實現,這樣就增大了單元測試的難度。

一個解決方法是將二者的依賴關係倒置,把控制器模組寫成一個接收 app 作為引數的函式,在函式內部裝配中介軟體與控制器。程式碼如下:

app.js
1
require('./controllers/book')(app);
controllers/book.js
1
2
3
4
5
6
7
8
9
10
11
12
13
var A = require('../middlewares/A');
var B = require('../middlewares/B');
var C = require('../middlewares/C');

module.exports = function (app) {
    app.get('/books', A, B, C, function (req, res) {
        var retA = req.A; // 中介軟體 A 的輸出結果
        var retB = req.B; // 中介軟體 B 的輸出結果
        var retC = req.C; // 中介軟體 C 的輸出結果
        // ... 其餘程式邏輯
    });
};
// ...

這樣做雖然提高了程式碼的內聚性,但是直接把 app 暴露給其它模組使得 app 有被濫用的風險,讓二者從面向介面的鬆散耦合變成了直接操縱例項的強耦合。同時這種方案不僅沒有提高可測試性,反而大大提高了單元測試的難度(想想現在都需要 mock 一個 app 了 T_T)。

為了更好的解決這個問題,Express 4.0 給出了更好的解決方式: express.Router

使用 express.Router 來組織控制器與中介軟體

express.Router 可以認為是一個微型的只用來處理中介軟體與控制器的 app,它擁有和 app 類似的方法,例如 getpostalluse 等等。上面的例子使用 express.Router 可以修改為:

app.js
1
app.use(require('./controllers/book'));
controllers/book.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var router = require('express').Rouer(); // 新建一個 router

var A = require('../middlewares/A');
var B = require('../middlewares/B');
var C = require('../middlewares/C');

// 在 router 上裝備控制器與中介軟體
router.get('/books', A, B, C, function (req, res) {
    var retA = req.A; // 中介軟體 A 的輸出結果
    var retB = req.B; // 中介軟體 B 的輸出結果
    var retC = req.C; // 中介軟體 C 的輸出結果
    // ... 其餘程式邏輯
});

// ...

// 返回 router 供 app 使用
module.exports = router;

通過 express.Router,控制器與中介軟體的程式碼緊密的聯絡在一起,並且避免了傳遞 app 的潛在風險。同時,一個 router 就是一個完整的功能模組,不需要任何裝配就可以執行。這一點對於單元測試來說非常簡單。

express.Router 的其他特性

中介軟體重用

上面提到過,express.Router 可以認為是一個迷你的 app,它擁有一個獨立的中介軟體佇列。這個特性可以用來共享一些常用的中介軟體,例如:

express 3.x
1
2
3
4
5
6
// parseBook 是個中介軟體
app.get('/books/:bookId', parseBook, viewBook);
app.get('/books/:bookId/edit', parseBook, editBook);
app.get('/books/:bookId/move', parseBook, moveBook);

app.get('/other_link', otherController);

Express 4.0 的寫法:

express 4.0
1
2
3
4
5
6
7
8
9
10
var bookRouter = express.Router();
app.use('/books', bookRouter);

bookRouter.use(parseBook);
// 下面三個控制器都會經過 parseBook 中介軟體
bookRouter.get('/books/:bookId', viewBook);
bookRouter.get('/books/:bookId/edit', editBook);
bookRouter.get('/books/:bookId/move', moveBook);

app.get('/other_link', otherController); // 不會經過 parseBook 中介軟體

這個例子中 bookRouter 使 parseBook 這個中介軟體得到了充分的重用。

搭建 rest-ful 服務

Code talks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var bookRouter = express.Router();
bookRouter
    .route('/books/:bookId?')
    .get(function (req, res) {
        // ...
    })
    .put(function (req, res) {
        // ...
    })
    .post(function (req, res) {
        // ...
    })
    .delete(function (req, res) {
        // ...
    })

小節

express.Router 是 express 4.0 中我最喜歡的更新,我認為 express 4.0 的程式碼裡應該儘量多使用 express.Router 來代替原先的 app.get 方式。原先 URL 路徑、中介軟體、控制器三者的鬆散關係可以藉由 express.Router 變得緊密,整個控制器變成了一個不依賴於任何外部例項的獨立模組,更有利於模組的拆分(想想把網站的各個模組都拆成獨立的 router 吧),同時對於測試也更加友好。

http://lostjs.com/2014/04/24/router-in-express-4/