Xu•thus Blog

前端跨域之JSONP的原理及简单实现

在前端开发中,熟练使用ajax从后台接口获取数据是必备的技能。但是为了安全,XmlHttpRequest对象在默认情况下只能访问同源(协议、域名、端口号完全相同)的数据,就是常说的跨域安全策略。在前后端分离的今天,合理跨域至关重要,所以在前端也有很多方法可以实现跨域,比较常用的是JSONP和CORS,当然还有nodejs配合webpack可以实现http代理来达到跨域效果,这个也比较常用。

我们先来用nodejs来起一个简单的服务,当请求为http://localhost:3000/user返回一个json数据

1
2
3
4
5
6
7
8
9
10
11
const http = require('http')
http.createServer((req, res) => {
const data = { name: 'Xuthus' }
if (req.url === '/user') {
res.end(JSON.stringify(data))
} else {
res.end('hello world')
}
}).listen(3000)

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function request(type, url, data) {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.responseText)
} else {
console.log(xhr.status)
}
}
}
xhr.open(type, url, true)
xhr.send(data)
}
request('get', 'http://localhost:3000/user', null)

执行后,就会出现报错,就是同源限制造成的

JSONP实现原理

JSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的一种方式。

JSONP的实现原理很简单,利用<script>标签没有同源限制的特点,也就是<script>的src链接可以访问不同源的。不受同源限制的还有<img><iframe><link>,对应这两个标签实现的跨域方法也有,比如图片ping等。

我们访问服务端,一般是获取存JSON数据,而JSONP则返回的是,包含函数的数据,将我们需要的JSON数据作为函数的参数

1
callback({"name": "xuthus"}

我们在客户端则一般通过<script>标签的src访问带有callback查询参数的请求,来获取返回带有函数的数据,然后执行它

1
2
3
4
5
6
<script>
function jsonpCb(result) {
console.log(result)
}
</script>
<script src="http://localhost:3000/user?callback=jsonpCb"></script>

我们先定义了回调函数jsonpCb,然后通过另外一个<script>标签获取数据,返回

1
jsonpCb({"name": "xuthus"})

返回后就会执行这个这个函数,就可以得到我们想要的json数据了

我们来模拟一下

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http')
const fs = require('fs')
const url = require('url')
http.createServer((req, res) => {
const data = {name: 'uthus'}
// 获取查询参数callback,就是我们在客户端定义的jsonpCb
const callback = url.parse(req.url, true).query.callback
if (callback) {
const str = callback + '(' + JSON.stringify(data) + ')'
// 以字符串的形式返回
res.end(str)
} else {
res.end('hello world')
}
}).listen(3000)

客户端就是我们前面所定义的

1
2
3
4
5
6
<script>
function jsonpCb(result) {
console.log(result)
}
</script>
<script src="http://localhost:3000/user?callback=jsonpCb"></script>

我们开启本地服务直接在浏览器输入http://localhost:3000/user?callback=jsonpCb,可以看到

1
2
3
jsonpCb({
"name": "Xuthus"
})

执行客户端html文件,可以看到输出结果,随后就可以操作他们了

1
Object {name: "Xuthus"}

封装

为了方便使用,封装一个jsonp工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 获取随机字符串,用于拼接
* @param {string} prefix [前导名字]
* @param {number} num [字符串长度]
*/
function getRandomName (prefix, num) {
return prefix + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, num)
}
/**
* 创建script标签
* @param {请求路径} url
*/
function createScript (url) {
const script = document.createElement('script')
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', url)
script.async = true
return script
}
/**
* 实现请求
* @param {路径} url
*/
function jsonp (url) {
return new Promise((resolve, reject) => {
const cbName = getRandomName('callback')
window[cbName] = function (data) {
resolve(data)
}
url += url.indexOf('?') > -1 ? '&' : '?'
const script = createScript(`${url}callback=${cbName}`)
script.onload = function () {
script.onload = null
if (script.parentNode) {
script.parentNode.removeChild(script)
}
window[cbName] = null
}
script.onerror = function () {
reject()
}
document.getElementsByTagName('head')[0].appendChild(script)
})
}

调用

1
2
3
4
5
jsonp('http://localhost:3000').then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})

补充:JSONP可以兼容老版的浏览器,但是只能发送get请求。