首页 > 解决方案 > 如何“等待”(或 .then())一个异步函数,但被包装在一个模块中并且不返回 Promise

问题描述

(警告:这个问题绝对是巨大的,因为这个问题有一个非常复杂的背景。哎呀,当我写完这个问题时,我实际上可能最终会逃避自己实际提出解决方案......)

(编辑:那没有发生。我仍然不知道该怎么做。我希望没有针对此类巨大问题的网站规则......这里是......)

我正在为与数据库交互的 Discord 机器人(使用 Node 和 Discord.js)编写代码。(特别是 MongoDB。)当然,这意味着异步行为加倍。当我以最简单的方式编写东西时,一切都运行得很好,而且我认为我大致了解 Promise、回调,并且await足够好,可以确保事情以正确的顺序发生。

然而,在重构我的代码以增强模块化时,我遇到了一个看似无法克服的烦恼:我失去了正确的错误捕获,并且事情打印出他们已经成功,同时它执行的模块(正确地)报告说命令失败。

首先,一点背景。

该机器人有许多使用数据库的命令;我们称它们为“!侮辱”和“!笑话”。这些命令背后的想法是,它们在程序上将用户添加到数据库中的组件构建的侮辱或笑话放在一起。每个命令都有一个单独的“集合”(MongoDB 术语,想想 SQL 表),其中包含用户输入的各自数据。

该机器人最初是由其他人编写的,他们为每个集合添加和删除内容的解决方案是使用四个单独的命令:“!insultadd”、“!insultdelete”、“!jokeadd”和“!jokedelete”。我看到它的第一个想法是“模块化,吃掉你的心。哎呀。” 代码库包含很多这样的代码重复,因此我的目标是抽象出足够多的功能,这样可以消除大部分重复,并且代码库总体上更容易扩展和维护。

所以,我想出了一个名为“!db”的命令。已经存在一层模块化:!db 所做的就是调用实现每个单独功能的“子命令”。这些子命令被称为“!dbadd”、“!dbdelete”等,它们不打算单独调用。需要注意的重要一点是,我首先编写了这些子命令,并且只有在它们都具有独立功能时,我才创建 !db 以简单的方式将它们包装起来,只使用 case 语句。(例如,调用!db add insultsCollection "ugly"insultsCollection侮辱性形容词的集合在哪里)只会!dbadd以适当的参数结束调用。)因此,最初,每个子命令都会自己打印结果,使用类似msg.channel.send('Inserted "' + selectedItem + '" into ' + selectedCollection + '.');.

最初,这工作得很好。!db 只需简单地做任何事情:

var dbadd = require('../commandsInternal/dbadd.js');
dbadd.execute(msg,args.slice(1),db);

并且 !dbadd 将负责向用户打印操作成功的信息,并报告将什么项目插入到数据库中。

然而,这个巨大重构的一个关键部分是外部行为和使用对最终用户来说基本保持不变——也就是说,!jokeadd 及其亲属将保留,但它们的内部将被挖出并替换为对相关的调用!db 函数。这就是我们开始遇到麻烦的地方。当我尝试调用类似 !insultadd 的东西时,会发生这种情况:

> !insultadd "ugly"
Inserted "ugly" into "insultsCollection". (This is printed by !dbadd.)
The bot can now call you "ugly"! (This is printed by !insultadd.)

这种行为是不受欢迎的,因为从根本上说,我们希望向用户呈现它就像是一个简单的形容词列表,因此我们希望避免引用例如数据库中的集合名称。那么,我是如何纠正这个问题的呢?我认为,最可扩展的方法是在子命令中添加某种标志,例如“ beQuiet”,以确定它是否打印自己的内容。如果这是一个“正常”的代码库,那可能就是我会做的。但...

这些命令是用 Node 模块编写的,这些模块导出了一些东西:命令的名称、命令的冷却时间等等……但最重要的是,一个名为execute(msg, args, db). 该功能是机器人的主要流程调用任意命令的方式。它查找命令的名称,将其映射到一个对象,然后尝试execute在该对象上执行该方法command。请注意,它execute需要三个参数……一个 Discord.jsMessage对象、命令的参数(字符串数组)和一个 MongoDBDb对象。为了将像“ beQuiet”这样的标志传递给 !dbadd,我将被迫添加另一个 arg 到execute,我非常不愿意这样做,因为这意味着某些命令会因为某些原因而获得“特殊”args 还有……呃。这有点像一致性的崩溃,让事情变得完全免费。

所以我不能传递一个标志。好的,接下来呢?“好吧,”我想,“我为什么不把印刷品搬进!db去呢?” 所以我就这么做了。我的 switch-case 语句现在看起来像:

switch (choice) {
case "add":
    dbadd.execute(msg,args.slice(1),db);
    msg.channel.send('Inserted "' + args[2] + '" into ' + args[1] + '.');
    break;
case "delete":
    dbdelete.execute(msg,args.slice(1),db);
    msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.');
    break;
// ... etc
}

