Koa Application 核心 API 详解:app.use 中间件栈、应用配置与错误处理机制

Koa Application 核心 API 详解:app.use 中间件栈、应用配置与错误处理机制 Koa Application 核心 API 详解app.use 中间件栈、应用配置与错误处理机制【免费下载链接】koaExpressive middleware for node.js using ES2017 async functions项目地址: https://gitcode.com/GitHub_Trending/ko/koa本篇技术指南以 Koa 官方 API 文档docs/api/index.md为主体系统讲解Application应用对象的全部核心能力——安装与环境要求、中间件级联Cascading执行模型、应用配置项、app.listen/app.callback/app.use/app.keys/app.context/app.currentContext以及错误处理机制。读完本文你可以从零搭建一个符合 Koa 3.x 规范的 Web 应用理解next()上下游控制流的真实执行过程并能结合 源码实现 掌握每个 API 的底层行为与适用前提。安装与环境要求Koa 要求Node.js v18.0.0 及以上版本以获得 ES2015 与 async function 支持。这一点与 package.json 中engines: { node: 18 }的声明完全一致当前仓库版本为3.2.1。你可以使用喜欢的版本管理器如 nvm快速安装受支持的 Node 版本$ nvm install 22 $ npm i koa $ node my-koa-app.jsApplicationKoa 应用的核心对象一个 Koa 应用是一个包含中间件函数数组的对象这些中间件会在收到请求时被组合compose并以栈的方式执行。Koa 与 Ruby 的 Rack、Connect 等中间件体系类似但做了一个关键设计决策在原本低级的中间件层之上提供高层语法糖包括内容协商content-negotiation、缓存新鲜度判断cache freshness、代理proxy支持、重定向redirection等常用任务的方法。尽管提供了相当多的辅助方法Koa 依然保持小巧的体积——它不捆绑任何中间件。最经典的 hello world 应用如下const Koa require(koa); const app new Koa(); app.use(async ctx { ctx.body Hello World; }); app.listen(3000);从源码结构看这个示例的每一步都有对应的实现new Koa()创建的是class Application extends Emitter见 构造函数它继承自 Node.js 的EventEmitter——这正是后文app.on(error, ...)可用的原因app.use(fn)只是把函数推入this.middleware数组并返回this见 use 实现非函数参数会抛出TypeError(middleware must be a function!)ctx.body Hello World赋值后由 respond 辅助函数 统一写入res支持字符串、Buffer、Stream、Blob、JSON 等 body 形态。中间件级联Cascadingnext() 的上下游执行流Koa 的中间件以传统方式级联这在 Node 早期基于回调的阶段很难写得友好而 async 函数让真正的中间件成为可能。与 Connect 那种把控制权沿函数序列传递直到某个函数返回的实现不同Koa 的中间件会调用next()向下游downstream走之后控制流再向上游upstream回流。官方示例应用最终响应 Hello World但请求先流经x-response-time和logging两个中间件标记请求开始时间然后把控制权让给响应中间件。当一个中间件调用next()时该函数挂起并把控制权交给下一个定义的中间件当下游没有更多中间件可执行时调用栈展开每个中间件被恢复执行以完成它的上游逻辑。const Koa require(koa); const app new Koa(); // logger app.use(async (ctx, next) { await next(); const rt ctx.response.get(X-Response-Time); console.log(${ctx.method} ${ctx.url} - ${rt}); }); // x-response-time app.use(async (ctx, next) { const start Date.now(); await next(); const ms Date.now() - start; ctx.set(X-Response-Time, ${ms}ms); }); // response app.use(async ctx { ctx.body Hello World; }); app.listen(3000);这段示例的执行顺序是logging(前) → x-response-time(前) → response → x-response-time(后) → logging(后)。测试用例 compose.test.js 用calls数组验证了这一点两个中间件按前置、后置交错记录最终断言calls深等于[1, 2, 3, 4]。底层组合逻辑在 app.callback() 中完成callback () { const fn this.compose(this.middleware) // ... const handleRequest (req, res) { const ctx this.createContext(req, res) // ... return this.ctxStorage.run(ctx, async () { return await this.handleRequest(ctx, fn) }) } return handleRequest }其中this.compose默认为koa-composepackage.json依赖中为^4.1.0负责把中间件数组组合成一个洋葱模型的可调用函数handleRequest 先把res.statusCode预置为 404无中间件设置 body 时的兜底状态再执行fnMiddleware(ctx).then(handleResponse).catch(onerror)即中间件 Promise 链成功后调用respond(ctx)写响应失败则交给ctx.onerror(err)。值得注意的是构造函数支持传入自定义的options.compose见 构造函数 的 JSDoc 参数列表测试用例 compose.test.js 中的第二个用例就演示了用自实现组合函数替代默认 compose 并得到相同的[1, 2, 3, 4]调用顺序。应用配置项Settings应用配置是app实例上的属性。官方文档列出当前支持的配置项并与 构造函数源码 中的默认值逐一对应配置项默认值说明源码依据app.envNODE_ENV缺省为development应用运行环境this.env options.env \|\| process.env.NODE_ENV \|\| developmentapp.keys无签名 Cookie 使用的密钥数组构造函数中if (options.keys) this.keys options.keysapp.proxyfalse为true时信任代理头字段this.proxy options.proxy \|\| falseapp.subdomainOffset2ctx.subdomains忽略的偏移量this.subdomainOffset options.subdomainOffset \|\| 2app.proxyIpHeaderX-Forwarded-For读取代理 IP 的请求头名this.proxyIpHeader options.proxyIpHeader \|\| X-Forwarded-Forapp.maxIpsCount0表示不限从代理 IP 头中最多读取的 IP 数量this.maxIpsCount options.maxIpsCount \|\| 0app.asyncLocalStorage未启用传true或一个AsyncLocalStorage实例以启用异步本地存储构造函数中创建this.ctxStorage配置既可以通过构造函数传入const Koa require(koa); const app new Koa({ proxy: true });也可以动态修改const Koa require(koa); const app new Koa(); app.proxy true;两种写法的等价性由测试用例 index.test.js 验证其中分别覆盖了env、proxy、keys、subdomainOffset等选项经构造函数注入后属性值正确的断言。proxy/proxyIpHeader/maxIpsCount的实际作用当app.proxy为true时ctx.ips才会解析代理头。见 request.js 中的 ips getterget ips () { const proxy this.app.proxy const val this.get(this.app.proxyIpHeader) let ips proxy val ? splitCommaSeparatedValues(val) : [] if (this.app.maxIpsCount 0) { ips ips.slice(-this.app.maxIpsCount) } return ips }即只有proxy为真且存在proxyIpHeader头时才解析逗号分隔的 IP 列表如client, proxy1, proxy2得到[client, proxy1, proxy2]其中proxy2是最下游maxIpsCount 0时只保留列表末尾的 N 个 IP。ctx.ip则取ips[0]否则回退到socket.remoteAddress见 ip getter。subdomainOffset的实际作用ctx.subdomains默认把主机名的最后两段视为主域名。如主机名为tobi.ferrets.example.com未设置时this.subdomains为[ferrets, tobi]设置app.subdomainOffset 3后变为[tobi]。实现见 subdomains getterhostname.split(.).reverse().slice(offset)。app.listen(...)Koa 应用与 HTTP 服务器不是一一对应的关系一个或多个 Koa 应用可以挂载在一起共享同一个 HTTP 服务器。app.listen(...)创建一个 HTTP 服务器并返回参数透传给 Node.js 的Server#listen()。最简示例是把应用绑定到端口 3000const Koa require(koa); const app new Koa(); app.listen(3000);app.listen(...)本质上是下面代码的语法糖与源码 listen 实现 完全一致const http require(http); const Koa require(koa); const app new Koa(); http.createServer(app.callback()).listen(3000);这意味着同一个应用可以同时以 HTTP 和 HTTPS 启动或监听多个地址const http require(http); const https require(https); const Koa require(koa); const app new Koa(); http.createServer(app.callback()).listen(3000); https.createServer(app.callback()).listen(3001);app.callback()app.callback()返回一个适合传给http.createServer()的回调函数来处理请求。你也可以利用这个回调把 Koa 应用挂载mount到 Connect/Express 应用中从而把 Koa 当作子应用嵌入既有 Node.js 服务。从源码结构看callback()每次调用都会基于当前this.middleware重新组合出处理函数并在首次调用时注册默认错误监听器if (!this.listenerCount(error)) this.on(error, this.onerror)见 callback 实现。app.use(function)向应用添加给定的中间件函数。app.use()返回this因此可以链式调用。以下两种写法等价app.use(someMiddleware) app.use(someOtherMiddleware) app.listen(3000)app.use(someMiddleware) .use(someOtherMiddleware) .listen(3000)链式能力正是 use 实现 末尾return this带来的。app.keys设置签名 Cookie 的密钥。这些密钥会传递给 KeyGrip 组件也可以直接传入你自己的KeyGrip实例。以下两种写法均可接受app.keys [OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg, MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7]; app.keys new KeyGrip([OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg, MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7], sha256);出于安全考虑请确保密钥足够长且随机。这些密钥支持轮换rotation并在以{ signed: true }选项签名 Cookie 时被使用ctx.cookies.set(name, tobi, { signed: true });app.keys如何被消费可以在 context.js 的 cookies getter 中看到ctx.cookies惰性创建new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure })即签名密钥在每次访问ctx.cookies时从应用实例读取。app.contextapp.context是创建每个ctx时所用的原型对象。你可以通过编辑app.context给ctx增加额外属性。这在给ctx添加跨整个应用共用的属性或方法的场景下有用可能更省性能无需中间件或更方便减少require()代价是更深地依赖ctx这可能被视为反模式。例如从ctx上引用你的数据库app.context.db db(); app.use(async ctx { console.log(ctx.db); });注意两点与 createContext 源码 相互印证ctx上的许多属性是通过 getter、setter 和Object.defineProperty()定义的你只能通过Object.defineProperty()在app.context上编辑它们且不推荐这么做被挂载mounted的应用当前复用父应用的ctx与配置因此挂载应用实际上只是一组中间件的集合。从源码看createContext 通过Object.create(this.context)创建新的 context 原型链并一次性装配request、response、app、req、res、originalUrl、state等字段这解释了为什么修改app.context会影响之后所有请求的ctx。app.currentContextKoa v3 新特性如果启用了asyncLocalStorageapp.currentContext会返回当前请求的上下文。例如const app new Koa({ asyncLocalStorage: true }) app.use(async (ctx, next) { callSomeFunction() }) function callSomeFunction () { const ctx app.currentContext /* 即上面中间件的 ctx */ }从v3.1.0开始你还可以传入自己的AsyncLocalStorage实例const asyncLocalStorage new AsyncLocalStorage() const app new Koa({ asyncLocalStorage }) app.use(async (ctx, next) { callSomeFunction() }) function callSomeFunction () { const ctx asyncLocalStorage.getStore() }它的典型用途是把请求 ID、用户身份等关键请求信息传递给内部服务而不必层层透传参数。源码实现印证了这一机制currentContext getter 只是this.ctxStorage.getStore()即AsyncLocalStorage中当前异步上下文存储的值构造函数通过getAsyncLocalStorage(options)区分true新建实例与自定义实例两种情况见 构造函数callback 中的 handleRequest 用this.ctxStorage.run(ctx, ...)把ctx绑定进整个请求处理过程的异步链路。测试用例 currentContext.test.js 覆盖了几个关键行为启用后中间件内包括跨setTimeout/setImmediate等异步边界后app.currentContext ctx请求处理之外请求前后该值为undefined未启用时恒为undefined错误处理器app.on(error, (err, ctx) ...)中同样能取到ctx以及支持自定义AsyncLocalStorage实例。此外源码对v8 启动快照v8.startupSnapshot.isBuildingSnapshot()场景做了特殊处理——构建快照时推迟到反序列化回调中再创建AsyncLocalStorage相关测试也在同一文件中。错误处理Error Handling默认情况下所有错误都会输出到stderr除非app.silent为true。默认错误处理器在err.status为404或err.expose为true时也不输出。要执行自定义错误处理逻辑如集中日志可以添加error事件监听器app.on(error, err { log.error(server error, err) });如果错误发生在 req/res 周期内、且已经无法响应客户端如响应头已发送Context实例也会被一并传入app.on(error, (err, ctx) { log.error(server error, err, ctx) });当错误发生且仍可以响应客户端即没有数据写入 socket时Koa 会以合适的 500 Internal Server Error 响应无论哪种情况都会为日志目的触发应用级的error事件。两个层面的源码实现对应这一行为应用级默认处理器Application.prototype.onerroronerror (err) { const isNativeError Object.prototype.toString.call(err) [object Error] || err instanceof Error if (!isNativeError) { throw new TypeError(util.format(non-error thrown: %j, err)) } if (err.status 404 || err.expose) return if (this.silent) return const msg err.stack || err.toString() console.error(\n${msg.replace(/^/gm, )}\n) }可见非 Error 抛出物会被包装为TypeError、404/expose/silent 时静默均在此实现行为细节由测试 onerror.test.js 逐项验证如app.onerror(foo)抛TypeError: non-error thrown: foo、status: 404时不写 stderr、app.silent true时不写 stderr 等。上下文级处理器Context.prototype.onerror 负责真正面向客户端的响应先this.app.emit(error, err, this)把(err, ctx)分发给应用级监听器若响应头已发送headerSent则无从补救直接返回否则清空已有响应头、按err.headers重设、强制text/plain类型把无效状态码归一化为 500并根据err.expose决定对外暴露err.message还是标准状态码文案。而 socket 层面的错误如连接中途被破坏则经由 handleRequest 中的onFinished(res, onerror)注册同样汇入app.emit(error, ...)。小结Application类是 Koa 一切能力的入口中间件栈app.usekoa-compose组合出洋葱模型执行流一组可在构造时或运行时修改的配置项env、keys、proxy、subdomainOffset、proxyIpHeader、maxIpsCount、asyncLocalStorage控制请求解析与 Cookie 签名行为app.listen/app.callback让同一应用可灵活挂载到多个 HTTP(S) 服务器app.context提供ctx原型级的扩展点app.currentContextv3基于AsyncLocalStorage实现无侵入的请求上下文透传app.on(error)与两级onerror处理器构成默认错误处理链。上述所有行为均可在当前仓库的 lib/application.js、lib/context.js、lib/request.js 与__tests__/application/下的测试用例中逐行核对适合作为二次开发如自定义 compose、挂载子应用、接入内部服务时的行为基线。【免费下载链接】koaExpressive middleware for node.js using ES2017 async functions项目地址: https://gitcode.com/GitHub_Trending/ko/koa创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考