前言

原文地址

仓库地址

jsonp(JSON with padding)你一定不会陌生,前端向后端拿数据的方式之一,也是处理跨域请求的得利助手。

我们早已习惯,早已熟练了jQ或者zepto的ajax调用方式。但是有可能还不太它内部具体是如何实现一个jsonp的,从请求的发出,到指定的成功(success)或失败(error)回调函数的执行。

  1. 这中间前端需要做什么?
  2. 后端又需要做些什么来支持?
  3. 超时场景又该如何处理?
  4. 整个生命周期会有多个钩子可以被触发,而我们可以监听哪些钩子来得知请求的状况?

让我们从zepto.js的源码出发,一步步揭开它的面纱。


(该篇文章重点是想说jsonp实现过程,如果你想了解跨域相关的更多的知识,可以谷歌,度娘一把)

絮叨一下jsonp的基本原理

jsonp是服务器与客户端跨源通信的常用方法之一,具有简单易用,浏览器兼容性好等特点。

基本思想是啥呢

  1. 客户端利用script标签可以跨域请求资源的性质,向网页中动态插入script标签,来向服务端请求数据。

  2. 服务端会解析请求的url,至少拿到一个回调函数(比如callback=myCallback)参数,之后将数据放入其中返回给客户端。

  3. 当然jsonp不同于平常的ajax请求,它仅仅支持get类型的方式

如何使用

这里简单的介绍一下zepto.js是如果使用jsonp形式请求数据的,然后从使用的角度出发一步步分析源码实现。

使用

$.ajax({
url: 'http://www.abc.com/api/xxx', // 请求的地址
type: 'get', // 当然参数可以省略
data: { // 传给服务端的数据,被加载url?的后面
name: 'qianlongo',
sex: 'boy'
},
dataType: 'jsonp', // 预期服务器返回的数据类型
jsonpCallback: 'globalCallback', // 全局JSONP回调函数的 字符串(或返回的一个函数)名
timeout: 100, // 以毫秒为单位的请求超时时间, 0 表示不超时。
success: function (data) { // 请求成功之后调用
console.log('successCallback')
console.log(data)
},
error: function (err) { // 请求出错时调用。 (超时,解析错误,或者状态码不在HTTP 2xx)
console.log('errorCallback')
console.log(err)
},
complete: function (data) { // 请求完成时调用,无论请求失败或成功。
console.log('compelete')
console.log(data)
}
}) function globalCallback (data) {
console.log('globalCallback')
console.log(data)
}

在zepto中一个常见的jsonp请求配置就是这样了,大家都很熟悉了。但是不知道大家有没有发现.

  1. 如果设置了timeout超时了,并且没有设置jsonpCallback字段,那么控制台几乎都会出现一处报错,如下图


  1. 同样还是发生在timeout,此时如果请求超时了,并且设置了jsonpCallback字段(注意这个时候是设置了),但是如果请求在超时之后完成了,你的jsonpCallback还是会被执行。照理说这个函数应该是请求在超时时间内完成才会被执行啊!为毛这个时候超时了,还是会被执行啊!!!

不急等我们一步步分析完就会知道这个答案了。

先看一下完整的代码

