如何优化承诺以避免 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 ]
)。
我正在为我的数据库使用 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 ]
)。