Express.js 和 Bluebird - 处理承诺链

Posted

技术标签:

【中文标题】Express.js 和 Bluebird - 处理承诺链【英文标题】:Express.js and Bluebird - Handling the promise chain 【发布时间】:2017-04-02 10:03:35 【问题描述】:

在后端 API 中,我有一个登录路由,应该执行以下操作序列:

给定用户名和密码,尝试根据 Active Directory 对用户进行身份验证。如果身份验证失败,回复状态 401。如果成功,继续。

在数据库中查找具有给定用户名的用户。如果未找到状态为 403 的回复,否则继续。

查找用户文档是否包含一些详细信息,例如电子邮件、显示名称等(以防这不是第一次登录)。如果是,则使用用户对象回复,否则继续。

从 Active Directory 中获取用户详细信息并更新数据库中的用户对象。回复更新的对象。

代码:

router.post('/login', (req, res, next) => 

  // capture credentials
  const username = req.body.username;
  const password = req.body.password;
  let user = null;

  // authenticate
  ad.authenticate(username, password)
    .then((success) => 
      if (!success) 
        res.status(401).send(); // authentication failed
        next();
      
      return User.findOne( username ).exec();
    )

    .then((found) => 
      if (!found) 
        res.status(403).send(); // unauthorized, no account in DB
        next();
      
      user = found;
      if (user.displayName) 
        res.status(201).json(user); // all good, return user details
        next();
      
      // fetch user details from the AD
      return ad.getUserDetails(username, password);
    )

    .then((details) => 
      // update user object with the response details and save
      // ...
      return user.save();
    )

    .then((update) => 
      res.status(201).json(update); // all good, return user object
      next();
    )

    .catch(err => next(err));

);

现在我用回调来运行它,但它确实是嵌套的。所以我想尝试一下 Bluebird 的 promise,但是我有两个问题:

看起来很混乱,有没有更好的方法来链接调用和处理响应?

每当我在回复后调用next() 停止请求时,都会继续执行到另一个.then()。尽管客户端收到了正确的响应,但在服务器日志中我发现执行仍在继续。例如,如果数据库中没有给定用户的帐户,客户端会收到403 响应,但在服务器日志中我看到异常failed to read property displayName of null,因为没有用户,它应该在@987654326 中停止@ 在res.status(403).send(); 之后。

【问题讨论】:

我目前使用的是最新的node版本7,并且开启了--harmony-async-await,然后你可以使用async/await模式,真的可以整理代码。 你必须return next() 【参考方案1】:

最好使用if/else 明确哪些分支会执行,哪些不会:

ad.authenticate(username, password).then((success) => 
  if (!success) 
    res.status(401).send(); // authentication failed
   else 
    return User.findOne( username ).exec().then(user => 
      if (!user) 
        res.status(403).send(); // unauthorized, no account in DB
       else if (user.displayName) 
        res.status(201).json(user); // all good, return user details
       else 
        // fetch user details from the AD
        return ad.getUserDetails(username, password).then(details => 
          // update user object with the response details and save
          // ...
          return user.save();
        ).then(update => 
          res.status(201).json(update); // all good, return user object
        );
      
    );
  
).then(() => next(), err => next(err));

then 调用的嵌套对于条件评估是非常必要的,你不能将它们线性链接并在中间“突破”(除了抛出异常,这真的很难看)。

如果您不喜欢所有这些 then 回调,您可以使用 async/await 语法(可能使用转译器 - 或使用 Bluebird 的 Promise.coroutine 以使用生成器语法模拟它)。然后你的整个代码就变成了

router.post('/login', async (req, res, next) => 
  try 
    // authenticate
    const success = await ad.authenticate(req.body.username, req.body.password);
    if (!success) 
      res.status(401).send(); // authentication failed
     else 
      const user = await User.findOne( username ).exec();
      if (!user) 
        res.status(403).send(); // unauthorized, no account in DB
       else if (user.displayName) 
        res.status(201).json(user); // all good, return user details
       else 
        // fetch user details from the AD
        const details = await ad.getUserDetails(username, password);
        // update user object with the response details and save
        // ...
        const update = await user.save();
        res.status(201).json(update); // all good, return user object
      
    
    next(); // let's hope this doesn't throw
   catch(err) 
    next(err);
  
);

