前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >koa中间件与async

koa中间件与async

作者头像
ayqy贾杰
发布2019-06-12 15:22:06
1.3K0
发布2019-06-12 15:22:06
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

写在前面

相比express的保守,koa则相对激进,目前Node Stable已经是v7.10.0了,async&await是在v7.6加入豪华午餐的,这么好的东西必须用起来

从目前历史来看,以顺序形式编写异步代码是自然选择的结果。微软出品的一系列语言,比如F# 2.0(2010年)就支持了该特性,C# 5.0(2012年)也添加了该特性,而JS在ES2016才考虑支持async&await,期间生态出现了一些过渡产品,比如EventProxy、Step、Wind等异步控制库,ES2015推出的Promise、yield,以及在此基础上实现的co模块,都是为了让异步流程控制更简单

async&await最自然的方式(顺序形式,与同步代码形式上没区别),也是目前最优的方案

P.S.关于JS异步编程的更多信息,请查看:

一.中间件

不像PHP内置了查询字符串解析、请求体接收、Cookie解析注入等基本的细节处理支持

Node提供的是赤果果的HTTP连接,没有内置这些细节处理环节,需要手动实现,比如先来个路由分发请求,再解析Cookie、查询字符串、请求体,对应路由处理完毕后,响应请求时要先包装原始数据,设置响应头,处理JSONP支持等等。每过来一个请求,这整个过程中的各个环节处理都必不可少,每个环节都是中间件

中间件的工作方式类似于车间流水线,过来一张订单(原始请求数据),路由分发给对应部门,取出Cookie字段,解析完毕把结果填上去,取出查询字符串,解析出各参数对,填上去,读取请求体,解析包装一下,填上去……根据订单上补充的信息,车间吐出一个产品……添上统一规格的简单包装(包装原始数据),贴上标签(响应头),考虑精装还是平装(处理JSONP支持),最后发货

所以中间件用来封装底层细节,组织基础功能,分离基础设施和业务逻辑

尾触发

最常见的中间件组织方式是尾触发,例如:

// 一般中间件的结构:尾触发下一个中间件
var middleware = function(err, req, res, next) {
   // 把处理结果挂到请求对象上
   req.middlewareData = handle(req);
   // 通过next传递err,捕获异步错误
   if (errorOccurs) {
       return next(error);
   }   next();
};

把所有中间件按顺序串起来,走到业务逻辑环节时,需要的所有输入项都预先准备好并挂在请求对象上了(由请求相关的中间件完成),业务逻辑执行完毕得到响应数据,直接往后抛,走响应相关的一系列中间件,最终请求方得到了符合预期的响应内容,而实际上我们只需要关注业务逻辑,前后的事情都是由一串中间件完成的

尾触发串行执行所有中间件,存在2个问题

  • 缺少并行优化
  • 错误捕获机制繁琐

对中间件按依赖关系分组,并行执行,能够提高性能,加一层抽象就能解决。错误需要手动往后抛,沿中间件链手动传递,比较麻烦,不容易解决

koa2.0中间件

看起来很漂亮:

