Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Q439】JSONP 的原理是什么,如何实现 #447

Open
shfshanyue opened this issue Dec 13, 2020 · 1 comment
Open

【Q439】JSONP 的原理是什么,如何实现 #447

shfshanyue opened this issue Dec 13, 2020 · 1 comment

Comments

@shfshanyue
Copy link
Owner

No description provided.

@shfshanyue shfshanyue added the js label Dec 13, 2020
@shfshanyue
Copy link
Owner Author

shfshanyue commented Dec 18, 2020

摘自山月的博客,原文地址: https://shanyue.tech/code/jsonp/

一个正常的请求: JSON

正常发请求时,curl 示例:

$ curl https://shanyue.tech/api/user?id=100

{
  "id": 100,
  "name": "shanyue",
  "wechat": "xxxxx",
  "phone": "183xxxxxxxx"
}

使用 fetch 发送请求,示例:

const data = await fetch('https://shanyue.tech/api/user?id=100', {
  headers: {
    'content-type': 'application/json',
  },
  method: 'GET',
}).then(res => res.json())

请求数据后,使用一个函数来处理数据

handleData(data)

一个 JSONP 请求

JSONP,全称 JSON with Padding,为了解决跨域的问题而出现。虽然它只能处理 GET 跨域,虽然现在基本上都使用 CORS 跨域,但仍然要知道它,毕竟面试会问

JSONP 基于两个原理:

  1. 动态创建 script,使用 script.src 加载请求跨过跨域
  2. script.src 加载的脚本内容为 JSONP: 即 PADDING(JSON) 格式

从上可知,使用 JSONP 跨域同样需要服务端的支持。curl 示例

$ curl https://shanyue.tech/api/user?id=100&callback=padding

padding({
  "id": 100,
  "name": "shanyue",
  "wechat": "xxxxx",
  "phone": "183xxxxxxxx"
})

对于正常的请求有何不同一目了然: 多了一个 callback=padding, 并且响应数据被 padding 包围,这就是 JSONP

那请求数据后,如何处理数据呢?此时的 padding 就是处理数据的函数。我们只需要在前端实现定义好 padding 函数即可

window.padding = handleData

基于以上两个原理,这里实现一个简单 jsonp 函数:

function jsonp_simple ({ url, onData, params }) {
  const script = document.createElement('script')

  // 一、默认 callback 函数为 padding
  script.src = `${url}?${stringify({ callback: 'padding', ...params })}`

  // 二、使用 onData 作为 window.padding 函数,接收数据
  window['padding'] = onData

  // 三、动态加载脚本
  document.body.appendChild(script)
}

// 发送 JSONP 请求
jsonp_simple({
  url: 'http://localhost:10010',
  params: { id: 10000 },
  onData (data) {
    console.log('Data:', data)
  }
})

此时会有一个问题: window.padding 函数会污染全局变量,如果有多个 JSONP 请求发送如何处理?

使 jsonp 的回调函数名作为一个随机变量,避免冲突,代码如下

function jsonp ({ url, onData, params }) {
  const script = document.createElement('script')

  // 一、为了避免全局污染,使用一个随机函数名
  const cbFnName = `JSONP_PADDING_${Math.random().toString().slice(2)}`

  // 二、默认 callback 函数为 cbFnName
  script.src = `${url}?${stringify({ callback: cbFnName, ...params })}`

  // 三、使用 onData 作为 cbFnName 回调函数,接收数据
  window[cbFnName] = onData;

  document.body.appendChild(script)
}

// 发送 JSONP 请求
jsonp({
  url: 'http://localhost:10010',
  params: { id: 10000 },
  onData (data) {
    console.log('Data:', data)
  }
})

服务器端代码

JSONP 需要服务端进行配合,返回 JSON With Padding 数据,代码如下:

const http = require('http')
const url = require('url')
const qs = require('querystring')

const server = http.createServer((req, res) => {
  const { pathname, query } = url.parse(req.url)
  const params = qs.parse(query)

  const data = { name: 'shanyue', id: params.id }

  if (params.callback) {
    // 服务端将要返回的字符串
    str = `${params.callback}(${JSON.stringify(data)})`
    res.end(str)
  } else {
    res.end()
  }
})

server.listen(10010, () => console.log('Done'))

完整代码附录

完整代码可见山月博客的 github 仓库: https://github.com/shfshanyue/blog/tree/master/code/jsonp/

JSONP 实现完整代码:

function stringify (data) {
  const pairs = Object.entries(data)
  const qs = pairs.map(([k, v]) => {
    let noValue = false
    if (v === null || v === undefined || typeof v === 'object') {
      noValue = true
    }
    return `${encodeURIComponent(k)}=${noValue ? '' : encodeURIComponent(v)}`
  }).join('&')
  return qs
}

function jsonp ({ url, onData, params }) {
  const script = document.createElement('script')

  // 一、为了避免全局污染,使用一个随机函数名
  const cbFnName = `JSONP_PADDING_${Math.random().toString().slice(2)}`
  // 二、默认 callback 函数为 cbFnName
  script.src = `${url}?${stringify({ callback: cbFnName, ...params })}`
  // 三、使用 onData 作为 cbFnName 回调函数,接收数据
  window[cbFnName] = onData;

  document.body.appendChild(script)
}

JSONP 服务端适配相关代码:

const http = require('http')
const url = require('url')
const qs = require('querystring')

const server = http.createServer((req, res) => {
  const { pathname, query } = url.parse(req.url)
  const params = qs.parse(query)

  const data = { name: 'shanyue', id: params.id }

  if (params.callback) {
    str = `${params.callback}(${JSON.stringify(data)})`
    res.end(str)
  } else {
    res.end()
  }

})

server.listen(10010, () => console.log('Done'))

JSONP 页面调用相关代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script src="./index.js" type="text/javascript"></script>
  <script type="text/javascript">
  jsonp({
    url: 'http://localhost:10010',
    params: { id: 10000 },
    onData (data) {
      console.log('Data:', data)
    }
  })
  </script>
</body>
</html>

JSONP 实现代码示例演示

文件结构

  • index.js: jsonp 的简单与复杂实现
  • server.js: 服务器接口形式
  • demo.html: 前端如何调用 JSONP

快速演示

// 开启服务端
$ node server.js

// 对 demo.html 起一个服务,并且按照提示在浏览器中打开地址,应该是 http://localhost:5000
// 观察控制台输出 JSONP 的回调结果
$ serve .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant