【前端面试小册】JS-第30节:实现 JSONP

一、核心概念

1.1 什么是 JSONP

JSONP(JSON with Padding)是一种跨域数据请求的解决方案。

原理

  • 利用 <script> 标签不受同源策略限制的特性
  • 通过动态创建 <script> 标签,请求跨域资源
  • 服务器返回的数据作为 JavaScript 代码执行

1.2 为什么需要 JSONP

由于浏览器的同源策略,普通的 XMLHttpRequestfetch 无法直接请求跨域资源。JSONP 提供了一种绕过同源策略的方法。

同源策略:协议、域名、端口必须完全相同。

二、实现原理

2.1 基本流程

graph TD
    A[客户端调用 jsonp] --> B[生成唯一回调函数名]
    B --> C[创建 script 标签]
    C --> D[设置 src 为跨域 URL + 回调函数名]
    D --> E[添加到页面]
    E --> F[服务器返回 JavaScript 代码]
    F --> G[执行回调函数]
    G --> H[获取数据]
    H --> I[移除 script 标签]

2.2 服务器响应格式

服务器需要返回如下格式的 JavaScript 代码:

callbackName({
    "data": "some data"
});

三、基础实现

3.1 完整实现

export function jsonp(obj) {
    // 生成唯一回调函数名
    const cbName = `jsonp_callback_${String(new Date().getTime())}`;

    // 判断查询字符串最后一位是否为 ? 或者是 &
    let queryString = obj.url.includes('?') ? '&' : '?';

    // 遍历传进来的 data 实参赋值给查询字符串
    for (const k in obj.data) {
        if (Object.prototype.hasOwnProperty.call(obj.data, k)) {
            queryString += `${k}=${encodeURIComponent(obj.data[k])}&`;
        }
    }

    // 查询字符串加上回调函数
    queryString += `callback=${cbName}`;

    // 创建 script 标签
    const ele = document.createElement('script');

    // 给 script 标签添加 src 属性值
    ele.src = obj.url + queryString;
    
    // 将回调函数挂载到 window 对象上
    window[cbName] = function(data) {
        obj.success(data);
        // 清理工作
        document.body.removeChild(ele);
        delete window[cbName];
    };

    // 错误处理
    ele.onerror = function() {
        document.body.removeChild(ele);
        delete window[cbName];
        if (obj.error) {
            obj.error(new Error('JSONP request failed'));
        }
    };

    // 添加到 body 尾部
    document.body.appendChild(ele);
}

3.2 关键点解析

3.2.1 唯一回调函数名

const cbName = `jsonp_callback_${String(new Date().getTime())}`;

原因

  • 避免多个 JSONP 请求冲突
  • 使用时间戳确保唯一性

3.2.2 查询字符串构建

let queryString = obj.url.includes('?') ? '&' : '?';

逻辑

  • 如果 URL 已包含 ?,使用 & 连接
  • 否则使用 ? 开始查询字符串

3.2.3 全局回调函数

window[cbName] = function(data) {
    obj.success(data);
    // 清理工作
};

作用

  • 将回调函数挂载到 window 对象
  • 服务器返回的 JavaScript 代码会调用此函数
  • 执行后清理资源

四、使用示例

4.1 基础使用

jsonp({
    url: '/path/info',
    data: {
        name: '愚公上岸说'
    },
    success(res) {
        console.log('成功:', res);
    },
    error(err) {
        console.error('失败:', err);
    }
});

4.2 实际请求示例

// 请求天气 API
jsonp({
    url: 'https://api.example.com/weather',
    data: {
        city: '北京',
        key: 'your-api-key'
    },
    success(data) {
        console.log('天气数据:', data);
        updateWeatherUI(data);
    },
    error(err) {
        console.error('获取天气失败:', err);
    }
});

五、增强版本

5.1 支持超时

export function jsonp(obj) {
    const cbName = `jsonp_callback_${String(new Date().getTime())}`;
    let queryString = obj.url.includes('?') ? '&' : '?';
    const timeout = obj.timeout || 5000;

    // 构建查询字符串
    for (const k in obj.data) {
        if (Object.prototype.hasOwnProperty.call(obj.data, k)) {
            queryString += `${k}=${encodeURIComponent(obj.data[k])}&`;
        }
    }
    queryString += `callback=${cbName}`;

    const ele = document.createElement('script');
    ele.src = obj.url + queryString;
    
    let isCompleted = false;
    
    // 超时处理
    const timeoutId = setTimeout(() => {
        if (!isCompleted) {
            isCompleted = true;
            cleanup();
            if (obj.error) {
                obj.error(new Error('JSONP request timeout'));
            }
        }
    }, timeout);

    // 清理函数
    function cleanup() {
        if (ele.parentNode) {
            ele.parentNode.removeChild(ele);
        }
        delete window[cbName];
        clearTimeout(timeoutId);
    }

    window[cbName] = function(data) {
        if (!isCompleted) {
            isCompleted = true;
            cleanup();
            obj.success(data);
        }
    };

    ele.onerror = function() {
        if (!isCompleted) {
            isCompleted = true;
            cleanup();
            if (obj.error) {
                obj.error(new Error('JSONP request failed'));
            }
        }
    };

    document.body.appendChild(ele);
}

