【问题标题】:NodeJS Express overlapping routersNodeJS Express 重叠路由器
【发布时间】:2022-01-07 17:35:28
【问题描述】:

我在构建 NodeJS express API 时遇到了一些错误(?)。显然一些端点相互重叠,这意味着我无法到达它们,请求在超时之前不会结束。

例如:

const load_dirs = (dirs, filename) => {
    const router_files = readdirSync(`./${dirs}`).filter(file => file.endsWith(`${filename}.js`));
    router_files.forEach(file => {
        const { 
            getUsers, getUser, addUser, editUser, removeUser, loginUser, updateUserStatus
        } = require(`../${dirs}/${file}`);

        router
            .route('/')
            .get(cors(corsOptions), getUsers)
            .post(cors(corsOptions), addUser);

        router
            .route('/login')
            .post(cors(corsOptions), loginUser);
        
        router
            .route('/:userId')
            .get(cors(corsOptions), getUser)
            .post(cors(corsOptions), editUser)
            .put(cors(corsOptions), removeUser);

        router
            .route('/:userId/status')
            .post(cors(corsOptions), updateUserStatus);
    });
}

['methods'].forEach(e => load_dirs(e, 'users'));

module.exports = (router);

如果登录方法高于通过 Id 获取用户,我无法通过 Id 访问用户,反之亦然。 而且由于这是一种使用节点的独特方式(还没有找到其他使用它的人),我无法弄清楚......

我错过了什么? 提前致谢!

编辑 1:

根据 cmets 的要求,我将getUsers 方法留在这里:

exports.getUsers = async (req, res, next) => {
    try  {
        await new Promise(async (resolved, rejected) => {
            let conn;

            try {
                conn = await db.pool.getConnection();
                const result = await conn.query(`
                    SELECT * FROM users
                `)

                .then((resp) => {                    
                    if (!resp.length) {
                        res
                            .status(200)
                            .json({ success: true, message: general.noResults });
                        resolved(resp);
                        return;
                    }

                    const data = resp;

                    res
                        .status(200)
                        .json({ success: true, utilizadores: data });
                    resolved(resp);
                })

                .catch ((err) => {
                    if (mode() === 'development' || mode() === 'test') console.log(`Error: ${ err }.`);
                    res
                        .status(400)
                        .json({ success: true, message: general.badRequest });
                    rejected(err);
                })
            } catch (err) {
                if (mode() === 'development' || mode() === 'test') console.log(`Error: ${ err }`);
                res
                    .status(400)
                    .json({ success: false, message: general.badRequest });
                rejected(err);
            } finally {
                if (conn) {
                    conn.end();
                }
            }
        })

        .then((message) => {
            if (mode() === 'development' || mode() === 'test') console.log(message);
        })

        .catch((err) => {
            if (mode() === 'development' || mode() === 'test') console.log(err);
        });
    } catch (err) {
        if (mode() === 'development' || mode() === 'test') console.log(`Error: ${ err }.`);
        res
            .status(400)
            .json({ success: false, message: general.badRequest });
    }
}

