JSONP揭开神秘面纱:它是什么以及它为什么存在

英文原文: https://blog.logrocket.com/jsonp-demystified-what-it-is-and-why-it-exists/

JSONP一直是所有Web开发中最难以解释的概念之一。这可能是由于其名称混乱和总体背景不佳。在采用跨域资源共享(CORS)标准之前,JSONP是从其他来源的服务器获取JSON响应的唯一选择。

将请求发送到不支持CORS的其他来源的服务器后,将引发以下错误:

看到这一点,许多人会谷歌搜索它,只是发现需要JSONP来绕开同源策略。然后,jQuery在当今无处不在,它将以其方便的JSONP实现直接出现在核心库中,因此我们只需切换一个参数即可使其工作。许多人从未理解过,彻底改变的是发送请求的基本机制。

$.ajax({
 url: 'http://twitter.com/status/user_timeline/padraicb.json?count=10',
 dataType: 'jsonp',
 success: function onSuccess() { }
});

为了理解幕后发生了什么,让我们来看看JSONP到底是什么。

What is JSONP?

带填充的JSON—简称JSONP—是一种允许开发人员通过使用 <script> 元素的性质来绕过浏览器强制的同源策略的技术。该政策禁止阅读任何来自不同网站的回复,这些网站的来源与目前使用的不同。顺便说一句,该策略允许发送请求,但不允许读取请求。

网站的起源由三部分组成. 首先是URI方案 (i.e., https://), 然后是主机名(i.e., logrocket.com), 最后是 (i.e., 443). 网站 http://logrocket.com 和 https://logrocket.com 由于URI方案的不同而有两个不同的起源.

如果你想更多地了解这项政策,就别再找了。

它是如何工作的?

假设我们在localhost:8000上,并向提供JSON API的服务器发送请求。

https://www.server.com/api/person/1

响应可能是这样的:

{
  "firstName": "Maciej",
  "lastName": "Cieslar"
}

但由于上述政策,请求将被阻止,因为网站的起源和服务器不同。

可以使用 <script> 元素而不是自己发送请求,策略并不适用于此元素—它可以从外部源加载和执行JavaScript。通过这种方式,位于https://logrocket.com 的网站可以从位于不同来源的提供商(即CDN)。

通过向<script> 的src属性提供API的端点URL,<script> 将获取响应并在浏览器上下文中执行它。

src="https://www.server.com/api/person/1"

但问题是,<script>元素会自动解析并执行返回的代码。在本例中,返回的代码是上面显示的JSON片段。JSON将被解析为JavaScript代码,并因此抛出一个错误,因为它不是有效的JavaScript。

必须返回一个完全工作的JavaScript代码,以便<script>正确地解析和执行它。如果我们将JSON代码分配给一个变量或将其作为参数传递给一个函数,JSON代码就可以正常工作—毕竟,JSON格式只是一个JavaScript对象。

因此,服务器可以返回JavaScript代码,而不是返回纯JSON响应。在返回的代码中,一个函数被包装在JSON对象周围。函数名必须由客户机传递,因为代码将在浏览器中执行。函数名在名为callback的查询参数中提供。

在查询中提供回调的名称之后,我们在全局(window)上下文中创建一个函数,一旦解析并执行响应,就会调用这个函数。

https://www.server.com/api/person/1?callback=callbackName
callbackName({
  "firstName": "Maciej",
  "lastName": "Cieslar"
})

即:

window.callbackName({
  "firstName": "Maciej",
  "lastName": "Cieslar"
})

代码在浏览器的上下文中执行。函数将在全局范围内<script>中下载的代码中执行。

为了让JSONP工作,客户机和服务器都必须支持它。虽然定义函数名称的参数没有标准名称,但是客户机通常会在名为callback的查询参数中发送它。

实现

让我们创建一个名为jsonp的函数,它将以jsonp的方式发送请求。

let jsonpID = 0;

function jsonp(url, timeout = 7500) {
  const head = document.querySelector('head');
  jsonpID += 1;

  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    const callbackName = `jsonpCallback${jsonpID}`;

    script.src = encodeURI(`${url}?callback=${callbackName}`);
    script.async = true;

    const timeoutId = window.setTimeout(() => {
      cleanUp();

      return reject(new Error('Timeout'));
    }, timeout);

    window[callbackName] = data => {
      cleanUp();

      return resolve(data);
    };

    script.addEventListener('error', error => {
      cleanUp();

      return reject(error);
    });

    function cleanUp() {
      window[callbackName] = undefined;
      head.removeChild(script);
      window.clearTimeout(timeoutId);
      script = null;
    }


    head.appendChild(script);
  });
}

如您所见,有一个名为jsonpID的共享变量—它将用于确保每个请求都有自己惟一的函数名。

首先,我们将对<head>对象的引用保存在一个名为head的变量中。然后增加jsonpID以确保函数名是惟一的。在为返回的promise提供的回调中,我们创建了一个<script>元素和callbackName,它由字符串jsonpCallback和惟一的ID组成。

然后,我们将<script>元素的src属性设置为提供的URL。在查询内部,我们将回调参数设置为等于callbackName。请注意,这个简化的实现不支持具有预定义查询参数的url,因此它不适用于https://logrocket.com/?param=true ,因为我们要追加?最后再一次。

我们还将async属性设置为true,以使脚本是非阻塞的。

该请求有三种可能的结果:

  1. 请求成功了,希望执行window[callbackName],它用结果解析承诺(JSON)
  2. <script>元素抛出一个错误,我们拒绝承诺
  3. 请求花费的时间比预期的长,超时回调开始起作用,抛出超时错误
const timeoutId = window.setTimeout(() => {
  cleanUp();

  return reject(new Error('Timeout'));
}, timeout);

window[callbackName] = data => {
  cleanUp();

  return resolve(data);
};

script.addEventListener('error', error => {
  cleanUp();

  return reject(error);
});

回调必须在window对象上注册,才能在创建的<script>上下文中使用。在全局范围内执行一个名为callback()的函数相当于调用window.callback()

通过在cleanup函数中抽象清理过程,三个回调—超时、成功和错误侦听器—看起来完全相同。唯一的区别是他们是否解决或拒绝承诺。

function cleanUp() {
  window[callbackName] = undefined;
  head.removeChild(script);
  window.clearTimeout(timeoutId);
  script = null;
}

cleanUp函数是为了在请求之后进行清理而需要做的事情的抽象。该函数首先删除注册在窗口上的回调,该回调在成功响应时调用。然后从<head>中删除<script>元素并清除超时。另外,只是为了确定,它将脚本引用设置为null,以便对其进行垃圾收集。

最后,我们将<script>元素附加到<head>以触发请求。<script>一旦请求被附加,将自动发送请求。

下面是这个用法的例子:

jsonp('https://gist.github.com/maciejcieslar/1c1f79d5778af4c2ee17927de769cea3.json')
 .then(console.log)
 .catch(console.error);

这是一个例子。

总结

通过了解JSONP的基本机制,您可能不会获得太多直接适用的web技能,但是看到人们的创造力如何绕过最严格的政策总是很有趣的。

JSONP是过去的,不该使用它由于众多的限制(例如,能够发送GET请求)和许多安全问题(例如,服务器可以应对任何JavaScript代码,它希望——不一定是一个我们期望——然后可以访问所有的窗口,包括localStorage和cookies)。

相反,我们应该依赖于CORS机制来提供安全的跨源请求。

英文原文: https://blog.logrocket.com/jsonp-demystified-what-it-is-and-why-it-exists/

李, 国轩