当前位置:主页 > 查看内容

【Pride】再谈风骚的跨源/域方案(上)

发布时间:2021-05-26 00:00| 位朋友查看

简介:前言 本文是笔者于 2008 年写的 《当年那些风骚的跨域操作》 的重制威力加强版。 古云温故而知新,重看当年的文章,还是感觉有颇多不足和疏漏,思考深度也还欠缺,故进行重制。 本人个人能力有限,欢迎批评指正。 正名与致歉 开头先来个一鞠躬。 吐槽一下翻……

前言

本文是笔者于 2008 年写的《当年那些风骚的跨域操作》的重制威力加强版。
古云温故而知新,重看当年的文章,还是感觉有颇多不足和疏漏,思考深度也还欠缺,故进行重制。
本人个人能力有限,欢迎批评指正。

正名与致歉

开头先来个一鞠躬。
吐槽一下翻译,浏览各类 Wiki 和 RFC 里英文名都是“cross-origin”,origin 应该翻译成“源”、“来源”,合起来准确的翻译是“跨源”。但不知怎么搞的,在中文区以讹传讹地变成了“跨域”(cross-domain),这也造成了很大的误解,下面有讲到 domian 和 origin 的关系,这糟心的翻译让多少萌新混淆了这两个概念(包括我)。
因此,本文只会用”跨源“这个准确翻译,也为自己以前文章的错误致歉。

演示案例

本次重制最重磅的一点是笔者实现了一套完整的演示案例,前后端代码都有,前端无第三方依赖,服务端基于 Express,源码细节一览无余,理论和实践完美结合。可在本地演示下述的所有跨源方案,有 NodeJS 环境就能玩,无需复杂配置、编译和容器。 传送门
首页截图:
首页

同源策略(Same-Origin Policy)

1995年,同源策略由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个安全策略。
本文要讲的“跨源”,正是要在确保安全的前提下绕过这个策略的限制。

核心概念

同源策略的目的是确保不同源提供的文件(资源)之间是相互独立的,类似于沙盒的概念。换句话说,只有当不同的文件脚本是由相同的源提供时才没有限制。限制可以细分为两个方面:

  • 对象访问限制
    主要体现在 iframe,如果父子页面属于不同的源,那将有下面的限制:

    • 不可以相互访问 DOM(Document Object Model),也就是无法取得 document 节点,document 下面挂载的方式和属性,包括其所有子节点都无法访问。这也是 Cookie 遵循同源策略的原因,因为 document.cookie 不可能访问。
    • 对于 BOM 只能有少量权限,也就是说可以互相取得 window 对象,但全部方法和大部分属性都无法用(比如 window.localStoragewindow.name 等),只有少量属性可以有限访问,比如下面两种:

      • 可读,window.length
      • 可写,window.location.href
  • 网络访问限制
    主要体现在 Ajax 请求,如果发起的请求目标源与当前页面的源不同,浏览器就会有下面的限制:

    • 拦截响应:对于简单请求,浏览器会发起请求,服务器正常响应,但只要服务器返回的响应报文头不符合要求,就会忽略所有返回的数据,直接报错。
    • 限制请求:对于非简单请求,现代浏览器都会先发起预检请求,但只要服务器返回的响应报文头不符合要求,就直接报错,不会再发起正式请求了,换句话说这种情况下服务器是拿不到任何有关这次请求的数据的。

何为同源(Same-Origin)

origin 在 Web 领域是有严格定义的,包含三个部分:协议、域和端口。

origin = scheme + domain + port

也就是说这三者都完全相同,才能叫同源。
举个例子,假设现在有一个源为 http://example.com 的页面,向如下源发起请求,结果如下:

origin(URL)resultreason
http://example.comsuccess协议、域和端口号均相同(浏览器默认 80 端口)
http://example.com:8080fail端口不同
https://example.comfail协议不同
http://sub.example.comfail域名不同

跨源方案(Cross-Origin)