5.2 支持 Promise

function jsonp(options) {
    return new Promise((resolve, reject) => {
        const cbName = `jsonp_callback_${String(new Date().getTime())}`;
        let queryString = options.url.includes('?') ? '&' : '?';

        // 构建查询字符串
        if (options.data) {
            for (const k in options.data) {
                if (Object.prototype.hasOwnProperty.call(options.data, k)) {
                    queryString += `${k}=${encodeURIComponent(options.data[k])}&`;
                }
            }
        }
        queryString += `callback=${cbName}`;

        const ele = document.createElement('script');
        ele.src = options.url + queryString;

        window[cbName] = function(data) {
            document.body.removeChild(ele);
            delete window[cbName];
            resolve(data);
        };

        ele.onerror = function() {
            document.body.removeChild(ele);
            delete window[cbName];
            reject(new Error('JSONP request failed'));
        };

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

// 使用
jsonp({
    url: '/api/data',
    data: { name: '愚公上岸说' }
}).then(data => {
    console.log('成功:', data);
}).catch(err => {
    console.error('失败:', err);
});

5.3 支持取消

function jsonp(options) {
    const cbName = `jsonp_callback_${String(new Date().getTime())}`;
    let queryString = options.url.includes('?') ? '&' : '?';
    let script = null;

    // 构建查询字符串
    if (options.data) {
        for (const k in options.data) {
            if (Object.prototype.hasOwnProperty.call(options.data, k)) {
                queryString += `${k}=${encodeURIComponent(options.data[k])}&`;
            }
        }
    }
    queryString += `callback=${cbName}`;

    const promise = new Promise((resolve, reject) => {
        script = document.createElement('script');
        script.src = options.url + queryString;

        window[cbName] = function(data) {
            cleanup();
            resolve(data);
        };

        script.onerror = function() {
            cleanup();
            reject(new Error('JSONP request failed'));
        };

        function cleanup() {
            if (script && script.parentNode) {
                script.parentNode.removeChild(script);
            }
            delete window[cbName];
        }

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

    // 取消方法
    promise.cancel = function() {
        if (script && script.parentNode) {
            script.parentNode.removeChild(script);
        }
        delete window[cbName];
    };

    return promise;
}

六、业务场景

6.1 跨域 API 调用

// 自己公司域下调用一些公共 API 获取数据,涉及到跨域,用 JSONP 解决
jsonp({
    url: 'https://api.example.com/public-data',
    data: {
        apiKey: 'your-key'
    },
    success(data) {
        console.log('获取到数据:', data);
        // 处理数据
        processData(data);
    }
});

6.2 第三方服务集成

// 集成第三方地图服务
jsonp({
    url: 'https://maps.example.com/api/geocode',
    data: {
        address: '北京市',
        key: 'your-api-key'
    },
    success(result) {
        console.log('地理编码结果:', result);
        displayOnMap(result);
    }
});

七、JSONP 的优缺点

7.1 优点

  1. 兼容性好:支持所有浏览器
  2. 实现简单:不需要复杂的配置
  3. 跨域支持:可以请求跨域资源

7.2 缺点

  1. 只能 GET 请求:无法使用 POST、PUT 等方法
  2. 安全性问题:容易受到 XSS 攻击
  3. 错误处理困难:无法获取 HTTP 状态码
  4. 只能传输 JSON:格式受限

7.3 现代替代方案

  • CORS:跨域资源共享(推荐)
  • 代理服务器:通过服务器转发请求
  • WebSocket:实时通信

八、安全注意事项

8.1 XSS 防护

// 验证回调函数名,防止 XSS
function isValidCallbackName(name) {
    return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
}

// 使用
if (!isValidCallbackName(cbName)) {
    throw new Error('Invalid callback name');
}

8.2 数据验证

window[cbName] = function(data) {
    // 验证数据格式
    if (!data || typeof data !== 'object') {
        console.error('Invalid data format');
        return;
    }
    
    obj.success(data);
    cleanup();
};

九、面试要点总结

核心知识点

  1. JSONP 原理:利用 <script> 标签跨域
  2. 实现步骤:生成回调名 → 创建 script → 设置 src → 执行回调 → 清理
  3. 优缺点:兼容性好但只能 GET,安全性较差
  4. 现代替代:CORS、代理服务器

常见面试题

Q1: 什么是 JSONP?如何实现?

答:JSONP 是一种跨域解决方案,利用 <script> 标签不受同源策略限制。实现步骤:生成唯一回调函数名,创建 script 标签,设置 src 为跨域 URL + 回调函数名,服务器返回 JavaScript 代码执行回调函数。

Q2: JSONP 的优缺点?

答:

  • 优点:兼容性好,实现简单,支持跨域
  • 缺点:只能 GET 请求,安全性问题,错误处理困难

Q3: JSONP 和 CORS 的区别?

答:

  • JSONP:利用 script 标签,只能 GET,需要服务器配合
  • CORS:HTTP 头机制,支持所有方法,更安全

实战建议

  • ✅ 理解 JSONP 的实现原理
  • ✅ 注意安全性问题(XSS 防护)
  • ✅ 现代项目优先使用 CORS
  • ✅ 了解 JSONP 的适用场景和限制
#前端面试##银行##百度##字节##阿里#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务