Express 自動路由加載的設計與實現

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容