同源策略提出的时代还是传统 MVC 架构(jsp,asp)盛行的年代,那时候的页面靠服务器渲染完成了大部分填充,开发者也不会维护独立的 API 服务,所以其实跨源的需求是比较少的。
新时代前后端的分离和第三方 JSSDK 的兴起,我们才开始发现这个策略虽然大大提高了浏览器的安全性,但有时很不方便,合理的用途也受到影响。比如:

  • 独立的 API 服务为了方便管理使用了独立的域名;
  • 前端开发者本地调试需要使用远程的 API;
  • 第三方开发的 JSSDK 需要嵌入到别人的页面中使用;
  • 公共平台的开放 API。

于是乎,如何解决这些问题的跨源方案就被纷纷提出,可谓百家争鸣,其中不乏令人惊叹的骚操作,虽然现在已有标准的 CORS 方案,但对于深入理解浏览器与服务器的交互还是值得学习的。

JSON-P(自填充JSON)

JSON-P 是各类跨源方案中流行度较高的一个,现在在某些要兼容旧浏览器的环境下还会被使用,著名的 jQuery 也封装其方法。

原理

请勿见名知义,名字中的 P 是 padding,“填充”的意思,这个方法在通信过程中使用的并不是普通的 json 格式文本,而是“自带填充功能的 JavaScript 脚本”。
如何理解“自带填充功能的 JavaScript 脚本”?看看下面的例子。
假设全局(Window)上有这个 getAnimal 函数,然后通过 script 标签的方式,引入一个调用该函数并传入数据的脚本,就可以实现跨源通信。

// 全局上有这个函数
function getAnimal(data){
  // 取得数据
  var animal = data.name
  // do someting
}

另一个脚本:

// 调用函数
getAnimal({
  name: 'cat'
})

也就是说利用浏览器引入 JavaScript 脚本时会自动运行的特点,就可以用来给全局函数传递数据。如果把这段调用函数的脚本作为服务端 API 的输出,就可以以此实现跨源通信。这就是 JSON-P 方法的核心原理,它填充的是全局函数的数据。

流程

  1. 在全局定义好回调函数,也就是服务端 API 输出的 js 脚本中要调用的函数;
  2. 新建 script 标签,src 即是 API 地址,将标签插入页面,浏览器便会发起 GET 请求;
  3. 服务器根据请求生成 js 脚本并返回;
  4. 页面等待 script 标签就绪,就会自动调用全局定义的回调函数,取得数据。
【PS】不只是 script 标签,所有可以使用 src 属性的标签都可以不受同源策略限制发起 GET 请求(CSP 未配置的情况),比如 img、object 等,但能自动运行 js 代码的只有 script 标签。

JSONP

错误处理

  • 前端通过 script 标签的 error 事件可以捕获到网络错误,但并不可知具体的错误原因,也就是说无法获取到服务器的响应状态码;
  • 服务端返回的脚本如果运行错误,前端只能通过全局 error 事件捕获。

实践提示

  • 前端

    • 为了避免污染全局,不建议前端生成大量随机名字的全局函数,可以用一个对象保存所有回调函数,并给每一个回调函数唯一的 id,全局仅暴露统一的执行器,依靠 id 去调用回调函数;
    • 如果遵从上一条的建议,全局对象内回调函数需要及时清理;
    • 每次请求都要生成新的 script 标签,应该在完成后及时清理;
    • 为了灵活性,还可与服务端约定将回调函数名作为参数传递,保留多个全局对象情况的扩展空间。
  • 服务端

    • 只需接收 GET 方法的请求,其他方法可判定为非法;
    • 只能在请求的 URL 里获取参数,比如 query 或 path;
    • 响应报文头 content-type 设为 text/javascript;
    • 强烈建议关闭 HTTP 协议缓存,以免数据不一致,方法参考笔者有关 HTTP的文章
    • 返回的脚本以纯文本格式写入响应报文体,由于脚本是直接运行的,应特别注意 XSS 攻击。

