1. 跨域请求的痛点与 JSONP 的诞生逻辑前后端分离开发的日常里跨域几乎是绕不开的一道坎。前端页面跑在http://localhost:8080后端接口部署在http://api.example.com浏览器控制台立刻甩出一行红字Access-Control-Allow-Origin缺失请求被拦截。这个场景在十年前比现在更常见因为那时候 CORS跨域资源共享还没有被所有浏览器和服务器广泛支持开发者急需一种能绕过同源策略、又能拿到数据的方案。JSONP 就是在这个背景下被广泛采用的。JSONP 全称 JSON with Padding直译过来就是“带填充的 JSON”。它的核心思路非常朴素浏览器虽然有同源策略限制 XHR 请求但script标签的src属性不受同源策略约束。也就是说你可以从任意域名加载一个 JavaScript 文件浏览器不会拦截。JSONP 正是利用了这个“漏洞”把后端返回的数据包装成一段可执行的 JavaScript 代码前端通过动态创建script标签来加载从而拿到数据。这个方案能解决什么问题简单说它让前端在 CORS 尚未普及的年代能够跨域获取 JSON 数据。适合谁学习如果你正在维护老项目、对接第三方接口、或者面试中被问到跨域方案JSONP 都是必须掌握的基础知识点。即便现在 CORS 已经是主流理解 JSONP 的原理依然有助于你更深入地理解浏览器的同源策略和脚本加载机制。我第一次接触 JSONP 是在一个对接第三方天气接口的项目里。对方只提供 JSONP 形式的接口文档里写着“callback 参数必传”当时还觉得奇怪后来才明白这就是 JSONP 的标准约定。踩过几次坑之后我对这套机制的理解才算真正落地。2. JSONP 核心原理深度拆解2.1 同源策略与 script 标签的“特权”要理解 JSONP必须先搞清楚浏览器的同源策略。同源策略要求协议、域名、端口三者完全一致否则就视为跨域。跨域情况下浏览器会限制 XHR/Fetch 请求读取响应内容但有一个例外script、img、link这类标签可以跨域加载资源。这不是设计缺陷而是历史遗留的“合理放行”——早期网页需要从 CDN 加载脚本和图片如果这些都被拦截互联网就没法正常运转了。JSONP 正是抓住了script标签这个特性。前端动态创建一个script元素把src指向后端接口地址同时带上一个callback参数。后端收到请求后不返回纯 JSON而是返回一段形如callbackName({...数据...})的 JavaScript 代码。浏览器加载这段脚本后会立即执行它而callbackName正是前端预先定义好的全局函数数据就通过函数参数传了进来。注意JSONP 只能用于 GET 请求因为script标签的加载本质就是 GET。这一点在实际开发中经常被忽略导致有人试图用 JSONP 发 POST 请求结果怎么调都不通。2.2 JSONP 的完整通信流程把整个流程拆开来看大致分为五步前端提前定义一个全局函数比如window.handleResponse用于接收数据。前端动态创建script标签src设置为http://api.example.com/data?callbackhandleResponse。浏览器向该地址发起 GET 请求。后端接收到callback参数后把数据包装成handleResponse({name:张三,age:25})这样的字符串返回。浏览器把返回内容当作 JavaScript 执行handleResponse被调用前端拿到数据。整个过程没有任何 XHR 参与完全是脚本加载机制在起作用。这也是为什么 JSONP 能绕过同源策略——它压根就没触发 XHR 的跨域检查。2.3 为什么 JSONP 逐渐被 CORS 取代JSONP 虽然巧妙但缺点也很明显。首先它只支持 GET无法满足 RESTful 风格中 POST、PUT、DELETE 的需求。其次错误处理非常困难script标签加载失败只会触发onerror拿不到具体的 HTTP 状态码和错误信息。再者安全性堪忧因为返回的是可执行脚本如果后端被劫持攻击者可以注入任意代码。最后回调函数名是全局的多个请求之间容易冲突需要额外管理。CORS 出现后服务器只需设置几个响应头就能支持所有 HTTP 方法还能精细控制哪些域名可以访问。所以现在新项目基本都用 CORSJSONP 更多出现在老系统和第三方接口对接中。但理解 JSONP 依然有价值因为它能帮你更深刻地理解浏览器的安全模型。3. 手把手实现一个完整的 JSONP 方案3.1 前端封装从零写一个 JSONP 函数先来看前端怎么封装一个通用的 JSONP 请求函数。核心要点有三个生成唯一回调名、动态创建 script 标签、请求完成后清理现场。function jsonp(url, params {}, timeout 5000) { return new Promise((resolve, reject) { // 生成唯一回调名避免多个请求冲突 const callbackName jsonp_cb_ Date.now() _ Math.floor(Math.random() * 1000); // 把 callback 参数拼接到 URL 上 const query new URLSearchParams({ ...params, callback: callbackName }); const fullUrl url (url.includes(?) ? : ?) query.toString(); // 创建 script 标签 const script document.createElement(script); script.src fullUrl; // 定义全局回调函数 window[callbackName] function(data) { resolve(data); cleanup(); }; // 超时处理 const timer setTimeout(() { reject(new Error(JSONP request timeout)); cleanup(); }, timeout); // 清理函数移除 script 标签、删除全局函数、清除定时器 function cleanup() { clearTimeout(timer); if (script.parentNode) { script.parentNode.removeChild(script); } delete window[callbackName]; } // 加载失败处理 script.onerror function() { reject(new Error(JSONP script load error)); cleanup(); }; document.head.appendChild(script); }); }这段代码有几个细节值得展开说。第一回调名用时间戳加随机数确保唯一性避免并发请求时互相覆盖。第二用Promise包装调用方可以用async/await写起来更顺手。第三cleanup函数负责移除 script 标签和删除全局函数防止内存泄漏。第四超时时间设了 5 秒实际项目中可以根据接口响应速度调整。实操心得delete window[callbackName]这一步很多人会漏掉。如果不删页面运行久了全局变量会越来越多虽然不至于立刻出问题但在单页应用里频繁发 JSONP 请求内存占用会明显上升。3.2 后端配合PHP 跨域 JSONP 接口实现热词里提到了“php跨域jsonp”说明很多老项目是 PHP 后端。PHP 实现 JSONP 接口非常简单核心就是读取callback参数然后把数据包一层。?php // 设置响应类型为 JavaScript header(Content-Type: application/javascript; charsetutf-8); // 获取回调函数名做安全过滤 $callback isset($_GET[callback]) ? $_GET[callback] : callback; // 严格校验回调名只允许字母、数字、下划线、点 if (!preg_match(/^[a-zA-Z_][a-zA-Z0-9_\.]*$/, $callback)) { http_response_code(400); echo invalid callback; exit; } // 准备数据 $data [ code 0, msg success, data [ name 张三, age 25, city 杭州 ] ]; // 输出 JSONP 格式 echo $callback . ( . json_encode($data, JSON_UNESCAPED_UNICODE) . );;这里最关键的是回调名的安全校验。如果不做过滤攻击者可以传入恶意字符串导致 XSS 漏洞。正则/^[a-zA-Z_][a-zA-Z0-9_\.]*$/限制了回调名只能以字母或下划线开头后面跟字母、数字、下划线或点。这样既能支持cb、myCallback这种常见命名也能支持obj.method这种命名空间形式。JSON_UNESCAPED_UNICODE这个参数也值得注意。默认情况下json_encode会把中文转成\uXXXX形式加上这个参数后中文会原样输出调试时更直观。3.3 前后端联调完整请求示例前端调用刚才封装的函数async function fetchUserInfo() { try { const result await jsonp(http://api.example.com/user.php, { userId: 123 }); console.log(拿到数据, result); } catch (err) { console.error(请求失败, err.message); } } fetchUserInfo();后端收到的请求大概是http://api.example.com/user.php?userId123callbackjsonp_cb_1699999999_456返回内容为jsonp_cb_1699999999_456({code:0,msg:success,data:{name:张三,age:25,city:杭州}});浏览器执行这段脚本后window.jsonp_cb_1699999999_456被调用Promise 的resolve触发前端就拿到了数据。整个过程行云流水没有任何跨域报错。注意后端返回的 Content-Type 建议设为application/javascript虽然浏览器对 script 标签的响应类型不严格校验但设对了更规范也方便调试。4. 常见问题排查与避坑指南4.1 回调函数未定义或未执行这是 JSONP 最常见的问题。现象是请求发出去了后端也返回了但前端回调就是不执行。原因通常有三个回调名拼写不一致、全局函数被覆盖、或者后端返回的格式不对。排查步骤可以按这个顺序来先在浏览器 Network 面板找到那个 script 请求看 Response 内容是不是callbackName({...})格式然后检查前端定义的全局函数名和后端返回的是否完全一致大小写都要对最后确认没有其他地方覆盖了同名全局变量。我遇到过一次特别隐蔽的情况两个模块都用了callback这个默认名结果后发的请求把先发的回调覆盖了导致先发的请求永远拿不到数据。后来改成动态生成唯一回调名就解决了。4.2 超时与错误处理失效script标签的onerror只在网络层面加载失败时触发比如 404、DNS 解析失败。如果后端返回了 500 错误但内容是一段 HTML浏览器会尝试执行这段 HTML通常不会触发onerror而是抛出语法错误。这种情况下 Promise 既不会 resolve 也不会 reject请求就“悬”在那里了。解决办法是加超时机制就像前面代码里那样用setTimeout。超时时间设多少合适一般接口 3 到 5 秒足够慢查询接口可以放宽到 10 秒。超时后主动 reject并清理 script 标签。4.3 安全性问题与防范JSONP 的安全风险主要有两个一是回调名注入二是返回内容被篡改。回调名注入前面已经讲了用正则严格校验就能防住。返回内容被篡改则比较麻烦因为 JSONP 本质是加载远程脚本如果中间人攻击或者后端被入侵攻击者可以返回任意 JavaScript 代码在用户浏览器里执行。防范措施包括只对接可信的第三方接口、尽量用 HTTPS、后端对返回数据做严格过滤。如果接口涉及敏感操作建议改用 CORS 加 Token 鉴权的方式不要用 JSONP。4.4 常见问题速查表问题现象可能原因排查方法解决方案回调不执行回调名不一致对比 Network 返回内容与前端函数名统一命名用动态唯一名请求无响应超时未处理检查是否有 setTimeout加超时 reject 逻辑控制台报语法错误后端返回非 JS 内容查看 Response 内容后端确保返回合法 JS并发请求数据错乱回调名冲突检查是否用了固定回调名每次请求生成唯一回调名内存泄漏全局函数未删除检查 cleanup 逻辑请求完成后 delete 全局函数中文乱码编码不一致检查 Content-Type设为 utf-8用 JSON_UNESCAPED_UNICODE5. JSONP 在现代开发中的定位与替代方案5.1 JSONP 与 CORS 的对比选型现在新项目基本不会主动选 JSONPCORS 是更现代、更安全的方案。但两者并不是完全替代关系有些场景下 JSONP 反而更合适。比如对接一些老旧的第三方接口对方只提供 JSONP 形式你没得选。再比如需要加载跨域的静态脚本资源这本身就是 script 标签的天然能力跟 JSONP 思路一致。从实现成本看CORS 需要后端配置响应头前端用标准 Fetch 即可代码更简洁。JSONP 需要前后端约定回调参数前端还要手动管理 script 标签和全局函数维护成本更高。从安全性看CORS 支持精细的域名白名单和凭证控制JSONP 几乎没有安全边界。所以只要条件允许优先选 CORS。5.2 用 CORS 替代 JSONP 的迁移思路如果你手上有老项目还在用 JSONP想迁移到 CORS可以按这个思路来。后端先加上 CORS 响应头比如Access-Control-Allow-Origin设为具体域名Access-Control-Allow-Methods加上需要的 HTTP 方法。然后前端把 JSONP 调用改成 Fetch 或 Axios去掉 callback 参数。最后灰度发布观察一段时间确认没问题再全量切换。迁移过程中要注意预检请求。如果请求带了自定义头或者用了 PUT/DELETE 方法浏览器会先发一个 OPTIONS 请求做预检后端需要正确处理 OPTIONS 并返回 204。这一点在 JSONP 时代是不存在的迁移时容易漏掉。5.3 其他跨域方案简述除了 JSONP 和 CORS还有几种跨域方案值得一提。一是 Nginx 反向代理把前端和后端放在同一个域名下从根上避免跨域。二是 postMessage适合 iframe 之间的通信。三是 WebSocket本身不受同源策略限制。每种方案都有适用场景实际项目中往往是组合使用。实操心得如果项目里既有 JSONP 又有 CORS建议统一封装一个请求层根据接口配置自动选择用哪种方式。这样业务代码不用关心底层细节迁移时也只需要改配置不用动业务逻辑。JSONP 这个技术点看起来简单但真正写好、用好、排查好问题还是需要不少实战经验的。我在实际项目中最大的体会是不要因为它“老”就轻视它很多老系统的稳定性恰恰依赖于这些看似过时的方案。理解它的原理和边界比单纯会用更重要。后续如果遇到需要兼容极老浏览器的场景JSONP 依然是一个可靠的备选方案。