原文地址:http://blog.fantasy.codes/node.js/2016/10/08/express-route-loader/ 歡迎訪問。
Express 的路由是內置在框架內的,在實例化之后可以直接調用聲明路由,例如:
const app = require('express')();
app.get('/', (req, res) => {
// ...
});
項目中經常會對目錄結構進行 MVC 的分層,所以很多情況下會這樣組織代碼:
- 定義一個 controller
exports.renderHomepage = (req, res) => {
res.render('home');
};
- 定義一個 router
const homeController = require('/path/to/controller');
app.get('/home', homeController.renderHomepage);
當然這邊一般會使用 express.Router() 對 router 進行拆分,當然這并不在這次的討論中。
這樣的聲明和定義方式確實沒有什么問題,但是當項目在日積月累的迭代過程中,這一部分代碼就會變得十分冗余。
因此需要實現一種自動路由加載的機制,而不再需要去寫這些可以簡化的代碼。
TL;DR
可以翻閱 express-load-router 的代碼,而不需要閱讀此文。
構思
Method
對應 HTTP 的各種請求方式,在 Express 中可以使用 app.get()
, app.post()
, app.delete()
, app.put()
等方式來定義對應的路由。
所以我們的這個機制需要分辨不同的 HTTP Method,對應使用 Express 的方法,好在這些方法和 Method 都是直接對應的。
參數
一般在定義項目路由的時候會通過兩種方式來傳遞參數到服務端:
- URL 參數
- Request body
對應到 Express 中而言就是 req.params
和 req.body
URL 規則
通常而言,項目中的 Controller 會按照業務邏輯進行劃分,因此可以根據 Controller 的文件目錄層級來進行路由的映射。
假定有以下目錄結構:
controllers
├── home
│ └── index.js
└── list
└── index.js
那么對應生成的路由應當為:/home
和 /list
以上三點基本上是自動路由模塊的比較核心的構思,當然其他一定還有不少可以添加的「輔助功能」。
實現
首先,需要獲取到所有指定目錄(一般而言是 controllers
)下的文件路徑。
不過這次使用的是 glob -- 一個用于快速文件匹配的模塊 -- 當然,也可以直接使用 Node.js 的核心模塊 path
對目錄進行遞歸遍歷。
不管用什么方式,在獲取到目錄下的所有文件之后,才可以開始實現真正的路由加載邏輯了。
使用 glob
的獲取方式:
const glob = require('glob');
glob.sync('*/**/*.js').forEach(file => {
// Do things with file
});
初步的實現
再來看看上文中提到的常用的 Controller 寫法,假設這個文件的路徑為 controllers/home/index.js
:
exports.renderHomepage = (req, res) => {
res.render('home');
};
在獲取到路徑之后,需要 require
之方可獲取文件內 exports 的方法,所以現在的方法就變成了這樣:
const glob = require('glob');
glob.sync('/controllers/**/*.js').forEach(file => {
const instance = require(file);
// Do things with instance
});
但是 renderHomepage
這樣的方法太過于與業務相關聯了,無法直接與 Express 的路由聯系上。
所以這邊需要修改一下 controllers/home/index.js
中的方法定義,使用 HTTP Method 作為方法名稱:
exports.GET = (req, res) => {
res.render('home');
};
這看起來是個不錯的主意,這樣就可以讓一個 Controller 文件中定義較少數量的方法,同時與對應的 HTTP Method 相映射。
接下來就需要來寫對應的路由來使用這個 Controller 中的函數了:
const glob = require('glob');
const app = require('express')();
glob.sync('/controllers/**/*.js').forEach(file => {
const instance = require(file);
// 生成 URL 路徑,去掉 .js 去掉 controllers
const urlPath = file.replace(/\.[^.]*$/, '').replace('/controllers', '');
// 獲取所有 Controller 中的方法
const methods = Object.keys(instance);
methods.forEach(method => {
app[method.toLowerCase()](urlPath, instance[method]);
});
});
這樣路由加載和核心就寫的差不多了,還是十分簡潔精煉的。
添磚加瓦
仔細想想這個模塊還缺少了什么?
是的,路由方法是可以加載了,但是沒有地方來聲明這樣的 URL 參數:
app.get('/detail/:id', (req, res) => {})
所以我們又需要對聲明路由方法的方式進行一個修改 -- 將其修改為一個對象,并約定兩個 Key 值分別聲明參數和處理函數:
exports.GET = {
params: ['/:id'],
handler (req, res) {
res.render('detail', {
id: req.params.id
});
}
};
當然,一個好的模塊肯定需要對原有的方式進行兼容,所以這里我們可能需要對原來的模塊進行一個不小的修改:
const glob = require('glob');
const app = require('express')();
glob.sync('/controllers/**/*.js').forEach(file => {
const instance = require(file);
// 生成 URL 路徑,去掉 .js 去掉 controllers
let urlPath = file.replace(/\.[^.]*$/, '').replace('/controllers', '');
// 獲取所有 Controller 中的方法
const methods = Object.keys(instance);
methods.forEach(method => {
let handler = instance[method];
// 判斷 Controller 中輸出的類型
switch (typeof handler) {
case 'object':
urlPath += `/${handler.params.join('/')}`;
handler = handler.handler;
break;
case 'function':
// Nothing to do with the pure handler.
break;
default:
return;
}
app[method.toLowerCase()](urlPath, handler);
});
});
至此,便已經完成了前文「構思」中提到的三個點。
我在 express-load-router 中還添加了兩個配置項:
- 可以傳入一個
excludeRules
的數組來配置例外規則,即不納入自動加載的路徑,例如:
['/list', '/detail']
- 可以傳入一個
rewriteRules
的 Map 來配置 rewrite 規則,重寫 URL 路徑,例如:
new Map([
['/home', '/']
])
--EOF--