40行封装一个jsonp

4,560 阅读3分钟

第一次发文章,有点小紧张。

什么是同源策略?

为什么要有跨域限制?

为什么script标签能跨域?

这些基本的概念想必不用过多说明了,我们切入正题:

在开发项目中如果有用到jsonp,一直使用的是一个jsonp包。使用过程中参数需要格式化,也未曾进行Promise化,需要自己封装。于是想想能不能在包内就把这些事给办了,以后调用就省心很多,于是点开jsonp包的源码研读一番,这里简单说明下jsonp的实现原理,简单来说就是这样的:

1. 客户端需要在自己这边有一个定义好的接收服务端数据的函数

2. 使用script标签发起一个服务端地址的请求

3. 服务端响应,对应的函数执行,传参完成

这是最简单的jsonp实现,这里需要写一个函数,需要写一个script标签,好像一点都不cool~

接下来我们手写一个自己的jsonp,当然了,服务端怎么知道你本地的准备函数是什么,需要客户端告诉它,而且服务端会根据客户端提供的其他参数动态生成数据给到我们。所以我们主要是实现以下几个点:

1. 调用时动态创建script标签

2. 注册一个全局的方法等待被执行

3. 等待函数一定要在标签发起请求之前准备好

4. 得到数据之后移除创建的script标签

5. 函数名不能重名,可能会同时发起多个请求

从上所述,在这里我们的方法需要以下几个参数:

. url: 后端地址

. prefix: 执行函数名的前缀,后缀使用自增来确保唯一性

. param: 与后端协商好的发起jsonp请求时的字段

. timeout: 请求超时时间

. data: 服务端需要的其他参数

jsonp实现跨域请求也有自己的局限,如只能发起get请求。我们这里再原有的包上进行一定功能拓展,使用es6重构并将它Promise化,重构之后的代码jsonp如下:

// 其他参数在opts内
function jsonp(url, opts) {
    // 实现Promise化
    return new Promise((resolve, reject) => {
      // 自增值初始化 
      let count = 0;
      //设置默认参数
      const { 
        prefix = '__jp',
        param = 'callback',
        timeout = 60000,
        data = {}
      } = opts;
      let name = prefix + count++;
      let timer;
      //清除script标签以及注册的全局函数以及超时定时器
      function cleanup() { // 清除函数
        if (script.parentNode) {
          script.parentNode.removeChild(script);
          window[name] = null;
          if (timer) {
            clearTimeout(timer);
          }
        }
      }
      if (timeout) { // 超时
        timer = setTimeout(() => {
          cleanup();
          reject('timeout');
        }, timeout);
      }
      // 注册全局函数,等待执行中...
      window[name] = res => {
        // 只要这个函数一执行,就表示请求成功,可以使用清除函数了
        if (window[name]) {
          cleanup();
        }
        // 将请求到的数据扔给then
        resolve(res);
      }
      // 以下将data对象格式的参数拼接到url的后面
      let str = '';
      for (const key in data) {
        const value = data[key] !== undefined ? data[key] : '';
        str += `&${key}=${encodeURIComponent(value)}`;
      }
      url = url + (url.indexOf('?') > 0 ? '' : '?') + str.substr(1);
      // 最后加上与服务端协商的jsonp请求字段
      url = `${url}&${param}=${name}`;
      const script = document.createElement('script');
      script.src = url;
      // 以下这条执行且成功后,全局等待函数就会被执行
      document.head.appendChild(script);
    })
  }

# 大功告成,我们请求一个QQ音乐的轮播图数据试下:

const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg';
  const opts = {
    data: {
      g_tk: 1928093487,
      inCharset: 'utf-8',
      outCharset: 'utf-8',
      notice: 0,
      format: 'jsonp',
      platform: 'h5',
      uin: 0,
      needNewCode: 1
    },
    // QQ音乐接口Jsonp字段
    param: 'jsonpCallback'
  }
  
  jsonp(url, opts)
    .then(res => {
      console.log(res);
    })
    .catch(ex => {
      console.log(ex);
    })

数据请求成功!