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 都是可以正常响应的.
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 然后返回消息。 |
2
lingo9 OP @a632079 谢谢大佬的解答,我好像能模糊的理解一点点,因为 next 函数将中间件函数放入了微任务队列中,而 authMiddleware 是同步执行, 并且没有等待 next 函数的执行结果,直接返回客户端响应导致 404 Not Found.我没理解的是为什么 /users/:id 接口能正确响应?这也应该在微任务队列中吧?
|
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 提醒,并给出修复意见来着。 |
5
a632079 2023-05-13 23:32:02 +08:00
@a632079 更正一下,UploadHandler 应该替换为 UserHandler 来着。UploadHandler 会因为有 io 等待而自动推到 microtask 队列里(等待轮询 io 状态改为完成后,继续执行),然后释放执行句柄,直接恢复到中间件上下文继续执行——因此中间件被返回了,此时直接开始依次返回,最终返回结果。
|
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();`
|
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) } } ``` |
9
ucun 2023-05-14 10:15:44 +08:00 1
学 nestjs ,工程化做得比较好。个人项目也方便复用。
|
10
DeWjjj 2023-05-14 11:12:25 +08:00 via iPhone
光看形容就能知道异步调用产生 token 对不上,解决问题方法是通过各种锁。
|
11
lingo9 OP @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 中注册的回调函数之前执行呢? |