Express 自動(dòng)路由加載的設(shè)計(jì)與實(shí)現(xiàn)

原文地址: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.paramsreq.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--

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘾境,一起剝皮案震驚了整個(gè)濱河市歧杏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌迷守,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旺入,死亡現(xiàn)場(chǎng)離奇詭異兑凿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)茵瘾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)礼华,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拗秘,你說(shuō)我怎么就攤上這事圣絮。” “怎么了雕旨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵扮匠,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我凡涩,道長(zhǎng)棒搜,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任活箕,我火速辦了婚禮力麸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己克蚂,他們只是感情好闺鲸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著埃叭,像睡著了一般翠拣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上游盲,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天误墓,我揣著相機(jī)與錄音,去河邊找鬼益缎。 笑死谜慌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的莺奔。 我是一名探鬼主播欣范,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼令哟!你這毒婦竟也來(lái)了恼琼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤屏富,失蹤者是張志新(化名)和其女友劉穎晴竞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體狠半,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡噩死,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了神年。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片已维。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖已日,靈堂內(nèi)的尸體忽然破棺而出垛耳,到底是詐尸還是另有隱情,我是刑警寧澤飘千,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布堂鲜,位于F島的核電站,受9級(jí)特大地震影響占婉,放射性物質(zhì)發(fā)生泄漏泡嘴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一逆济、第九天 我趴在偏房一處隱蔽的房頂上張望酌予。 院中可真熱鬧磺箕,春花似錦、人聲如沸抛虫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)建椰。三九已至雕欺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棉姐,已是汗流浹背屠列。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伞矩,地道東北人笛洛。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像乃坤,于是被迫代替她去往敵國(guó)和親苛让。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容