最近系统的学了一下 ES6,发现以往编码的项目还有很多可以改进的空间,所以我使用 ES6 对部分过去的项目进行重构
Promise
这是之前用 node.js 写的一个接口,查询作者信息和当前用户的关系,基本思路是
- 查询粉丝和博主的关系(关注,未关注)
- 查询博主的所有粉丝
里面有一个 connection 查询是很明显的回调地狱,让我们用 Promise 解决掉它
app.get("/authorInfo", function (req, res) {
const fanId = req.query.fan_id;
const authorId = req.query.blogger_id;
const r = obEmpty();
connection.query(
"select * from fans where blogger_id = ? and fan_id = ?;",
[authorId, fanId],
function (error, result) {
if (error) throw error;
// 结果基类一定要初始化置空
r.code = 200;
r.msg = "success";
r.data = {};
r.data.relate = result.length ? true : false;
connection.query(
"select count(*) as total from fans where blogger_id = ?;",
[authorId],
function (error, result) {
if (error) throw error;
r.data.fanTotal = result[0].total;
res.send(r);
}
);
}
);
});
使用 Promise 解决后
app.get("/authorInfo", function (req, res) {
const fanId = req.query.fan_id;
const authorId = req.query.blogger_id;
const promise = new Promise((resolve, reject) => {
connection.query(
"select * from fans where blogger_id = ? and fan_id = ?;",
[authorId, fanId],
function (error, result) {
if (error) reject(error);
resolve(result.length ? true : false);
}
);
});
promise
.then((value) => {
connection.query(
"select count(*) as total from fans where blogger_id = ?;",
[authorId],
function (error, result) {
if (error) throw error;
res.send(
new Result(200, "success", {
relate: value,
fanTotal: result[0].total,
})
);
}
);
})
.catch((err) => {
console.error("服务器出错了" + err);
res.send(new Result(200, "查询失败"));
});
});
代码行数由 28 => 37,但优化了错误处理,值传递的清晰性和代码可读性,这里其实并不能很明显的看出 Promise 解决回调地狱的明显优势,而且这上面还存在一些问题,
问题一
如果在 promise.then() 里面中出现了错误我们应该如何处理?
问题二
一开始的 Promise 没有需要返回的信息但和 promise.then() 中的函数存在关系,此时 resolve 要怎么处理?同理 then 之中要怎么做?
对于第一个问题,可以使用 vs code 简单的看一下 promise 中的声明文件(.d.ts)
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
里面说明了,then(() => {...}) 中返回值是作为 TResult1,所以我们使用 return 可以为下一个 then(() => {...}) 传递信息,这点与一开始的 promise 不同,因为其中的 resovle() 函数返回值为 void
所以第一个的解决方案如下
const promise = new Promise((resolve, reject) => {
resolve(true);
});
promise
.then(() => {
// 抛出一个 Promise 对象,并设置状态为 reject
return new Promise((resolve, reject) => {
reject("出错了");
});
})
.then(() => {
console.log("in");
})
.catch((err) => {
throw err;
});
// 我觉得下面这种更方便
const promise = new Promise((resolve, reject) => {
resolve(true);
});
promise
.then(() => {
throw new Error("出错了");
})
.then(() => {
console.log("in");
})
.catch((err) => {
throw err;
});
对于第二个问题
是否可以 return 一些值表示状态呢?可以使用 vs code 简单的看一下 promise 中的声明文件(.d.ts)
/**
* Creates a new Promise.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used to resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
executor() 返回值为 void,返回值没有意义,使用 resolve 才能够传递相应的状态,所以即使没有需要返回的数据也应该表示一下
resolve();
注,如果你什么都不干,new Promise() 返回的状态为 pending,这种状态是无法进入到下一个 then() 的,Promise 的几种状态可以去百度一下
而在 then(() => {}) 你什么都不返回,其实是没有问题的,默认返回 undefined
用 3 个或以上的例子就能够更好的看清楚,后来我又找到了一个例子,工作流程为
- 读取服务器文章 HTML 文件中的内容
- 将更新的 HTML 的内容写入服务器文章 HTML 文件中
- 最后更新数据库中文章的标题
app.get("/updateArticle", function (req, res) {
// 对象模型解构属性名称需相同
let { title, articleName, aid, html } = req.query;
// 可以对 articleName 作一定校验
articleName = config.remoteFileDefaultPath + articleName;
fs.readFile(articleName, (err, data) => {
if (err) console.error(err);
const root = HTMLParser.parse(data.toString());
root.querySelector(".main").innerHTML = html;
fs.writeFile(articleName, root.innerHTML, (err) => {
if (err) console.error(err);
const sql = "UPDATE new SET new_name = ? WHERE new_id = ?;";
connection.query(sql, [title, aid], function (error, results) {
if (error) throw error;
});
});
});
});
变更如下
app.get("/updateArticle", function (req, res) {
// 对象模型解构属性名称需相同
let { title, articleName, aid, html } = req.query;
// 可以对 articleName 作一定校验
articleName = config.localFileDefaultPath + articleName;
const promise = new Promise((resolve, reject) => {
fs.readFile(articleName, (err, data) => {
if (err) reject(err);
const root = HTMLParser.parse(data.toString());
root.querySelector(".main").innerHTML = html;
resolve(root.innerHTML);
});
});
promise
.then((htmlContent) => {
fs.writeFile(articleName, htmlContent, (err) => {
if (err) throw err;
});
})
.then(() => {
const sql = "UPDATE new SET new_name = ? WHERE new_id = ?;";
connection.query(sql, [title, aid], function (error, results) {
if (error) throw error;
res.send(new Result(200, "更新成功"));
});
})
.catch((err) => {
console.error("服务器出错了" + err);
res.send(new Result(200, "查询失败"));
});
});
代码结构清晰了很多,可能看到这里已经有人想说,这代码也 TM 太烂了,用 SpringBoot 根本不会写得这么烂,的确用 SpringBoot 几个层来写肯定不会这么烂,主要原因是这是使用 express 写的,而且当初也只是随便写写,根本没考虑这么多,而且现在使用 node.js 的更完善的框架已经能够避免这些问题了
生成器(Generator)
写了这么多不如把标题改成 用 Promise 解决你的回调地狱
,其实不然,还可以使用生成器(Generator)来解决回调
用生成器解决后
app.get("/updateArticle", function (req, res) {
// 对象模型解构属性名称需相同
let { title, articleName, aid, html } = req.query;
// 可以对 articleName 作一定校验
articleName = config.localFileDefaultPath + articleName;
function readFile() {
fs.readFile(articleName, (err, data) => {
if (err) throw err;
const root = HTMLParser.parse(data.toString());
root.querySelector(".main").innerHTML = html;
iterator.next(root.innerHTML);
});
}
function writeFile(htmlContent) {
fs.writeFile(articleName, htmlContent, (err) => {
if (err) throw err;
iterator.next();
});
}
function update() {
const sql = "UPDATE new SET new_name = ? WHERE new_id = ?;";
connection.query(sql, [title, aid], function (error, results) {
if (error) throw error;
res.send(new Result(200, "更新成功"));
iterator.next();
});
}
function* gen() {
const htmlContent = yield readFile();
yield writeFile(htmlContent);
yield update();
}
const iterator = gen();
try {
iterator.next();
} catch (error) {
throw error;
}
});
优势就是解决了回调地狱,但劣势和 Promise 比较还是比较突出的,我认为是过于耦合,参数传递需要通过 iterator,为了解决回调地狱牺牲过大
Async await
Async await 算是基于 Promise 的另一种方式,我以前大部分都是配合 axios 使用,但这里并不需要 Promise,重构后如下
app.get("/updateArticle2", async function (req, res) {
// 对象模型解构属性名称需相同
let { title, articleName, aid, html } = req.query;
// 可以对 articleName 作一定校验
articleName = config.localFileDefaultPath + articleName;
try {
const htmlContent = await (() => {
return new Promise((resolve,reject) => {
fs.readFile(articleName, (err, data) => {
if (err) throw err;
const root = HTMLParser.parse(data.toString());
root.querySelector(".main").innerHTML = html;
resolve(root.innerHTML);
});
})
})();
await fs.writeFile(articleName, htmlContent, (err) => {
if (err) throw err;
});
const sql = "UPDATE new SET new_name = ? WHERE new_id = ?;";
await connection.query(sql, [title, aid], function (error, results) {
if (error) throw error;
res.send(new Result(200, "更新成功"));
});
} catch (err) {
console.error("服务器出错了" + err);
res.send(new Result(200, "查询失败"));
}
});
async await 重构后代码结构更清晰,相对于 Promise 链式调用要更灵活一些,但有些时候可能又没有直接 Promise 链式调用好,比如上面的三个回调都要返回数据,则三个回调都要在外层套上 Promise,如果是简单的代码其实得不偿失,没有必要,总而言之就是没有最好的代码,只有最合适的代码。