因为zepto中完成jsonp请求的处理基本都在$.ajaxJSONP完成,我们直接从该函数出发开始分析。先整体看看这个函数,有一个大概的印象,已经加了大部分注释。或者可以点击这里查看

 $.ajaxJSONP = function (options, deferred) {
// 直接调ajaxJSONP没有传入type,去走$.ajax
if (!('type' in options)) return $.ajax(options)
// 获取callback函数名,此时未指定为undefined
var _callbackName = options.jsonpCallback,
// jsonpCallback可以是一个函数或者一个字符串
// 是函数时,执行该函数拿到其返回值作为callback函数
// 为字符串时直接赋值
// 没有传入jsonpCallback,那么使用类似'Zepto3726472347'作为函数名
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
// 创建一个script标签用来发送请求
script = document.createElement('script'),
// 先读取全局的callbackName函数,因为后面会对该函数重写,所以需要先保存一份
originalCallback = window[callbackName],
responseData,
// 中止请求,触发script元素上的error事件, 后面带的参数是回调函数接收的参数
abort = function (errorType) {
$(script).triggerHandler('error', errorType || 'abort')
},
xhr = { abort: abort }, abortTimeout if (deferred) deferred.promise(xhr)
// 给script元素添加load和error事件
$(script).on('load error', function (e, errorType) {
// 清除超时定时器
clearTimeout(abortTimeout)
// 移除添加的元素(注意这里还off了,不然超时这种情况,请求回来了,还是会走回调)
$(script).off().remove()
// 请求出错或后端没有给callback中塞入数据,将触发error
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
// 请求成功,调用成功回调,请塞入数据responseData[0]
ajaxSuccess(responseData[0], xhr, options, deferred)
}
// 将originalCallback重新赋值回去
window[callbackName] = originalCallback
// 并且判断originalCallback是不是个函数,如果是函数,便执行
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
// 清空闭包,释放空间
originalCallback = responseData = undefined
}) if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
// 重写全局上的callbackName
window[callbackName] = function () {
responseData = arguments
}
// 将回调函数名追加到?后面
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
// 添加script元素
document.head.appendChild(script)
// 超时处理函数
if (options.timeout > 0) abortTimeout = setTimeout(function () {
abort('timeout')
}, options.timeout) return xhr
}

参数的基本处理

在执行原理的第一步时,zepto会先处理一下我们传入的参数。

我们先来看看针对上面的例子我们发送请求的url最终会变成什么样子,而参数处理正是为了得到这条url

传了jsonpCallback时的url

http://www.abc.com/api/xxx?name=qianlongo&sex=boy&_=1497193375213&callback=globalCallback

没有传jsonpCallback时的url

http://www.abc.com/api/xxx?name=qianlongo&sex=boy&_=1497193562726&callback=Zepto1497193562723

相信你已经看出来这两条url有什么不同之处了。

_后面跟的时间戳不一样

callback后面跟的回调函数名字不一样

也就是说如果你指定了成功的回调函数就用你的,没指定他自己生成一个。

上参数处理代码

var jsonpID = +new Date()

var _callbackName = options.jsonpCallback,
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++))

对于回调函数名的处理其实挺简单的,根据你是否在参数中传了jsonpCallback,传了是个函数就用函数的返回值,不是函数就直接用。
否则的话,就生成类似Zepto1497193562723的函数名。

继续看

// 创建一个script标签用来发送请求
script = document.createElement('script'),
// 先读取全局的callbackName函数,因为后面会对该函数重写,所以需要先保存一份
originalCallback = window[callbackName],
// 请求完成后拿到的数据
responseData,
// 中止请求,触发script元素上的error事件, 后面带的参数是回调函数接收的参数
abort = function (errorType) {
$(script).triggerHandler('error', errorType || 'abort')
},
xhr = { abort: abort }, abortTimeout
// 对.then或者.catch形式调用的支持,本文暂时不涉及这方面的解析
if (deferred) deferred.promise(xhr)

好啦,看到这里我们主要要关注的是

  1. originalCallback = window[callbackName]

  2. abort函数

对于1为什么要把全局的callbackName函数先保存一份呢?这里涉及到一个问题。

请求回来的时候到底是不是直接执行的你传入的jsonpCallback函数?

解决这个问题请看

// 重写全局上的callbackName
window[callbackName] = function () {
responseData = arguments
}

zepto中把全局的callbackName函数给重写掉了,,导致后端返回数据时执行该函数,就干了一件事,就是把数据赋值给了responseData这个变量。

那说好的真正的callbackName函数呢? 如果我传了jsonpCallback,我是会在里面做一些业务逻辑的啊,你都把我给重写了,我的逻辑怎么办?先留个疑问在这里

对于关注点2abort函数,这个函数的功能,就是手动触发添加在创建好的script元素身上的error事件的回调函数。后面的超时处理timeout以及请求出错都是利用的该函数。

超时处理

在看监听script元素on error事件回调逻辑前,我们直接看最后一点东西

// 将回调函数名追加到?后面
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
// 添加script元素
document.head.appendChild(script)
// 超时处理函数
if (options.timeout > 0) abortTimeout = setTimeout(function () {
abort('timeout')
}, options.timeout)

