如何在Express中获取所有已注册的路由?

背景

我想统计出 Express 的Web项目中已注册的路由,即统计出有哪些可调用的接口
示例项目:

const express = require("express");
const app = express();
const api = express();

api.all('/api/*',express.json());
api.get('/api/get',function (req,res){
    
    res.send(req.route);})
api.post('/api/post/:id',function (req,res){
    
    res.send(req.route);})
api.route('/api/route')
    .get(function (req,res){
    
    res.send(req.route);})
    .post(function (req,res){
    
    res.send(req.route);})

app.use('/test',api);
app.listen(8899,'localhost',function (){
    
    console.log('Express server listening at http://localhost:8899');})

统计要求

[
    {
    
    "method": "GET","path": "/test/api/get"},
    {
    
    "method": "POST","path": "/test/api/post/:id"},
    {
    
    "method": "GET","path": "/test/api/route"},
    {
    
    "method": "POST","path": "/test/api/route"},
    {
    
    "method": "GET","path": "/test/get_all_routes"}
]

要求:

  1. path中要包含挂载点路径,如上 app.use(“/test”,api),路径要包含 /test
  2. 不需要 api.all() 中的路由,该方法实际上只是对匹配到指定路由的HTTP方法,触发回调
  3. 对于挂载到api应用的路由也给统计出来,如 express.Router() 创建的路由里的path

获取已注册路由

测试项目

先给出用来测试获取已注册路由的项目

.
├── app.js
├── routes
│	├── users
│   │	├── index.js
│   │	├── user_v1.js
│   │	└── user_v2.js
│	└──	orders
│   │	├── index.js
│   │	├── order_v1.js
│	│	└── order_v2.js

给出各文件的主要代码

app.js 主应用启动文件

const express = require("express");
const app = express();
const api = express();
const cookieParser = require('cookie-parser');

const userapi = require("./routes/users");
const orderapi = require("./routes/orders");
const orderv2 = require("./routes/orders/order_v2");

api.use('/userapi', userapi);
api.use('/orderapi', orderapi);
api.use('/orderapi', orderv2);

api.use(cookieParser());
api.all('/api/*',express.json());
api.get('/api/get',function (req,res){
    
    res.send(req.route);})
api.post('/api/post/:id',function (req,res){
    
    res.send(req.route);})
api.route('/api/route')
    .get(function (req,res){
    
    res.send(req.route);})
    .post(function (req,res){
    
    res.send(req.route);})

app.use('/test',api);
app.listen(8899,'localhost',function (){
    
    
    console.log('Express server listening at http://localhost:8899');
})

/routes/users 相关路由文件

//  /routes/users/index.js 文件
const express = require('express');
const userapi = express.Router();
userapi.use('/v1', require('./user_v1'));
userapi.use('/v2', require('./user_v2'));
module.exports = userapi;

//  /routes/users/user_v1.js 文件
const express = require('express');
const user = express.Router();
user.param('userid',function (req,res,next,id){
    
    req.userid = id;next();});
user.get('/user/demo',function (req,res){
    
    
		let ret = req.method.toUpperCase()+'  '+req.originalUrl+'  '+req.userid??"undefined";
        res.send(ret);
    })
user.post('/user/:userid',function (req,res){
    
    
        let ret = req.method.toUpperCase()+'  '+req.originalUrl+'  '+req.userid??"undefined";
        res.send(ret);
    })
module.exports = user;


//  /routes/users/user_v2.js 文件
const express = require('express');
const user = express.Router();

user.all('/*',express.json());
user.route('/user/:userid')
    .get(function (req,res){
    
    
        let ret = req.method.toUpperCase()+'  '+req.originalUrl+'  '+req.userid??"undefined";
        res.send(ret);
    })
    .post(function (req,res){
    
    
        let ret = req.method.toUpperCase()+'  '+req.originalUrl+'  '+req.userid??"undefined";
        res.send(ret);
    })
module.exports = user;

/routes/orders 相关路由文件

//  /routes/orders/index.js 文件
const express = require('express');
const userapi = express.Router();
userapi.use('/v1', require('./order_v1'));
// userapi.use('/v2', require('./order_v2'));
module.exports = userapi;


//  /routes/orders/order_v1.js 文件
const express = require('express');
const order = express.Router();
order.get('/order/:orderid',function (req,res){
    
    
    let ret = req.method.toUpperCase()+'  '+req.originalUrl;
    res.send(ret);
})
order.post('/order/orderid/[0-9]{3}',function (req,res){
    
    
    let ret = req.method.toUpperCase()+'  '+req.originalUrl;
    res.send(ret);
})
module.exports = order;


