【前端面试小册】JS-第30节:实现 JSONP
一、核心概念
1.1 什么是 JSONP
JSONP(JSON with Padding)是一种跨域数据请求的解决方案。
原理:
- 利用
<script>标签不受同源策略限制的特性 - 通过动态创建
<script>标签,请求跨域资源 - 服务器返回的数据作为 JavaScript 代码执行
1.2 为什么需要 JSONP
由于浏览器的同源策略,普通的 XMLHttpRequest 或 fetch 无法直接请求跨域资源。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 优点
- 兼容性好:支持所有浏览器
- 实现简单:不需要复杂的配置
- 跨域支持:可以请求跨域资源
7.2 缺点
- 只能 GET 请求:无法使用 POST、PUT 等方法
- 安全性问题:容易受到 XSS 攻击
- 错误处理困难:无法获取 HTTP 状态码
- 只能传输 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();
};
九、面试要点总结
核心知识点
- JSONP 原理:利用
<script>标签跨域 - 实现步骤:生成回调名 → 创建 script → 设置 src → 执行回调 → 清理
- 优缺点:兼容性好但只能 GET,安全性较差
- 现代替代: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天学完,上岸银行总行!