背景
我想统计出 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"}
]
要求:
- path中要包含挂载点路径,如上 app.use(“/test”,api),路径要包含 /test
- 不需要 api.all() 中的路由,该方法实际上只是对匹配到指定路由的HTTP方法,触发回调
- 对于挂载到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 版本
- 通过
express()
创建的应用,如const app = express()
可通过app._router.stack
获取路由堆栈,通过app.mountpath 获取 app 挂载在父级的路径; - 通过
express.Router()
创建的应用,如const router = express.Router()
可通过router.stack
获取路由堆栈,通过 router.stack.regexp 获取 router 挂载在父级的路径(是正则表达式,需要处理); - 实际上 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' ]
}
]
总结
自己的解决方案,还有网上的解决方案,都没有解决无法获取挂载的子应用里已注册路由的问题。
只能通过规范化路由的使用方式来规避。。。