ajax的jsonp原理,JSONP原理及实现

基本原理

基本原理: 主要就是利用了 script 标签的src没有跨域限制来完成的。

执行过程

执行过程:

前端定义一个解析函数(如: jsonpCallback = function (res) {})

通过params的形式包装script标签的请求参数,并且声明执行函数(如cb=jsonpCallback)

后端获取到前端声明的执行函数(jsonpCallback),并以带上参数且调用执行函数的方式传递给前端

前端在script标签返回资源的时候就会去执行jsonpCallback并通过回调函数的方式拿到数据了。

优缺点

缺点:

只能进行GET请求

优点:

兼容性好,在一些古老的浏览器中都可以运行

案例分析

先来看看我们要实现一个什么效果:

在一个叫index.html的文件中有以下代码:

window.jsonpCallback = function (res) {

console.log(res)

}

然后我本地有一个文件server.js它会使用node提供一个服务,来模拟服务器。

并且定义一个接口/api/jsonp来查询id对应的数据。

当我打开index.html的时候就会加载script标签,并执行了此次跨域请求。

前期准备

我在本地新建一个文件夹node-cors

并在此目录下npm init,初始化package.json

安装koa(node的一个轻量级框架)

新建文件夹jsonp,并新建index.html和server.js,一个写前端代码,一个写后端

mkdir node-cors && cd node-cors

npm init

cnpm i --save-dev koa

mkdir jsonp && cd jsonp

touch index.html

touch server.js

后端代码

由于JSONP的实现需要前后端配合,先来写一下后端的实现:

(看不懂没关系,下面的前端简单实现会做解释)

const Koa = require('koa');

const app = new Koa();

const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]

app.use(async (ctx, next) => {

if (ctx.path === '/api/jsonp') {

const { cb, id } = ctx.query;

const title = items.find(item => item.id == id)['title']

ctx.body = `${cb}(${JSON.stringify({title})})`;

return;

}

})

console.log('listen 8080...')

app.listen(8080);

写完之后,保存。

并在jsonp这个文件夹下执行:

node server.js

来启动服务,可以看到编辑器的控制台中会打印出"listen 8080..."

前端简单实现

OK👌,后端已经实现了,现在让我们来看看前端最简单的一种实现方式,也就是写死一个script并发送请求:

index.html中:

window.jsonpCallback = function (res) {

console.log(res)

}

这两个script的意思是:

第一个,创建一个jsonpCallback函数。但是它还没有被调用

第二个,加载src中的资源,并等待请求的内容返回

整个过程就是:

当执行到第二个script的时候,由于请求了我们的8080端口,并且把id和cb这两个参数放到URL里。那么后台就可以拿到URL里的这两个参数。

也就是在后端代码中的const { id, cb } = ctx.query这里获取到了。

那么后端在拿到这两个参数之后,可能就会根据id来进行一些查询,当然,我这里只是模拟的查询,用了一个简单的find来进行一个查找。查找到id为1的那项并且取title。

第二个参数cb,拿到的就是"jsonpCallback"了,这里也就是告诉后端,前端那里是会有一个叫做jsonpCallback的函数来接收后端想要返回的数据,而后端你只需要在返回体中写入jsonpCallback()就可以了。

前端在得到了后端返回的内容jsonpCallback({"title":"title1"}),发现里面是一段执行函数的语句,因此就会去执行第一个script中的jsonpCallback方法了,并且又是带了参数的,所以此时浏览器控制台会打印出{ title: 'title1' }

以此来达到一个简单的跨域的效果。

其实你想想,如果我们把第二个script标签换成以下代码,是不是也能达到同样的效果呢?

jsonpCallback({ title: 'title1' })

jQuery中ajax实现

上面👆我们介绍了用script标签来实现,在jQuery的$.ajax()方法其实也提供了jsonp。

让我们一起来看看:

$.ajax({

url: "http://localhost:8080/api/jsonp",

dataType: "jsonp",

type: "get",

data: {

id: 1

},

jsonp: "cb",

success: function (data) {

console.log(data);

}

});

在success回调中同样可以拿到数据。

封装一个JSONP方法

(此章会一步一步教你如何封装一个比较完美的JSONP方法)

简易版

先看下我们要实现的功能

定义一个JSONP方法,它接收四个参数:

url

params

callbackKey:与后台约定的回调函数是用哪个字段(如cb)

callback:拿到数据之后执行的回调函数

function JSONP({

url,

params = {},

callbackKey = 'cb',

callback

}) {

// 定义本地的一个callback的名称

const callbackName = 'jsonpCallback';

// 把这个名称加入到参数中: 'cb=jsonpCallback'

params[callbackKey] = callbackName;

// 把这个callback加入到window对象中,这样就能执行这个回调了

window[callbackName] = callback;

// 得到'id=1&cb=jsonpCallback'

const paramString = Object.keys(params).map(key => {

return `${key}=${params[key]}`

}).join('&')

// 创建 script 标签

const script = document.createElement('script');

script.setAttribute('src', `${url}?${paramString}`);

document.body.appendChild(script);

}

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: { id: 1 },

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

这样写打开页面也可是可以看到效果的。

同时多个请求

上面我们虽然实现了JSONP,但有一个问题,那就是如果我同时多次调用JSONP:

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: { id: 1 },

