异步处理

Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,很多接口都是异步的,如:文件操作、网络请求。虽然提供了文件操作的同步接口,但这些接口是阻塞式的,非特殊情况下不要使用它。

对于异步接口,官方的 API 都是 callback 形式的,如:

  1. const fs = require('fs');
  2. fs.readFile(filepath, 'utf8', (err, content) => {
  3. if(err) return ;
  4. ...
  5. })

这种方式下,当业务逻辑复杂后,很容易出现 callback hell 的问题。为了解决这个问题,相继出现了 event、thunk、Promise、Generator function、Async functions 等解决方案,最终 Async functions 方案胜出,ThinkJS 也直接选用这种方案来解决异步问题。

Async functions

Async functions 使用 async/await 语法定义函数,如:

  1. async function fn() {
  2. const value = await getFromApi();
  3. doSomethimgWithValue();
  4. }
  • 有 await 时必须要有 async,但有 async 不一定非要有 await
  • Async functions 可以是普通函数的方式,也可以是 Arrow functions 的方式
  • await 后面需要接 Promise,如果不是 Promise,则不会等待处理
  • 返回值肯定为 Promise
    返回值和 await 后面接的表达式均为 Promise,也就是说 Async functions 以 Promise 为基础。如果 await 后面的表达式返回值不是 Promise,那么需要通过一些方式将其包装为 Promise。

项目中使用

ThinkJS 3.0 直接推荐大家使用 Async functions 来解决异步的问题,并且框架提供的所有异步接口都是 Promise 的方式,方便开发者直接使用 Async functions 调用。

  1. module.exports = class extends think.Controller {
  2. async indexAction() {
  3. // select 接口返回 Promise,方便 await 使用
  4. const list = await this.model('user').select();
  5. return this.success(list);
  6. }
  7. }

虽然使用 Async functions 解决异步问题时比较优雅,但需要 Node.js 的版本 >=7.6.0 才支持,如果在之前的版本中使用,需要借助 Babel 进行转译(由于框架只是要求 Node.js 版本大于 6.0,所以默认创建的项目是带 Babel 转译的,将 Async functions 转译为 Generator functions + co 的方式)。

和 Generator 区别

虽然 Async functions 和 Generator 从语法糖上看起来很相似,但其实还是有很多的区别,具体为:

  • 为解决异步而生,async/await 更加语义化。而 Generator 本身是个迭代器,只是被发现可以用来解决异步问题
  • 要求 await 后面必须是 Promise 接口,而 yield 后面没有任何限制
  • 不需要额外的执行器,Generator 需要借助 co 这样的执行器
  • 可以定义为 Arrow functions 的方式,而 Generator function 不能
  • 没有类似 yield 和 yield * 的问题

    promisify

Async functions 需要 await 后面接的表达式返回值为 Promise,但很多接口并不是返回 Promise,如:Node.js 原生提供异步都是 callback 的方式,这个时候就需要将 callback 方式的接口转换为 Promise 方式的接口。

由于 callback 方式的接口都是 fn(aa, bb, callback(err, data)) 的方式,这样就不需要每次都手工将 callback 接口包装为 Promise 接口,框架提供了 think.promisify 用来快速转换,如:

  1. const fs = require('fs');
  2. const readFile = think.promisify(fs.readFile, fs);
  3. const parseFile = async (filepath) => {
  4. const content = await readFile(filepath, 'utf8'); // readFile 返回 Promise
  5. doSomethingWithContent();
  6. }

对于回调函数不是 callback(err, data) 形式的函数,就不能用 think.promisify 快速包装了,这时候需要手工处理,如:

  1. const exec = require('child_process').exec;
  2. return new Promise((resolve, reject) => {
  3. // exec 的回调函数有多个参数
  4. exec(filepath, (err, stdout, stderr) => {
  5. if(err) return reject(err);
  6. if(stderr) return reject(stderr);
  7. resolve(stdout);
  8. })
  9. })

错误处理

在 Node.js 中,错误处理是个很麻烦的事情,稍不注意,请求可能就不能正常结束。对 callback 接口来说,需要在每个 callback 里进行判断处理,非常麻烦。

采用 Async functions 后,错误会自动转换为 Rejected Promise,当 await 后面是个 Rejected Promise 时会自动中断后续的执行,所以只需要捕获 Rejected Promise 就可以了。

