我已经将这个项目从新命名为 pjsonp 而且在 npm 上发布啦,欢迎在你的项目中使用,并在 GitHub 提交 issue 和 pull request。git
npm install pjsonp --save
复制代码
这篇文章经过实现一个生产环境中可用的,Promise API 封装的 jsonp 来说解 jsonp 的原理。github
因为浏览器跨域限制的存在,一般状况下,咱们不能够经过 AJAX 发起跨域请求。但考虑以下事实:npm
这样咱们就找到一种跨域方案了:json
function_name(data)
, data 就是咱们想要得到的数据,通常是 JSON 格式function_name
,这个函数是一个闭包,记住了调用位置的做用域链,这样咱们就能够在这个闭包里调用业务代码下面来看实现。跨域
咱们要求调用者这样调用 pjsonp(url, params, options)
,传入三个参数:promise
url
:请求的 URL,应该像这样:http://somehostname[:someport]/to/some/path[?with=true&orWithoutQueries=false]
params
:可选,请求参数。这是一个简单的 object,包含请求的参数。由于 jsonp 只能用于 GET 请求,因此参数都要写在 URL 中,而支持这个参数能够给使用者带来便利。options
:可选,jsonp 的配置信息。
prefix
:回调函数的前缀,用于生成回调函数名timeout
:超时事件,超时后请求会被撤销,并向调用者报错name
:特别指定的回调函数名param
:在请求的 URL 中,回调函数名参数的 keyif (!options) {
options = params
params = {}
}
if (!options) options = {}
// merge default and user provided options
options = Object.assign({}, defaultOptions, options)
const callbackName = options.name || options.prefix + uid++
复制代码
首先是对参数的处理。因为 params
只是个添头功能,因此咱们容许用户不传入params
而只传入 options
,这时就要进行处理。而后咱们将默认的 options
和用户指定的 options
合并起来(你会发现用 Object.assign
比传统的 ||
更加简单!)。最后,产生一个回调函数名。浏览器
而后,咱们须要准备一些引用:服务器
let timer
let script
let target
复制代码
分别指向超时计时器,插入 DOM 中的 script 标签和插入的位置。闭包
而后帮助调用者准备参数。注意,咱们还要将 &${enc(options.param)}=${enc(callbackName)}
插入到 URL 的末尾,要求服务器在返回的 js 文件中,以 callbackName
做为回调函数名。app
// prepare url
url += url.indexOf('?') > 0 ? '' : '?'
for (let key in params) {
let value = params[key] || ''
url += `&${enc(key)}=${enc(value)}`
}
url += `&${enc(options.param)}=${enc(callbackName)}`
url = url.replace('?&', '?')
复制代码
接下来,咱们在 DOM 中插入 script 标签。
// insert the script to DOM and here we go!
target = document.getElementsByTagName('script')[0] || document.head
script = document.createElement('script')
script.src = url
target.parentNode.appendChild(script, target)
复制代码
最后咱们返回一个 Promise 对象,为了简单起见,咱们只在 Promise 里写绝对必要的代码。咱们在 window[callbackName]
上赋值了一个函数(的引用),从而构成了一个闭包。能够看到这个函数在被调用的时候,一是会 resolve 收到的 data,这样调用者就能够用获取到的数据来执行他们的代码了;二是会调用 clean
函数。除了绑定这个函数以外,咱们还设置了一个定时器,超时以后,就会 reject 超时错误,同时也调用 clean
函数。
return new Promise((resolve, reject) => {
/** * bind a function on window[id] so the scripts arrived, this function could be. triggered * data would be a JSON object from the server */
window[callbackName] = function(data) {
clean()
resolve(data)
}
if (options.timeout) {
timer = setTimeout(() => {
clean()
reject('[ERROR] Time out.')
}, options.timeout)
}
})
复制代码
clean
函数很是重要,它负责回收资源。它会去 DOM 中移除这个 script 标签,清除超时定时器,而且将 window[callbackName]
设置成一个什么都不作的函数(为了防止调用非 function 报错),这样原来引用的那个闭包就会被垃圾回收掉了,避免了闭包带来的内存泄露问题。
function clean() {
script.parentNode && script.parentNode.removeChild(script)
timer && clearTimeout(timer)
window[callbackName] = doNothing // use nothing function instead of null to avoid crash
}
复制代码
以上就是所有的代码了,结合文章开头说的 jsonp 的执行原理,很容易就能读懂。完整代码:
/** * This module uses Promise API and make a JSONP request. * * @copyright MIT, 2018 Wendell Hu */
let uid = 0
const enc = encodeURIComponent
const defaultOptions = {
prefix: '__jp',
timeout: 60000,
param: 'callback'
}
function doNothing() {}
/** * parameters: * - url: like http://somehostname:someport/to/some/path?with=true&orWithoutParams=false * - params: a plain object so we can help to parse them into url * - options: options to promise-jsonp * - prefix {String} * - timeout {Number} * - name {String}: you can assign the callback name by your self, if provided, prefix would be invalid * - param {String}: the key of callback function in request string * * thanks to Promise, you don't have to pass a callback or error handler * * @param {String} url * @param {Object} options * @param {Object} params * @returns {Promise} */
function pjsonp(url, params = {}, options) {
if (!options) {
options = params
params = {}
}
if (!options) options = {}
// merge default and user provided options
options = Object.assign({}, defaultOptions, options)
const callbackName = options.name || options.prefix + uid++
let timer
let script
let target
// remove a jsonp request, the callback function and the script tag
// this is important for performance problems caused by closure
function clean() {
script.parentNode && script.parentNode.removeChild(script)
timer && clearTimeout(timer)
window[callbackName] = doNothing // use nothing function instead of null to avoid crash
}
// prepare url
url += url.indexOf('?') > 0 ? '' : '?'
for (let key in params) {
let value = params[key] || ''
url += `&${enc(key)}=${enc(value)}`
}
url += `&${enc(options.param)}=${enc(callbackName)}`
url = url.replace('?&', '?')
// insert the script to DOM and here we go!
target = document.getElementsByTagName('script')[0] || document.head
script = document.createElement('script')
script.src = url
target.parentNode.appendChild(script, target)
/** * returns a Promise to tell user if this request succeeded or failed * as less code as possible here for clarity */
return new Promise((resolve, reject) => {
/** * bind a function on window[id] so the scripts arrived, this function could be triggered * data would be a JSON object from the server */
window[callbackName] = function(data) {
clean()
resolve(data)
}
if (options.timeout) {
timer = setTimeout(() => {
clean()
reject('[ERROR] Time out.')
}, options.timeout)
}
})
}
module.exports = pjsonp
复制代码
这篇文章就到这里,但愿你已经彻底理解了 jsonp 而且会实现它了。欢迎和我交流。