callbackKey: 'cb',

callback (res) {

console.log(res) // No.1

}

})

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: { id: 2 },

callbackKey: 'cb',

callback (res) {

console.log(res) // No.2

}

})

可以看到这里我调用了两次JSONP,只是传递的参数不同。但是并不会按我们预期的在No.1和No.2中分别打印,而是都会在No.2中打印出结果。这是因为后面一个callback把JSONP里封装的第一个callback给覆盖了,它们都是共用的同一个callbackName,也就是jsonpCallback。如下所示:

88bb82718517

jsonp2.png

两次结果都是从76行打印出来的。

所以我们得改造一下上面的JSONP方法:

让callbackName是一个唯一的,可以使用递增

不要把回调定义在window中这样会污染全局变量,可以把它扔到JSON.xxx中

OK👌,来看看改造之后的代码:

function JSONP({

url,

params = {},

callbackKey = 'cb',

callback

}) {

// 定义本地的唯一callbackId,若是没有的话则初始化为1

JSONP.callbackId = JSONP.callbackId || 1;

let callbackId = JSONP.callbackId;

// 把要执行的回调加入到JSON对象中,避免污染window

JSONP.callbacks = JSONP.callbacks || [];

JSONP.callbacks[callbackId] = callback;

// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'

params[callbackKey] = `JSONP.callbacks[${callbackId}]`;

// 得到'id=1&cb=JSONP.callbacks[1]'

const paramString = Object.keys(params).map(key => {

return `${key}=${params[key]}`

}).join('&')

// 创建 script 标签

const script = document.createElement('script');

script.setAttribute('src', `${url}?${paramString}`);

document.body.appendChild(script);

// id自增,保证唯一

JSONP.callbackId++;

}

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: { id: 1 },

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: { id: 2 },

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

可以看到现在调用了两次回调,但是会分别执行JSONP.callbacks[1]和JSONP.callbacks[2]:

88bb82718517

jsonp3.png

最终版JSONP方法

其实上面已经算比较完美了,但是还会有一个小问题,比如下面这种情况:

我改一下后端的代码

const Koa = require('koa');

const app = new Koa();

const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]

app.use(async (ctx, next) => {

if (ctx.path === '/api/jsonp') {

const { cb, id } = ctx.query;

const title = items.find(item => item.id == id)['title']

ctx.body = `${cb}(${JSON.stringify({title})})`;

return;

}

if (ctx.path === '/api/jsonps') {

const { cb, a, b } = ctx.query;

ctx.body = `${cb}(${JSON.stringify({ a, b })})`;

return;

}

})

console.log('listen 8080...')

app.listen(8080);

增加了一个/api/jsonps的接口。

然后前端代码增加了一个这样的请求:

JSONP({

url: 'http://localhost:8080/api/jsonps',

params: {

a: '2&b=3',

b: '4'

},

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

可以看到,参数的a中也会有b这个字符串,这样就导致我们获取到的数据不对了:

88bb82718517

jsonp1.png

后台并不知道a的参数是一个字符串,它只会按照&来截取参数。

所以为了解决这个问题,可以使用URI编码。

也就是使用:

encodeURIComponent('2&b=3')

// 结果为

"2%26b%3D3"

只需要改一下JSONP方法中参数的生成:

// 得到'id=1&cb=JSONP.callbacks[1]'

const paramString = Object.keys(params).map(key => {

return `${key}=${encodeURIComponent(params[key])}`

}).join('&')

来看一下完整版的JSONP方法:

function JSONP({

url,

params = {},

callbackKey = 'cb',

callback

}) {

// 定义本地的唯一callbackId,若是没有的话则初始化为1

JSONP.callbackId = JSONP.callbackId || 1;

let callbackId = JSONP.callbackId;

// 把要执行的回调加入到JSON对象中,避免污染window

JSONP.callbacks = JSONP.callbacks || [];

JSONP.callbacks[callbackId] = callback;

// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'

params[callbackKey] = `JSONP.callbacks[${callbackId}]`;

// 得到'id=1&cb=JSONP.callbacks[1]'

const paramString = Object.keys(params).map(key => {

return `${key}=${encodeURIComponent(params[key])}`

}).join('&')

// 创建 script 标签

const script = document.createElement('script');

script.setAttribute('src', `${url}?${paramString}`);

document.body.appendChild(script);

// id自增,保证唯一

JSONP.callbackId++;

}

JSONP({

url: 'http://localhost:8080/api/jsonps',

params: {

a: '2&b=3',

b: '4'

},

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

JSONP({

url: 'http://localhost:8080/api/jsonp',

params: {

id: 1

},

callbackKey: 'cb',

callback (res) {

console.log(res)

}

})

注意⚠️:

encodeURI和encodeURIComponent的区别:

encodeURI()不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;

而encodeURIComponent()则会对它发现的任何非标准字符进行编码

例如:

var url = 'https://lindaidai.wang'

encodeURI(url) // "https://lindaidai.wang"

encodeURIComponent(url) // "https%3A%2F%2Flindaidai.wang"

另外,可以使用decodeURIComponent来解码。

decodeURIComponent("https%3A%2F%2Flindaidai.wang")

// 'https://lindaidai.wang'

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值