app.use(async (ctx, next) => {
 const start = new Date();
 await next();
 const ms = new Date() - start;
 console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

一个简单的响应耗时记录中间件,如果放到中间件队首,就能得到所有中间件执行的总耗时

与上面介绍的尾触发不同,有了await就可以在任意位置触发后续中间件了,例如上面两个时间戳之间的next(),这样就不需要按照非常严格的顺序来组织中间件了,灵活很多

之前之所以用尾触发,就是因为异步中间件会立即返回,只能通过回调函数控制,所以约定尾触发顺序执行各中间件

async&await能够等待异步操作结束(这里的等待是真正意义上的等待,机制类似于yield),不用再特别关照异步中间件,尾触发就不那么必要了

二.路由

路由也是一种中间件,负责分发请求,例如:

router
 .get('/', function (ctx, next) {
   ctx.body = 'Hello World!';
 })
 .post('/users', function (ctx, next) {
   // ...
 })
 .put('/users/:id', function (ctx, next) {
   // ...
 })
 .del('/users/:id', function (ctx, next) {
   // ...
 })
 .all('/users/:id', function (ctx, next) {
   // ...
 });

常见的RESTful API,把请求按methodurl分发给对应的route。路由与一般中间件的区别是路由通常与主要业务逻辑紧密相关,可以把请求处理过程分成3段:

请求预处理 -> 主要业务逻辑 -> 响应包装处理

对应到中间件类型:

请求相关的中间件 -> 路由 -> 响应相关的中间件

虽然功能不同,但从结构上看,路由和一般的中间件没有任何区别。router是请求分发中间件,用来维护urlroute的关系,把请求交给对应route

三.错误捕获

await myPromise方式中reject的错误能够被外层try...catch捕获,例如:

(async () => {
   try {
       await new Promise((resolve, reject) => {
           setTimeout(() => {
               let err = new Error('err');
               reject(err);
           }, 100);
       });
   } catch (ex) {
       console.log('caught ' + ex);
   }
})();
console.log('first log here');

注意try...catch错误捕获仅限于reject(err),直接throw的或者运行时异常无法捕获。此外,只有在异步函数创建的那层作用域的try...catch才能捕获到异常,外层的不行,例如:

try {
   (async () => {
       await new Promise((resolve, reject) => {
           setTimeout(() => {
               let err = new Error('err');
               reject(err);
           }, 100);
       });
   })();
   console.log('first log here');
} catch (ex) {
   console.log('caught ' + ex);
}

因为异步函数自身执行后立即返回,外层try...catch无法捕获这样的异步异常,会先看到first log here100ms后抛出未捕获的异常

Promise有一个特殊机制:

特殊的:如果resolve的参数是Promise对象,则该对象最终的[[PromiseValue]]会传递给外层Promise对象后续的then的onFulfilled/onRejected

(摘自完全理解Promise)

也就是说通过resolve(nextPromise)建立的Promise链上任意一环的reject错误都会沿着Promise链往外抛,例如:

(async () => {
   try {
       await new Promise((resolve, reject) => {
           resolve(new Promise((rs, rj) => {
               rs(new Promise((s, j) => {
                   setTimeout(() => {
                       j(new Error('err'));
                   }, 100);
               }))
           }))
       });
   } catch (ex) {
       console.log('caught ' + ex)
   }
})();

仍然能够捕获到最内层的错误

捕获中间件错误

利用这个特性,可以实现用来捕获中间件错误的中间件,如下:

// middleware/onerror.js
// global error handling for middlewares
module.exports = async (ctx, next) => {
   try {
       await next();
   } catch (err) {
       err.status = err.statusCode || err.status || 500;
       let errBody = JSON.stringify({
           code: -1,
           data: err.message
       });
       ctx.body = errBody;
   }
};

把这个中间件放在最前面,就能捕获到后续所有中间件reject的错误以及同步错误

全局错误捕获

上面捕获了reject的错误和同步执行过程中产生的错误,但异步throw的错误(包括异步运行时错误)还是捕获不到

而轻轻一个Uncaught Error就能让Node服务整个挂掉,所以有必要添上全局错误处理作为最后一道保障:

// global catch
process.on('uncaughtException', (error) => {
   console.error('uncaughtException ' + error);
});

这个自然要尽量放在所有代码之前执行,而且要保证自身没有错误

粗暴的全局错误捕获不是万能的,比如无法在错误发生后响应一个500,这部分是错误捕获中间件的职责

四.示例Demo

一个简单的RSS服务,中间件组织如下:

middleware/
 header.js     # 设置响应头
 json.js       # 响应数据转规格统一的JSON
 onerror.js    # 捕获中间件错误
route/
   html.js     # /index对应的路由
   index.js    # /html/:url对应的路由
   pipe.js     # /pipe对应的路由
   rss.js      # /rss/:url对应的路由

按顺序应用各中间件:

// global catch for middles error
app.use(onerror);// router
router
   .get('/', function (ctx, next) {
       ctx.body = 'RSSHelper';
   })
   .get('/index', require('./route/index.js'))
   .get('/rss/:url', require('./route/rss.js'))
   .get('/html/:url', require('./route/html.js'))
   .get('/pipe', require('./route/pipe.js'))
app
   .use(router.routes())
   .use(router.allowedMethods())// custom middlewares
app
   .use(header)
   .use(json)

请求预处理和响应数据包装都由前后的中间件完成,路由只负责产生输出(原始响应数据),例如:

// route /html
const fetch = require('../fetch/fetch.js');
module.exports = async (ctx, next) => {
   await new Promise((resolve, reject) => {
       const url = ctx.params.url;       let onsuccess = (data) => {
           data = data || {};
           ctx.state.data = data;
           resolve();
       }
       let onerror = reject;
       fetch('html', url)
           .on('success', onsuccess)
           .on('error', onerror)
   });   next();
};

抓取成功后,把data挂到ctx.state上,resolve()通知等待结束,next()交由下一个中间件包装响应数据,非常清爽

项目地址:https://github.com/ayqy/RSSHelper/tree/master/node

参考资料

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.中间件
    • 尾触发
      • koa2.0中间件
      • 二.路由
      • 三.错误捕获
        • 捕获中间件错误
          • 全局错误捕获
          • 四.示例Demo
            • 参考资料
            相关产品与服务
            消息队列 TDMQ
            消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档