写在前面
相比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异步编程的更多信息,请查看:

模拟EventProxy_Node异步流程控制1

Step源码解读_Node异步流程控制2

模拟Promise_Node异步流程控制3

向WindJS致敬_Node异步流程控制4

一.中间件
不像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

(摘自完全理解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 middlewaresmodule.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 catchprocess.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 errorapp.use(onerror);// routerrouter    .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 middlewaresapp    .use(header)    .use(json)

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

// route /htmlconst 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

参考资料
koa

koa github

koa-router 7.x

express

更多相关文章

  1. Java 开发者写 SQL 时常犯的 10 个错误
  2. 太强了!这两款数据库中间件,完美解决 Spring Boot 数据库的版本管
  3. 太强了!这款轻量级的数据库中间件完美解决了Spring Boot 中分库分
  4. 我通过jQuery-ajax创建了__PHP_Incomplete_Class对象此错误
  5. 带有括号的某些字符串导致Ajax POST操作失败,出现403错误(禁止)
  6. 使用Selectize和Ajax时,在Bootstrap模式中显示Rails错误消息
  7. WCF获取URL长度限制问题:错误的请求-无效的URL。
  8. 为什么这个jQuery。ajax不会引发错误吗?
  9. 点击JSON数据加载Galleria画廊。我需要新鲜的眼睛来看我的错误

随机推荐

  1. javascript 构造函数中的属性与原型上属
  2. 使用Node.js初始化和配置AWS
  3. 深入浅出 Ajax 读书摘记2——【Ajax请求
  4. Javascript学习:案例7--对象属性和方法的
  5. css选择在IE中不起作用
  6. 注入html行模板的最佳方法
  7. require():使用module.exports vs直接分配给
  8. iOS uiwebview无法从javascript方法加载
  9. 为什么括号用于包装javascript函数调用? [
  10. js 不同类型var的boolean运算验证