淺談 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 對映寫法如下:
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); |
1 2 3 4 5 6 7 |
exports.index = function (req, res) { var retA = req.A; // 中介軟體 A 的輸出結果 |
控制器與中介軟體的相互依賴關係是完全隱式的,不能通過程式碼分析來得到任何的保證。中介軟體的插入與控制器的程式碼被分離在了 app.js
與 controllers/book.js
兩個檔案中,不僅閱讀起來不直觀,修改起來也很容易出錯。假如有一天團隊的新同事修改了 book.js
去掉了中介軟體 C
的邏輯,但是忘記了修改 app.js
(這是一個非常容易犯的錯誤),那麼
express 不會報任何錯誤,code review 也很難發現,因為這兩處程式碼離得實在是太遠了。
這種設計同時會導致測試非常困難,因為單獨 require
一個控制器是毫無意義的,因為控制器本身不可能獨立於中介軟體來執行。如果要測試控制器,要麼在單元測試程式碼在控制器前裡現場裝配中介軟體,要麼就
mock 請求通過中介軟體後的資料。無論哪種做法,都需要單元測試完全瞭解中介軟體與控制器的業務邏輯才可能實現,這樣就增大了單元測試的難度。
一個解決方法是將二者的依賴關係倒置,把控制器模組寫成一個接收 app
作為引數的函式,在函式內部裝配中介軟體與控制器。程式碼如下:
1 |
require('./controllers/book')(app); |
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
類似的方法,例如 get
、post
、all
、use
等等。上面的例子使用 express.Router
可以修改為:
1 |
app.use(require('./controllers/book')); |
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
,它擁有一個獨立的中介軟體佇列。這個特性可以用來共享一些常用的中介軟體,例如:
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/