前方有坑,请绕道——Zepto 中使用 CORS

众所周知,现在移动 Web 开发中,Zepto.js 是使用频率比较高的库之一。由于它的体积小,加载速度快,有着和 jquery 类似的 API,而备受开发者喜爱。可随着时间的推移,我们遇到了不少 Zepto 的坑,而且文件体积的大小跟代码的执行效率并没有什么关系,最后我们发现 Zepto 并没有太大的卵用。
jsperf 上有个 zepto 和 jquery DOM 操作的对比测试,有兴趣可以看一下:zepto vs jquery - selectors
开源项目好坏的一个评判标准之一:是否有一个强大的社区和一批积极的贡献者
我们简单的看一个对比:

很明显,Zepto 的活跃度远远没有 jquery 高。不过言归正传,还是回到 Zepto 的话题上。
一直以来,我们在移动端上面使用 zepto并没有出现太大的问题。直至我们将 Ajax 跨域请求从 iframe 的方式切换成 CORS 之后,一个比较隐蔽的 Bug 出现了。
问题描述
- 页面在Webview中,点击按钮无效
- 页面在部分浏览器中,无法拉取到用户的信息
问题定位
我通过 Fiddler 或 Charles 抓包发现,在 webview 中,点击按钮之后的 Ajax 请求并未发出,但是页面在手机QQ浏览器和 PC 上表现都是正常的。因为是在切换 CORS 之后,页面才出现异常的,在此之前并没有版本迭代。所以 CORS 代码首当其冲要进行深层次的 code-review,于是我直接在 CORS 的代码块上进行 try-catch,结果捕获到异常:
INVALID_STATE_ERR: DOM Exception 11
问题深入剖析
先来看看测试代码:
| 
 | 
 | 
这段代码在大多数浏览器中都可以正常执行,但是在 Android 的 webview 和一些旧版本的手机浏览器中会抛出错误。
以上代码和普通的 Ajax 请求不同的地方在于设置了 CORS 的 withCredentials 属性。(CORS 请求默认是不会带上 cookies 等身份信息的,如果需要在请求中带上 cookies,则需要设置 XMLHttpRequest 的 withCredentials 属性值为 true)
下面通过两个例子来分析一下:
例一:
| 
 | 
 | 
这段代码在部分浏览器中依旧会抛出异常:INVALID_STATE_ERR: DOM Exception 11
例二:
| 
 | 
 | 
这段代码可以正常执行,并不会抛出异常
为什么 xhr.withCredentials 赋值在 xhr.open() 方法之前就会出错呢?
秉着科(xian)学(de)严(dan)谨(teng)的态度,翻看了 W3C 在 2011 年和 2012 年关于 XMLHttpRequest 的规范文档,发现使用 withCredentials 属性的规范发生了改变。
2011 年的规范:
2012 年的规范:
对比两份文档,我们重点看一下 step 1:
2011 年的规范中规定当 XMLHttpRequest 的 readyState 状态不是 OPENED 时,会报错;
2012 年的规范中规定当 XMLHttpRequest 的 readyState 状态不是 UNSENT 或 OPENDED 时,会报错;
下面简单介绍一下 XMLHttpRequest 的 readyState 值:
| Value | State | Description | 
|---|---|---|
| 0 | UNSENT | open() has not been called yet. | 
| 1 | OPENED | send() has not been called yet. | 
| 2 | HEADERS_RECEIVED | send() has been called,and headers and status are available. | 
| 3 | LOADING | Downloading;responseText holds partial data | 
| 4 | DONE | The operation is complete | 
由此可以看出,当一个 XMLHttpRequest 对象被创建时,默认的 readyState 状态为 UNSENT,只有执行了 open() 方法并且还没有执行 send() 方法时,readyState 的状态才为 OPENED。
由于一些老版本的浏览器是按照 2012 年之前的规范来实现的,所以这一部分浏览器中,open() 方法要在设置 withCredentials 属性之前调用。因此为了兼容,正确的做法应该是在 open() 方法之后再设置 withCredentials 属性。
下面来看看 zepto.js v1.1.3 的源码:
| 
 | 
 | 
zepto 是在 open() 方法之前设置 XMLHttpRequest 的属性值的,所以这会导致在使用 CORS 并且设置 withCredentials 的时候,代码在部分浏览器中报错。Android webview 中重现的几率很大。
总结:在使用 CORS 时,如果要给 withCredentials 赋值,请务必要在 open() 方法之后,否则无法向后兼容。
对于 zepto.js 的问题,已经有用户向作者提交了 PR,作者也表示会在下个版本中修复(可是直到今天,都更新到 v1.1.6 版本了,还是没有修复这个问题,更改一下代码顺序就那么难吗?!难怪阿里也嫌 zepto 更新速度太慢,问题多,所以自己 fork 代码进行了定制化)。
所以目前如果要用 zepto 来进行 CORS 的话,还是需要自己更改 zepto 的 ajax 模块代码,然后手动构建。
参考资料:
XMLHttpRequest Level 2 2011
XMLHttpRequest Level 1 2014
XMLHttpRequest Level 2 2014
Zepto issues