JSONP

基础知识

在深入实现之前,我们需要理解 JSONP 是什么,以及它为什么会出现。

  1. 同源策略 (Same-Origin Policy): 这是浏览器的一个核心安全策略。它限制了从一个源加载的文档或脚本,如何与来自另一个源的资源进行交互。简单来说,如果你在 a.com 页面,你的 XMLHttpRequest (Ajax) 请求默认无法访问 b.com 的数据接口。这是为了防止恶意网站读取其他网站的敏感数据。

  2. <script> 标签的“特权”: 同源策略主要限制的是 Ajax 这类数据请求,但对于一些 HTML 标签则没有此限制,其中最重要的就是 <script> 标签。一个页面的 <script src="..."></script> 可以无视同源策略,加载并执行来自任何域的 JavaScript 文件。

  3. JSONP (JSON with Padding): JSONP 并非一种新的技术,而是一种利用 <script> 标签“特权”来实现跨域数据请求的模式约定。它通过一种巧妙的方式,让服务器返回的数据能够被不同域的客户端所接收。这里的 "Padding" (填充物) 指的就是包裹在 JSON 数据外层的那个函数调用。

核心思路

JSONP 的核心思路是一场客户端与服务端之间的“双人舞”,双方需遵循一个共同的约定。

  1. 客户端准备“接球”的函数: 客户端(浏览器端)首先在全局作用域中定义一个回调函数,比如 handleResponse。这个函数是用来接收和处理服务器返回的数据的。

  2. 客户端发起“特殊的”请求: 客户端不使用 XMLHttpRequest,而是动态地创建一个 <script> 标签。将其 src 属性指向服务端的接口地址,并且在 URL 的查询参数中,带上一个特殊的参数(通常是 callbackjsonp),其值就是第一步中准备好的函数名。

    • 例如: <script src="http://api.b.com/data?callback=handleResponse"></script>
  3. 服务端返回“包裹好的”数据: 服务端接收到请求后,会解析出 callback 参数的值(在这里是 handleResponse)。然后,它不会直接返回纯粹的 JSON 数据(如 {"name": "张三"}),而是将这些数据作为参数,包裹在一个函数调用中,并以 JavaScript 脚本的形式返回。

    • 服务端返回的内容是这样一个字符串: handleResponse({"name": "张三"});
  4. 客户端执行脚本,完成数据交接: 浏览器下载并执行这个由 <script> 标签加载的脚本。执行 handleResponse({...}) 就相当于直接调用了我们在第一步中定义的全局函数,从而成功地将跨域数据传递到了我们的页面中。

代码实现

下面是一个封装好的、使用 Promise 的现代化 jsonp 函数实现。

/**
 * 封装一个支持 Promise 的 JSONP 请求函数
 * @param {string} url - 请求的 URL 地址
 * @param {object} params - 附加的查询参数对象
 * @param {string} [callbackKey='callback'] - 与后端约定的回调函数参数名
 * @returns {Promise<any>} - 返回一个 Promise,成功时 resolve 数据,失败时 reject 错误
 */
function jsonp(url, params = {}, callbackKey = 'callback') {
  // 返回一个 Promise 对象
  return new Promise((resolve, reject) => {
    // 1. 创建一个 script 标签
    const script = document.createElement('script');

    // 2. 生成一个唯一的、挂载在 window 上的回调函数名
    const callbackName =
      'jsonp_callback_' + Date.now() + Math.random().toString().substr(2);

    // 3. 在 window 上创建这个回调函数,用于接收数据
    window[callbackName] = (data) => {
      // 数据接收成功后,清理工作
      delete window[callbackName]; // 删除全局函数
      document.body.removeChild(script); // 移除 script 标签
      // Promise 状态变为 resolved
      resolve(data);
    };

    // 4. 监听 script 加载失败事件
    script.onerror = () => {
      // 失败后,同样执行清理工作
      delete window[callbackName];
      document.body.removeChild(script);
      // Promise 状态变为 rejected
      reject(new Error('JSONP request to ' + url + ' failed.'));
    };

    // 5. 组合最终的请求 URL
    const queryParams = { ...params, [callbackKey]: callbackName };
    const queryString = Object.keys(queryParams)
      .map(
        (key) =>
          `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`
      )
      .join('&');

    script.src = `${url}${url.includes('?') ? '&' : '?'}${queryString}`;

    // 6. 将 script 标签插入到 DOM 中,浏览器会自动发起请求
    document.body.appendChild(script);
  });
}

代码解析

  1. Promise 封装:整个函数返回一个 Promise,这使得我们可以使用 .then().catch() 或者 async/await 来处理异步操作,是现代 JavaScript 的标准实践。
  2. 唯一回调函数名const callbackName = 'jsonp_callback_...'。我们不能使用固定的函数名,因为页面上可能同时发起多个 JSONP 请求,固定的名字会产生冲突。通过时间戳和随机数生成一个几乎不可能重复的函数名是最佳实践。
  3. 创建全局回调(window[callbackName] = ...):这是 JSONP 的客户端核心。我们必须将回调函数挂载到全局对象 window 上,这样从 <script> 标签返回的 callbackName({...}) 才能在全局作用域中找到并执行它。在函数内部,一旦 resolve(data) 被调用,就意味着请求成功,我们立即执行清理工作:删除 window 上的这个临时函数和 DOM 中的这个临时 <script> 标签,避免内存泄漏和页面污染。
  4. 错误处理(script.onerror)script 标签无法像 XMLHttpRequest 那样提供详细的 HTTP 状态码。它只有一个 onerror 事件,可以捕获到脚本加载失败的情况(如网络错误、404 Not Found 等)。在 onerror 时,我们也必须执行同样的清理工作,并 reject 这个 Promise
  5. URL 构造:这部分代码负责将用户传入的 urlparams 对象以及我们生成的 callback 参数拼接成一个完整的请求 URL。它会妥善处理 ?& 的连接,并对参数进行 URL 编码。
  6. 注入并执行document.body.appendChild(script) 是触发整个流程的“扳机”。一旦 script 标签被插入到 DOM 中,浏览器就会立即对其 src 属性发起 GET 请求。
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""