初学 nodejs 的框架 koa,遇到一个问题解决不了,关于 await next() - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lingo9
V2EX    Node.js

初学 nodejs 的框架 koa,遇到一个问题解决不了,关于 await next()

  •  
  •   lingo9 2023-05-13 22:01:43 +08:00 2975 次点击
    这是一个创建于 884 天前的主题,其中的信息可能已经有所发展或是发生改变。
    const Koa = require("koa"); const multer = require("@koa/multer"); const bodyParser = require("koa-bodyparser"); const Router = require("@koa/router"); const jwt = require("jsonwebtoken"); const crypto = require("crypto"); const app = new Koa(); const router = new Router({ prefix: "/api" }); const secretKey = crypto.randomBytes(32).toString("hex"); const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, "./uploads"); }, filename: function (req, file, cb) { cb(null, "123" + file.fieldname + "-" + Date.now() + file.originalname); // cb(null, file.originalname); }, }); const upload = multer({ storage }); const authMiddleware = (ctx, next) => { const token = ctx.headers.authorization; if (!token) { ctx.status = 401; return (ctx.body = { code: -1001, message: "未提供令牌" }); } next(); }; router.use(bodyParser()); router.post("/login", (ctx) => { const { name, password } = ctx.request.body; if (name === "lingo123" && password === "123456") { const token = jwt.sign({ name }, secretKey); ctx.body = { code: 0, data: { id: 1, name, token, }, }; } }); router.use(authMiddleware); router.get("/users/:id", (ctx) => { const id = ctx.request.params.id; ctx.body = id; }); router.post("/upload", upload.single("photo"), (ctx) => { console.log("upload"); ctx.body = "upload"; }); app.use(router.routes()); app.listen(3000, () => { console.log("服务器启动成功~"); }); 

    初学 nodejs,想请教大佬,我想在文件上传这个接口模拟一个 token 校验功能,但是出现了问题,通过 postman 发送上传请求, koa 这边没有问题, postman 接收到的是 404 Not Found,而在将 authMiddleware 修改为异步后就没有问题了?这是为什么?其他的比如 /users/:id 都是可以正常响应的.

    14 条回复    2023-05-14 16:55:56 +08:00
    a632079
        1
    a632079  
       2023-05-13 22:32:01 +08:00   4
    改成 async function 后加了 next() 前加了个 await 是吧。

    出现这个疑问是因为,LZ ,没有深刻认识到基于 async 方法的 Koa 洋葱模型 以及 Node.js/Javascript 异步的处理机制。

    以下为分析过程。

    简单版:Koa 是洋葱模型,next 是一个 Promise<T> 的函数,如果不等待的话,按照 JS 正常的执行逻辑就直接返回了,此时 Promise 虽然还在执行,但是由于 Response 已经被发回,就算修改了,也体现不到你的客户端响应里。


    至于为啥就 upload 接口会出现这个问题,可以参考 Node.JS 的数据竞争问题。
    分析:这里有个大前提:Node.js 的 JS Runtime 是单线程单进程,io 任务是基于 libuv 的多线程微任务。由于 Upload 有一个 Stream 处理的过程,这个是一个异步 io 等待任务,一般会安排到下一次 eventloop 进行状态检查,而 由于大前提,此时自然而然的就把函数返回了,然后将 Response 发回。当进行 n 次 eventloop 后,发现上传的 io 处理完了但就算再怎么修改状态也没用了。
    同样的,其他接口为什么正确也很好理解了,由于单线程单进程,此时没有 io 等待,这个微任务立马开始处理,response 成功被修改,然后在是中间件返回,整个堆栈依次 pop 然后返回消息。
    lingo9
        2
    lingo9  
    OP
       2023-05-13 23:10:07 +08:00
    @a632079 谢谢大佬的解答,我好像能模糊的理解一点点,因为 next 函数将中间件函数放入了微任务队列中,而 authMiddleware 是同步执行, 并且没有等待 next 函数的执行结果,直接返回客户端响应导致 404 Not Found.我没理解的是为什么 /users/:id 接口能正确响应?这也应该在微任务队列中吧?
    a632079
        3
    a632079  
       2023-05-13 23:18:30 +08:00   1
    @lingo9 后面解释了:因为 JS 运行时的单进程单线程机制,调用一个函数如果没有需要 io 等待的话,会立即执行完这个方法因此在此种情况下你那个方法的逻辑等价于 中间件任务 -->UploadHandler --> Handler 返回 --> 中间件返回。这也是为啥很多情况下我们会通过 nextTick 这个操作来手动将任务放到微任务队列最顶端。

    可以补充个例子来帮助你理解:为啥 forEach 和 for 的行为表现类似?都是执行完这个代码块后直接执行后面的步骤。

    如果还是不理解的话,可以直接简记为(虽然不是绝对的,但是完全这样是没错的):Koa 中任何方法都为 async 方法即可.

    P.S 如果直接在函数里面错误使用 Async 方法闭包的话(可以直接转换为同步函数),配置好的 ESLINT 应该会给出 error 或 warning 提醒,并给出修复意见来着。
    a632079
        4
    a632079  
       2023-05-13 23:23:38 +08:00
    @a632079 更正一下,使用 nextTick 一般是用于优化执行逻辑的。上段文字少了这句注解。
    a632079
        5
    a632079  
       2023-05-13 23:32:02 +08:00
    @a632079 更正一下,UploadHandler 应该替换为 UserHandler 来着。UploadHandler 会因为有 io 等待而自动推到 microtask 队列里(等待轮询 io 状态改为完成后,继续执行),然后释放执行句柄,直接恢复到中间件上下文继续执行因此中间件被返回了,此时直接开始依次返回,最终返回结果。
    lingo9
        6
    lingo9  
    OP
       2023-05-13 23:45:04 +08:00
    @a632079 感谢大佬的解答,我可能还需要在捋一捋 nodejs 的事件循环.
    zbinlin
        7
    zbinlin  
       2023-05-13 23:59:47 +08:00   1
    你要在 `/users/:id` 重现这个错误,可以将其改成 async 函数,然后在 `const id = ctx.request.params.id;` 前面加上一行:`await require('node:timers/promises').setImmediate();`
    Nazz
        8
    Nazz  
       2023-05-14 09:40:30 +08:00   1
    洋葱模型实现起来其实可以非常简单, 只是 koa 的代码喜欢炫技让人看不懂

    ```go
    type Context struct {
    // 中间件游标
    // middleware cursor
    index int

    // 缓存
    // session storage
    storage Any

    // 中间件
    // handler chains
    handlers []HandlerFunc

    // 请求
    Request *Request

    // 响应写入器
    Writer ResponseWriter
    }

    type HandlerFunc func(ctx *Context)

    func (c *Context) Next() {
    c.index++
    if c.index <= len(c.handlers) {
    c.handlers[c.index-1](c)
    }
    }
    ```
    ucun
        9
    ucun  
       2023-05-14 10:15:44 +08:00   1
    学 nestjs ,工程化做得比较好。个人项目也方便复用。
    DeWjjj
        10
    DeWjjj  
       2023-05-14 11:12:25 +08:00 via iPhone
    光看形容就能知道异步调用产生 token 对不上,解决问题方法是通过各种锁。
    lingo9
        11
    lingo9  
    OP
       2023-05-14 16:38:04 +08:00
    @zbinlin 感谢大佬的解答,我向 ChatGPT 询问了事件循环的阶段
    1. Timers 阶段:处理定时器相关的回调函数,例如 setTimeout() 和 setInterval() 的回调。
    2. Pending I/O 阶段:处理某些系统操作的回调函数,例如网络请求、文件 I/O 等待的回调。
    Idle, Prepare 阶段:内部使用,忽略。
    3. Poll 阶段:处理除了定时器和 I/O 之外的回调函数。在这个阶段,Node.js 会检查是否有新的 I/O 事件、计时器到期或者进入了一些回调函数的 setImmediate()。
    4. Check 阶段:处理通过 setImmediate() 注册的回调函数。
    5. Close Callbacks 阶段:处理通过 close 事件注册的回调函数,例如关闭的文件描述符或者套接字的回调。
    然后尝试了,在 `/users/:id` 重现错误
    router.get("/users/:id", (ctx) => {
    const id = ctx.request.params.id;

    Promise.resolve().then(() => {
    ctx.body = id;
    });

    setTimeout(() => {
    ctx.body = id;
    }, 1000);

    process.nextTick(() => {
    ctx.body = id;
    });
    // ctx.body = id;
    });
    我现在的理解是,我的代码中, authMiddleware 中间件,因为同步执行,没有等待 next() 函数的结果,也就是不能获取到
    ` /upload ` 中间件中的 `ctx.body = "upload"`, 在 koa 源码中,进入了 `catch` 返回 404 Not Found
    ```js
    handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404; // 修改为 401
    const Onerror= err => ctx.onerror(err);
    const handleRespOnse= () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }
    ```
    在修改了 `res.statusCode = 401` 后, 确实也能在客户端接收到 401 的错误, 根据 koa 的源码,响应结果会在所有中间件执行完毕后,在 then 中执行回调,所以 setImmediate, setTimeout 会在响应之后执行,无法实现对 ctx.body 赋值,完成响应.
    不知道我的理解是否正确, 现在疑惑的是, nextTick 为什么没在 then 中注册的回调函数之前执行呢?
    lingo9
        12
    lingo9  
    OP
       2023-05-14 16:48:20 +08:00
    @Nazz 我大概能理解一点 koa 的洋葱模型,相比于 express 都是通过递归调用,不过 koa 的中间件函数执行会返回一个 Promise, 然后可以通过 async await 等待异步函数的执行
    lingo9
        13
    lingo9  
    OP
       2023-05-14 16:50:09 +08:00
    @ucun 刚开始学习 nodejs, 所以先从 express 和 koa 开始了,后面是打算学 nestjs
    lingo9
        14
    lingo9  
    OP
       2023-05-14 16:55:56 +08:00
    @DeWjjj 谢谢大佬,我去了解一下 Node.js 中的锁和数据竞争
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3630 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 10:33 PVG 18:33 LAX 03:33 JFK 06:33
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86