【问题讨论】:

    标签: javascript node.js api express routes


    【解决方案1】:

    循环是问题所在。我猜你正在尝试为每个文件设置一个路由。您不需要这样做,因为您可以使用路由参数来替换文件访问代码中的 userId。

    import express from 'express';
    
    const app = express();
    const router = express.Router();
    app.use(router);
    
    const port = 3000;
    
    const render1 = (req, res) => {
      res.send(`Render 1 says: hello world`);
    }
    
    const render2 = (req, res) => {
      res.send(`Render 2 says: logging in user...`);
    }
    
    const render3 = (req, res) => {
      res.send(`Render 3 says: reading user data from file ./userdata/${req.params.userId}.json`);
    }
    
    const render4 = (req, res) => {
      res.send(`Render 4 says: User ${req.params.userId} is logged in.`);
    }
    
    router.route('/').get(render1);
    
    router.route('/login').get(render2);
    
    router.route('/:userId').get(render3);
    
    router.route('/:userId/status').get(render4);
    
    
    app.listen(port, () => {
      console.log(`Example app listening at http://localhost:${port}`)
    })
    

    【讨论】:

    • 实际上,我将所有方法设置在一个文件中,如函数['methods'].forEach(e => load_dirs(e, 'users')) 中定义的那样,“方法”是我存储项目所有方法的文件夹......和“用户”是带有方法的实际文件......所以我循环遍历方法文件夹以找到users.js,当它导入所有定义的方法时,路由器也是如此......在我的app.js中我有读取路由器文件的循环。
    • 正如@jfriend00 所指出的,问题在于路由不应该在循环中。
    • router_files.forEach(file => { routers = require('../${dirs}/${file}'); }); 并定义外部路由器对吗?
    • 现在我了解了您要完成的工作(动态加载路由模块),但您的代码存在更多问题。您也不希望将路由处理程序函数的传播放在循环中。您想将它们加载到一个对象中,然后使用它们的名称访问它们(例如:routeHandler.getUsers 等)或将您的 routeHandler 对象传播到名称。这个Stack Overflow answer 应该会给你一些建议。
    • 我部分完成了...唯一剩下的就是loadDirs()函数...最初的想法是有一个文件夹,其方法名称为user,在user内部我会将方法分成不同的文件,一个用于获取,一个用于发布等等......
    【解决方案2】:

    这个结构:

          router
            .route('/:userId')
            .get(cors(corsOptions), getUser)
            .post(cors(corsOptions), editUser)
            .put(cors(corsOptions), removeUser);
    

    创建一个通配符路由,匹配任何具有顶级路径的 http 请求,例如:

    /abc
    /xxx
    /123
    

    因此,当您完成循环的第一次迭代并创建这些顶级通配符路由中的第一个时,它将优先于在其后添加到路由器的任何其他顶级路由。因此,它将接受循环未来迭代的请求,尝试为/login 定义处理程序。


    但是,整个模型都被破坏了。即使您删除了通配符路由,这也不是唯一的问题。由于循环的每次迭代只是一遍又一遍地定义完全相同的路线,因此在循环的第一次迭代中定义的路线将优先于循环的后续迭代中的所有其他路线。

    循环的后续迭代需要定义不同的路线。如果我们删除您在此处遇到的所有复杂情况并仅显示:

    app.get("/login", someHandler1);
    app.get("/login", someHandler2);
    

    然后,除非 someHandler1 碰巧决定不发送响应,而是调用 next() 以将控制权传递到下游,否则第二个定义将永远没有机会看到 /login 路由。

    因此,在循环中一遍又一遍地定义相同路线的整个模型似乎存在缺陷。此外,我倾向于避免使用顶级通配符路由,因为它会在您扩展功能时与其他顶级路由产生冲突。相反,要么执行/id/:userId,要么定义一个仅匹配顶级 userId 的正则表达式,但为不匹配该正则表达式的其他顶级路由留出空间。


    要提出一个解决方案,我必须更好地理解当您有多个文件具有所有相同的导出请求处理程序和所有相同的路由路径时,您究竟想要做什么。从表面上看,这对我来说毫无意义。您不会多次处理完全相同的传入请求。您只需处理一次,因此需要一段代码(而不是 N 段代码)来处理任何给定的路由。


    仅供参考,这是您的 getUsers() 函数的简化版本。你不需要在承诺中进行包装,也不需要太多重复的错误处理:

    exports.getUsers = async (req, res, next) => {
        let conn;
        try {
            conn = await db.pool.getConnection();
            const result = await conn.query(`SELECT * FROM users`);
            if (!result.length) {
                res.json({ success: true, message: general.noResults });
            } else {
                res.json({ success: true, utilizadores: results });
            }
        } catch(err) {
            if (mode() === 'development' || mode() === 'test') console.log(`Error:`, err);
            res.status(400).json({ success: true, message: general.badRequest });
        } finally {
            if (conn) {
                conn.end();
            }
        }
    });
    

    变更摘要:

    1. 删除 await new Promise() 包装,因为这不是必需的
    2. 删除.then() 和.catch() 所以我们只使用await 和try/catch
    3. 将所有错误处理放在一个 catch 块中
    4. 将所有错误处理程序合并到一个地方
    5. 修复错误对象的日志记录
    6. 删除 .status(200),因为 200 已经是默认值。

    仅供参考,我认为您确实想记录错误,即使是在生产中,而不仅仅是在开发中。如果您在部署的生产服务器中开始出现问题,您将需要日志信息来告诉您实际的内部错误是什么。所以,我建议你删除只记录开发或测试错误的if。

    【讨论】:

    • 第一次看OP的代码时,我错过了循环内的路由定义。
    • 自从我启动我的 discordjs 机器人以来,我一直在使用这种方法......这似乎是避免 app.js 具有 if else 结构以及所有命令和逻辑的好方法......
    • 我正在尝试颠倒路由器的顺序并添加 /id/ 参数以避免匹配并获得待处理的请求
    • @andremonteiro1996 - 这里的基本问题是你为什么要定义多条路径都具有相同的路径?你从来没有解释过,这很不寻常。也许如果您在这些路线中显示代码,那将有助于理解。而且,通配符路由会产生一个普遍的问题,因为您想要修改您的系统。如果有人创建的用户 ID 与您想在设计中使用的某些未来路径冲突怎么办?您是否在您的路由中调用 next() 以允许进一步路由?
    • @andremonteiro1996 - 这个问题现在完成了吗?或者,您还有什么想寻求帮助的吗?仅供参考,在getUsers.js 中获得getUsers() 并以这种方式动态加载并不难,但是如果你想讨论这个问题,请写一个新问题并给我评论那个问题,因为那不是什么这个问题完全描述了。
    猜你喜欢
    • 1970-01-01
    • 2017-03-11
    • 2016-04-01
    • 2014-10-03
    • 1970-01-01
    • 1970-01-01
    • 2021-10-19
    • 2023-03-31
    • 2015-05-22
    相关资源
    最近更新 更多