php yield 中间件,koa中间件与async

写在前面

相比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,把请求按method和url分发给对应的route。路由与一般中间件的区别是路由通常与主要业务逻辑紧密相关,可以把请求处理过程分成3段:

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

对应到中间件类型:

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

虽然功能不同,但从结构上看,路由和一般的中间件没有任何区别。router是请求分发中间件,用来维护url到route的关系,把请求交给对应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 here,100ms后抛出未捕获的异常

而Promise有一个特殊机制:

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

也就是说通过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()交由下一个中间件包装响应数据,非常清爽

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值