内容简介:这篇文章通过实现一个生产环境中可用的,Promise API 封装的 jsonp 来讲解 jsonp 的原理。由于浏览器跨域限制的存在,通常情况下,我们不可以通过 AJAX 发起跨域请求。但考虑如下事实:这样我们就找到一种跨域方案了:
这篇文章通过实现一个生产环境中可用的,Promise API 封装的 jsonp 来讲解 jsonp 的原理。
由于浏览器跨域限制的存在,通常情况下,我们不可以通过 AJAX 发起跨域请求。但考虑如下事实:
- img link script 标签是可以跨域的
- script 标签里的代码在下载完成后就会被解析执行
- script 标签请求的不一定是一个静态文件,也可以是服务端根据 URL 生成的
- JavaScript 支持闭包
这样我们就找到一种跨域方案了:
- 在请求跨域资源的时候,我们生成一个 script 标签,将它的 src 设置为请求参数,插入 DOM 中发起请求
-
要求服务器返回一个 .js 文件,格式为
function_name(data)
, data 就是我们想要获得的数据,一般是 JSON 格式 -
在全局作用域绑定一个函数,函数名就是上面的
function_name
,这个函数是一个闭包,记住了调用位置的作用域链,这样我们就可以在这个闭包里写业务代码 - 收到服务器返回的文件后浏览器自动解析执行,执行这个闭包
下面来看实现,代码已经上传到 GitHub ,欢迎 star。
我们要求调用者这样调用 pjsonp(url, params, options)
,传入三个参数:
-
url
:请求的 URL,应该像这样:http://somehostname[:someport]/to/some/path[?with=true&orWithoutQueries=false]
-
params
:可选,请求参数。这是一个简单的 object,包含请求的参数。因为 jsonp 只能用于 GET 请求,所以参数都要写在 URL 中,而支持这个参数可以给使用者带来便利。 -
options
:可选,jsonp 的配置信息。-
prefix
:回调函数的前缀,用于生成回调函数名 -
timeout
:超时事件,超时后请求会被撤销,并向调用者报错 -
name
:特别指定的回调函数名 -
param
:在请求的 URL 中,回调函数名参数的 key
-
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++
首先是对参数的处理。由于 params
只是个添头功能,所以我们允许用户不传入 params
而只传入 options
,这时就要进行处理。然后我们将默认的 options
和用户指定的 options
合并起来(你会发现用 Object.assign
比传统的 ||
更加简单!)。最后,产生一个回调函数名。
然后,我们需要准备一些引用:
let timer let script let target
分别指向超时计时器,插入 DOM 中的 script 标签和插入的位置。
然后帮助调用者准备参数。注意,我们还要将 &${enc(options.param)}=${enc(callbackName)}
插入到 URL 的末尾,要求服务器在返回的 js 文件中,以 callbackName
作为回调函数名。
// 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 并且会实现它了。欢迎和我交流。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 【译】采用微前端架构
- DevOps采用现状情况报告
- Swift 采用语言服务器协议
- jquery减少了Silverlight的采用?
- TypeScript 官方决定全面采用 ESLint
- Monibuca 2.0 发布,采用新型转发机制
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。