实现 JSONP 跨域请求

手写 JSONP 函数实现跨域数据获取

问题

实现一个 JSONP 函数,用于跨域请求数据。

解答

基本实现

function jsonp(url, options = {}) {
  return new Promise((resolve, reject) => {
    const { params = {}, timeout = 5000, callbackKey = 'callback' } = options;

    // 生成唯一的回调函数名
    const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).slice(2)}`;

    // 将回调函数挂载到全局
    window[callbackName] = (data) => {
      cleanup();
      resolve(data);
    };

    // 清理函数
    const cleanup = () => {
      delete window[callbackName];
      document.body.removeChild(script);
      clearTimeout(timer);
    };

    // 超时处理
    const timer = setTimeout(() => {
      cleanup();
      reject(new Error('JSONP request timeout'));
    }, timeout);

    // 构建请求 URL
    const queryParams = { ...params, [callbackKey]: callbackName };
    const queryString = Object.entries(queryParams)
      .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
      .join('&');
    const separator = url.includes('?') ? '&' : '?';
    const requestUrl = `${url}${separator}${queryString}`;

    // 创建 script 标签
    const script = document.createElement('script');
    script.src = requestUrl;
    script.onerror = () => {
      cleanup();
      reject(new Error('JSONP request failed'));
    };

    document.body.appendChild(script);
  });
}

使用示例

// 基本使用
jsonp('https://api.example.com/data', {
  params: { id: 123 },
  timeout: 3000
})
  .then(data => console.log(data))
  .catch(err => console.error(err));

// 服务端返回格式(以 Node.js 为例)
// jsonp_1699999999999_abc123({ "name": "test", "value": 100 })

服务端配合(Node.js 示例)

const http = require('http');
const url = require('url');

http.createServer((req, res) => {
  const { query } = url.parse(req.url, true);
  const callback = query.callback;

  const data = { name: 'test', value: 100 };

  // 返回函数调用形式
  res.writeHead(200, { 'Content-Type': 'application/javascript' });
  res.end(`${callback}(${JSON.stringify(data)})`);
}).listen(3000);

简化版本

function jsonpSimple(url, callbackName) {
  return new Promise((resolve) => {
    window[callbackName] = (data) => {
      resolve(data);
      delete window[callbackName];
      document.body.removeChild(script);
    };

    const script = document.createElement('script');
    script.src = `${url}?callback=${callbackName}`;
    document.body.appendChild(script);
  });
}

关键点

  • 原理:利用 <script> 标签不受同源策略限制的特性
  • 只支持 GET:因为是通过 URL 加载脚本,无法发送 POST 请求
  • 需要服务端配合:服务端返回 callback(data) 格式的 JS 代码
  • 安全风险:执行第三方返回的代码,存在 XSS 风险
  • 已被淘汰:现代开发推荐使用 CORS 跨域