在现代Web开发中,跨域请求是一个绕不开的话题。JSONP作为早期的跨域解决方案,虽然已被CORS逐渐取代,但理解其原理对于深入掌握Web技术仍然具有重要意义。本文将带你深入了解JSONP的工作机制,并通过实际代码演示其应用。
01|JSONP的基本概念与诞生背景
JSONP(JSON with Padding,填充式JSON)是一种跨域数据交互协议,它利用<script>标签不受同源策略限制的特性,实现了跨域数据的获取。在CORS出现之前,JSONP是解决跨域问题的标准方案。
同源策略的限制
浏览器出于安全考虑,实施了同源策略(Same-Origin Policy):只有当协议、域名、端口都相同时,才允许进行AJAX请求。这个策略虽然保护了用户安全,但也给前后端分离的开发模式带来了挑战。
// 这是一个会被浏览器拦截的跨域AJAX请求
fetch('https://api.other-domain.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('跨域错误:', error));JSONP的解决思路
JSONP巧妙地利用了<script>标签的src属性不受同源策略限制的特性。通过动态创建script标签,并将回调函数名作为参数传递给服务器,服务器返回一段可执行的JavaScript代码,从而实现跨域数据的获取。
02|JSONP的工作机制详解
核心原理
JSONP的工作流程可以分为以下几个步骤:
- 前端定义回调函数:在全局作用域中定义一个处理数据的函数
- 动态创建script标签:构造带有回调函数名的URL
- 服务器返回包装数据:服务器将JSON数据包装在回调函数中
- 浏览器执行响应:script标签加载完成后自动执行返回的JavaScript代码
- 回调函数处理数据:定义的回调函数被调用,接收服务器数据
工作流程图
sequenceDiagram
participant Browser as 浏览器
participant Script as Script标签
participant Server as 服务器
Browser->>Browser: 定义回调函数handleData()
Browser->>Script: 动态创建script标签
Script->>Server: 请求https://api.com/data?callback=handleData
Server->>Server: 包装数据:handleData({"name": "张三"})
Server->>Script: 返回:handleData({"name": "张三"})
Script->>Browser: 执行返回的JavaScript代码
Browser->>Browser: 调用handleData函数处理数据
03|前端实现JSONP的完整代码
基础实现版本
/**
* JSONP请求工具函数
* @param {string} url - 请求的URL
* @param {object} params - 请求参数
* @param {string} callbackName - 回调函数名(可选)
* @param {number} timeout - 超时时间(毫秒)
*/
function jsonp(url, params = {}, callbackName = null, timeout = 5000) {
return new Promise((resolve, reject) => {
// 生成唯一的回调函数名
const callback = callbackName || `jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 创建script标签
const script = document.createElement('script');
// 构建完整的URL
const queryParams = new URLSearchParams({
...params,
callback: callback
});
const fullUrl = `${url}${url.includes('?') ? '&' : '?'}${queryParams.toString()}`;
// 设置超时处理
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('JSONP请求超时'));
}, timeout);
// 定义全局回调函数
window[callback] = (data) => {
cleanup();
resolve(data);
};
// 清理函数
function cleanup() {
clearTimeout(timeoutId);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callback];
}
// 错误处理
script.onerror = () => {
cleanup();
reject(new Error('JSONP请求失败'));
};
// 发送请求
script.src = fullUrl;
document.head.appendChild(script);
});
}
// 使用示例
jsonp('https://api.example.com/user', { id: 123 })
.then(data => {
console.log('获取到的数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});增强版本(支持取消请求)
class JSONPClient {
constructor() {
this.activeRequests = new Map();
}
request(url, params = {}, options = {}) {
const {
callbackName = null,
timeout = 5000,
charset = 'utf-8'
} = options;
const requestId = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const callback = callbackName || `jsonp_callback_${requestId}`;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.charset = charset;
// 构建URL
const queryParams = new URLSearchParams({
...params,
callback: callback
});
const fullUrl = `${url}${url.includes('?') ? '&' : '?'}${queryParams.toString()}`;
// 超时处理
const timeoutId = setTimeout(() => {
this._cleanup(requestId);
reject(new Error('JSONP请求超时'));
}, timeout);
// 存储请求信息
this.activeRequests.set(requestId, {
script,
timeoutId,
callback,
resolve,
reject
});
// 定义回调函数
window[callback] = (data) => {
this._cleanup(requestId);
resolve(data);
};
// 错误处理
script.onerror = () => {
this._cleanup(requestId);
reject(new Error('JSONP请求失败'));
};
// 发送请求
script.src = fullUrl;
document.head.appendChild(script);
});
}
cancel(requestId) {
if (this.activeRequests.has(requestId)) {
this._cleanup(requestId);
return true;
}
return false;
}
cancelAll() {
this.activeRequests.forEach((_, requestId) => {
this._cleanup(requestId);
});
}
_cleanup(requestId) {
const request = this.activeRequests.get(requestId);
if (request) {
const { script, timeoutId, callback } = request;
clearTimeout(timeoutId);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callback];
this.activeRequests.delete(requestId);
}
}
}
// 使用示例
const jsonpClient = new JSONPClient();
// 发起请求
const requestPromise = jsonpClient.request('https://api.example.com/data', { type: 'user' })
.then(data => {
console.log('成功获取数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
// 取消请求(如果需要)
// jsonpClient.cancel(requestId);04|服务端配合JSONP的实现
Node.js实现
const express = require('express');
const app = express();
// JSONP中间件
function jsonpMiddleware(req, res, next) {
const originalJson = res.json;
res.json = function(data) {
const callback = req.query.callback;
if (callback && typeof callback === 'string') {
// 验证回调函数名的合法性(防止XSS攻击)
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(callback)) {
return res.status(400).json({ error: 'Invalid callback parameter' });
}
// 设置正确的Content-Type
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
// 返回包装后的数据
const jsonpResponse = `${callback}(${JSON.stringify(data)})`;
return res.send(jsonpResponse);
}
// 如果不是JSONP请求,使用原始的json方法
return originalJson.call(this, data);
};
next();
}
// 使用中间件
app.use(jsonpMiddleware);
// 示例路由
app.get('/api/user', (req, res) => {
const userData = {
id: req.query.id || 1,
name: '张三',
email: 'zhangsan@example.com',
timestamp: Date.now()
};
res.json(userData);
});
app.get('/api/weather', (req, res) => {
const weatherData = {
city: req.query.city || '北京',
temperature: 25,
description: '晴朗',
humidity: 60
};
res.json(weatherData);
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`JSONP服务器运行在端口 ${PORT}`);
});PHP实现
<?php
class JSONPHandler {
/**
* 发送JSONP响应
* @param mixed $data 要返回的数据
* @param string $callback 回调函数名
* @param int $statusCode HTTP状态码
*/
public static function jsonp($data, $callback = null, $statusCode = 200) {
// 设置HTTP状态码
http_response_code($statusCode);
// 获取回调函数名
if (!$callback) {
$callback = isset($_GET['callback']) ? $_GET['callback'] : null;
}
// 设置 响应头
header('Content-Type: application/javascript; charset=utf-8');
// 处理JSONP响应
if ($callback && self::isValidCallback($callback)) {
// 返回JSONP格式数据
echo $callback . '(' . json_encode($data, JSON_UNESCAPED_UNICODE) . ')';
} else {
// 返回普通JSON数据
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
exit;
}
/**
* 验证回调函数名的合法性
* @param string $callback
* @return bool
*/
private static function isValidCallback($callback) {
// 验证回调函数名是否合法(防止XSS攻击)
return preg_match('/^[a-zA-Z_$][a-zA-Z0-9_$]*$/', $callback);
}
/**
* 创建API响应数据
* @param bool $success
* @param mixed $data
* @param string $message
* @return array
*/
public static function createResponse($success, $data = null, $message = '') {
return [
'success' => $success,
'data' => $data,
'message' => $message,
'timestamp' => time()
];
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = isset($_GET['action']) ? $_GET['action'] : '';
switch ($action) {
case 'user':
$userData = [
'id' => isset($_GET['id']) ? intval($_GET['id']) : 1,
'name' => '李四',
'email' => 'lisi@example.com',
'phone' => '13800138000'
];
JSONPHandler::jsonp(JSONPHandler::createResponse(true, $userData));
break;
case 'products':
$products = [
['id' => 1, 'name' => '产品A', 'price' => 99.99],
['id' => 2, 'name' => '产品B', 'price' => 149.99],
['id' => 3, 'name' => '产品C', 'price' => 199.99]
];
JSONPHandler::jsonp(JSONPHandler::createResponse(true, $products));
break;
default:
JSONPHandler::jsonp(JSONPHandler::createResponse(false, null, '未知操作'), null, 404);
}
}
?>05|JSONP的优缺点分析
优点
- 兼容性好:支持所有浏览器,包括古老的IE6+
- 实现简单:不需要复杂的配置,前后端实现都很直接
- 无需 服务器特殊配置:不像CORS需要配置复杂的响应头
- 轻量级:没有额外的HTTP头部信息,传输效率高
缺点
- 仅支持GET请求:无法使用POST、PUT、DELETE等其他HTTP方法
- 安全性问题:容易受到XSS攻击,需要严格的回调函数名验证
- 错误处理困难:script标签的错误处理机制不完善
- 无法设置请求头:不能自定义HTTP头部信息
- 不支持现代特性:如进度监控、超时控制等
06|JSONP与CORS的对比
| 特性 | JSONP | CORS |
|---|---|---|
| 浏览器支持 | 所有浏览器 | 现代浏览器(IE10+) |
| HTTP方法 | 仅GET | 所有方法(GET、POST、PUT、DELETE等) |
| 安全性 | 较低,需额外防护 | 较高,浏览器原生支持 |
| 错误处理 | 困难 | 完善 |
| 请求头自定义 | 不支持 | 支持 |
| 响应头访问 | 受限 | 完整访问 |
| 实现复杂度 | 简单 | 需要服务器配置 |
| 性能 | 较好(无额外头部) | 稍差(需要预检请求) |
现代开发建议
在新项目中,推荐使用CORS而不是JSONP,因为:
- CORS提供了更完整的跨域解决方案
- 安全性更高,浏览器原生支持
- 支持所有HTTP方法和自定义头部
- 错误处理机制完善
JSONP主要适用于:
- 需要支持极老浏览器的项目
- 简单的GET请求跨域场景
- 第三方API只支持JSONP的情况