6

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

 3 years ago
source link: https://segmentfault.com/a/1190000040070036
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文是笔者于 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)resultreasonhttp://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 规范。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK