拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Egg 框架 async function 开发指南:从 Generator 到 async/await 的完整迁移实践

后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa项目地址https://gitcode.com/gh_mirrors/egg11/egg点击查看免费下载本教程以 Eggeggjs/egg框架为背景系统讲解如何用语言原生的 async function 替代 generator function 编写 Controller、Service、定时任务与中间件涵盖 Node.js 版本前提、语法迁移规则、中间件参数签名变化、通过 co.wrap 桥接旧式 generator API以及两者在并发处理上的细微差异。读完本文你将掌握在 Egg 应用各层中安全、规范地使用 async/await 的全部要领并理解框架与测试用例test/async/index.test.js、test/fixtures/apps/async-app/对 async function 的官方支持方式。一、背景与运行前提在 Egg 与 Koa 的关系 章节中已经介绍过async function 是 JS 语言层面提供的异步解决方案。在 async function 出现之前社区的主流方案是在 Promise 基础上借助 Generator 的上下文切换能力配合 co 等第三方库以同步写法编排异步代码。而 async function 正是这一模式的语言级语法糖——用 async function 编写的代码看起来就像用 co generator 编写的代码但不再需要依赖任何第三方运行时。Node.js 从 7.6.0 开始将 V8 升级到 5.5此后 async function不再需要开启任何 flag 即可直接使用。Egg 框架默认支持 async function在所有支持 generator function 的地方都可以用 async function 替代。注意在基于 async function 编写应用代码时请确保你的代码运行在 Node.js 7.6 的版本上。这一版本门槛在仓库测试代码中也有体现。test/async/index.test.js 在加载 async 专项测试前会先解析当前 Node 版本号仅当nodeVersion 7.6时才require(./_async)注释明确写着 only node 7.6 support async function without flags。这与文档中的前提完全一致。另外需要说明框架自身的演进策略从 docs/source/en/intro/egg-and-koa.md 的说明可以看到Egg 当时基于 Koa 1.x其 LTS 版本尚未支持 async function因此框架核心与官方插件使用 generator function 编写以兼容 Node.js LTS而应用层Controller、Service 等完全支持 async function 写法并与 Koa 2.x 中间件格式完全兼容。应用开发者可以在 Node.js 7.6async function与 Node.js 6.0generator function之间选择这也是package.json中engines.node声明为 6.0.0的原因。二、Controller 与 Service 中使用 async function在 Controller 章节中框架提供了两种 Controller 写法基于类的写法与普通方法写法。其中所有用 generator function 实现的地方都可以改用 async function 实现代码逻辑没有任何变化仅需将yield语法改成await语法。同样Service 与 Controller 一样所有异步方法都可以用 async function 替换 generator function。将 Controller 文档中的示例改造成 async function 模式// app/controller/post.js module.exports app { class PostController extends app.Controller { // 从 * create() 换成 async create() async create() { const { ctx, service } this; const createRule { title: { type: string }, content: { type: string }, }; // 校验参数 ctx.validate(createRule); // 组装参数 const author ctx.session.userId; const req Object.assign(ctx.request.body, { author }); // 调用 service 进行业务处理 // 从 yield 换成 await const res await service.post.create(req); // 设置响应内容和响应状态码 ctx.body { id: res.id }; ctx.status 201; } } return PostController; }注意上面的 Controller 中用await调用了service.post.create()方法如果该方法本身是 generator function 类型则也需要将其改造成 async function 接口之后才能被await调用。也就是说await只能等待 Promise或 async function 的返回值无法直接等待 generator。仓库中的 test/fixtures/apps/async-app/app/controller/api.js 与 test/fixtures/apps/async-app/app/service/api.js 就是这一写法的真实示例// app/controller/api.js module.exports app { return class ApiController extends app.Controller { async index() { const result await this.service.api.getName(); this.ctx.body.push(result); this.ctx.body.push(controller); } }; };// app/service/api.js module.exports app { return class ApiService extends app.Service { async getName() { await sleep(100); return service; } }; }; function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); }可以看到 Service 中的getName()被声明为asyncController 才能用await this.service.api.getName()安全等待其完成——这恰好印证了上面注意事项里service 也要同步改造的要求。三、定时任务中使用 async function框架提供的定时任务同样支持 async function只需将task按照上面的规则从 generator function 替换成 async function 即可。通过schedule属性配置执行间隔、执行类型等task是定时任务真正执行时被运行的函数其第一个参数是一个匿名的 Context 实例// app/schedule/xxx.js module.exports { // 通过 schedule 属性来设置定时任务的执行间隔等配置 schedule: { interval: 1m, // 1 分钟间隔 type: all, // 指定所有的 worker 都需要执行 }, // task 是真正定时任务执行时被运行的函数第一个参数是一个匿名的 Context 实例 async task(ctx) { const res await ctx.curl(http://www.api.com/cache, { dataType: json, }); ctx.app.cache res.data; }, };其中interval可设置为1m1 分钟、1h1 小时等人类可读的时间字符串也可直接指定毫秒数type支持all所有 worker 都执行与worker随机一个 worker 执行等取值。仓库中的 test/fixtures/apps/async-app/app/schedule/async.js 给出了 async 风格的定时任务并在task中通过await Promise.resolve()模拟异步操作后将状态写入ctx.app供测试断言exports.schedule { type: worker, interval: 1000000, }; exports.task async (ctx) { await Promise.resolve(); ctx.app.scheduleExecuted true; };配套的 test/fixtures/apps/async-app/app.js 演示了在app.beforeStart中通过await app.runSchedule(async)手动触发该定时任务并在app.beforeClose中执行异步清理module.exports app { app.beforeStart(async () { await Promise.resolve(); await app.runSchedule(async); app.beforeStartExectuted true; }); app.beforeClose(async () { await Promise.resolve(); app.beforeCloseExecuted true; }); };四、中间件中使用 async function框架中所有的中间件——包括标准定义方式以及在路由中定义的中间件——都可以通过 async function 来编写。但与 generator function 格式的中间件相比参数列表发生了变化与 Koa v2.x 保持一致第一个参数为ctx代表当前请求的上下文是 Context 的实例第二个参数为next用await执行它来调用后续中间件的逻辑。下面以 gzip 压缩中间件为例展示 async function 写法// app/middleware/gzip.js const isJSON require(koa-is-json); const zlib require(zlib); module.exports (options, app) { return async function gzip(ctx, next) { // 注意和 generator function 格式的中间件不同此时 next 是一个方法必须要调用它 await next(); // 后续中间件执行完成后将响应体转换成 gzip let body ctx.body; if (!body) return; // 支持 options.threshold if (options.threshold ctx.length options.threshold) return; if (isJSON(body)) body JSON.stringify(body); // 设置 gzip body修正响应头 ctx.body zlib.createGzip().end(body); ctx.set(Content-Encoding, gzip); }; };对比 中间件文档 中基于 generator function 的 gzip 写法可以清晰看出两个关键差异签名不同generator 版为function* gzip(next)通过yield next继续执行async 版为async function gzip(ctx, next)通过await next()继续执行且上下文从this显式改为参数ctx。工厂函数形态相同两者都遵循module.exports (options, app) { return middleware; }的约定框架会把app.config.gzip中的配置作为options传入把 Application 实例作为app传入。next在 async 中间件中是一个必须调用的方法await next()而不是 generator 中间件中可直接yield的对象这是迁移时最容易踩的坑。仓库的 test/fixtures/apps/async-app/app/middleware/async.js 与 test/fixtures/apps/async-app/app/middleware/router.js 分别演示了全局中间件与路由中间件的 async 写法二者都以先初始化ctx.bodyawait next()后再追加标记的方式验证了洋葱模型在 async 中间件下依然成立// app/middleware/async.js —— 全局中间件 module.exports () { return async (ctx, next) { ctx.body []; await next(); ctx.body.push(middleware); }; };// app/middleware/router.js —— 路由级中间件 module.exports () { return async (ctx, next) { ctx.body []; await next(); ctx.body.push(router); }; };路由配置test/fixtures/apps/async-app/app/router.js将路由中间件挂在/api上并指向 async 风格的api.index动作module.exports app { app.get(/api, app.middlewares.router(), api.index); };最终一次GET /api请求会依次经过 全局中间件 → 路由中间件 → Controller → Service测试断言响应体为[service, controller, router, middleware]见 test/async/_async.js从输出顺序即可反推洋葱模型的执行轨迹await next()之前的代码先执行之后的代码后执行最内层是 Service。五、调用 generator function 类型的旧式 API由于一些已有的插件或框架早期的官方插件直接提供的是 generator function 的 API无法在 async function 中直接通过await调用。此时可以通过 co 提供的wrap方法将 generator function 包装成一个返回 Promise 的接口即可在 async function 中使用const co require(co); app.mysql.query co.wrap(app.mysql.query); // 如果想要直接使用 query 方法需要将 this 绑定为 app.mysql const query co.wrap(app.mysql.query).bind(app.mysql); // 包装之后即可在 async function 中使用 async function getUser() { // return await app.mysql.query(); return await query(); }这里有两个要点co.wrap(fn)返回的是一个新函数调用它时内部会运行 generator并把最终结果 resolve 为 Promise从而可以被await等待generator 内部若通过this访问上下文例如this.query()包装后调用时需要用.bind(app.mysql)显式绑定this否则会丢失原对象上下文导致运行时报错。框架自身的依赖中就包含co见 package.json 中co: ^4.6.0而框架核心在内部协调 generator 风格代码与 async 风格代码时也正是依赖这类 Promise 桥接机制这正是官方插件用 generator 编写、应用层用 async function 编写这一双轨策略能够并存的技术基础。六、与 generator function 的细微差别虽然两者的编程模型基本一致但 co 做了一些特殊处理例如支持yield一个数组对象这些在 async function 中无法原生做到。不过基于 Promise 提供的方法以及一些工具库可以轻松实现同样的功能generator functionfunction* () { const tasks [ task(1), task(2), task(3) ]; let results; // 并行 results yield tasks; // 控制最大并发数为 2 results yield require(co-parallel)(tasks, 2); }async functionasync () { const tasks [ task(1), task(2), task(3) ]; let results; // 并行 results await Promise.all(tasks); // 控制最大并发数为 2 results await require(p-all)(tasks, { concurrency: 2 }); }两者的差异集中体现在两点能力generator functioncoasync function并行等待一组任务yield [task1, task2, task3]await Promise.all([...])限制最大并发数yield require(co-parallel)(tasks, 2)await require(p-all)(tasks, { concurrency: 2 })等待 thunk原生支持不支持需先包装为 Promise由此可见async function 的原生能力更纯粹只认 Promise但借助Promise.all、p-all、p-map等 Promise 工具库可以覆盖 co 提供的绝大多数便利功能且语义更直白、可读性更好。社区中也存在大量基于 Promise 的 helper 方法库例如 sindresorhus 维护的 promise-fun 系列灵活运用它们配合 async function能让代码更具可读性。另外值得注意的是上述对比代码中 generator 版存在一处笔误result yield ...应为results迁移时顺手统一命名即可而p-all的并发限制通过{ concurrency: 2 }选项显式声明比 co-parallel 的函数参数形式意图更清晰。七、框架级验证async-app 测试用例为了确保middleware、controller、service 支持 async function不是一句空话仓库提供了完整的专项测试。入口 test/async/index.test.js 按 Node 版本条件加载测试体const nodeVersion Number(process.version.match(/^v(\d\.\d)\./)[1]); // only node 7.6 support async function without flags if (nodeVersion 7.6) { require(./_async); }测试体 test/async/_async.js 使用 egg-mock 加载apps/async-app测试应用并在启动后断言三件事app.beforeStartExectuted为真——证明app.beforeStart中的 async 回调含await app.runSchedule(async)被正常执行app.scheduleExecuted为真——证明 async 风格的定时任务task被真正跑通请求GET /api返回[service, controller, router, middleware]——证明 async 风格的 Service、Controller、全局中间件、路由中间件在一条调用链上全部正常工作。关闭应用时还断言app.beforeCloseExecuted验证app.beforeClose中的 async 清理逻辑同样生效。这套用例从启动生命周期、定时任务、请求链路三个维度完整覆盖了文档中提到的所有 async 应用场景是迁移时可直接对照参考的可运行范例。八、总结迁移 checklist将现有 generator function 代码迁移到 async function遵循以下清单即可确认运行环境为 Node.js 7.6否则测试会自动跳过 async 相关用例Controller / Service方法签名function* name()→async name()内部yield x→await x业务逻辑零改动定时任务task改为async task(ctx)内部改用await中间件签名从function* name(next)yield next改为async name(ctx, next)await next()上下文从this改为显式ctx旧式插件对 generator 风格的 API 先用co.wrap(...)必要时.bind(this)包装为 Promise 再await并发场景yield [tasks]改为await Promise.all(tasks)限流用p-all等 Promise 工具库实现保持一致性await只能等待 Promise/async function被调用的 generator 方法必须同步改造否则会得到非预期结果。完整的多层 async 示例可直接参考仓库中的 test/fixtures/apps/async-app/ 目录以及 test/async/_async.js 中的断言逻辑两者结合即是本文全部结论的可执行验证。赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa项目地址https://gitcode.com/gh_mirrors/egg11/egg点击查看免费下载相关推荐Egg 框架 async function 开发指南从 generator 到 async/await 的完整迁移实践Egg 框架 async function 开发指南从 generator 到 async/await 的完整迁移实践 本指南以 Egg当前仓库 eggjs后端Web框架Egg 2.x 升级指南从 co/generator 全面迁移到 async/awaitEgg 2.x 升级指南从 co/generator 全面迁移到 async/await 本指南以 Egg 官方升级文档 site/docs/intro/mi后端Web框架EggJS 2.0 升级指南从 Generator 到 Async/Await 的平滑迁移EggJS 2.0 升级指南从 Generator 到 Async/Await 的平滑迁移 前言 随着 Node.js 8 LTS 版本的发布EggJS 框后端Web框架创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门