
1. 跨域请求的痛点与JSONP的诞生逻辑1.1 为什么会有跨域这回事浏览器里有一条铁律叫同源策略。简单说A页面的脚本想直接读取B域名的数据浏览器会拦下来。这个策略不是故意找麻烦而是为了保护用户——如果没有它你打开一个恶意页面它就能悄悄用你的登录态去别的网站拉数据、发请求后果不堪设想。但实际开发中跨域取数据是刚需。比如前端页面部署在a.example.com后端接口在b.example.com两边域名不同直接发 Ajax 请求就会被浏览器拦截。这时候就需要一套合法的“绕行方案”JSONP 就是其中最早、最经典的一种。1.2 JSONP的核心思路借道script标签JSONP 全称 JSON with Padding。它的巧妙之处在于浏览器虽然禁止跨域 Ajax 读取响应但对script src...这种标签却网开一面。你想想CDN 上的 jQuery、各种统计脚本哪个不是跨域加载的如果 script 标签也被同源策略卡死整个互联网的脚本生态就崩了。所以 JSONP 的思路就是我不发 Ajax我动态创建一个 script 标签把请求地址塞进它的 src 属性里。服务器收到请求后不返回纯 JSON而是返回一段“函数调用”形式的 JavaScript 代码。这段代码被浏览器当作脚本执行数据就通过函数参数传进来了。打个比方Ajax 像是你直接去邻居家拿东西门卫同源策略不让进JSONP 则是你请邻居把东西包成一个“快递包裹”函数调用通过快递通道script 标签送到你手上门卫对快递通道不管。1.3 JSONP能解决什么、不能解决什么它能解决的是GET 类型的跨域数据读取。典型场景包括前端调用第三方天气接口、获取某些公开的配置数据、老系统之间的数据互通。它不能解决的是POST、PUT、DELETE 等非 GET 请求也不能读取响应头、不能设置自定义请求头、不能获取 HTTP 状态码。因为本质上它加载的是一段脚本不是一次标准的 HTTP 通信。这一点很多人一开始会误解以为 JSONP 是“跨域 Ajax”其实它跟 Ajax 是两套完全不同的机制。注意JSONP 只支持 GET 请求这是由 script 标签的加载机制决定的没有任何配置能改变这一点。2. JSONP的完整工作原理拆解2.1 一次JSONP请求的完整生命周期我把整个过程拆成六个步骤方便你理解每一步到底发生了什么前端声明回调函数在全局作用域定义一个函数比如function handleData(res) { console.log(res); }。这个函数必须挂在 window 上因为后面服务器返回的脚本要在全局环境执行。动态创建script标签用document.createElement(script)创建一个脚本元素。拼接请求地址把接口地址和回调函数名拼在一起形如https://api.example.com/data?callbackhandleData。这里的callback参数名是前后端约定好的可以是cb、jsonp、callback等只要两边一致就行。插入DOM触发请求把 script 标签 append 到 document 里浏览器立即发起请求。服务器包装数据服务器拿到callback参数的值把要返回的 JSON 数据包在这个函数名里输出handleData({name:张三,age:25})。浏览器执行脚本这段代码被当作 JS 执行等价于调用了你预先定义的handleData函数数据作为参数传入。请求完成后通常再把 script 标签移除保持 DOM 干净。2.2 回调函数名为什么必须全局唯一如果你同时发多个 JSONP 请求都用同一个回调函数名后返回的数据会覆盖先返回的或者根本分不清哪个数据对应哪个请求。所以标准做法是每次请求生成一个唯一的函数名比如用时间戳加随机数jsonp_1735689012345_8271。服务器返回的脚本里用的就是这个唯一名字执行时精确调用对应的那个函数。请求完成后再把这个临时函数从 window 上删掉避免内存泄漏。这个细节很多简易教程不讲但在实际项目里非常关键。2.3 服务器端到底做了什么服务器端的逻辑其实很简单用 PHP 举例?php $callback $_GET[callback]; $data [name 张三, age 25]; header(Content-Type: application/javascript); echo $callback . ( . json_encode($data) . );核心就三步取回调名、序列化数据、拼接输出。注意Content-Type要设成application/javascript虽然浏览器对 script 标签的响应类型不那么严格但规范设置是好习惯。提示服务器端一定要对 callback 参数做白名单校验只允许字母、数字、下划线防止被人注入恶意脚本内容。这是安全底线。3. 手写一个可复用的JSONP工具函数3.1 从零实现的核心代码网上很多现成的库但自己写一遍才能真正理解。下面是一个生产可用的实现function jsonp(options) { return new Promise((resolve, reject) { const { url, params {}, timeout 5000, callbackKey callback } options; // 生成唯一回调名 const callbackName jsonp_ Date.now() _ Math.floor(Math.random() * 100000); // 挂载全局回调 window[callbackName] function(data) { resolve(data); cleanup(); }; // 拼接参数 const query Object.keys(params) .map(k encodeURIComponent(k) encodeURIComponent(params[k])) .join(); const fullUrl url (url.includes(?) ? : ?) (query ? query : ) callbackKey callbackName; // 创建script const script document.createElement(script); script.src fullUrl; // 超时处理 const timer setTimeout(() { reject(new Error(JSONP request timeout)); cleanup(); }, timeout); // 清理函数 function cleanup() { clearTimeout(timer); if (script.parentNode) script.parentNode.removeChild(script); delete window[callbackName]; } script.onerror function() { reject(new Error(JSONP request failed)); cleanup(); }; document.head.appendChild(script); }); }3.2 关键设计点逐一说明Promise 封装用 Promise 把回调式的 JSONP 包起来调用时就能用await代码更清爽。这是现代前端的基本要求。超时机制script 标签加载失败时onerror不一定在所有浏览器都触发。所以必须加一个定时器兜底超过指定时间就判定失败。我一般设 5 到 10 秒看接口的响应速度定。清理逻辑请求结束后要移除 script 标签、删除全局函数、清除定时器。这三件事缺一不可。不移除 script页面里会堆积大量无用标签不删全局函数window 上会挂一堆垃圾属性。参数编码所有参数值都要encodeURIComponent否则遇到中文、特殊符号就会出问题。这个坑我踩过当时一个带空格的参数导致请求直接 400。3.3 调用示例与效果验证async function fetchWeather() { try { const data await jsonp({ url: https://api.example.com/weather, params: { city: 北京 }, callbackKey: callback, timeout: 8000 }); console.log(拿到数据:, data); } catch (err) { console.error(请求失败:, err.message); } }调用后打开浏览器 Network 面板你能看到一条类型为script的请求响应内容形如jsonp_1735689012345_8271({temp: 25})。这就说明整条链路通了。4. 实战中的坑与排查手册4.1 常见问题速查表问题现象可能原因排查方向控制台报xxx is not defined回调函数名不匹配检查前后端 callback 参数名是否一致请求发出但无响应服务器未按 JSONP 格式返回看响应内容是否是纯 JSON 而非函数调用超时无回调接口慢或 script 加载失败加超时兜底检查网络面板中文乱码参数未编码用 encodeURIComponent 处理参数多次请求数据错乱回调函数名重复确保每次请求生成唯一函数名报安全警告callback 参数被注入服务器端做白名单校验4.2 三个我踩过的真实坑坑一回调函数被覆盖。早期我图省事所有请求都用固定的callback函数名。结果页面同时发三个请求数据全乱了。后来改成唯一函数名才解决。这个问题的本质是全局命名空间污染跟变量重名是一个道理。坑二错误处理形同虚设。JSONP 的onerror在部分场景下不触发比如服务器返回了 404 页面但内容是 HTML。这时候 script 加载“成功”了但执行时报语法错误。所以除了onerror还要靠超时机制兜底双保险。坑三忘记清理导致内存泄漏。单页应用里频繁发 JSONP 请求如果不清理全局函数和 script 标签跑几个小时页面就卡了。用 Chrome 的 Memory 面板一看window 上挂了几百个函数。加上 cleanup 逻辑后问题消失。4.3 安全性必须重视JSONP 有一个天然的安全隐患它执行的是服务器返回的任意脚本。如果接口被劫持返回的脚本可以干任何事——读取你的 cookie、篡改页面、发起其他请求。所以只对可信的接口使用 JSONP服务器端严格校验 callback 参数只允许合法字符敏感数据不要通过 JSONP 传输能用 CORS 的场景优先用 CORSJSONP 是历史遗留方案注意callback 参数如果不过滤攻击者可以传入alert(1)//这类内容服务器拼接后就变成alert(1)//({...})直接执行恶意代码。这是经典的 XSS 注入点。5. JSONP与CORS的选型对比及现代替代方案5.1 两种跨域方案的正面对比维度JSONPCORS支持方法仅 GET全部 HTTP 方法浏览器兼容极老浏览器也支持需要较新浏览器服务器改动需包装返回格式需设置响应头错误处理弱难获取状态码完整可读状态码安全性较低执行任意脚本较高可控请求头自定义不支持支持文件上传不支持支持结论很清晰新项目一律用 CORSJSONP 只在维护老系统或对接不支持 CORS 的第三方接口时才用。5.2 CORS的基本配置服务器端设置几个响应头就能开启 CORS?php header(Access-Control-Allow-Origin: https://your-site.com); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE); header(Access-Control-Allow-Headers: Content-Type, Authorization); header(Access-Control-Allow-Credentials: true);前端用标准的fetch或XMLHttpRequest即可不需要任何特殊处理。这比 JSONP 干净太多。5.3 什么时候JSONP仍然是唯一选择有一种情况 CORS 搞不定对接的第三方接口只提供 JSONP 形式你无法修改对方服务器。比如某些老牌地图服务、天气服务、统计数据服务它们的 API 文档里明确写着“仅支持 JSONP”。这时候你没得选只能用它。还有一种情况是需要兼容非常老的浏览器比如某些内嵌设备上的浏览器内核版本很低不支持 CORS。这种场景现在越来越少但在工业控制、老旧终端领域仍然存在。5.4 现代替代方案一览除了 CORS还有几种跨域方案值得了解代理服务器前端请求同源的后端后端再去请求目标接口把数据转发回来。这是最通用、最安全的方案适合生产环境。postMessage用于 iframe 之间的跨域通信适合嵌入场景。WebSocket不受同源策略限制适合实时通信场景。Nginx 反向代理在部署层面把跨域请求转成同源请求前端完全无感知。这些方案各有适用场景选型时根据实际约束来定。JSONP 作为最古老的方案理解它的原理有助于你搞懂跨域这件事的本质但实际项目中应该优先考虑更现代的方案。我在维护一个老系统时接口层全是 JSONP迁移到 CORS 花了整整两周因为要逐个接口改返回格式、加响应头、回归测试。所以如果你现在正在设计新接口直接上 CORS别给自己留技术债。