//  /routes/orders/order_v2.js 文件
const express = require('express');
const order = express();
order.get('/order/:orderid',function (req,res){
    
    
    let ret = req.method.toUpperCase()+'  '+req.originalUrl;
    res.send(ret);
})
order.post('/order/:orderid',function (req,res){
    
    
    let ret = req.method.toUpperCase()+'  '+req.originalUrl;
    res.send(ret);
})
module.exports = order;

如何实现

通过查阅资料,得知在 Express 4.x 版本

  1. 通过 express() 创建的应用,如 const app = express() 可通过 app._router.stack 获取路由堆栈,通过app.mountpath 获取 app 挂载在父级的路径;
  2. 通过 express.Router() 创建的应用,如 const router = express.Router() 可通过router.stack 获取路由堆栈,通过 router.stack.regexp 获取 router 挂载在父级的路径(是正则表达式,需要处理);
  3. 实际上 app._router.stack 是个列表(里边是一个个layer对象),layer.name===“router” 的就是路由实例对应的stack,可以通过 layer.handle.stack 获取 到路由实例下注册的路由堆栈(如果路由实例下又挂载了子路由实例,那就需要继续向下找子路由的stack)

注意:stack堆栈里可能包含中间件,需要进行过滤后使用

下面是我实现的代码

// 在 app.js 文件内,提供一个接口,用来获取所有已注册的路由
api.get('/get_all_routes',function (req,res){
    
    
    const listRoutes = function (routes,stack,parent){
    
    
        parent = parent || '';		//parent 代表 express()或express.Router() 实例所挂载的路径
        if(!stack){
    
    
        	// 首次调用 listRoutes() 时,从 api._router.stack 获取 api挂载的所有路由及中间件
            return listRoutes([],api._router.stack,parent + api.mountpath);
        }else{
    
    
        	// 过滤调中间件,只保留api下直接注册的路由或挂载的路由实例
            stack = stack.filter(layer => layer.route || layer.name === "router");
            stack.forEach(function (layer){
    
    
            	// 通过 <anonymous> 过滤掉 app.all()或router.all()方法对应的路由
                if(layer.route){
    
    
                    layer.route.stack.forEach(ly => {
    
    
                        if(ly.name === "<anonymous>"){
    
    
                            Object.keys(layer.route.methods).forEach(method => {
    
    
                                routes.push({
    
    "method":method.toUpperCase(),"path":parent+layer.route.path});
                            })
                        }
                    })
                }else if(layer.handle && layer.name==="router"){
    
    
                    // 如果是路由实例,需要获取到路由实例的stack,然后递归调用 listRoutes()方法,记得拼接上对应的路由挂载路径
                    const mountpath = layer.regexp ? layer.regexp.toString().replace("/^\\","").replace("\\/?(?=\\/|$)/i","") : "";
                    return listRoutes(routes,layer.handle.stack,parent + mountpath);
                }
            })
            return routes;
        }
    }
    res.send(listRoutes());
})

实现效果

调用接口:http://localhost:8899/test/get_all_routes
返回结果:

[
	{
    
    "method":"GET","path":"/test/userapi/v1/user/demo"},
	{
    
    "method":"POST","path":"/test/userapi/v1/user/:userid"},
	{
    
    "method":"GET","path":"/test/userapi/v2/user/:userid"},
	{
    
    "method":"POST","path":"/test/userapi/v2/user/:userid"},
	{
    
    "method":"GET","path":"/test/orderapi/v1/order/:orderid"},
	{
    
    "method":"POST","path":"/test/orderapi/v1/order/orderid/[0-9]{3}"},
	{
    
    "method":"GET","path":"/test/api/get"},
	{
    
    "method":"POST","path":"/test/api/post/:id"},
	{
    
    "method":"GET","path":"/test/api/route"},
	{
    
    "method":"POST","path":"/test/api/route"},
	{
    
    "method":"GET","path":"/test/get_all_routes"}
]

存在问题

从输出结果来看,routes/orders/order_v2.js 路由文件里注册的路由,并未获取到。
这是因为这个路由文件里使用的是 order = express() 创建的应用,并非Router()。

const mounted_app = app._router.stack.filter(layer => layer.name==="mounted_app")
console.dir(mounted_app);

