JSONP初探

同源&跨域

在了解 jsonp 之前,需要先了解两个概念: 同源、跨域

什么是同源?简单来说,所谓的同源,就是若这堆 url 中的 协议/域名/端口 这三个要素分别相等,则我们判定这堆 url 同源;
什么是跨域?它实际上的意思跟同源相反,就是若这堆 url 中的 协议/域名/端口 有任何一个不同,则我们判定这堆 url 跨域

以下是表
| url | 结果 | 原因 |
|—————————–|——|————|
| http://127.0.0.1:3000/index | 同源 | 三个都相同 |
| https://127.0.0.1:3000 | 跨域 | 协议不同 |
| https://localhost:3000 | 跨域 | 域名不同 |
| http://127.0.0.1:3001 | 跨域 | 端口不同 |

现在浏览器的同源策略变得很严格,为什么? 因为它要保证用户信息的安全,防止恶意窃取用户信息,你登录一个网站需要 cookie,然后你又去浏览其他网站,
若没有同源策略限制,其他网站就能够读取到你登录的网站的 cookie(这就是跨域访问了),冒充你登录,查看你 cookie 的隐私信息,就很危险。所以对于非同源
来说,就有如下三种限制

  1. 不能读取 cookie/localStorage/indexDB
  2. 不能获取 DOM 节点
  3. 不能发送一般的 AJAX 请求

但是有时候做开发我们又希望能够有以上三种限制的权限好去完成一些需求,这个时候老一辈工程师就想了一些办法(我就是想跨个域)

跨域方式

很容易想到的方式就是用标签,我们知道 iframe,form,img,link,script 这些标签都可以发送请求,但是它们的表现怎么样呢?
假设 html 主要代码如下

<h1 id="amount">{{amount}}</h1>
<button id="button">发送请求</button>

后端 node.js 主要代码如下

...
if(path === '/pay') {
let amount = fs.readFileSync('./db', 'utf8')
amount -= 1
fs.writeFileSync('./db', amount)
// 返回 js 代码
response.setHeader('Content-Type', 'application/javascript')
// 返回时浏览器会自动执行这句话 amount.innertext = '88'
response.write('amount.innerText = ' + amount)
response.end()
}
...

iframe 发送请求

button.addEventListener('click', (e)=> {
let dom = document.createElement('iframe')
document.body.appendChild(dom)
dom.src = '/pay'
dom.onload = function (e) { // 状态码是 200~299 则表示成功
e.currentTarget.remove()
alert('成功')
}
dom.onerror = function (e) { // 状态码大于等于 400 则表示失败
e.currentTarget.remove()
alert('失败')
}
})

这种方式是可以,但是会出现一个页面 bug,就是只有它先被 body append 之后才能发送请求,之后还要销毁掉它,这样就不好了

form 发送请求

button.addEventListener('click', (e)=> {
let dom = document.createElement('form')
dom.src = '/pay'
dom.method = 'POST'
document.body.appendChild(dom)
dom.submit()
dom.onload = function (e) { // 状态码是 200~299 则表示成功
e.currentTarget.remove()
alert('成功')
}
dom.onerror = function (e) { // 状态码大于等于 400 则表示失败
e.currentTarget.remove()
alert('失败')
}
})

但是这个每次点击 submit 后就直接刷新页面,所以后端也没接收到 ‘/pay’ 的相关请求, form 方案也是不行的

img 发送请求

button.addEventListener('click', (e)=>{
let dom = document.createElement('img')
dom.src = '/pay'
dom.onload = function(){
alert('成功')
}
dom.onerror = function(){
alert('失败')
}
})

img 虽然能发送请求吧,但是由于img标签本身需要一个图片url(有效的),表明其加载的资源有效,才不会触发 error 事件被 window
捕获到,这也是不行的

button.addEventListener('click', (e)=>{
let dom = document.createElement('link')
dom.rel = '/pay'
dom.onload = function(){
alert('成功')
}
dom.onerror = function(){
alert('失败')
}
})

嗯。。。完全没反应啊,哈哈,所以实际上动态创建 link 发请求的方式也不行啊

用 script 发送请求

好了终于用到 script 标签了,看看它的表现如何

button.addEventListener('click', (e)=>{
let dom = document.createElement('script')
dom.src = '/pay'
document.body.appendChild(dom)
dom.onload = function(){
alert('成功')
e.currentTarget.remove()
}
dom.onerror = function(){
alert('失败')
e.currentTarget.remove()
}
})

试验结果,很好,我们终于找到一个标签,它能够正确的发送请求,以及正确的触发了 load 事件,我们终于可以跨域了

终结

我们刚刚尝试出了一种方式,用 script 标签发送请求后,后端返回一串 javascript 代码字符串然后被浏览器自动执行了,
这是什么?这就是 SRJ(Server Rendered Javascript),也是一种无刷新并能进行局部页面更新的一套方案

举个实例,我们在引用另一个网站的 js 代码的时候,比如

<script src=//code.juery.com/jquery-2.1.1.min.js></script>

就是在请求另一个网站的 script, 也是上述方案的应用

JSONP

好了,有了以上的理解,那么 JSONP 就基本上知道是怎么回事了,JSONP 就是通过动态加载 script 标签来完成对目标 url 的请求
它的全称叫 JSON + padding, 也叫 String + padding
以下是 JSONP 方式过程

请求方: baidu.com 的前端程序员(浏览器)
响应方: tencent.com 的后端程序员(服务器)

  • 请求方创建 script, src 指向响应方,同时传一个查询参数像 ?callbackName=yyy

    function addScriptTag(src) {
    var script = document.createElement('script');
    script.setAttribute("type","text/javascript");
    script.src = src;
    document.body.appendChild(script);
    }
    function foo(data) {
    console.log('Your public IP address is: ' + data.ip);
    };

    window.onload = function () {
    addScriptTag('http://tencent.com/ip?callback=foo');
    }
  • 响应方则根据查询参数 callbackName ,构造形如

    • yyy.call(undefind, '你要的数据')
    • yyy('你要的数据')
      这样的响应

      foo({
      "ip": "8.8.8.8"
      })
  • 浏览器接收到响应,就会执行 yyy.call(undefined, '你要的数据')

  • 那么请求方就知道了它要的数据

以上就是 JSONP

以下是其相关的约定:

  1. callbackName -> callback
  2. yyy -> 随机数 frank 随机数()

试验代码示例在这儿呢

特点归纳

  1. 安全问题,因为请求的代码可能是恶意代码
  2. script 没有 onerror 事件,所以确定 JSONP 请求成功与否不是很容易

jQuery 实现

先引入 jQuery

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

然后添加如下代码

$.ajax({
url: "http://jack.com:8002/pay",
dataType: "jsonp",
success: function( response ) {
console.log(response)
if (response === 'success') {
amount.innerText = amount.innerText - 1
}
}
})

然而以上跟 ajax 没有啥关系

面试题

JSONP 为什么不支持 POST 请求?

  • 因为 JSONP 是通过动态创建 script 实现的
  • 而动态创建的 script 只能用 GET,不能用 POST

参考链接

阮一峰-浏览器同源政策以及其规避方法
跨域 demo 实验