在前后端分离的开发模式中,前端页面常常需要请求不同域名下的接口。由于浏览器同源策略的限制,普通的Ajax请求会被直接拦截,而JSONP作为一种经典的跨域方案,至今仍在一些老系统或特定场景中被使用。它并不依赖XMLHttpRequest对象,而是借助HTML中script标签天然允许跨域加载脚本的特性来完成数据获取。

JSONP的底层原理
浏览器的同源策略规定,协议、域名、端口三者任一不同就属于跨源,此时通过fetch或XMLHttpRequest发起的请求会被限制。然而,页面中通过script标签引入的外部JavaScript文件并不受此约束,这也是CDN资源能够跨域加载的基础。JSONP正是利用了这一点:前端动态创建一个script节点,将其src指向目标接口,并约定一个回调函数名作为参数传递给服务端。
服务端接收到请求后,不再返回纯JSON数据,而是将JSON数据作为实参,包裹在指定回调函数的调用语句中返回,例如返回 handleData({"name":"test"})。当这段脚本被加载执行时,全局作用域下的handleData函数就会被触发,从而让前端拿到跨域数据。这种机制本质上是一种脚本注入执行,而非传统意义上的数据传输协议。
前端实现示例
下面是一段原生JavaScript实现的JSONP请求封装。它通过动态创建script标签、设置超时清理以及错误处理,保证基本可用性。注意回调函数名需要保持全局唯一,避免多次请求互相覆盖。
function jsonp(url, callbackName, timeout) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
var timer = null;
window[callbackName] = function(data) {
cleanup();
resolve(data);
};
function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callbackName];
if (timer) {
clearTimeout(timer);
}
}
script.onerror = function() {
cleanup();
reject(new Error('JSONP请求失败'));
};
script.src = url + (url.indexOf('?') > -1 ? '&' : '?') + 'callback=' + callbackName;
if (timeout) {
timer = setTimeout(function() {
cleanup();
reject(new Error('JSONP请求超时'));
}, timeout);
}
document.body.appendChild(script);
});
}
// 使用示例
jsonp('https://api.ipipp.com/user', 'cb_123', 5000)
.then(function(res) {
console.log('跨域数据:', res);
})
.catch(function(err) {
console.error(err);
});
上述代码中,我们将回调函数名cb_123挂到window对象上,服务端返回的内容应当类似 cb_123({"id":1,"name":"demo"})。一旦脚本执行,Promise就会进入resolved状态。通过超时机制和onerror监听,可以有效防止script加载异常时导致页面回调永远不触发的问题。
如果项目中使用了jQuery,也可以直接调用 $.ajax 并设置 dataType 为 jsonp,框架会自动完成回调函数名的生成与清理。但在现代开发中,更推荐理解原生实现,以便在遇到接口异常时能快速定位是参数拼接错误还是服务端返回格式不正确。
服务端返回格式
以Node.js的Express框架为例,服务端需要根据query中的callback参数动态拼接响应内容。如果缺失该参数,则应当降级为普通JSON返回或返回错误,避免将函数调用直接暴露给非JSONP客户端。
const express = require('express');
const app = express();
app.get('/user', function(req, res) {
var data = { id: 1, name: 'demo' };
var cb = req.query.callback;
if (cb) {
// 返回JavaScript语句,执行全局回调函数
res.type('text/javascript');
res.send(cb + '(' + JSON.stringify(data) + ')');
} else {
res.json(data);
}
});
app.listen(3000);
这里使用 res.type('text/javascript') 明确响应类型,使浏览器正确将其作为脚本解析。若服务端未做callback判断而强制返回函数调用,当被普通Ajax请求误用时就会产生语法解析错误。因此接口设计上应当保持兼容,让同一URL既能支持JSONP也能支持标准JSON。
JSONP的局限与安全风险
JSONP只能使用GET方法,所有参数都暴露在URL中,不适合传输敏感信息或大量数据。同时,由于它依赖执行外部脚本,如果第三方接口被劫持或返回恶意代码,前端页面将面临XSS注入风险。因此在对接不可信域名时,必须谨慎评估,或采用服务端代理转发来隔离风险。
另外,JSONP无法像CORS那样通过HTTP状态码感知错误,只能依赖超时或script的onerror事件。在弱网络环境下,错误排查相对困难。现代浏览器已普遍支持CORS与fetch,新项目建议优先使用标准跨域方案,JSONP更多用于兼容IE8、IE9等不支持CORS的老旧环境。
与CORS方案的对比
下面的表格列出了两种常见跨域方式的主要差异,帮助开发者根据项目需求做选择。
| 对比维度 | JSONP | CORS |
|---|---|---|
| 请求方法 | 仅GET | 支持全部HTTP方法 |
| 浏览器兼容 | 几乎所有浏览器 | IE10及以上 |
| 错误处理 | 依赖超时或onerror | 标准HTTP状态 |
| 安全性 | 有XSS风险 | 可配合凭证与白名单 |
从表格可以看出,JSONP在兼容性和实现简易度上有优势,但在功能完整性和安全性上明显弱于CORS。如果目标用户群仍包含老旧浏览器,且接口仅做公开数据读取,JSONP仍是可行的过渡方案。反之,则应推动后端配置Access-Control-Allow-Origin等响应头,使用更规范的跨域通信方式。