代理做了简单的注释,这里除了将script元素插入网页还定义了一个超时处理函数,判断条件是传入的参数timeout是否大于0,所以当你传小于0或者负数啥的进去,是不会当做超时处理的。超时后其实就是触发了script元素的error事件,并传了参数timeout

真正的回调逻辑处理

接下来就是本文的重点了,zepto通过监听script元素的load事件来监听请求是否完成,以及给script添加了error事件,方便请求出错和超时处理。而用户需要的成功和失败的处理也是在这里面完成

clearTimeout(abortTimeout)
$(script).off().remove()
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}
window[callbackName] = originalCallback
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
originalCallback = responseData = undefined

script元素真正的事件处理程序代码也不多,开头有这两句话

// 清楚超时定时器
clearTimeout(abortTimeout)
// 从网页中移除创建的script元素以及将挂在它上面的所有事件都移除
$(script).off().remove()

起什么作用呢?

第一句自然是针对超时处理,如果请求在指定超时时间之前完成,自然是要把他清除一下,不然指定的时间到了,超时的回调还是会执行,这是不对的。

第二句话,把创建的script元素从网页中给删除掉,绑定的事件('load error')也全部移除,干嘛要把事件都给移除呢?你想想,一个请求已经发出去了,我们还能让他半途停止吗?该是不能吧,但是我们能够阻止请求回来之后要做的事情呀!而这个回调不就是请求回来之后要做的事情么。

请求成功或失败的处理

if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}

那么再接下来,就是请求的成功或失败的处理了。失败的条件就是触发了error事件(不管是超时还是解析错误,又或者状态码不在HTTP 2xx),甚至如果后端没有正确给到数据responseData也是错误。

再回顾一下responseData是怎么来的

// 重写全局上的callbackName
window[callbackName] = function () {
responseData = arguments
}

ajaxErro函数究竟做了些啥事呢?

ajaxError

// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context
// 执行用户传进去的error函数,注意这里的context决定了error函数中的this执行
settings.error.call(context, xhr, type, error)
if (deferred) deferred.rejectWith(context, [xhr, type, error])
// 触发全局的钩子ajaxError
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
// 调用ajaxComplete函数
ajaxComplete(type, xhr, settings)
}

可以看到他调用了我们穿进去的error函数,并且触发了全局的ajaxError钩子,所以我们其实可以在document上监听一个钩子


$(document).on('ajaxError', function (e) {
console.log('ajaxError')
console.log(e)
})

这个时候便可以拿到请求出错的信息了

ajaxComplete

// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context
// 调用传进来的complete函数
settings.complete.call(context, xhr, status)
// 触发全局的ajaxComplete钩子
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
// 请求结束
ajaxStop(settings)
}

ajaxStop


function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}

同理我们可以监听ajaxCompleteajaxStop钩子

$(document).on('ajaxComplete ajaxStop', function (e) {
console.log('ajaxComplete')
console.log(e)
})

处理完失败的情况那么接下来就是成功的处理了,主要调用了ajaxSuccess函数

ajaxSuccess

function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context, status = 'success'
// 调用传进来的成功的回调函数
settings.success.call(context, data, status, xhr)
if (deferred) deferred.resolveWith(context, [data, status, xhr])
// 触发全局的ajaxSuccess
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
// 执行请求完成的回调,成功和失败都执行了该回调
ajaxComplete(status, xhr, settings)
}

原来我们平时传入的success函数是在这里被执行的。但是有一个疑问啊!,我们知道我们是可以不传入success函数的,当我们指定jsonpCallback的时,请求成功同样会走jsonpCallback函数,但是好像ajaxSuccess没有执行这个函数,具体在处理的呢?

继续往下看

// 重写全局上的callbackName
window[callbackName] = function () {
responseData = arguments
} // 将originalCallback重新赋值回去
window[callbackName] = originalCallback
// 并且判断originalCallback是不是个函数,如果是函数,便执行
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])

为了彻底搞清楚zepto把我们指定的回调函数重写的原因,我再次加了重写的代码在这里。可以看出,重写的目的,就是为了拿到后端返回的数据,而拿到数据之后便方便我们在其他地方灵活的处理了,当然指定的回调函数还是要重新赋值回去(这也是开头要保留一份该函数的本质原因),如果是个函数,就将数据,塞进去执行。

