Nodejs 初学框架 koa,遇到一个问题解决不了,关于 await next()
const Koa = require("koa");
const multer = require("[@koa](/user/koa)/multer");
const bodyParser = require("koa-bodyparser");
const Router = require("[@koa](/user/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 都是可以正常响应的.
Nodejs 初学框架 koa,遇到一个问题解决不了,关于 await next()
改成 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 然后返回消息。
谢谢大佬的解答,我好像能模糊的理解一点点,因为 next 函数将中间件函数放入了微任务队列中,而 authMiddleware 是同步执行, 并且没有等待 next 函数的执行结果,直接返回客户端响应导致 404 Not Found.我没理解的是为什么 /users/:id 接口能正确响应?这也应该在微任务队列中吧?
后面解释了:因为 JS 运行时的单进程单线程机制,调用一个函数如果没有需要 io 等待的话,会立即执行完这个方法——因此在此种情况下你那个方法的逻辑等价于 中间件任务 -->UploadHandler --> Handler 返回 --> 中间件返回。这也是为啥很多情况下我们会通过 nextTick 这个操作来手动将任务放到微任务队列最顶端。
可以补充个例子来帮助你理解:为啥 forEach 和 for 的行为表现类似?都是执行完这个代码块后直接执行后面的步骤。
如果还是不理解的话,可以直接简记为(虽然不是绝对的,但是完全这样是没错的):Koa 中任何方法都为 async 方法即可.
P.S 如果直接在函数里面错误使用 Async 方法闭包的话(可以直接转换为同步函数),配置好的 ESLINT 应该会给出 error 或 warning 提醒,并给出修复意见来着。
更正一下,使用 nextTick 一般是用于优化执行逻辑的。上段文字少了这句注解。
更正一下,UploadHandler 应该替换为 UserHandler 来着。UploadHandler 会因为有 io 等待而自动推到 microtask 队列里(等待轮询 io 状态改为完成后,继续执行),然后释放执行句柄,直接恢复到中间件上下文继续执行——因此中间件被返回了,此时直接开始依次返回,最终返回结果。
感谢大佬的解答,我可能还需要在捋一捋 nodejs 的事件循环.
你要在 /users/:id
重现这个错误,可以将其改成 async 函数,然后在 const id = <a target="_blank" href="http://ctx.request.params.id" rel="nofollow noopener">ctx.request.params.id</a>;
前面加上一行:await require('node:timers/promises').setImmediate();
洋葱模型实现起来其实可以非常简单, 只是 koa 的代码喜欢炫技让人看不懂go<br>type Context struct {<br> // 中间件游标<br> // middleware cursor<br> index int<br><br> // 缓存<br> // session storage<br> storage Any<br><br> // 中间件<br> // handler chains<br> handlers []HandlerFunc<br><br> // 请求<br> Request *Request<br><br> // 响应写入器<br> Writer ResponseWriter<br>}<br><br>type HandlerFunc func(ctx *Context)<br><br>func (c *Context) Next() {<br> c.index++<br> if c.index <= len(c.handlers) {<br> c.handlers[c.index-1](c)<br> }<br>}<br>
学 nestjs ,工程化做得比较好。个人项目也方便复用。
光看形容就能知道异步调用产生 token 对不上,解决问题方法是通过各种锁。
感谢大佬的解答,我向 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 Foundjs<br> handleRequest(ctx, fnMiddleware) {<br> const res = ctx.res;<br> res.statusCode = 404; // 修改为 401<br> const onerror = err => ctx.onerror(err);<br> const handleResponse = () => respond(ctx);<br> onFinished(res, onerror);<br> return fnMiddleware(ctx).then(handleResponse).catch(onerror);<br> }<br>
在修改了 res.statusCode = 401
后, 确实也能在客户端接收到 401 的错误, 根据 koa 的源码,响应结果会在所有中间件执行完毕后,在 then 中执行回调,所以 setImmediate, setTimeout 会在响应之后执行,无法实现对 ctx.body 赋值,完成响应.
不知道我的理解是否正确, 现在疑惑的是, nextTick 为什么没在 then 中注册的回调函数之前执行呢?
我大概能理解一点 koa 的洋葱模型,相比于 express 都是通过递归调用,不过 koa 的中间件函数执行会返回一个 Promise, 然后可以通过 async await 等待异步函数的执行
刚开始学习 nodejs, 所以先从 express 和 koa 开始了,后面是打算学 nestjs
谢谢大佬,我去了解一下 Node.js 中的锁和数据竞争
在 Koa 框架中,await next()
是一个非常重要的概念,它用于控制中间件(middleware)的执行顺序。如果你遇到了关于 await next()
的问题,可能是以下几种情况之一:
-
中间件顺序问题:确保你在需要调用
await next()
的地方正确调用了它,并且它的位置是在你希望控制后续中间件执行之前。 -
异步操作未完成:如果你在
await next()
之前进行了异步操作,并且这些操作没有正确完成,可能会导致问题。 -
错误处理:确保你正确处理了错误,Koa 的中间件可以通过
ctx.throw()
或try...catch
来抛出和捕获错误。
以下是一个简单的 Koa 中间件示例,展示了如何正确使用 await next()
:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('Before next');
await next(); // 等待下一个中间件执行
console.log('After next');
});
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
在这个例子中,第一个中间件在 await next()
之前和之后分别打印了日志。第二个中间件设置了响应体。
如果你能提供具体的错误信息或代码片段,我可以提供更具体的帮助。希望这些信息对你有所帮助!