try/catch

一种捕获错误的方式是使用 try/catch,像同步方式的代码里加 try/catch 一样,如:

  1. module.exports = class extends think.Contoller {
  2. async indexAction() {
  3. try {
  4. await getDataFromApi1();
  5. await getDataFromApi2();
  6. await getDataFromApi3();
  7. } catch(e) {
  8. // capture error
  9. }
  10. }
  11. }

通过在外层添加 try/catch,可以捕获到错误。但这种方式有个问题,在 catch 里捕获到的错误并不知道是哪个接口触发的,如果要根据不同的接口错误返回不同的错误信息就比较困难了,难不成在每个接口都单独加个 try/catch?那样的话会让代码非常难看。这种情况下可以用 then/catch 来处理。

then/catch

对于 Promise,我们知道有 then 和 catch 方法,用来处理 resolve 和 reject 下的行为。由于 await 后面跟的是 Promise,那么就可以对 Rejected Promise 进行处理来规避错误的发生。可以把 Rejected Promise 转换为 Resolved Promise 防止触发错误,然后我们在手工处理对应的错误信息就可以了。

  1. module.exports = class extends think.Controller {
  2. async indexAction() {
  3. // 通过 catch 将 rejected promise 转换为 resolved promise
  4. const result = await getDataFromApi1().catch(err => {
  5. return think.isError(err) ? err : new Error(err)
  6. });
  7. // 这里判断如果返回值是转换后的错误对象,然后对其处理。
  8. // 接口正常情况下不会返回 Error 对象
  9. if (think.isError(result)) {
  10. // 这里将错误信息返回,或者返回格式化后的错误信息也都可以
  11. return this.fail(1000, result.message);
  12. }
  13. const result2 = await getDataFromApi2().catch(err => {
  14. return think.isError(err) ? err : new Error(err)
  15. });
  16. if(think.isError(result2)) {
  17. return this.fail(1001, result.message);
  18. }
  19. // 如果不需要错误信息,可以在 catch 里返回 false
  20. // 前提是接口正常情况下不返回 false,如果可能返回 false 的话,可以替换为其他特殊的值
  21. const result3 = await getDataFromApi3().catch(() => false);
  22. if(result3 === false) {
  23. return this.fail(1002, 'error message');
  24. }
  25. }
  26. }

通过 Promise 后面接 catch 将 Rejected Promise 转化为 Resolved Promise 的方式,可以轻松定制要输出的错误信息。

trace

有些情况下,并不方便在外层添加 try/catch,也不太方便在每个 Promise 后面加上 catch 将 Rejected Promise 转换为 Resolved Promise,这时候系统提供 trace 中间件来处理错误信息。

  1. // src/config/middleware.js
  2. module.exports = [
  3. ...
  4. {
  5. handle: 'trace',
  6. options: {
  7. sourceMap: false,
  8. debug: true, // 是否打印详细的错误信息
  9. error(err) {
  10. // 这里可以根据需要对错误信息进行处理,如:上报到监控系统
  11. console.error(err);
  12. }
  13. }
  14. }
  15. ...
  16. ];

当出现错误后,trace 模块会自动捕获错误,debug 模式下会显示详细的错误信息,并根据请求类型输出对应的数据返回。

异步/错误处理 - 图1

timeout

有时候需要延迟处理一些事务,最常见的办法就是通过 setTimeout 函数来处理,但 setTimeout 本身并不返回 Promise,这时候如果里面的执行函数报错了是无法捕获到的,这时候需要装成 Promise。

框架提供了 think.timeout 方法可以快速包装成 Promise,如:

  1. return think.timeout(3000).then(() => {
  2. // 3s 后执行到这里
  3. })

或者是:

  1. module.exports = class extends think.Controller {
  2. async indexAction() {
  3. await think.timeout(3000);// 等待 3s 执行后续的逻辑
  4. return this.success();
  5. }
  6. }

常见问题

项目中是不是不能使用 Generator?

是的,ThinkJS 3.x 中不再支持 Generator,异步都用 Async functions 来处理,配合 Promise,是目前最优雅的解决异步问题的方案。

原文: https://thinkjs.org/zh-cn/doc/3.0/async.html