# koa-jsonp中间件
30 秒速记
koa-jsonp:业务照常写ctx.body = 对象,中间件在返回阶段检测到callback参数就自动包成cb(...)- 没带
callback的请求原样返回JSON,同一接口能同时服务新老调用方 - 洋葱模型:
jsonp()要注册在设置ctx.body的中间件之前,它在await next()之后改写响应 - 只给明确需要的路由挂,别全局包装;回调名校验、
nosniff要自己确认有没有做 - 这个包很久没更新,用之前看它和当前
Koa版本是否兼容;新项目直接CORS
koa-jsonp 把手写 JSONP 那几行封装成了中间件,业务代码只管 ctx.body = data,要不要包成回调由它根据请求自动判断。 它利用的是 Koa 的洋葱模型:先 await next() 让下游把 ctx.body 设好,回来的时候看请求里有没有 callback,有就把响应改写成 cb(JSON) 并改掉响应类型,没有就原样放行。所以它必须注册在业务中间件前面。我用这种包会注意两点:只挂在需要兼容老页面的路由上;看一下它对回调名有没有做过滤,没有就自己在前面补一层校验。
理解「koa-jsonp中间件」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
koa-jsonp 解决的问题是:已经有一批返回 JSON 的接口,现在要让老页面也能用 JSONP 调,但不想每个接口都手写一遍拼接逻辑。
它的工作方式可以用一段等价代码理解:
function jsonp({ callbackName = 'callback' } = {}) {
const CB_RE = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*$/
return async (ctx, next) => {
await next() // 先让业务把 ctx.body 设好
const cb = ctx.query[callbackName]
if (!cb) return // 没有回调参数,原样返回 JSON
if (!CB_RE.test(cb)) {
ctx.status = 400
ctx.body = 'invalid callback'
return
}
ctx.type = 'text/javascript'
ctx.set('X-Content-Type-Options', 'nosniff')
ctx.body = `/**/ ;${cb}(${JSON.stringify(ctx.body)})`
}
}
关键在 await next() 这一行。Koa 的中间件像洋葱一样一层层进去再一层层出来,这个中间件在「出来」的时候才去看 ctx.body,所以它必须注册在设置响应的业务中间件之前:
app.use(jsonp()) // 外层:返回时包装
app.use(router.routes()) // 内层:设置 ctx.body
请求带了 ?callback=fn 就返回 fn({...}),没带就是普通 JSON。这种「按参数切换」的设计让同一个接口能同时服务新老调用方,代价是缓存要区分 callback 参数。实际项目里建议只把它挂在需要兼容的那几条路由上,而不是全局启用,避免所有接口都多出一个可执行脚本的出口。
面试官追问
追问 1有人说装了 koa-jsonp,POST 接口也能跨域调了,对吗?
不对。JSONP 靠 <script> 发请求,只能 GET,也带不了自定义头。中间件只负责包装响应,管不了浏览器怎么发请求,POST 跨域要配 CORS。
追问 2app.use(jsonp()) 写在路由后面,为什么不生效?
洋葱模型里,注册在后面的中间件只有前面调了 next() 才会进去,而且它包装响应依赖 await next() 之后的阶段。放在路由后面时,路由已经设好 body 返回了,它根本没机会改写。
追问 3同一个接口既给移动端返回 JSON,又给老页面返回 JSONP,缓存会不会出问题?
会。CDN 要是只按路径缓存,先被老页面请求,移动端就可能拿到一段 cb(...) 脚本。缓存键要带上 callback 参数,或者干脆拆成两个路径。
追问 4线上报 callback is not defined,服务端看是 200,从哪查起?
先抓响应体,看回调名是不是请求里传的那个;再看页面在插 script 前有没有定义这个函数。中间件默认的参数名是 callback,前端要是传的是 cb=xxx,就不会被识别成 JSONP。
追问 5这个包要不要用在新项目里?
不用。新项目没有老客户端包袱,直接 @koa/cors 配白名单。koa-jsonp 这种老包维护不活跃,引进来以后升级 Koa 大版本时还得多查一次兼容性。
# koa-jsonp中间件
koa.js 官方wiki中也介绍了不少jsonp的中间件

其中koa-jsonp是支持koa2的,使用方式也非常简单,koa-jsonp的官方demo也很容易理解
# 快速使用
30 秒速记
- 三步:
npm install koa-jsonp→app.use(jsonp())→ 业务里照常ctx.body = data - 请求带
?callback=fn返回fn({...}),不带就是普通JSON - 先跑通仓库里的
jsonp-use-middleware示例,再接到自己的服务里 - 接入时补上:只挂到需要的路由、回调名校验、
callback缺失时的约定(返回JSON还是400)
快速上手就是安装、注册中间件、正常写业务这三步,demo 仓库里有完整可跑的例子。 我建议先把示例原样跑起来,用 curl 分别请求带 callback 和不带 callback 的地址,确认两种响应都符合预期,再往自己的项目里搬。搬的时候别照抄全局注册,示例为了简单把它挂在了整个应用上,真实服务里只给需要兼容老页面的那几条路由用就够了。
curl "http://localhost:3000/?callback=fn" # ;fn({"success":true,...})
curl "http://localhost:3000/" # {"success":true,...}
面试官追问
追问 1能不能把 demo 直接复制到业务项目里上线?
不建议。demo 只证明了最小链路能跑,路由范围、回调名校验、错误处理和日志都没管。拿它当参考,接入时把这些补上。
追问 2示例跑起来了,但请求后没看到回调包装,先查什么?
先确认请求参数名是不是 callback,中间件默认只认这个名字;再看 jsonp() 是否注册在设置 ctx.body 的中间件之前。顺序反了它就改写不了响应。
追问 3公司内网访问不了 GitHub,新人要复现这个例子怎么办?
把示例代码连同 package.json 和锁文件放进内部仓库,固定版本号。示例总共就几十行,没必要依赖外网链接长期可用。
追问 4不带 callback 的请求,应该返回 JSON 还是 400?
看接口定位。同时服务新老调用方的就返回普通 JSON;专门给老页面开的 .jsonp 路径,我倾向返回 400,让调用方早点发现参数没传。约定好写进接口文档,别让行为靠猜。
# 安装
30 秒速记
npm install --save koa-jsonp;npm 5以后--save是默认行为,可以省略- 装完检查两处:
package.json的dependencies有它、锁文件也更新了,都要提交 - 在实际
require('koa-jsonp')的那个项目目录里装,monorepo别装错位置 - 本项目用
yarn的话就yarn add koa-jsonp,别混用包管理器,两份锁文件会打架 - 别用全局安装,服务运行时依赖必须写进项目依赖
安装就一条命令 npm install --save koa-jsonp,npm 5 之后不加 --save 也会写进 dependencies。 真正要注意的是装在哪、记没记下来。要在实际引用它的那个项目目录里执行,装完确认 package.json 和锁文件都变了,一起提交,这样别人 npm ci 或 CI 构建时才能装上同一个版本。项目要是用 yarn 或 pnpm,就用对应的命令,混着用会出现两份锁文件,依赖版本对不上。
npm install --save koa-jsonp
理解「安装」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