前端“一个对象保存所有回调函数”的设计思路:

function initJSONPCallback() {
  // 保存回调对象的对象
  const cbStore = {}
  // 这里形成了一个闭包,只能用特定方法操作 cbStore。
  return {
    // 统一执行器(函数)。
    run: function (statusCode, data) {
      const { callbackId, msg } = data
      try {
        // 运行失败分支。
        if (...) {
          cbStore[callbackId].reject(new Error(...))
          return
        }
        // 运行成功分支。
        cbStore[callbackId].resolve(...)
      } finally {
        // 执行清理。
        delete cbStore[callbackId]
      }
    },
    // 设置回调对象,发起请求时调用。
    set: function (callbackId, resolve, reject) {
      // 回调对象包含成功和失败两个分支函数。
      cbStore[callbackId] = {
        resolve,
        reject
      }
    },
    // 删除回调对象,清理时调用。
    del: function (callbackId) {
      delete cbStore[callbackId]
    }
  }
}

// 初始化
const JSONPCallback = initJSONPCallback()
// 全局暴露执行器,这也是 API 返回脚本调用的函数。
window.JSONPCb = JSONPCallback.run

具体代码请参考演示案例 JSONP 部分源码。

总结

  • 优点

    • 简单快速,相比需要 iframe 的方案确实快(演示案例里体验一下就知道);
    • 支持上古级别的浏览器(IE8-);
    • 对域无要求,可用于第三方 API。
  • 缺点

    • 只能是 GET 方法,无法自定义请求报文头,无法写入请求报文体;
    • 请求数据量受 URL 最大长度限制(不同浏览器不一);
    • 调试困难,服务器错误无法检测到具体原因;
    • 需要特殊接口支持,不能使用标准的 API 规范。

