JSONP是什么

301 阅读4分钟

JSONP 是什么

Jsonp (JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。

需求:点击按钮付款一块钱

<h5>您的账户余额是<span id="amount">&&&amount&&&</span></h5>
<button id="button">付款</button>
  • form 发请求
<form action="/pay" method="POST" target="result">
  <input type="submit" value="付款">
</form>
<iframe name="result" src="about:black" frameborder="0" height="200"></iframe>

后端部分代码:

if(path === '/'){
  let string=fs.readFileSync('./index.html','utf8')
  let amount=fs.readFileSync('./db','utf8')
  string=string.replace('&&&amount&&&',amount)
  response.statusCode = 200
  response.setHeader('Content-Type', 'text/html;charset=utf-8')
  response.write(string)
  response.end()
}else if (path === '/pay') {
  let amount = fs.readFileSync('./db', 'utf8')
  let number = amount - 1
  if(Math.random() > 0.5){	//假设大于0.5就成功
    fs.writeFile('./db', number)
    response.write('success')
  }else{
    response.write('fail')
  }
  response.end()
}

form 表单提交之后一定会刷新当前页面,这样用户体验不好,利用 iframe 局部刷新页面,优化用户体验。

有没有想过,不返回 HTML,返回 JS

  • 方案一:用图片造 get 请求

浏览器有个特点一旦发现你在内存里创建了一个 img,就会去请求这个 img ,这个方法可以悄无声息的创造一个请求,但有个缺陷无法设置 POST。那就 发GET请求吧,总比用 iframe 刷新页面好。

button.addEventListener('click', () => {
  let image = document.createElement('img')
  image.src = './pay'
  image.onload = function(){
    alert('付款成功')
    amount.innerText = amount.innerText -1
  }
  image.onerror = function(){
    alert('付款失败')
  }
})

浏览器怎么知道一个图片请求是成功还是失败呢?状态码说的很清楚,假设如果成功就返回 200,失败返回 400

if (path === '/pay') {
  let amount = fs.readFileSync('./db', 'utf8')
  let number = amount - 1
  if(Math.random() > 0.5){
    fs.writeFile('./db', number)
    response.setHeader('Content-Type', 'image/png')
    response.statusCode = 200
    response.write(fs.readFileSync('./th.jpg'))
  }else{
    response.statusCode = 400
    response.write('fail')
  }
  response.end()
}

这样就做到了无刷新的也没有用什么特殊的技术,只用了一个小技巧就是创建了一个 img 用它发请求。现在的问题就是无法 POST,因为浏览器没有给我们一个接口让我们 POST,所以那就只好 GET 了, 这种方法只能知道成功或失败,不能知道更多的数据。

  • 方案二:用 scriptget 请求

这种方法一定要把 script 放到页面里面,浏览器才会发起请求。怎么样才能知道是请求成功还是失败了呢,当然 script 也有 onloadonerror 事件。

button.addEventListener('click', () => {
  let script = document.createElement('script')
  script.src = '/pay'
  document.body.appendChild(script)
  script.onload = function(){
    alert('付款成功')
    amount.innerText = amount.innerText -1
  }
  script.onerror = function(){
    alert('付款失败')
  }
}

改进之后的好处就是不用返回图片了,返回一个字符串就可以了,这样请求就会快一点了。

点击 button 后,就会去创建一个 script 标签会放在页面的最后方,由于页面中出现了一个 script ,浏览器就会将 /pay 里面的内容执行掉。这样就不需要去监听 onload 事件,就监听 onerror 事件就可以了。 onload 之间由 /pay 里面的内容执行,服务器直接返回了在浏览器执行的一个 js 字符串(代码)。

if (path === '/pay') {
  let amount = fs.readFileSync('./db', 'utf8')
  let number = amount - 1
  if(Math.random() > 0.5){
    fs.writeFile('./db', number)
    response.setHeader('Content-Type', 'application/javascript')
    response.statusCode = 200
    response.write(`
      alert("付款成功")
      amount.innerText = amount.innerText -1
    `)
  }else{
    response.statusCode = 400
    response.write('fail')
  }
  response.end()
}

不过还有一个问题就是没点击一次按钮,页面中都会多出现一个 script ,虽然说不会出现 BUG,但是会很难看,所以在请求成功或失败之后再删除它。

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

这就是整个的完整方案,当用户点击一个动作的时候,生成一个 script ,然后 scriptsrc 就是要请求的路径。然后把 script 放到页面里,这样浏览器就是去发起一个这样路径的 GET 请求(没办法 POST )。如果这个请求成功了,那么它首先会执行那个服务器返回的 javascript 响应,这个响应就是操作页面的局部刷新。

这种技术就叫做 SRJ - Server Rendered JavaScript,服务器返回的 JavaScript。这个就是在 AJAX 出现之前用的无刷新局部更新页面内容。

上面的这个 SRJ 方案,如果没有做任何的安全措施的话,任何一个网站都可以去请求这个 API 操作付款,所以像付款这些重要的操作要使要 POSTGET 太容易伪造了。

JSONP

上面都是都一个网站的前端和后端交流,那如果这个网站的前端想和另一个域名下的接口交流怎么办呢?

前端给后端一个函数,然后后端调用这个函数,要执行的代码不要管,然后返回一个结果。那后台怎么知道这个函数名呢,我们可以在请求的时候传参。

window.yyy = function(result) {
  if(result === 'success'){
    amount.innerText = amount.innerText - 1
  }else{

  }
}
button.addEventListener("click", () => {
  let script = document.createElement("script");
  script.src = 'http://jackma.com:8002/pay?callback=yyy'
  document.body.appendChild(script)
    script.onload = function(e) {
    e.currentTarget.remove()
  }
  script.onerror = function(e) {
    e.currentTarget.remove()
  }
})
if (path === '/pay') {
  let amount = fs.readFileSync('./db', 'utf8')
  let number = amount - 1
  fs.writeFile('./db', number)
  response.setHeader('Content-Type', 'application/javascript')
  response.statusCode = 200
  response.write(`
    ${query.callback}.call(undefined, 'success')
  `)
  response.end()
}

给什么就调什么,这样就完全耦合了, 这就叫做 JSONPJSONP 要结决的一个问题就是两个网站之间怎么交流,我们用一个 script 标签就可以交流了, script 标签是不受域名限制的。既然不受限制,就可以告诉对方网站我要请求一个数据,对方给数据之后再调用我们准备的函数,把参数传到函数的第一个参数里面,我们就可以得到了。

请求方:frank.com 的前端程序员(浏览器)

响应方:jack.com 的后端程序员(服务器)

  1. 请求方创建 scriptsrc 指向响应方,同时传一个查询参数 ?callback=yyy

    yyy 一般是要随机数,这样就不用出现函数名重复的问题。

  2. 响应方根据查询参数 callback,构造形如

    1. yyy.call(undefined, '你要的数据')
    2. yyy('你要的数据') 这样的响应
  3. 浏览器接收到响应,就会执行 yyy.call(undefined, '你要的数据')

  4. 那么请求方就知道了他要的数据

这就是 JSONP

jQuery 的写法:

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

JSONP 为什么不支持 POST 请求

因为 JSONP 是通过动态创建 script 实现的,动态创建 script 的时候只能用 GET 请求,没有办法用POST 请求。