异步处理

异步处理

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

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

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

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

Async functions

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

async function fn() { const value = await getFromApi( doSomethimgWithValue( }

  • 有 await 时必须要有 async,但有 async 不一定非要有 await

返回值和 await 后面接的表达式均为 Promise,也就是说 Async functions 以 Promise 为基础。如果 await 后面的表达式返回值不是 Promise,那么需要通过一些方式将其包装为 Promise。

项目中使用

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

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

虽然使用 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 本身是个迭代器,只是被发现可以用来解决异步问题

promisify

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

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

const fs = require('fs' const readFile = think.promisify(fs.readFile, fs const parseFile = async (filepath) => { const content = await readFile(filepath, 'utf8' // readFile 返回 Promise doSomethingWithContent( }

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

const exec = require('child_process').exec; return new Promise((resolve, reject) => { // exec 的回调函数有多个参数 exec(filepath, (err, stdout, stderr) => { if(err) return reject(err if(stderr) return reject(stderr resolve(stdout }) })

错误处理

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

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

try/catch

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

module.exports = class extends think.Contoller { async indexAction() { try { await getDataFromApi1( await getDataFromApi2( await getDataFromApi3( } catch(e) { // capture error } } }

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

then/catch

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

module.exports = class extends think.Controller { async indexAction() { // 通过 catch 将 rejected promise 转换为 resolved promise const result = await getDataFromApi1().catch(err => { return think.isError(err) ? err : new Error(err) } // 这里判断如果返回值是转换后的错误对象,然后对其处理。 // 接口正常情况下不会返回 Error 对象 if (think.isError(result)) { // 这里将错误信息返回,或者返回格式化后的错误信息也都可以 return this.fail(1000, result.message } const result2 = await getDataFromApi2().catch(err => { return think.isError(err) ? err : new Error(err) } if(think.isError(result2)) { return this.fail(1001, result.message } // 如果不需要错误信息,可以在 catch 里返回 false // 前提是接口正常情况下不返回 false,如果可能返回 false 的话,可以替换为其他特殊的值 const result3 = await getDataFromApi3().catch(() => false if(result3 === false) { return this.fail(1002, 'error message' } } }

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

trace

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

// src/config/middleware.js module.exports = [ ... { handle: 'trace', options: { sourceMap: false, debug: true, // 是否打印详细的错误信息 error(err) { // 这里可以根据需要对错误信息进行处理,如:上报到监控系统 console.error(err } } } ... ];

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

timeout

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

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

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

或者是:

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

常见问题

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

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