好吧,酷!所以让我们执行它......好吧,很酷,似乎工作正常。现在,让我们用一些无效的输入来测试它......

> !db delete insultsCollection asdfasdf
Did the user give a collection that exists? : true (Debugging output)
Error: No matches in given collection. (Correct error output from !dbdelete)
"asda" has been removed from hugs. (Erroneous output from !db)

哦哦。那么,为什么会发生这种情况?本质上,这是因为异步性。所有对数据库内容的调用都要求您提供回调或处理 Promise。(如果可能,我更喜欢后者。)所以,!dbdelete 有这样的东西:

var query = { value: { $eq: selectedItem} };
let numOfFind = await db.collection(selectedCollection)
                        .find(query)
                        .count();
// Note that .count() returns a Promise that resolves to an int.
// Hence the await.

if (numOfFind == 0) {
    msg.channel.send("Error: No matches in given collection.");
    return;
}

方便,对吧?使execute()函数(上面的代码包含在其中)成为一个async函数使得一切都更容易编写。我.then()在适当的地方使用,而且一切都很好。但问题本质上在于return...

(哎呀,有那么一分钟我以为我已经在解决这个问题了。但显然只是添加throw是行不通的。)

好的,所以...问题是...无论我使用return还是throw!db 都不在乎。我的想法是,进行异步函数调用(如 db.collection().find())会导致一个独立的“工作”开始。(我确信我对此非常错误,但这种思维模式到目前为止已经奏效了。)通过看到这样的事情:

db.collection(selectedCollection).deleteMany(query, function(err, result) {
    if (err) {
        throw err;

        console.log('Something went wrong!');
        return;
    }
    console.log('"' + selectedItem + '" has been removed from ' + selectedCollection + '.');
});
console.log("Success! Deleted the thing.");

实际上会打印“成功!” 在实际删除该项目之前,我已经了解到,当您调用异步内容时,脚本会以它的快乐方式进行,如果您希望它在之后实际打印它您需要(在上述情况下)将其放入回调,或使用.then(),或await结果。你必须这样做。

但问题是……由于 !dbdelete 的模块化,我不能做任何这些。这些不起作用:

// Option 1: Callbacks.
// Doesn't work because execute() doesn't take a callback!
case "delete":
    dbdelete.execute(msg,args.slice(1),db, function(err, result) {
        msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    });
    break;

// Option 2: .then().
// Doesn't work because execute() doesn't return a Promise!
case "delete":
    dbdelete.execute(msg,args.slice(1),db)
    .then(function(err, result) {
        msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    });
    break;

// Option 3: await.
// Doesn't work because... I don't really know why but I know it doesn't work.
// Also, again, execute() doesn't return a promise so we can't await it.
case "delete":
    await dbdelete.execute(msg,args.slice(1),db);
    msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    break;

所以,我走到了尽头。我不知道如何解决这个问题。老实说,我正在认真考虑让 .execute() 返回一个 Promise 以便我可以 .then() 它。但我真的不想那样做,尤其是因为我不知道怎么做。简而言之:有没有办法在不返回承诺的函数上执行 .then() ?如果我可以让它阻塞,我们会没事的。

更新:这是 dbdelete.js 的代码:https ://pastebin.com/LdHm3ybU 更新 2:根据 Mark Meyer 的说法,因为我使用了await关键字,execute()实际上确实返回了一个 Promise!事实证明,这解决了一个问题:

case "delete":
    let throwaway = await dbdelete.execute(msg,args.slice(1),db);
    message.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.');
    break;

此代码导致更接近预期的结果:即使失败,打印语句仍然始终运行,但是......然后我只dbdelete.execute()返回一个布尔值,false如果 !db 不应该打印任何东西!所以,这两个问题现在都解决了!谢谢大家这么快回复!你真的很有帮助!<3

标签: javascriptnode.jsmongodbasynchronouspromise

解决方案


如果您的.execute()方法是异步的,那么调用者可以知道它何时完成或知道它的返回值是什么的唯一方法,如果您设计到 API 和异步机制来了解这一点。同步函数将在函数内部的异步操作完成之前很久就返回,因此调用者无法知道它何时完成或知道它获得了什么结果。

因此,您需要创建一种机制,让调用者知道何时.execute()完成以及结果是什么。常见的机制有:

  1. 返回一个用最终结果解决/拒绝的承诺。调用者使用.then()await跟踪它。

  2. 接受将在最终处置已知时调用的回调。

  3. 使用一些其他机制,例如在某个已知对象上触发的事件(流使用此方案)。

您将需要找到调用者已经知道您可以触发事件的某个已知对象,或者您需要更改 API 以具有异步接口。Javascript 中无法将异步操作转换为同步返回值,因此您需要更改接口。

对于一次性返回的结果(不是一些多次触发的持续事件),Javascript 中“现代”的做事方式是返回一个承诺,然后调用者可以使用.then()await在该承诺上。


推荐阅读