// order_v2.js 对应的 stack 信息如下
[
  Layer {
    
    
    handle: [Function: mounted_app],
    name: 'mounted_app',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/orderapi\/?(?=\/|$)/i {
    
     fast_star: false, fast_slash: false },
    route: undefined
  }
]

mounted_app.forEach(layer => {
    
    
    console.dir(layer.handle._router);	// 输出 undefined
    console.dir(layer.handle.stack);	// 输出 undefined
});

也就是说,没有办法通过 app._router.stack 的堆栈,获取到 子应用(express()创建的应用)里的路由信息。
唯一的办法就是通过应用实例获取对应路由,如测试项目中 const orderv2 = require(“./routes/orders/order_v2”) ,可以通过 orderv2._router.stack 获取到堆栈,然后进而获取到路由。
然而这里又有问题,如果 order_v2.js 先注册到 routes/orders/index.js,然后 app.js 通过 routes/orders/index.js 导入路由,那你就没法拿到 order_v2.js 的路由实例了。如果express() 应用 与 Router() 路由 嵌套关系更复杂,那就更麻烦了。

解决方案

上边关于无法获取到子应用(express()创建的应用)注册路由的问题,暂时没想到什么解决方案。既然解决不了问题,那就解决提问题的人。。。。。。

不,这里还有一个方案,那就是。。。规范的使用路由。。。(意思是 模块化的路由里都统一使用 Router() 来创建路由)。例如 测试项目中,order_v2.js 里,const order = express() 改为 const order = express.Router()

网上已有的获取已注册路由的方案

我在网上也找了几个获取已注册路由的项目,并用我提供的测试项目进行了测试,效果如下。

express-routes-catalogue

npm 地址:express-routes-catalogue
使用方法请参考官方文档

// npm install -D express-routes-catalogue
// 在app.js里添加如下代码
const routeList = require("express-routes-catalogue");
if (app.settings.env === "development") {
    
    
	// 在我的测试项目里,传app参数运行会报错,这里我改为了api
    routeList.default.terminal(api);
}

下面是我的试用结果:
( 对于api.all() 未过滤处理,对于挂载的orderv2也未获取到对应路由,另外 app.use(‘/test’,api) 这里的 /test 未拼接到URI里 )

.--------------------------------------------------.
|                 List All Routes                  |
|--------------------------------------------------|
|   Method    |                URI                 |
|-------------|------------------------------------|
| GET         | userapi/v1/user/demo               |
| POST        | userapi/v1/user/:userid            |
| GET         | userapi/v2/user/:userid            |
| POST        | userapi/v2/user/:userid            |
| GET         | orderapi/v1/order/:orderid         |
| POST        | orderapi/v1/order/orderid/[0-9]{
    
    3} |
