JSONP
基础知识
在深入实现之前,我们需要理解 JSONP 是什么,以及它为什么会出现。
同源策略 (Same-Origin Policy): 这是浏览器的一个核心安全策略。它限制了从一个源加载的文档或脚本,如何与来自另一个源的资源进行交互。简单来说,如果你在
a.com
页面,你的XMLHttpRequest
(Ajax) 请求默认无法访问b.com
的数据接口。这是为了防止恶意网站读取其他网站的敏感数据。<script>
标签的“特权”: 同源策略主要限制的是 Ajax 这类数据请求,但对于一些 HTML 标签则没有此限制,其中最重要的就是<script>
标签。一个页面的<script src="..."></script>
可以无视同源策略,加载并执行来自任何域的 JavaScript 文件。JSONP (JSON with Padding): JSONP 并非一种新的技术,而是一种利用
<script>
标签“特权”来实现跨域数据请求的模式或约定。它通过一种巧妙的方式,让服务器返回的数据能够被不同域的客户端所接收。这里的 "Padding" (填充物) 指的就是包裹在 JSON 数据外层的那个函数调用。
核心思路
JSONP 的核心思路是一场客户端与服务端之间的“双人舞”,双方需遵循一个共同的约定。
客户端准备“接球”的函数: 客户端(浏览器端)首先在全局作用域中定义一个回调函数,比如
handleResponse
。这个函数是用来接收和处理服务器返回的数据的。客户端发起“特殊的”请求: 客户端不使用
XMLHttpRequest
,而是动态地创建一个<script>
标签。将其src
属性指向服务端的接口地址,并且在 URL 的查询参数中,带上一个特殊的参数(通常是callback
或jsonp
),其值就是第一步中准备好的函数名。- 例如:
<script src="http://api.b.com/data?callback=handleResponse"></script>
- 例如:
服务端返回“包裹好的”数据: 服务端接收到请求后,会解析出
callback
参数的值(在这里是handleResponse
)。然后,它不会直接返回纯粹的 JSON 数据(如{"name": "张三"}
),而是将这些数据作为参数,包裹在一个函数调用中,并以 JavaScript 脚本的形式返回。- 服务端返回的内容是这样一个字符串:
handleResponse({"name": "张三"});
- 服务端返回的内容是这样一个字符串:
客户端执行脚本,完成数据交接: 浏览器下载并执行这个由
<script>
标签加载的脚本。执行handleResponse({...})
就相当于直接调用了我们在第一步中定义的全局函数,从而成功地将跨域数据传递到了我们的页面中。
代码实现
下面是一个封装好的、使用 Promise 的现代化 jsonp
函数实现。
/**
* 封装一个支持 Promise 的 JSONP 请求函数
* @param {string} url - 请求的 URL 地址
* @param {object} params - 附加的查询参数对象
* @param {string} [callbackKey='callback'] - 与后端约定的回调函数参数名
* @returns {Promise<any>} - 返回一个 Promise,成功时 resolve 数据,失败时 reject 错误
*/
function jsonp(url, params = {}, callbackKey = 'callback') {
// 返回一个 Promise 对象
return new Promise((resolve, reject) => {
// 1. 创建一个 script 标签
const script = document.createElement('script');
// 2. 生成一个唯一的、挂载在 window 上的回调函数名
const callbackName =
'jsonp_callback_' + Date.now() + Math.random().toString().substr(2);
// 3. 在 window 上创建这个回调函数,用于接收数据
window[callbackName] = (data) => {
// 数据接收成功后,清理工作
delete window[callbackName]; // 删除全局函数
document.body.removeChild(script); // 移除 script 标签
// Promise 状态变为 resolved
resolve(data);
};
// 4. 监听 script 加载失败事件
script.onerror = () => {
// 失败后,同样执行清理工作
delete window[callbackName];
document.body.removeChild(script);
// Promise 状态变为 rejected
reject(new Error('JSONP request to ' + url + ' failed.'));
};
// 5. 组合最终的请求 URL
const queryParams = { ...params, [callbackKey]: callbackName };
const queryString = Object.keys(queryParams)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`
)
.join('&');
script.src = `${url}${url.includes('?') ? '&' : '?'}${queryString}`;
// 6. 将 script 标签插入到 DOM 中,浏览器会自动发起请求
document.body.appendChild(script);
});
}
代码解析
- Promise 封装:整个函数返回一个
Promise
,这使得我们可以使用.then()
和.catch()
或者async/await
来处理异步操作,是现代JavaScript
的标准实践。 - 唯一回调函数名:
const callbackName = 'jsonp_callback_...'
。我们不能使用固定的函数名,因为页面上可能同时发起多个JSONP
请求,固定的名字会产生冲突。通过时间戳和随机数生成一个几乎不可能重复的函数名是最佳实践。 - 创建全局回调(window[callbackName] = ...):这是
JSONP
的客户端核心。我们必须将回调函数挂载到全局对象window
上,这样从<script>
标签返回的callbackName({...})
才能在全局作用域中找到并执行它。在函数内部,一旦resolve(data)
被调用,就意味着请求成功,我们立即执行清理工作:删除window
上的这个临时函数和DOM
中的这个临时<script>
标签,避免内存泄漏和页面污染。 - 错误处理(script.onerror):
script
标签无法像XMLHttpRequest
那样提供详细的HTTP
状态码。它只有一个onerror
事件,可以捕获到脚本加载失败的情况(如网络错误、404 Not Found
等)。在onerror
时,我们也必须执行同样的清理工作,并reject
这个Promise
。 - URL 构造:这部分代码负责将用户传入的
url
、params
对象以及我们生成的callback
参数拼接成一个完整的请求URL
。它会妥善处理?
和&
的连接,并对参数进行URL
编码。 - 注入并执行:
document.body.appendChild(script)
是触发整个流程的“扳机”。一旦script
标签被插入到DOM
中,浏览器就会立即对其src
属性发起GET
请求。