V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
waibunleung
V2EX  ›  Node.js

koa2 框架中的中间件同步还是异步的问题?

  •  1
     
  •   waibunleung · 2018-03-27 12:14:52 +08:00 · 6124 次点击
    这是一个创建于 2428 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题 1:为什么 koa2 框架中的中间件要用 async 的形式写,很少见用同步模式(即不加 async)? 如:

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

    查阅了一些资料,看到阮一峰的 koa 教程中,有同步写法的例子:

    const one = (ctx, next) => {
      console.log('>> one');
      next();
      console.log('<< one');
    }
    
    const two = (ctx, next) => {
      console.log('>> two');
      next(); 
      console.log('<< two');
    }
    
    const three = (ctx, next) => {
      console.log('>> three');
      next();
      console.log('<< three');
    }
    
    app.use(one);
    app.use(two);
    app.use(three);
    

    这里完全没有 async 的出现,阮神只是在后面提到: “迄今为止,所有例子的中间件都是同步的,不包含异步操作。如果有异步操作(比如读取数据库),中间件就必须写成 async 函数。”

    接着找资料发现 koa2 中间件的 next 返回的是 promise,因为源码中的 dispatch(i)返回的就是 promise, 这样我又有一个疑问, 问题 2:为什么要设计成返回 promise ?不返回 promise 就达不到中间件串联的效果吗? 因为中间件的执行在理解上是一个同步的过程,所以设计成异步要怎么去理解??

    问题 3:是不是用 koa2 写的代码都存在很多 async 的 function(公司项目代码到处都是 async 的,返回输出 json 的时候也需要 async 吗?)? 有没有不需要写 async 的场景或者例子?

    32 条回复    2018-03-28 14:22:18 +08:00
    yamedie
        1
    yamedie  
       2018-03-27 12:24:23 +08:00 via Android
    我也想问。
    zhouyg
        2
    zhouyg  
       2018-03-27 13:00:48 +08:00
    返回 promise 是因为要配合 await 使用
    cheetah
        3
    cheetah  
       2018-03-27 13:15:37 +08:00
    首先你要了解 async/await 是什么:async/await 是用类似同步的语法完成异步操作,以解决 callback hell 的问题
    whypool
        4
    whypool  
       2018-03-27 14:11:13 +08:00
    因为你不用 async,就得写回调了
    这玩意也只是约等于同步
    因为 node 里面的 loop 都是异步的,真正的同步阻塞线程什么的,木有
    cloudzqy
        5
    cloudzqy  
       2018-03-27 15:22:35 +08:00   ❤️ 1
    看了几遍才看懂你的疑问,其实光看阮一峰这个例子好有误导性呀。
    “迄今为止,所有例子的中间件都是同步的,不包含异步操作。如果有异步操作(比如读取数据库),中间件就必须写成 async 函数。”
    只要有一个是异步的,如果要保持这个例子的输出,就必须要用 async 和 await。

    假如这个例子只有 one 和 two,把 two 换成异步
    例一
    ```
    const one = (ctx, next) => {
    console.log('>> one');
    next();
    console.log('<< one');
    }
    const two = (ctx, next) => {
    console.log('>> two');
    setTimemout(() => {
    next();
    console.log('<< two');
    });
    }
    ```
    你这个时候输出的将会是 >> one >> two <<one <<two。
    然而中间件希望的输出是 >> one >> two << two << one。
    所以你需要
    例二
    ```
    const one = async (ctx, next) => {
    console.log('>> one');
    await next();
    console.log('<< one');
    }
    const two = (ctx, next) => {
    console.log('>> two');
    setTimemout(() => {
    next();
    console.log('<< two');
    });
    }
    ```
    这就是为什么 next 要返回 promise。只有全部 promise 才能达到你预期的状态。
    三个问题一起解答,当你期望的执行顺序是例一的时候可以不用 async,这样比较混乱,因为你不知道你后面的中间件有多少异步多少同步,无法预测执行顺序,所以 koa 的中间件思想就是例二这种,不管 two 是不是异步,执行顺序都不会变。
    VDimos
        6
    VDimos  
       2018-03-27 16:04:30 +08:00
    Koa2 已经不推荐使用 Generator 函数,换成 Async 函数了。另外,Koa 用的是洋葱模型,所以理论上是得加上 await 的,不加的话,next 会被放到异步里面去了,就不是洋葱模型了
    waibunleung
        7
    waibunleung  
    OP
       2018-03-27 18:22:10 +08:00
    我觉得你们都没有真正能解答我的问题,我知道 async/await 以及 promise 的用法,只是不明白为什么 next 要返回 promise,为什么中间件要写成 async 的形式,因为如果全部都不写 async 的话也能达到目的,仅仅是为了配合 next 返回的 promise 而使用 await 进而要用 async 包裹起函数作为中间件这种说法显然不能说服我。。。另外 @cloudzqy 的回答差不多是那么个意思但是我还是不能尽然明白...
    iugo
        8
    iugo  
       2018-03-27 18:26:53 +08:00
    如果 next() 不需要 await, 就没有什么意义.
    cloudzqy
        9
    cloudzqy  
       2018-03-27 18:36:55 +08:00 via Android
    @waibunleung
    先考虑目的:实现洋葱模型,这是 koa 基本思想,理由够充分,可以从阮一峰的例子了解这个模型。
    然后再考虑方式,用 async。如果不返回 promise,不用 async,当有中间件用异步的时候,无法实现洋葱模型,如例 2。或者你可以自己试试?
    当然,回调,promise,generator,async 都可以实现,但是 async 更优雅。
    waibunleung
        10
    waibunleung  
    OP
       2018-03-27 18:44:31 +08:00
    @iugo 你说没什么意义的意思是?能否举一下具体的例子?
    Torpedo
        11
    Torpedo  
       2018-03-27 18:47:14 +08:00
    @waibunleung 从错误处理的角度来说,如果你的中间件有一个不返回 promise,那么你的
    try{
    next()
    } catch(e){
    }
    就不能捕获错误。
    所以即使同步的中间件,也应该写成这种。
    (ctx,next)=>{
    xxx
    return next().then(xxx);
    }
    waibunleung
        12
    waibunleung  
    OP
       2018-03-27 18:49:52 +08:00
    @cloudzqy 我提问之前就试过,这是我的测试代码:
    const one = (ctx, next) => {
    console.log('>> one');
    next();
    console.log('<< one');
    }

    const two = async (ctx, next) => {
    console.log('>> two');
    await next();
    console.log('<< two');
    }

    const three = async (ctx, next) => {
    console.log('>> three');
    await next();
    console.log('<< three');
    }

    app.use(one);
    app.use(two);
    app.use(three);

    //output:
    >> one
    >> two
    >> three
    << one
    << three
    << two

    当全部不使用 async 的时候,是能按照洋葱模型返回的

    或者你能不能解释一下我这样测试的执行过程是怎么样的,看看是否与我的想法相符?
    ps:不是想做伸手党,只是想找个人验证一下想法...
    waibunleung
        13
    waibunleung  
    OP
       2018-03-27 18:53:03 +08:00
    @Torpedo 你的这种写法返回的还是 promise,是属于 koa2 中支持的中间件写法之一,其实跟 await 没有太大区别,这样我还是没有能明白对我的疑问有什么帮助....(哭)
    iugo
        14
    iugo  
       2018-03-27 19:03:25 +08:00
    @waibunleung 比如你说的例子:

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

    app.use(async (ctx, next) => {
    return new Promise(resolve => {
    setTimeout(function(){
    resolve("🙃");
    }, 1000);
    });
    await next()
    })
    ```

    这时候你再看看第一个中间件执行时间是多少.
    Torpedo
        15
    Torpedo  
       2018-03-27 19:04:55 +08:00
    @waibunleung 从能运行的角度是可以的。但是从错误处理的角度,如果你不是每个中间件都返回一个 promise,那么你的中间件错误处理就可能有问题。
    如果有一个中间件写成同步的模式,那么这中间件后面的错误就不能被
    try{
    next()
    } catch(e){
    }
    捕获。
    iugo
        16
    iugo  
       2018-03-27 19:05:43 +08:00
    如果所有中间件都是同步的. 那你不用异步也可以.

    一旦之后的中间件可能是异步的, 那么前面的中间件都要是异步的, 并且要 await(等待) next() 执行完成.
    cloudzqy
        17
    cloudzqy  
       2018-03-27 19:09:23 +08:00 via Android
    @waibunleung
    你说的能,是因为后面的中间件没有异步,而后端代码有异步才是普遍情况,假如有异步:
    1. 使用 await next()会等你后面的中间件的异步全部执行完,再执行 next()后面的代码:
    >>one
    ...其他中间件,包含同步和异步...
    <<one
    2. 而不使用 await 的情况,执行顺序是:
    >>one
    ...其他中间件的同步...
    <<one
    其他中间件的异步

    我需要的是最外层的>>one 最先执行,然后<<one 最后执行,2 的输出不符合模型
    per
        18
    per  
       2018-03-27 19:15:48 +08:00 via iPhone
    如果你的代码里有请求 ajax 的操作。你不用异步的话就乱套了
    zhouyg
        19
    zhouyg  
       2018-03-27 19:38:58 +08:00
    还有一种方法,参考 koa 源码,然后去实现一个简单版本的 koa。
    这样就可以理解 koa 所处理的场景,以及 koa 如此实现的原因
    千言万语不如造一遍轮子
    VDimos
        20
    VDimos  
       2018-03-27 20:01:59 +08:00
    之所以使用 async 的原因,是因为它可以 await 一个 Promise。这样就是完美的中间件了。阮一峰那个 blog 应该还是停留在 Koa1.0 的时代,现在 Koa2.x 已经提倡使用 async 了。async 天然的可以等待一个 Promise 执行完成。你想象以下,你现在有个需求,接收到用户上传的文件,然后处理文件以后返回提示信息。如果不使用 async,那么你在 Promise 还没执行完就会返回信息了。现在只需要 await next()。然后再返回信息。其实本质上就是把整个过程变成了同步执行的了,但是这样做的好处是选择面更强了,因为它满足了洋葱一层一层的模型。还有个原因是因为这是一个约定,如果你调用的其他中间件使用了 async,那么你不使用,那么将无法等待这个中间件的执行。换句话说,async 把一系列的异步函数给顺序调用了,如果不是 async 的话,那么所有的异步函数将没有任何关系。
    waibunleung
        21
    waibunleung  
    OP
       2018-03-27 20:45:56 +08:00
    @VDimos 不知道你有没有见过那个 blog,那个 blog 一直在更新,我看到的文章是 20117 年下半年的,这个时候怎么都是 koa2 了吧?而且我读你的话的前半段,意思就是因为要等待 next 返回的 promise,所以用 await,因为用 await,所以要 async,我觉得这不是一个说得过去的解释....
    hzzhzzdogee
        22
    hzzhzzdogee  
       2018-03-27 22:58:25 +08:00
    可以用同步, 就变得像 express
    hzzhzzdogee
        23
    hzzhzzdogee  
       2018-03-27 22:59:19 +08:00
    有那么好用的 async await, 干嘛不用. async 正是 koa 期望的呀
    waibunleung
        24
    waibunleung  
    OP
       2018-03-27 23:13:27 +08:00
    @hzzhzzdogee 此话怎讲呢?
    VDimos
        25
    VDimos  
       2018-03-27 23:16:03 +08:00 via Android
    @waibunleung koa 用的洋葱模型,要求一层层进去再一层层出来。nodejs 是异步的,koa 遵循了这个原则。而以前的异步最大的问题在于,如果要实现洋葱模型,就必须写大量的回调函数。因此就算是 koa1 也是采用了 generator 这种折中的方法。如果你使用过 co,你就会对洋葱模型有感觉了。其实 koa1 内部就是调用了 co。koa2 改用了新的 async 编写,async 实现的功能本身就是 generator 加 co。你如果用非 async 函数,koa 内部会把它转换成一个 async 函数的。
    kohos
        26
    kohos  
       2018-03-27 23:51:09 +08:00
    1. 函数前面加 async 的原因是因为函数里面用了 await,没用的话不加也行;
    2. 返回 Promise 的函数前面加个 await 就能得到异步返回的结果,使用起来和同步一样(实际是异步);
    3. koa2 的特性就是用 async/await/Promise 代替以前的回调,使代码更简洁易懂;
    以前那套回调,用 Promise 包装一下就可以拿来给 koa2 使用;
    不用 async/await/Promise 还继续用回调函数的话,不如继续用 expressjs
    POPOEVER
        27
    POPOEVER  
       2018-03-28 01:37:38 +08:00
    用不用 async/await 跟用 express 或是 koa 有关系吗?我在 express 里面也会用 async 啊
    leemove
        28
    leemove  
       2018-03-28 09:14:00 +08:00
    https://leemove.github.io/2018/03/19/koa-compose/#more 这个就是 Koa 中间件的原理....看了这个应该就不会疑惑了..
    waibunleung
        29
    waibunleung  
    OP
       2018-03-28 10:43:46 +08:00
    @POPOEVER 我的意思是,koa2 的中间件几乎都是异步的写法,就不存在同步的写法或者场景吗?为什么要全部安排成异步的?
    waibunleung
        30
    waibunleung  
    OP
       2018-03-28 10:46:35 +08:00
    @leemove 我看过更详细的原理分析,就是看了才不明不为什么会安排成异步的,就是为什么 dispatch(i)要返回 promise 而不是普通函数或者对象,按照 middleware[]的顺序执行递归执行下去,应该可以达到洋葱模型吧?那这里的 promise 的意义是什么?
    connection
        31
    connection  
       2018-03-28 14:12:51 +08:00
    问题 2:promise 一个程度上可以使得异步更精准,也就是在失败成功都可以获得它的状态改变。promise 也有助于回调的扁平。使用 promise 的话,promise 链条状,可以使得外层中间件可以获得内层中间件的状态。在错误捕获上也是有优势.
    以前版本的 co+generator+thunkify 约等于 async/await
    问题 3:事实上如果你的下一个中间件或者当前代码是不需要异步操作的,比如直接是返回的一些硬编码,是不用 async await 的
    leemove
        32
    leemove  
       2018-03-28 14:22:18 +08:00
    @waibunleung 设计成同步的那一旦一个 next 执行失败,就会影响整个同步执行的过程,错误处理不够优雅.而借助 promise 的特性,可以便于管理错误.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5289 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 05:42 · PVG 13:42 · LAX 21:42 · JFK 00:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.