分析到这里我相信你已经几乎明白了jsonp实现的基本原理,文章顶部说的几个问题,我们也在这个过程中解答了。

  1. 这中间前端需要做什么?
  2. 后端又需要做些什么来支持?(接下来以例子说明)
  3. 超时场景又该如何处理?
  4. 整个生命周期会有多个钩子可以被触发,而我们可以监听哪些钩子来得知请求的状况?

砰砰砰!!!,亲们还记得开头的时候留了这两个问题吗?

在zepto中一个常见的jsonp请求配置就是这样了,大家都很熟悉了。但是不知道大家有没有发现.

  1. 如果设置了timeout超时了,并且没有设置jsonpCallback字段,那么控制台几乎都会出现一处报错,如下图


  1. 同样还是发生在timeout,此时如果请求超时了,并且设置了jsonpCallback字段(注意这个时候是设置了),但是如果请求在超时之后完成了,你的jsonpCallback还是会被执行。照理说这个函数应该是请求在超时时间内完成才会被执行啊!为毛这个时候超时了,还是会被执行啊!!!

问题1:为什么会报错呢?

对于没有指定jsonpCallback

此时我们给后端的回调函数名是类似Zepto1497193562723

window[callbackName] = originalCallback

超时的时候同样会走load error的回调,当这句话执行的时候,Zepto1497193562723被设置成了undefined,当然后端返回数据的时候去执行

Zepto1497193562723({xxx: 'yyy'})

自然就报错了。

问题2呢? 其实同样还是上面那句话,只不过此时我们指定了jsonpCallback,超时的时候虽然取消了script元素的的load error事件,意味着在超时之后请求即便回来了,也不会走到对应的回调函数中去。但是别忘记,超时我们手动触发了script元素的error事件

$(script).triggerHandler('error', errorType || 'abort')

原本被重写的callback函数也会被重新赋值回去,此刻,即便script元素的load error回调不会被执行,但我们指定的jsonpCallback还是会被执行的。这也就解了问题2.

用koa做服务端,zepto发jsonp请求

最后我们再用koa,模拟服务端的api,用zepto来请求他。

如果你对源码感兴趣可以点击这里查看koa-todo-list

找到根目录的testJsonp.js文件即是服务端主要代码

前端代码

html


<button>请求后端jsonp数据</button>

js

$('button').on('click', () => {
$.ajax({
type: 'get',
url: '/showData',
data: {
name: 'qianlongo',
sex: 'boy'
},
dataType: "jsonp",
success: function (res) {
console.log('success')
console.log(res)
$('<pre>').text(JSON.stringify(res)).appendTo('body')
},
error: function (res) {
console.log('error')
console.log(res)
}
})
})

服务端主要代码

var koa = require('koa');
var route = require('koa-route');
var path = require('path');
var parse = require('co-body');
var render = require('./app/lib/render.js');
var app = koa(); app.use(route.get('/showJsonpPage', showJsonpPage))
app.use(route.get('/showData', showData)) function * showJsonpPage () {
var sHtml = yield render('jsonp')
this.body = sHtml
} function * showData (next) {
let {callback, name, sex, randomNum} = this.query this.type = 'text/javascript'
let callbackData = {
status: 0,
message: 'ok',
data: {
name,
sex,
randomNum
}
} this.body = `${callback}(${JSON.stringify(callbackData)})`
console.log(this.query)
} app.listen(3000);
console.log('listening port 3000');

运行截图


结尾

希望把jsonp的实现原理说清楚了,欢迎大家拍砖。

如果对你有一点点帮助,点击这里,加一个小星星好不好呀

如果对你有一点点帮助,点击这里,加一个小星星好不好呀

如果对你有一点点帮助,点击这里,加一个小星星好不好呀

