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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容