原文地址:http://blog.fantasy.codes/node.js/2016/10/08/express-route-loader/ 歡迎訪(fǎng)問(wèn)胁附。
Express 的路由是內(nèi)置在框架內(nèi)的酒繁,在實(shí)例化之后可以直接調(diào)用聲明路由,例如:
const app = require('express')();
app.get('/', (req, res) => {
// ...
});
項(xiàng)目中經(jīng)常會(huì)對(duì)目錄結(jié)構(gòu)進(jìn)行 MVC 的分層控妻,所以很多情況下會(huì)這樣組織代碼:
- 定義一個(gè) controller
exports.renderHomepage = (req, res) => {
res.render('home');
};
- 定義一個(gè) router
const homeController = require('/path/to/controller');
app.get('/home', homeController.renderHomepage);
當(dāng)然這邊一般會(huì)使用 express.Router() 對(duì) router 進(jìn)行拆分州袒,當(dāng)然這并不在這次的討論中。
這樣的聲明和定義方式確實(shí)沒(méi)有什么問(wèn)題弓候,但是當(dāng)項(xiàng)目在日積月累的迭代過(guò)程中郎哭,這一部分代碼就會(huì)變得十分冗余。
因此需要實(shí)現(xiàn)一種自動(dòng)路由加載的機(jī)制菇存,而不再需要去寫(xiě)這些可以簡(jiǎn)化的代碼夸研。
TL;DR
可以翻閱 express-load-router 的代碼,而不需要閱讀此文依鸥。
構(gòu)思
Method
對(duì)應(yīng) HTTP 的各種請(qǐng)求方式亥至,在 Express 中可以使用 app.get()
, app.post()
, app.delete()
, app.put()
等方式來(lái)定義對(duì)應(yīng)的路由。
所以我們的這個(gè)機(jī)制需要分辨不同的 HTTP Method贱迟,對(duì)應(yīng)使用 Express 的方法姐扮,好在這些方法和 Method 都是直接對(duì)應(yīng)的。
參數(shù)
一般在定義項(xiàng)目路由的時(shí)候會(huì)通過(guò)兩種方式來(lái)傳遞參數(shù)到服務(wù)端:
- URL 參數(shù)
- Request body
對(duì)應(yīng)到 Express 中而言就是 req.params
和 req.body
URL 規(guī)則
通常而言衣吠,項(xiàng)目中的 Controller 會(huì)按照業(yè)務(wù)邏輯進(jìn)行劃分茶敏,因此可以根據(jù) Controller 的文件目錄層級(jí)來(lái)進(jìn)行路由的映射。
假定有以下目錄結(jié)構(gòu):
controllers
├── home
│ └── index.js
└── list
└── index.js
那么對(duì)應(yīng)生成的路由應(yīng)當(dāng)為:/home
和 /list
以上三點(diǎn)基本上是自動(dòng)路由模塊的比較核心的構(gòu)思缚俏,當(dāng)然其他一定還有不少可以添加的「輔助功能」惊搏。
實(shí)現(xiàn)
首先,需要獲取到所有指定目錄(一般而言是 controllers
)下的文件路徑忧换。
不過(guò)這次使用的是 glob -- 一個(gè)用于快速文件匹配的模塊 -- 當(dāng)然恬惯,也可以直接使用 Node.js 的核心模塊 path
對(duì)目錄進(jìn)行遞歸遍歷。
不管用什么方式包雀,在獲取到目錄下的所有文件之后宿崭,才可以開(kāi)始實(shí)現(xiàn)真正的路由加載邏輯了。
使用 glob
的獲取方式:
const glob = require('glob');
glob.sync('*/**/*.js').forEach(file => {
// Do things with file
});
初步的實(shí)現(xiàn)
再來(lái)看看上文中提到的常用的 Controller 寫(xiě)法才写,假設(shè)這個(gè)文件的路徑為 controllers/home/index.js
:
exports.renderHomepage = (req, res) => {
res.render('home');
};
在獲取到路徑之后葡兑,需要 require
之方可獲取文件內(nèi) exports 的方法,所以現(xiàn)在的方法就變成了這樣:
const glob = require('glob');
glob.sync('/controllers/**/*.js').forEach(file => {
const instance = require(file);
// Do things with instance
});
但是 renderHomepage
這樣的方法太過(guò)于與業(yè)務(wù)相關(guān)聯(lián)了赞草,無(wú)法直接與 Express 的路由聯(lián)系上讹堤。
所以這邊需要修改一下 controllers/home/index.js
中的方法定義,使用 HTTP Method 作為方法名稱(chēng):
exports.GET = (req, res) => {
res.render('home');
};
這看起來(lái)是個(gè)不錯(cuò)的主意厨疙,這樣就可以讓一個(gè) Controller 文件中定義較少數(shù)量的方法洲守,同時(shí)與對(duì)應(yīng)的 HTTP Method 相映射。
接下來(lái)就需要來(lái)寫(xiě)對(duì)應(yīng)的路由來(lái)使用這個(gè) Controller 中的函數(shù)了:
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]);
});
});
這樣路由加載和核心就寫(xiě)的差不多了梗醇,還是十分簡(jiǎn)潔精煉的知允。
添磚加瓦
仔細(xì)想想這個(gè)模塊還缺少了什么?
是的叙谨,路由方法是可以加載了温鸽,但是沒(méi)有地方來(lái)聲明這樣的 URL 參數(shù):
app.get('/detail/:id', (req, res) => {})
所以我們又需要對(duì)聲明路由方法的方式進(jìn)行一個(gè)修改 -- 將其修改為一個(gè)對(duì)象,并約定兩個(gè) Key 值分別聲明參數(shù)和處理函數(shù):
exports.GET = {
params: ['/:id'],
handler (req, res) {
res.render('detail', {
id: req.params.id
});
}
};
當(dāng)然手负,一個(gè)好的模塊肯定需要對(duì)原有的方式進(jìn)行兼容涤垫,所以這里我們可能需要對(duì)原來(lái)的模塊進(jìn)行一個(gè)不小的修改:
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 中輸出的類(lèi)型
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);
});
});
至此竟终,便已經(jīng)完成了前文「構(gòu)思」中提到的三個(gè)點(diǎn)蝠猬。
我在 express-load-router 中還添加了兩個(gè)配置項(xiàng):
- 可以傳入一個(gè)
excludeRules
的數(shù)組來(lái)配置例外規(guī)則,即不納入自動(dòng)加載的路徑统捶,例如:
['/list', '/detail']
- 可以傳入一個(gè)
rewriteRules
的 Map 來(lái)配置 rewrite 規(guī)則榆芦,重寫(xiě) URL 路徑,例如:
new Map([
['/home', '/']
])
--EOF--