| ACL         | api/*                              |
| BIND        | api/*                              |
| CHECKOUT    | api/*                              |
| CONNECT     | api/*                              |
| COPY        | api/*                              |
| DELETE      | api/*                              |
| GET         | api/*                              |
| HEAD        | api/*                              |
| LINK        | api/*                              |
| LOCK        | api/*                              |
| M-SEARCH    | api/*                              |
| MERGE       | api/*                              |
| MKACTIVITY  | api/*                              |
| MKCALENDAR  | api/*                              |
| MKCOL       | api/*                              |
| MOVE        | api/*                              |
| NOTIFY      | api/*                              |
| OPTIONS     | api/*                              |
| PATCH       | api/*                              |
| POST        | api/*                              |
| PROPFIND    | api/*                              |
| PROPPATCH   | api/*                              |
| PURGE       | api/*                              |
| PUT         | api/*                              |
| REBIND      | api/*                              |
| REPORT      | api/*                              |
| SEARCH      | api/*                              |
| SOURCE      | api/*                              |
| SUBSCRIBE   | api/*                              |
| TRACE       | api/*                              |
| UNBIND      | api/*                              |
| UNLINK      | api/*                              |
| UNLOCK      | api/*                              |
| UNSUBSCRIBE | api/*                              |
| GET         | api/get                            |
| POST        | api/post/:id                       |
| GET         | api/route                          |
| POST        | api/route                          |
| GET         | get_all_routes                     |
'--------------------------------------------------'

get-routes

npm 地址:get-routes
使用方法请参考官方文档

// npm install -D get-routes
// 在app.js里添加如下代码
const {
    
     getRoutes } = require('get-routes');
const routes = getRoutes(api);	//在测试项目里 传参app会报错
console.log(routes);

// 注意,需要把 express().all()、Router().all() 方法注释掉,不然会运行报错!
// app.js 里的 api.all('/api/*',express.json()); 
// user_v2.js 里的 user.all('/*',express.json());

在测试项目里运行的结果:
( get-routes 只处理 get、post、put、delete、patch ,除此之外的方法会报错,未拼接主应用的 /test 路径,也未处理挂载的子应用 orderv2 里的路由)

{
    
    
  get: [
    '/userapi/v1/user/demo',
    '/userapi/v2/user/:userid',
    '/orderapi/v1/order/:orderid',
    '/api/get',
    '/api/route',
    '/get_all_routes'
  ],
  post: [
    '/userapi/v1/user/:userid',
    '/orderapi/v1/order/orderid/[0-9]{3}',
    '/api/post/:id'
  ],
  put: [],
  patch: [],
  delete: []
}

express-list-endpoints

npm 地址:express-list-endpoints
使用方法请参考官方文档

// npm install -D express-list-endpoints
// 在app.js里添加如下代码
const listEndpoints = require('express-list-endpoints');

// 传参app时,只输出了:[ { path: '', methods: [], middlewares: [] } ]
console.log(listEndpoints(api));

在测试项目里运行的结果:
( express-list-endpoints 未过滤掉 app.all()或router.all()方法对应的路由,未拼接主应用的 /test 路径,也未处理挂载的子应用 orderv2 里的路由 )

[
  {
    
    
    path: '/userapi/v1/user/demo',
    methods: [ 'GET' ],
    middlewares: [ 'anonymous' ]
  },
  {
    
    
    path: '/userapi/v1/user/:userid',
    methods: [ 'POST' ],
    middlewares: [ 'anonymous' ]
  },
  // 这是user_v2.js里的 user.all('/*',express.json()) 对应结果
  {
    
     path: '/userapi/v2/*', methods: [], middlewares: [ 'jsonParser' ] },
  {
    
    
    path: '/userapi/v2/user/:userid',
    methods: [ 'GET', 'POST' ],
    middlewares: [ 'anonymous', 'anonymous' ]
  },
  {
    
    
    path: '/orderapi/v1/order/:orderid',
    methods: [ 'GET' ],
    middlewares: [ 'anonymous' ]
  },
  {
    
    
    path: '/orderapi/v1/order/orderid/[0-9]{3}',
    methods: [ 'POST' ],
    middlewares: [ 'anonymous' ]
  },
  // 这是app.js里的 api.use('/orderapi', orderv2); 对应结果
  {
    
     path: '/orderapi', methods: [], middlewares: [] },
  // 这是app.js里的 api.all('/api/*',express.json()); 对应结果
  {
    
    	
    path: '/api/*',
    methods: [
      'ACL',         'BIND',       'CHECKOUT',
      'CONNECT',     'COPY',       'DELETE',
      'GET',         'HEAD',       'LINK',
      'LOCK',        'M-SEARCH',   'MERGE',
      'MKACTIVITY',  'MKCALENDAR', 'MKCOL',
      'MOVE',        'NOTIFY',     'OPTIONS',
      'PATCH',       'POST',       'PROPFIND',
      'PROPPATCH',   'PURGE',      'PUT',
      'REBIND',      'REPORT',     'SEARCH',
      'SOURCE',      'SUBSCRIBE',  'TRACE',
      'UNBIND',      'UNLINK',     'UNLOCK',
      'UNSUBSCRIBE'
    ],
    middlewares: [
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser', 'jsonParser', 'jsonParser',
      'jsonParser'
    ]
  },
  {
    
    
    path: '/api/get',
    methods: [ 'GET' ],
    middlewares: [ 'anonymous' ]
  },
  {
    
    
    path: '/api/post/:id',
    methods: [ 'POST' ],
    middlewares: [ 'anonymous' ]
  },
  {
    
    
    path: '/api/route',
    methods: [ 'GET', 'POST' ],
    middlewares: [ 'anonymous', 'anonymous' ]
  },
  {
    
    
    path: '/get_all_routes',
    methods: [ 'GET' ],
    middlewares: [ 'anonymous' ]
  }
]

总结

自己的解决方案,还有网上的解决方案,都没有解决无法获取挂载的子应用里已注册路由的问题。
只能通过规范化路由的使用方式来规避。。。

参考资料:如何在Express中获取所有已注册的路由?

猜你喜欢

转载自blog.csdn.net/B11050729/article/details/129381714