【讨论】:

太棒了。 Async/await 似乎与承诺的库配合得很好,就是这种情况。 是的,如果router.post 具有promise-aware 功能,我们根本不需要next 回调,那就更棒了:-)【参考方案2】:

要回答你的第二点,你必须在调用next() 后拒绝你的承诺(或者至少返回一些东西,否则后面的行将被执行)。类似的东西

next();
return Promise.reject()

并更改您的捕获,以便在您没有错误时可以正常工作

.catch(err => 
  if (err)     
    next(err)
);

【讨论】:

也许我误解了,但是如果他调用next,OP 不想再执行then。因此,一种方法是拒绝承诺(有或没有错误) 感谢您的编辑。没有yourErrorHere,只有在发生真正的错误时才调用next(err),这是我在第一次修订中遗漏的两件重要事情。【参考方案3】:

首先回答你的第二个问题:没有办法打破/停止承诺链,除非你的回调抛出错误

doAsync()
.then(()=>
    throw 'sth wrong'
)
.then(()=>
    // code here never runs
)

您可以简单地尝试下面的演示来验证第二个回调仍然运行。

doAsync()
.then(()=>
    res.end('end')
)
.then(()=>
    // code here always runs
)


doAsync()
.then(()=>
    return;
)
.then(()=>
    // code here always runs
)

关于你的第一个问题:在then()中使用第二个参数,表示拒绝。并且每次都将逻辑分成两部分。

var p = new Promise(function(resolve, reject) 
    return
    ad.auth(username, password).then(()=
        // check if 401 needed. If needed, return reject
        if (dont needed 401 in your logic) 
            resolve(username)
        else
            reject( msg: 'authentication has failed', status: 401 )
    )
);

p
.then( (username)=>
    // this only runs when the previous resolves
    return User.findOne( username ).exec()
, (data)=>
    // in fact in your case you dont even have to have the reject callback
    return data
 )


.then( (found)=>
    return
    new Promise(function(resolve, reject) 
        if (found && /*your logic to determine it's not 403*/)
            resolve(user)
        else
            reject( msg: 'unauthorized, no account in DB', status: 403 )
    )
 )


.then( (found)=>
    return
    new Promise(function(resolve, reject) 
        if (found && /*your logic to determine it's not 403*/)
            resolve(user)
        else
            reject( msg: 'unauthorized, no account in DB', status: 403 )
    )
 )


.then( (user)=>
    return
    new Promise(function(resolve, reject) 
        if (/*your logic to determine it has the full info*/)
            resolve(user)
        else
            return ad.getUserDetails(username, password)
    )
 )


.then( (user)=>
    // all is good, do the good logic
, (data)=>
    // something wrong, so here you can handle all the reject in one place
    res.send(data) 
 )

【讨论】:

这让事情变得更加复杂。不过,还是感谢您的详细解答! .then(…, data => return data; ) 肯定不会做你想做的事 避免使用Promise constructor antipattern!您的代码中绝对不需要任何 new Promise 调用。 谢谢@Bergi,如果不使用new Promise(),如何实现reject部分?你可以做一些同步检查,看看 promise 是否应该被拒绝或解决。 请查看this、@João Pereira,你想要的答案是什么?

以上是关于Express.js 和 Bluebird - 处理承诺链的主要内容,如果未能解决你的问题,请参考以下文章

nodejs使用request和bluebird编写的http请求模块

使用 bluebird 和 coffeescript 的简单 promise 示例在一半时间内有效

bluebird

与 fs 和 bluebird 的承诺

NPM CI 和 Bluebird 承诺警告

如何使用 Bluebird 为游标和/或集合的 toArray() Promisify Node 的 mongodb 模块?