首页 > 后端 > Node.js > 文章详情

科普文:Koa Callback 新手不完全指南

## 前言

老实说,在 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 的同学,回去学习下:


## 相关阅读

相关文章分享