实现JSONP方法

手写实现JSONP跨域请求方法,支持动态创建script标签、回调函数处理和超时控制

问题

JSONP(JSON with Padding)是一种解决跨域请求的方案。由于浏览器的同源策略限制,XMLHttpRequest无法直接进行跨域请求,但<script>标签不受此限制。JSONP利用这一特性,通过动态创建script标签来实现跨域数据获取。

需要实现一个JSONP方法,支持:

  • 动态创建script标签发起请求
  • 自动生成全局回调函数
  • 请求成功/失败的处理
  • 超时控制
  • 资源清理

解答

function jsonp({ url, params = {}, callbackKey = 'callback', timeout = 10000 }) {
  return new Promise((resolve, reject) => {
    // 生成唯一的回调函数名
    const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    
    // 创建script标签
    const script = document.createElement('script');
    
    // 定义全局回调函数
    window[callbackName] = (data) => {
      // 请求成功,返回数据
      resolve(data);
      // 清理资源
      cleanup();
    };
    
    // 构建请求参数
    const queryParams = {
      ...params,
      [callbackKey]: callbackName
    };
    
    // 将参数对象转换为查询字符串
    const queryString = Object.keys(queryParams)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
      .join('&');
    
    // 拼接完整URL
    const separator = url.includes('?') ? '&' : '?';
    script.src = `${url}${separator}${queryString}`;
    
    // 设置超时处理
    let timer = null;
    if (timeout) {
      timer = setTimeout(() => {
        reject(new Error('JSONP request timeout'));
        cleanup();
      }, timeout);
    }
    
    // 处理script加载错误
    script.onerror = () => {
      reject(new Error('JSONP request failed'));
      cleanup();
    };
    
    // 清理函数
    function cleanup() {
      // 清除定时器
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      
      // 删除全局回调函数
      if (window[callbackName]) {
        delete window[callbackName];
      }
      
      // 移除script标签
      if (script.parentNode) {
        script.parentNode.removeChild(script);
      }
    }
    
    // 将script标签添加到页面中,触发请求
    document.body.appendChild(script);
  });
}

使用示例

// 基本使用
jsonp({
  url: 'https://api.example.com/data',
  params: {
    id: 123,
    name: 'test'
  }
})
  .then(data => {
    console.log('请求成功:', data);
  })
  .catch(error => {
    console.error('请求失败:', error.message);
  });

// 自定义回调参数名
jsonp({
  url: 'https://api.example.com/user',
  params: { userId: 456 },
  callbackKey: 'cb', // 有些API使用cb而不是callback
  timeout: 5000 // 5秒超时
})
  .then(data => {
    console.log('用户数据:', data);
  })
  .catch(error => {
    console.error('获取用户数据失败:', error);
  });

// 使用async/await
async function fetchData() {
  try {
    const result = await jsonp({
      url: 'https://api.example.com/list',
      params: { page: 1, size: 10 }
    });
    console.log('数据列表:', result);
  } catch (error) {
    console.error('请求出错:', error);
  }
}

fetchData();

关键点

  • 动态生成回调函数名:使用时间戳和随机数组合生成唯一的回调函数名,避免多个请求之间的冲突

  • Promise封装:将JSONP异步操作封装成Promise,使其支持现代的async/await语法,提升代码可读性

  • 参数序列化:将参数对象转换为URL查询字符串,注意使用encodeURIComponent进行编码,防止特殊字符导致的问题

  • 超时控制:通过setTimeout实现超时机制,避免请求长时间无响应导致的资源占用

  • 资源清理:请求完成后(成功、失败或超时)必须清理全局回调函数和script标签,防止内存泄漏

  • 错误处理:监听script的onerror事件,捕获网络错误或404等情况

  • 灵活配置:支持自定义回调参数名(callbackKey),因为不同API可能使用不同的参数名(如callback、cb、jsonp等)