手写一个JSONP的实现,重温那个跨域的“石器时代”


在今天,当我们在谈论前端的“跨域”问题时,脑海中浮现的几乎都是 CORS(Cross-Origin Resource Sharing)。我们配置着 Access-Control-Allow-Origin ,处理着预检请求(preflight request),一切都显得那么“理所当然”。

然而,在 CORS 成为标准之前,Web 世界曾经历过一个充满限制却又闪耀着智慧火花的“石器时代”。

在那个时代,浏览器严格遵守着“同源策略”(Same-Origin Policy),像一堵无法逾越的高墙,阻止着不同源之间的 AJAX 请求。

为了凿开这堵墙,前辈们发明了一种极其巧妙,甚至可以说是“hacky”的技术—— ** JSONP ** (JSON with Padding)。

今天,就让我们亲手实现一个 JSONP,不仅是为了学习一段代码,更是为了触摸那段历史,理解前辈们是如何用智慧绕过限制,点亮了早期 Web 应用的交互之光。

JSONP 的核心思想:一个“美丽的误会”

同源策略限制了 XMLHttpRequest ,但它有一个“漏洞”:它并不限制带有 src 属性的标签,比如 <script><img><iframe> 。它们天生就具备从任何地方加载资源的能力,否则你就无法在你的网站上使用 CDN
的图片或脚本了。

JSONP 正是利用了 <script> 标签的这个特性。它的核心思想可以概括为以下对话:

  • ** 前端页面 ** :(对服务器喊话)“你好,我需要一些数据。但我不能直接用 AJAX 拿。这样吧,我动态创建一个 <script> 标签来请求你的一个地址。另外,我已经在我的页面上准备好了一个叫 myCallbackFunction 的函数,请你把数据作为参数,放进这个函数调用里,然后把整个 myCallbackFunction({ ...data... }) 作为一段 JavaScript 文本返回给我。”
  • ** 服务器 ** :(收到请求后)“好的,没问题。这是你要的数据,我已经用你指定的回调函数名‘包裹’(Padding)好了。”

于是,当前端页面加载完这个 <script> 标签后,它实际上是在执行一段从服务器返回的 JavaScript
代码,这段代码恰好调用了我们预先定义好的全局函数,数据也就自然而然地被传递了进来。

整个过程没有使用 XMLHttpRequest ,完美绕过了同源策略。

动手实现:打造我们自己的 JSONP 工具

让我们来封装一个通用的 jsonp 函数。它应该接收一个包含 url params callback
的配置对象。

/**  
 * 一个简单的 JSONP 实现  
 * @param {object} options - 配置对象  
 * @param {string} options.url - 请求的 URL 地址  
 * @param {object} [options.params] - 需要传递的参数  
 * @param {string} [options.callbackKey='callback'] - 与后端约定的回调函数参数名  
 * @param {function} options.callback - 成功时调用的回调函数  
 */  
function jsonp({ url, params = {}, callbackKey = 'callback', callback }) {  
 // 1. 生成一个独一无二的回调函数名,防止并发请求时冲突  
 const callbackName = `jsonp_callback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;  
  
 // 2. 将回调函数挂载到 window 上,使其成为全局函数  
 // 这是最关键的一步,因为服务器返回的脚本将直接调用这个函数  
 window[callbackName] = function(data) {  
    // 成功接收到数据后,调用我们真正的回调函数  
    callback(data);  
  
    // 3. 清理工作:用完即焚  
    // 移除挂载到 window 上的函数,避免内存泄漏和全局污染  
    delete window[callbackName];  
    // 移除注入的 script 标签  
    document.head.removeChild(scriptElement);  
  };  
  
 // 4. 准备参数,并将其拼接到 URL 上  
 const paramsArray = [];  
 for (let key in params) {  
    paramsArray.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);  
  }  
 // 加上与后端约定的回调函数名参数  
  paramsArray.push(`${callbackKey}=${callbackName}`);  
  
 const paramsString = paramsArray.join('&');  
 const finalUrl = url.includes('?') ? `${url}&${paramsString}` : `${url}?${paramsString}`;  
  
 // 5. 创建并注入 <script> 标签,发起请求  
 const scriptElement = document.createElement('script');  
  scriptElement.src = finalUrl;  
 document.head.appendChild(scriptElement);  
  
 // (可选)错误处理  
}  

如何使用它?

假设我们有一个第三方天气服务,它支持 JSONP。

// 发起一次 JSONP 请求  
jsonp({  
 url: 'https://api.fedjavascript.com/jsonp/city',  
 params: {  
    name: 'Beijing'  
  },  
 // 后端接口约定的回调参数名,默认为 'callback'  
 // callbackKey: 'cb',   
 callback: function(data) {  
    // 在这里,我们成功拿到了跨域数据!  
    console.log('当前天气:', data);  
    // 预期的 data 格式: { temperature: '35°C', condition: '晴' }  
  }  
});  
  
// 此时,浏览器网络面板会看到一个类似这样的请求:  
// https://api.fedjavascript.com/jsonp/city?name=Beijing&callback=jsonp_callback_1752844122915_12345  
  
// 服务器会返回这样的内容(Content-Type: application/javascript):  
// jsonp_callback_1752844122915_12345({ "temperature": "35°C", "condition": "晴" });  

当浏览器加载并执行这段返回的脚本时,我们挂载在 window 上的 jsonp_callback_...
函数就被调用了,数据被成功捕获,而那个临时的 <script> 标签和全局函数也随之被销毁,整个过程干净利落。

“石器时代”的终结:JSONP 的局限性

尽管 JSONP 非常巧妙,但它终究是特定历史时期的产物,带着明显的时代烙印和局限性:

  1. ** 只支持 GET 请求 ** :因为 <script> 标签的 src 只能发起 GET 请求,所以 JSONP 天然无法实现 POST、PUT 等操作
  2. ** 安全性问题 ** :JSONP 的安全性是它最大的软肋。你必须完全信任提供 JSONP 服务的第三方。如果该服务器被恶意攻击,返回的不再是数据,而是一段恶意脚本,你的网站将毫无防备地执行它,这可能导致 XSS 攻击
  3. ** 简陋的错误处理 ** :我们无法像 XMLHttpRequest 那样精确地捕获 HTTP 状态码(如 404, 500)。我们只能通过 onerror 事件或者设置一个计时器(timeout)来判断请求是否失败,但这并不能告诉我们失败的具体原因

随着 Web 的发展,我们需要更安全、更强大、更标准的跨域解决方案。于是,CORS 应运而生,它通过标准的 HTTP
头部字段,让服务器和浏览器进行协商,从而安全地实现了跨域资源访问。CORS 支持所有 HTTP 方法,提供了完善的错误处理机制,成为了现代 Web
开发的事实标准。

为什么要回顾历史?

今天,我们几乎不再需要手写 JSONP。但重温这段历史,亲手实现这个“古老”的工具,能带给我们超越代码本身的思考:

  • ** 理解限制是创新的源泉 ** :正是因为同源策略这堵“墙”,才催生了 JSONP 这样不拘一格的解决方案
  • ** 看见 Web 标准的演进 ** :从 JSONP 到 CORS ,我们能清晰地看到 Web 标准是如何在解决实际问题的过程中一步步走向成熟和安全的
  • ** 掌握问题的本质 ** :理解了 JSONP,我们就从根本上理解了浏览器安全模型的一部分,也更能体会 CORS 设计的精妙之处

JSONP 就像 Web 发展史上的一块活化石,它向我们展示了早期 Web 开发者们的智慧与创造力。