SubHostProxy(子域名代理

子域名代理在特定的环境条件下是很实用跨源方案,它能提供与正常 Ajax 请求无差别的体验。

原理

先搞清楚何为子域(domain)?域名的解析是从右往左的,我们去申请域名,就是申请最靠右的两段(以点为分段),而之后的部分是可以给所有者自定义的,你想多加几段都可以,这些衍生的域就是子域。举个例子,api.demo.com 就是 demo.com 的子域。
理论上例子里的两个算是不同的域,依照上面提到的 domain 是 origin 的一部分,因此也算是不同的源,但浏览器允许将页面 document 的域改为当前域的父级,也就是在 api.demo.com 的页面运行如下代码就可以改为 demo.com,但这种修改只对 document 的权限有影响,对 Ajax 是无影响的。

// 在 api.demo.com 页面写如下代码
document.domain = 'demo.com'
【PS】document.domain 的特点:只能设置一次;只能更改域部分,不能修改页面的端口号和协议;会重置当前页面的端口为协议默认端口(即 80 或 433);仅对 document 起作用,不影响其他对象的同源策略。

因此,该方案的原理就是通过这种方法使父级页面拥有子域页面 document 的访问权限,子域恰好又是 API 的域,进而通过子域页面代理发起请求,实现跨源通信。

流程

假设服务端 API 的域为 api.demo.com ,页面域为 demo.com ,共同运行在 http 协议,端口为 80。

  1. 子域下部署一个代理页,设置其域为 demo.com ,并可以包含发起 Ajax 的工具(jQuery、Axios等);
  2. 主页面也设置域为 demo.com
  3. 主页面新建 iframe 标签链接到代理页;
  4. 当 iframe 内的代理页就绪时,父页面就可以使用 iframe.contentWindow 取得代理页的控制权,使用其发起 Ajax 请求。

SubHostProxy

错误处理

  • iframe 的 error 事件在大部分浏览器是无效的(默认),因此 iframe 内错误对主页面来说是不可知的;
  • 通过 iframe 的 load 事件可以检查代理页是否被加载,以此间接判断是否有网络错误,但并不可知具体的错误原因,也就是说无法获取到服务器的响应状态码;
  • 当主页面获取代理页的控制权后,错误处理与正常 Ajax 无异。

实践提示

  • 前端

    • 加载代理页是需要耗时的(其实挺慢的),因此要注意发起请求的时机,免在代理页还未加载完的时候请求;
    • 并不需要每次请求都加载新的代理页,强烈建议只保留一个,多个请求共享;
    • 如果遵从上一条的建议,还需考虑代理页加载失败的情况,避免一次失败后后续均不可以;
    • 可以使用预加载的方式提前加载代理页,以免增加请求的时间;
    • 主页面必须要使用 document.domain 设置,即是当前域已经满足要求,也就是说当前页面虽然已经域是 xxx,但还是得调用一遍 document.domain='xxx'
  • 服务端

    • 只能使用标准的 80(http)或 443(https)端口部署(或使用反向代理);
    • 代理页的域必须与 API 的域是一致的,并且与主页的域面有共同的父级(或主页面的域就是父级);
    • 理论上代理页只要是执行了 document.domain=xxx 的 HTML 格式文件即可,因此可以尽量精简。

共享 iframe 的设计思路:

// 将创建 iframe 用 promise 封装,并保存起来。
let initSubHostProxyPromise = null

// 每次请求之前都应先调用这个函数。
function initSubHostProxy() {
  if (initSubHostProxyPromise != null) {
    // 如果 promise 已经存在,则直接返回,由于这个 promise 已经 resolve,其实就相当于返回了已有的 iframe。
    return initSubHostProxyPromise
  }
  // 没有则重新创建。
  initSubHostProxyPromise = new Promise((resolve, reject) => {
    const iframe = document.createElement('iframe')
    // 填入代理页地址。
    iframe.src = '...'
    iframe.onload = function (event) {
      // 这是一种 hack 的检测错误的方法,见演示案例 README 。
      if (event.target.contentWindow.length === 0) {
        // 失败分支
        reject(new Error(...))
        setTimeout(() => {
          // 清理掉失败的 promise,这样下次就会重新创建。
          initSubHostProxyPromise = null
          // 这里还需移除 iframe。
          document.body.removeChild(iframe)
        })
        return
      }
      // 成功分支,返回 iframe DOM 对象。
      resolve(iframe)
    }
    document.body.appendChild(iframe)
  })
  return initSubHostProxyPromise
}

具体代码请参考演示案例 SubHostProxy 部分源码。

总结

  • 优点

    • 可以发送任意类型的请求;
    • 可以使用标准的 API 规范;
    • 能提供与正常 Ajax 请求无差别的体验;
    • 错误捕获方便准确(除了 iframe 的网络错误);
    • 支持上古级别的浏览器(IE8-)。
  • 缺点

    • 对域有严格要求,不能用于第三方 API;
    • iframe 对浏览器性能影响较大;
    • 无法使用非协议默认端口。

HTML-P/MockForm(自填充HTML/模拟表单)

网上一般称这种方案是“模拟表单”,但我觉得并不准确,使用表单发起请求并不是它的核心特征(后面也还有几种方案用到),它的核心应该是“自填充HTML”。

原理

我将它称为 HTML-P 是借鉴了 JSON-P 的叫法,它的思路也与 JSON-P 方案很像,服务端 API 返回一个 js 脚本可以自动运行进行数据填充,那直接返回整个 HTML 页面不也可以。
但实际上 HTML 要实现数据填充还是有限制的,首先就是同源限制,父子页面如果不同源,就无法互相访问,解决办法自然是“子域代理”里提到的 document.domain 修改大法,但它的目的恰好与“子域代理”相反,通过修改 document 的域,使子页面获取主页面的访问权限,以此对主页面的数据填充,实现跨源通信。

// API 返回包含如下脚本的 HTML ,就可访问父级页面的全局函数进行数据填充。
document.domain = 'xxx'
window.parent.callbackFunction(...)

至于表单的作用,其实是利用了表单的 target 的属性,当表单 submit 的时候它会使指定 name 的 iframe 进行跳转,跳转其实就是发起请求,因此浏览器表单组件原生支持的请求方法都可以使用,也正因为使用了表单发起请求,服务端 API 必须返回一个 HTML 格式的文本。

流程

假设服务端 API 的域为 api.demo.com ,页面域为 demo.com ,共同运行在 http 协议,端口为 80。

  1. 在全局定义好回调函数,也就是服务端 API 输出的 HTML 中要调用的函数;
  2. 主页面设置域为 demo.com
  3. 主页面新建 iframe 标签并指定 name ;
  4. 新建 form 标签,指定 target 为刚才的 iframe 的 name,并添加数据、配置请求;
  5. 提交表单,iframe 内跳转;
  6. 服务端接收到请求,依据请求参数生成 HTML 页面并返回,其域设为 demo.com
  7. iframe 完成 HTML 的加载,子页面调用主页面全局定义的回调函数,主页面取得数据。

MockForm

错误处理

  • iframe 的 error 事件在大部分浏览器是无效的(默认),因此 iframe 内错误对主页面来说是不可知的;
  • 通过 iframe 的 load 事件可以检查代理页是否被加载,以此间接判断是否有网络错误,但并不可知具体的错误原因,也就是说无法获取到服务器的响应状态码;
  • 子页面调用主页面发生的错误属于 iframe 内错误,因此也是不可知的。

实践提示

  • 前端

    • 为了避免污染全局,不建议前端生成大量随机名字的全局函数,可以用一个对象保存所有回调函数,这点可以参考上面 JSON-P ;
    • 主页面必须要使用 document.domain 设置,即是当前域已经满足要求。
    • 由于 iframe 内的页面每次请求都不同,因此可以复用 iframe 标签,但不可复用页面;
    • 并发时会同时生成多个 iframe 页面,这将导致性能极度下降,并发场景并不适用该方案;
    • form 和 iframe 标签应该在完成后及时清理;
  • 服务端

    • 只能使用标准的 80(http)或 443(https)端口部署(或使用反向代理);
    • API 的域与主页的域面有共同的父级(或主页面的域就是父级);
    • 响应报文头 content-type 设为 text/html;
    • 强烈建议关闭 HTTP 协议缓存,以免数据不一致,方法参考笔者有关 HTTP的文章
    • 返回的 HTML 以纯文本格式写入响应报文体,由于其中的脚本是直接运行的,应特别注意 XSS 攻击;
    • 生成的 HTML 应尽量精简。

具体代码请参考演示案例 MockForm 部分源码。

总结

该方案可以说是“JSON-P”与“子域代理”的缝合版,优缺点均有继承。

  • 优点

    • 可以发送任意类型的请求(以浏览器 form 标签支持为准);
    • 相比“子域代理”来说,无需代理页算是个优点,
    • 支持上古级别的浏览器(IE8-)。
  • 缺点

    • 对域有严格要求,不能用于第三方 API;
    • iframe 对浏览器性能影响较大,并且并发需要多个 iframe ,基本不能用于需要并发的场景;
    • 无法使用非协议默认端口。
    • 错误捕获困难,服务器错误无法检测到具体原因,运行错误也无法捕获;
    • 需要特殊接口支持,不能使用标准的 API 规范。

WindowName

这是一个以 window.name 特性为核心的方案。

原理

这方案利用了 window.name 的特性:一旦被赋值后,当窗口(iframe)被重定向到一个新的 url 时不会改变它的值。虽然 window.name 依然遵循同源策略,只有同源才能读取到值,但我们只要在非同源页面写入值,再重定向到同源页面读取值即可实现跨源通信。
发起请求的方法与“HTML-P”相同,都是用 form 触发 iframe 跳转实现。

// 通过 iframe 的 load 事件取得 window.name 的值。
iframe.onload = function (event) {
  const res = event.target.contentWindow.name
}

流程

  1. 主页面新建 iframe 标签并指定 name ;
  2. 新建 form 标签,指定 target 为刚才的 iframe 的 name,并添加数据、配置请求;
  3. 提交表单,iframe 内跳转;
  4. 服务端接收到请求,依据请求参数生成 HTML 页面并返回;
  5. iframe 加载 HTML ,运行其中脚本将数据设置到 window.name ,并重定向;
  6. iframe 再次加载 HTML ,完成时触发 load 事件;
  7. 主页面监听到 iframe 的 load 事件,获取其 window.name 的值。

WindowName

错误处理

  • iframe 的 error 事件在大部分浏览器是无效的(默认),因此 iframe 内错误对主页面来说是不可知的;
  • 通过 iframe 的 load 事件可以检查代理页是否被加载(非同源需要 hack 方法),以此间接判断是否有网络错误,但并不可知具体的错误原因,也就是说无法获取到服务器的响应状态码;
  • 其余错误可正常捕捉即可。

实践提示

  • 前端

    • form 和 iframe 相关注意点与“HTML-P”相同;
    • 重定向到同域的页面理论上无需任何内容,只要有 HTML 格式即可,应尽量精简,而且由于无需改变,可进行长期缓存;
    • 虽然理论上 iframe 的 load 事件会触发两次(一次非同源页、一次同源页),但实际上只要 load 触发前重定向,非同源页面的 load 事件是不会接收到的;
    • 重定向应使用 window.location.replace ,这样才不会产生 history ,会影响主页面的后退操作;
    • 为了灵活性,建议将重定向页面的 url 传递给服务端。
  • 服务端

    • 响应报文头 content-type 设为 text/html;
    • 强烈建议关闭 HTTP 协议缓存,以免数据不一致,方法参考笔者有关 HTTP的文章
    • 返回的 HTML 以纯文本格式写入响应报文体;
    • 生成的 HTML 应尽量精简。

具体代码请参考演示案例 WindowName 部分源码。

总结

  • 优点

    • 可以发送任意类型的请求(以浏览器 form 标签支持为准);
    • 对域无要求,可用于第三方 API ;
    • 支持上古级别的浏览器(IE8-)。
  • 缺点

    • iframe 对浏览器性能影响较大,两次跳转雪上加霜,并且并发需要多个 iframe ,基本不能用于需要并发的场景;
    • 近乎是空白的同源重定向页,可以说是无意义的流量,影响流量统计;
    • 错误捕获困难,服务器错误无法检测到具体原因,运行错误也无法捕获;
    • 需要特殊接口支持,不能使用标准的 API 规范。

WindowHash

这是一个以 url 上 hash 部分为核心的方案。

原理

这个方案利用了 window.location.hash 的特性:不同域的页面,可以写不可读。而只改变哈希部分(井号后面)不会导致页面跳转。也就是可以让非同源的子页面写主页面 url 的 hash 部分,主页面通过监听 hash 变化,实现跨源通信。
发起请求的方法与“HTML-P”相同,都是用 form 触发 iframe 跳转实现。

// 现代浏览器有 hashchange 事件可以监听。
window.addEventListener('hashchange', function () {
  // 读取 hash
  const hash = window.location.hash
  // 清理 hash
  if (hash && hash !== '#') {
    location.replace(url + '#')
  } else {
    return
  }
})
// 降级方案,循环读取 hash 进行“监听”。
var listener = function(){
    // 读取 hash
    var hash = window.location.hash
    // 清理 hash
    if (hash && hash !== '#') {
      location.replace(url + '#')
    }
    // 继续监听
    setTimeout(listener, 100)
}
listener()

流程

  1. 主页面新建 iframe 标签并指定 name ;
  2. 新建 form 标签,指定 target 为刚才的 iframe 的 name,并添加数据、配置请求;
  3. 提交表单,iframe 内跳转;
  4. 服务端接收到请求,依据请求参数生成 HTML 页面并返回;
  5. iframe 加载 HTML ,运行其中脚本修改主页面的 hash;
  6. 主页面监听 hash 的变化,每次获取 hash 值后清空 hash。

WindowHash

错误处理

  • iframe 的 error 事件在大部分浏览器是无效的(默认),因此 iframe 内错误对主页面来说是不可知的;
  • 通过 iframe 的 load 事件可以检查代理页是否被加载(非同源需要 hack 方法),以此间接判断是否有网络错误,但并不可知具体的错误原因,也就是说无法获取到服务器的响应状态码;
  • 其余错误可正常捕捉即可。

实践提示

  • 前端

    • form 和 iframe 相关注意点与“HTML-P”相同;
    • 设置主页面 hash 应该用 window.location.replace ,这样才不会产生 history ,会影响主页面的后退操作;
    • 每次 hash 设置都需要一定的冷却,并发可能发生错了;
    • 没必要每次请求都去监听 hashchange 事件,可以在初始化时设置一个统一事件处理器,用一个对象将每次请求的回调保存起来,分配唯一的 id ,通过统一的事件处理器按 id 调用回调;
    • 如果遵从上一条的建议,全局对象内回调函数需要及时清理;
    • 由于 iframe 内是非同源页面(服务端生成),不可知主页面 url ,因此需要将 url 通过参数传递给服务端。
  • 服务端

    • 响应报文头 content-type 设为 text/html;
    • 强烈建议关闭 HTTP 协议缓存,以免数据不一致,方法参考笔者有关 HTTP的文章
    • 返回的 HTML 以纯文本格式写入响应报文体;
    • 生成的 HTML 应尽量精简。

前端“统一事件处理器”的设计思路:

function initHashListener() {
  // 保存回调对象的对象
  const cbStore = {}
  // 设置监听,只需一个。
  window.addEventListener('hashchange', function () {
    // 处理 hash。
    ...
    try {
      // 运行失败分支。
      if (...) {
        cbStore[callbackId].reject(new Error(...))
        return
      }
      // 运行成功分支。
      cbStore[callbackId].resolve(...)
    } finally {
      // 执行清理。
      delete cbStore[callbackId]
    }
  })
  // 这里形成了一个闭包,只能用特定方法操作 cbStore。
  return {
    // 设置回调对象的方法。
    set: function (callbackId, resolve, reject) {
      // 回调对象包含成功和失败两个分支函数。
      cbStore[callbackId] = {
        resolve,
        reject
      }
    },
    // 删除回调对象的方法。
    del: function (callbackId) {
      delete cbStore[callbackId]
    }
  }
}
// 初始化,每次请求都调用其 set 方法设置回调对象。
const hashListener = initHashListener()

具体代码请参考演示案例 WindowHash 部分源码。

总结

  • 优点

    • 可以发送任意类型的请求(以浏览器 form 标签支持为准);
    • 对域无要求,可用于第三方 API ;
    • 支持上古级别的浏览器(IE8-)。
  • 缺点

    • iframe 对浏览器性能影响较大,并且并发需要多个 iframe ,基本不能用于需要并发的场景;
    • 并发场景很容易出现 hash 操作撞车的问题,这个问题如果采用循环读取 hash 的方法监听则更加严重,除非有更加严密的防撞车机制,否则强烈不建议并发使用;
    • 请求数据量受 URL 最大长度限制(不同浏览器不一);
    • 错误捕获困难,服务器错误无法检测到具体原因,运行错误也无法捕获;
    • 需要特殊接口支持,不能使用标准的 API 规范。

本文转自网络,版权归原作者所有,原文链接:https://segmentfault.com/a/1190000040070036
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:老公,人家线上服务CPU 100%了,肿么办嘛 下一篇:没有了

推荐图文


随机推荐