jsonp 的原理和采用 Promise API 的实现

我已经将这个项目重新命名为 pjsonp 并且在 npm 上发布啦,欢迎在你的项目中使用,并在 GitHub 提交 issue 和 pull request。

npm install pjsonp --save
复制代码

这篇文章通过实现一个生产环境中可用的,Promise API 封装的 jsonp 来讲解 jsonp 的原理。

由于浏览器跨域限制的存在,通常情况下,我们不可以通过 AJAX 发起跨域请求。但考虑如下事实:

  • img link script 标签是可以跨域的
  • script 标签里的代码在下载完成后就会被解析执行
  • script 标签请求的不一定是一个静态文件,也可以是服务端根据 URL 生成的
  • JavaScript 支持闭包

这样我们就找到一种跨域方案了:

  1. 在请求跨域资源的时候,我们生成一个 script 标签,将它的 src 设置为请求参数,插入 DOM 中发起请求
  2. 要求服务器返回一个 .js 文件,格式为 function_name(data), data 就是我们想要获得的数据,一般是 JSON 格式
  3. 在全局作用域绑定一个函数,函数名就是上面的 function_name,这个函数是一个闭包,记住了调用位置的作用域链,这样我们就可以在这个闭包里调用业务代码
  4. 收到服务器返回的文件后浏览器自动解析执行,执行这个闭包

下面来看实现。

我们要求调用者这样调用 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 并且会实现它了。欢迎和我交流。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值