## 前言
老实说,在 9012 年的今天,还有很多前端新人,对 Callback 的理解不够,在使用 Koa、Egg 时,经常会在调用一个 Callback 类型的 SDK 时,犯一些低级错误,这让人很惊讶。
在 Egg 答疑过程中,多次遇到类似问题,无奈中有了本文。
但本文并不打算展开讲解它们的原理,只是想通过一个快速的 Case 来展示如何使用。
## 一个简单的场景
假设这样一个场景:
## Express 的实现
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();
app.use((req, res, next) => {
const filePath = path.join(__dirname, 'index.html');
// 读取文件
fs.readFile(filePath, (err, content) => {
// 读取文件错误
if (err) return next(err);
// 读取文件成功
res.type('html');
res.status(200);
res.end(content.toString());
});
});
app.listen(3000, () => console.log('Server running at http://127.0.0.1:3000/'));
如上,在中间件里面调用一个带有 Callback 的 SDK,需要在回调里分别处理成功和失败。
Express 的中间件模型,在不主动调用 next(err)
或 res.end()
时,是不会继续往下走和响应给用户的。因此可以直接在回调里面处理。
## Koa 的错误实现
如果你依葫芦画瓢,如下实现:
const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
const filePath = path.join(__dirname, 'index.html');
// 读取文件
fs.readFile(filePath, (err, content) => {
ctx.type = 'html';
ctx.status = 200;
ctx.body = content.toString();
});
});
app.listen(3000, () => console.log('Server running at http://127.0.0.1:3000/'));
你会发现,还没等待文件读完,客户端就收到了 404 响应。
因为在 Koa 里面,洋葱模型是会一个接着一个执行的,如果你没有通过 await Promise
去控制执行时序的话,也会一路执行下去,到最后会自动 response 给用户,如果 ctx.body
为空则响应 404。
## Koa 的正确实现
const fs = require('fs');
const path = require('path');
const util = require('util');
const Koa = require('koa');
const app = new Koa();
// 把 fs.readFile 封装为 Promise 形式
const readFile = util.promisify(fs.readFile);
app.use(async (ctx, next) => {
const filePath = path.join(__dirname, 'index.html');
// 通过 await 方式读取文件
const result = await readFile(filePath);
ctx.type = 'html';
ctx.status = 200;
ctx.body = result;
});
app.listen(3000, () => console.log('Server running at http://127.0.0.1:3000/'));
如上,所有的 Callback 的 SDK,都需要转换为 Promise 的形式,这样才能在 Koa 中使用。
## Promisify
如上, util.promisify(fs.readFile)
是 Node.js 8.x 的语法糖,等价于如下封装:
// 把 fs.readFile 封装为 Promise 方式
async function readFile(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, (err, content) => {
// 读取错误则调用 reject
if (err) return reject(err);
// 读取成功则调用 resolve
return resolve(content.toString());
});
});
}
我们还可以类似把 setTimeout
封装为 sleep
:
// 定义
async function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
// 使用:
await sleep(1000);
如果你使用的 SDK 只提供 Callback 方式,那你也可以类似的做一层封装,再来使用。
当然,大部分情况下,我们直接使用 util.promisify
或一些封装好的类即可,如 Egg 就经常使用到:
const { mkdirp, rimraf, sleep } = require('mz-modules');
const { fs } = require('mz');
async function run() {
// 非阻塞方式删除目录
await rimraf('/path/to/dir');
// +1s
await sleep('1s');
// 非阻塞的 mkdir -p
await mkdirp('/path/to/dir');
// 读取文件,请把 `fs.readFileSync` 从你的头脑里面彻底遗忘。
const content = await fs.readFile('/path/to/file.md', 'utf-8');
}
## 参考资料
强烈建议 2019 年了还不熟悉 Promise 和 Async 的同学,回去学习下:
- http://es6.ruanyifeng.com/#docs/promise
- http://es6.ruanyifeng.com/#docs/async
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
- https://github.com/sindresorhus/promise-fun
- 以及 Koa 的洋葱模型的相关资料。