如何优化承诺以避免 callback/promise 地狱

How to optimize promises to avoid callback/promise hell

我正在为我的数据库使用 knex.js,并且我有一个查询依赖于之前的查询。

示例:

用户table

|用户名(pk) | first_name | last_name |

登录table

|用户名(pk/fk) |散列 |

进程是:

插入到用户 > 插入到登录

登录依赖于用户,所以如果插入到用户还没有完成,它会return一个错误。

这是我的代码:

const handleSignup = (req, res, db, logger, bcrypt) => {
    const {
        username,
        password,
        firstName,
        lastName,
    } = req.body;
    const hash = bcrypt.hashSync(password);
    if (username || !firstName || !lastName ) {
        res.json({
            haveEmpty: true
        });
        return;
    } else {
        db.transaction((trx) => {
                db.select('*').from('user').where('username', '=', username)
                    .then(data => {
                        if (!data[0]) {
                            db('user')
                                .returning('*')
                                .insert({
                                    username: username,
                                    first_name: firstName,
                                    last_name: lastName,
                                })
                                .then(user => {
                                    db('login')
                                        .returning('*')
                                        .insert({
                                            username: username,
                                            hash: hash
                                        })
                                        .then(login => {
                                            if (login[0]) {
                                                res.json({
                                                    isSuccess: true
                                                });
                                                return;
                                            } else {
                                                res.json({
                                                    isSuccess: false
                                                });
                                                return;
                                            }
                                        })
                                        .then(trx.commit)
                                        .catch(err => {
                                            logger.error(err);
                                            trx.rollback;
                                            res.render('pages/error-500');
                                        });
                                })
                                .then(trx.commit)
                                .catch(err => {
                                    logger.error(err);
                                    trx.rollback;
                                    res.render('pages/error-500');
                                });
                        } else {
                            res.json('User already Exist!');
                            return;
                        }
                    })
                    .then(trx.commit)
                    .catch(err => {
                        logger.error(err);
                        trx.rollback;
                        res.render('pages/error-500');
                    });
            })
            .catch(err => logger.error(err));
    }
}

而且我不知道我是否正确使用了交易。但这就是我想出来的。之前,当我将查询分成两个承诺时,我得到一个错误,因为似乎第一个插入(用户)没有完成。

此代码有效,但我知道有更正确的编码方法。

在 then 回调中返回一个 Promise 将一个接一个地执行 promise,如下所示:

const handleSignup = (req, res, db, logger, bcrypt) => {
    const {
        username,
        password,
        firstName,
        lastName,
    } = req.body;
    const hash = bcrypt.hashSync(password);
    if (username || !firstName || !lastName) {
        res.json({
            haveEmpty: true
        });
        return;
    }

    db.transaction((trx) => {
        db.select('*').from('user').where('username', '=', username)
            .then(data => {
                if (data[0]) {
                    res.json('User already Exist!');
                    return;
                }

                return db('user')
                    .returning('*')
                    .insert({
                        username: username,
                        first_name: firstName,
                        last_name: lastName,
                    });
            })
            .then(user => {
                return db('login')
                    .returning('*')
                    .insert({
                        username: username,
                        hash: hash
                    });
            })
            .then(login => {
                if (!login[0]) {
                    res.json({
                        isSuccess: false
                    });
                    return;
                }

                res.json({
                    isSuccess: true
                });
            })
            .then(trx.commit)
            .then(trx.commit)
            .then(trx.commit)
            .catch(err => {
                logger.error(err);
                trx.rollback;
                res.render('pages/error-500');
            });
    })
        .catch(err => logger.error(err));
}

我不能 100% 确定您的代码是因为您只会回滚最后一个查询而不是所有查询。关注这个。

根据我的经验,一旦您不再试图将它们全部塞进同一个函数中,promises 就会开始变得更加自然! (但我们所有人都可能曾经或多次写过与您的示例类似的东西,别担心。)

更小的代码块往往也更容易测试和调试。例如,如果您知道您对请求正文中的变量的检查是正确的,那么问题可能出在堆栈的更深处。

这是一个使用小型中间件堆栈的示例。这允许将操作分解成一口大小的块,同时仍然保证一件事先于另一件事发生。

const bcrypt = require("bcrypt");
const express = require("express");
const knex = require("knex");
const config = require("./knexfile").development;

const app = express();
app.use(express.json());
const db = knex(config);

const detailValidator = (req, res, next) => {
  // You can do more robust validation here, of course
  if (!req.body.firstName || !req.body.lastName) {
    return next(new Error("Missing user details."));
  }
  next();
};

const userUniqueValidator = (req, res, next) => {
  db("users")
    .where("username", req.body.username)
    .then(users => {
      if (users.length !== 0) {
        return next(new Error("User exists."));
      }
      next();
    });
};

const userCreator = (req, res, next) => {
  const { username, password, firstName, lastName } = req.body;
  const hash = bcrypt.hashSync(password, 10);

  db.transaction(trx =>
    trx("users")
      .insert({
        username,
        first_name: firstName,
        last_name: lastName
      })
      .then(([userId]) => trx("auth").insert({ user_id: userId, hash }))
      .then(() => res.json({ success: true }))
  ).catch(err => next(err));
};

app.post("/", detailValidator, userUniqueValidator, userCreator);

app.use((err, req, res, next) => res.json({ error: err.message }));

app.listen(4000, () => console.log("yup"));

关于 Knex 中的交易:如果使用上述语法,您实际上根本不需要调用 commit。但是,您确实需要使用 trx 参数作为查询构建器。该文档还建议了另一种选择,即 transacting 语法:请参阅 docs.

最后,我真的不建议使用您的用户名作为主键。它们经常需要更改,并且总是存在意外泄漏 URL 或日志中的风险。但是,我建议包括一个唯一约束。也许是这样的?

exports.up = knex =>
  knex.schema.createTable("users", t => {
    t.increments("id");
    t.string("username").unique();
    t.string("first_name");
    t.string("last_name");
  });

exports.up = knex =>
  knex.schema.createTable("auth", t => {
    t.increments("id");
    t.integer("user_id").references("users.id");
    t.string("hash");
  });

值得注意的是,我在这个快速示例中使用了 SQLite3,它仅支持在插入后返回行 ID(因此在用户插入后 then 子句中的 [ userId ])。