原来你是这样的 jsonp(原理与具体实现细节)的更多相关文章

  1. Ajax跨域:Jsonp原理解析

    推荐先看下这篇文章:JS跨域(ajax跨域.iframe跨域)解决方法及原理详解(jsonp) JavaScript是一种在Web开发中经常使用的前端动态脚本技术.在JavaScript中,有一个很重 ...

  2. JQuery实现Ajax跨域访问--Jsonp原理

    JavaScript是一种在Web开发中经常使用的前端动态脚本技术.在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略).这一策略对于Java ...

  3. jsonp原理,封装,应用(vue项目)

    jsonp原理 JSON是一种轻量级的数据传输格式. JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题.由于同源策略,一般来说位于 ...

  4. 面试汇总——知道什么是同源策略吗?那怎么解决跨域问题?知道 JSONP 原理吗?

    本文是面试汇总分支——知道什么是同源策略吗?那怎么解决跨域问题?知道 JSONP 原理吗?. 同源策略 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能, ...

  5. 简单透彻理解JSONP原理及使用

    首先提一下JSON这个概念,JSON是一种轻量级的数据传输格式,被广泛应用于当前Web应用中.JSON格式数据的编码和解析基本在所有主流语言中都被实现,所以现在大部分前后端分离的架构都以JSON格式进 ...

  6. JSONP原理及jQuery中的使用

    JSONP原理   JSON和JSONP   JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,用于在浏览器和服务器之间交换信息.   JSONP(JSON ...

  7. 跨域JSONP原理及调用详细演示样例

      上篇博客介绍了同源策略和跨域訪问概念,当中提到跨域经常使用的基本方式:JSONP和CORS.   那这篇博客就介绍JSONP方式.   JSONP原理   在同源策略下,在某个server下的页面 ...

  8. Javascript的jsonp原理

    Javascript的jsonp原理   首先JSON是一种基于文本的数据交换方式,或者叫做数据描述格式 当一个网页在请求JavaScript文件时则不受是否跨域的影响,凡是拥有”src”这个属性的标 ...

  9. 跨域及JSONP原理

    什么是跨域:a.com 域名下的js无法操作b.com或是c.a.com域名下的对象 为什么浏览器要引入跨域问题? 跨域问题来源于浏览器的同源策略,为啥要有这个策略呢? 为了安全.假设现在有a.com ...

随机推荐

  1. double write 双写

    Oracle 8KB Postgresql 8KB MySQL Innodb 16KB buffer page block首先,要DML数据,需要先把page读取到index page中,之后对内存中 ...

  2. Python3迭代器与生成器

    迭代器 迭代是Python最强大的功能之一,是访问集合元素的一种方式. 迭代器是一个可以记住遍历的位置的对象. 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退 ...

  3. Qt5.8.0编译QtMqtt库并使用该库连接有人云的例子

    一 编译QtMqtt库Qt5.10才官方支持MQTT,但我用的Qt版本是5.8.0 Mingw_32BIT, 为了在Qt5.8.0上添加MQTT支持,需要自己编译源码 步骤: (1) git clon ...

  4. BZOJ 4899 记忆的轮廓

    话说BZOJ 是不是死了啊 (已经没有传送门了) 设 $f[i][j]$ 表示走到第 $j$ 个位置确定了 $i$ 个存档点时的最小代价,并强制第 $j$ 个位置有一个存档点 那么设 $cst[i][ ...

  5. SQLServer 导入大容量sql文件

    cmd命令行,管理员身份运行 执行以下语句:E:\dbbak\abhs\SmartEnglish_data.sql 为文件路径,AbhsEnglish 为要导入的数据库 sqlcmd -i E:\db ...

  6. Spring boot data jpa 示例

    一.maven pom.xml文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns ...

  7. C#面向对象19 值传递和引用传递

    值类型:int double char decimal bool enum struct引用类型:string 数组 自定义类 集合 object 接口 **值传递和引用传递1.值类型在复制的时候,传 ...

  8. 【Git的基本操作九】ssh免密登录

    SSH免密登录 1. 进入用户家目录 cd ~ 2. 删除原有的 .ssh 目录 rm -r .ssh 3. 运行命令生成 .ssh 目录 ssh-keygen -t rsa -C github或gi ...

  9. video 轮播视频

    <video controls :src="product.videoUrl" :poster="resURL + defaultImg">< ...

  10. js 操作对象的小技巧

    来源:https://www.w3cplus.com/javascript/javascript-tips.html 1.使用...运算符合并对象或数组中的对象 同样使用ES的...运算符可以替代人工 ...