Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Ajax】我在同步 ajax 的 cookie 上栽了个"无语"的跟头 #6

Open
zwwill opened this issue Nov 10, 2017 · 0 comments
Open
Milestone

Comments

@zwwill
Copy link
Owner

zwwill commented Nov 10, 2017

前言

遇到这种问题实属无奈,前端的浏览器兼容性一直是一个让人头痛的问题

仅以此文记录如此尴尬无奈的一天。拿来替大伙儿解闷T_T

场景再现

同事:快来!快来!线上出问题了!!
我:神马?! 咩?! WHAT?! なに?!
同事:是这次发布造成的吗?
我:回滚!回滚!(为什么要在快吃饭的时候掉链子!顾不上肚子了!快查吧)
......

一通混乱的对话后只能静下心来“扫雷”了。

回滚、代理、抓包、对比、单因子排查。。。

一套组合拳打完,大概一炷香的时间,终于找到了破绽,竟然是 ajax 同步回调的问题!不合理啊!不应该啊!还有这种操作?!

问题复现

一句话概括问题

使用 ajax 做“同步”请求,此请求会返回一个 cookie,在success回调中读取此目标cookie 失败!ajax执行结束后 document.cookie 才会被更新

影响范围

PC 端和 Android 端影响范围小,属于偶现。

IOS 端是重灾区,出来 Chrome 和 Safari 浏览器外的绝大多说浏览器都会出现此问题,并且 App 内置的 Webview 环境同样不能幸免。

在本同步请求回调内预读取本请求返回的 cookie 会产生问题。

半壁江山都沦陷了,我要这铁棒有何用!

追因溯果

小范围的兼容问题我姑且可以饶你,奈何你如此猖狂,怎能任你瞒天过海!

纵向对比

排除一些干扰项,还原其本质,我们分别用框架nej,jQueryjs写几个相同功能的“同步” demo,走着瞧着。。

【nej.html】使用 NEJ

<!DOCTYPE html>
<html>
<head>
	<title>nej</title>
	<meta charset="utf-8" />
</head>
<body>
	test
	<script src="http://nej.netease.com/nej/src/define.js?pro=./"></script>
	<script>
		define([
			'{lib}util/ajax/xdr.js'
		], function () {
			var _j = NEJ.P('nej.j');
			_j._$request('/api', {
				sync: true,
				method: 'POST',
				onload: function (_data) {
					alert("cookie:\n" + document.cookie)
				}
			});
		});
	</script>
</body>
</html>

【jquery.html】使用 jQuery 库

<!DOCTYPE html>
<html>
<head>
	<title>jquery</title>
	<meta charset="utf-8" />
</head>
<body>
	jquery
	<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
	<script>
		$.ajax({
			url: '/api',
			async: false,
			method: 'POST',
			success: function (result) {
				alert("cookie:\n" + document.cookie)
			}
		});
	</script>
</body>
</html>

【js.html】自己实现的 ajax 请求函数

<!DOCTYPE html>
<html>
<head>
    <title>JS</title>
    <meta charset="utf-8" />
</head>
<body>
    js
    <script>
        var _$ajax = (function () {
            /**
            * 生产XHR兼容IE6
            */
            var createXHR = function () {
                if (typeof XMLHttpRequest != "undefined") { // 非IE6浏览器
                    return new XMLHttpRequest();
                } else if (typeof ActiveXObject != "undefined") {   // IE6浏览器
                    var version = [
                        "MSXML2.XMLHttp.6.0",
                        "MSXML2.XMLHttp.3.0",
                        "MSXML2.XMLHttp",
                    ];
                    for (var i = 0; i < version.length; i++) {
                        try {
                            return new ActiveXObject(version[i]);
                        } catch (e) {
                            return null
                        }
                    }
                } else {
                    throw new Error("您的系统或浏览器不支持XHR对象!");
                }
            };
            /**
            * 将JSON格式转化为字符串
            */
            var formatParams = function (data) {
                var arr = [];
                for (var name in data) {
                    arr.push(name + "=" + data[name]);
                }
                arr.push("nocache=" + new Date().getTime());
                return arr.join("&");
            };
            /**
            * 字符串转换为JSON对象,兼容IE6
            */
            var _getJson = (function () {
                var e = function (e) {
                    try {
                        return new Function("return " + e)()
                    } catch (n) {
                        return null
                    }
                };
                return function (n) {
                    if ("string" != typeof n) return n;
                    try {
                        if (window.JSON && JSON.parse) return JSON.parse(n)
                    } catch (t) {
                    }
                    return e(n)
                };
            })();

            /**
            * 回调函数
            */
            var callBack = function (xhr, options) {
                if (xhr.readyState == 4 && !options.requestDone) {
                    var status = xhr.status;
                    if (status >= 200 && status < 300) {
                        options.success && options.success(_getJson(xhr.responseText));
                    } else {
                        options.error && options.error();
                    }
                    //清空状态
                    this.xhr = null;
                    clearTimeout(options.reqTimeout);
                } else if (!options.requestDone) {
                    //设置超时
                    if (!options.reqTimeout) {
                        options.reqTimeout = setTimeout(function () {
                            options.requestDone = true;
                            !!this.xhr && this.xhr.abort();
                            clearTimeout(options.reqTimeout);
                        }, !options.timeout ? 5000 : options.timeout);
                    }
                }
            };
            return function (options) {
                options = options || {};
                options.requestDone = false;
                options.type = (options.type || "GET").toUpperCase();
                options.dataType = options.dataType || "json";
                options.contentType = options.contentType || "application/x-www-form-urlencoded";
                options.async = options.async;
                var params = options.data;
                //创建 - 第一步
                var xhr = createXHR();
                //接收 - 第三步
                xhr.onreadystatechange = function () {
                    callBack(xhr, options);
                };
                //连接 和 发送 - 第二步
                if (options.type == "GET") {
                    params = formatParams(params);
                    xhr.open("GET", options.url + "?" + params, options.async);
                    xhr.send(null);
                } else if (options.type == "POST") {
                    xhr.open("POST", options.url, options.async);
                    //设置表单提交时的内容类型
                    xhr.setRequestHeader("Content-Type", options.contentType);
                    xhr.send(params);
                }
            }
        })();
        _$ajax({
            url: '/api',
            async: false,
            type: 'POST',
            success: function (result) {
                alert("cookie:\n" + document.cookie)
            }
        });
    </script>
</body>
</html>

三个文件都是一样的,在html 加载完之后发起一个同步请求,该请求会返回一个 cookie,在回调中将document.cookie打印出来,检测是否已经在回调时写入的了 cookie。

下面使用 node 实现这个可写 cookie 的服务。
【serve.js】

var express = require("express");
var http = require("http");
var fs = require("fs");
var app = express();

var router = express.Router();
router.post('/api', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
    res.header("Set-Cookie", ["target=ccccccc|" + new Date()]);
    res.end('ok');
});

router.get('/test1', function (req, res, next) {
    fs.readFile("./nej.html", function (err, data) {
        res.end(data);
    });
});

router.get('/test2', function (req, res, next) {
    fs.readFile("./jquery.html", function (err, data) {
        res.end(data);
    });
});

router.get('/test3', function (req, res, next) {
    fs.readFile("./js.html", function (err, data) {
        res.end(data);
    });
});

app.use('/', router);
http.createServer(app).listen(3000); 

好了,万事大吉,run 一把

$ node serve.js

操作

我们依次执行如下操作,

  1. 使用 ios 端 QQ 浏览器,清空所有缓存
  2. 加载其中一个页面,观察是否有目标 cookie 输出
  3. 执行刷新操作,观察是否有目标 cookie 输出,比较 cookie 输出的时间戳,确认是否为上次 cookie 的同步结果而非本次请求获取的 cookie,
  4. 清空所有缓存,切换目标 html 文件,循环执行2,3,4步骤

结果

【nej.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,读取到上一次请求返回的 cookie

【jquery.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,未读取到目标 cookie

【js.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,未读取到目标 cookie

咦?结果不一样!使用 nej 的第二次加载读取到了第一次 cookie。其他的两次均为获取到。

原因

nej 依赖框架的加载是异步的,当同步请求发起时,dom 已经加载完毕,回调相应时,document.cookie已经呈“ready”状态,可读可写。但请求依然获取不到自身返回携带的 cookie。

而其他两种加载的机制阻塞了 dom 的加载,导致同步请求发起时,dom 尚未加载完成,回调相应时,document.cookie依然不可写。

单因子对照

我们将以上几个 html 文件的逻辑做下修改。
将同步请求推迟到 document 点击触发时再发起。
如下

$('document').click(function () {
    // TODO 发起同步请求
});

依然是上面的执行步骤,来看看此次的结果

结果

【nej.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,读取到上一次请求返回的 cookie

【jquery.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,读取到上一次请求返回的 cookie

【js.html】

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,读取到上一次请求返回的 cookie

结果和预期一样,本次请求无法获取本期返回的目标 cookie,请求回调执行后,目标cookie才会更新到document.cookie上。

特例

在执行以上操作是,发现,【jquery.html】的执行结果时不时会有两种结果

  • 纯净环境加载,未读取到目标 cookie
  • 刷新加载,读取到上一次请求返回的 cookie
    另外一种几率较小,但也会出现
  • 纯净环境加载,读取到目标 cookie
  • 刷新加载,读取到目标 cookie

产生原因

一言不合看源码

我们在 jquery 的源码中看到,jquery 的success回调绑定在了 onload 事件上

https://code.jquery.com/jquery-3.2.1.js :9533行

而我自己实现的和 nej 的实现均是将success回调绑定在了 onreadystatechange 事件上,唯一的区别就在于此

一个正向的 ajax 请求,会先触发两次onreadystatechange,在触发onload,或许原因在于document.cookie的同步有几率在onload事件触发前完成??I'm not sure.

问题结论

  1. 在 PC 端,Android 端,IOS 端Chrome、Safari 浏览器环境下,ajax 的同步请求的回调方法中,取到本请求返回的 cookie 失败几率低
  2. IOS 端,QQ 浏览器、App 内置Webview浏览器环境下,失败率极高。

解决方案

只有问题没有方案的都是在耍流氓!

方案1 - 明修栈道暗度陈仓

将回调方法中的 cookie 获取方法转化为异步操作。

_$ajax({
    url: '/api',
    async: false,
    type: 'POST',
    success: function (result) {
        setTimeout(function(){
            // do something 在此处获取 cookie 操作是安全的
        },0)
    }
});

方案2 - 不抵抗政策

没有把握的方案,我们是要斟酌着实施的。

如果你不能100%却被操作的安全性,那并不建议你强行使用 ajax 的同步操作,很多机制并不会像我们自以为是的那样理所应当。

@zwwill zwwill added this to the Y-2017 milestone Nov 10, 2017
@zwwill zwwill added the Weex label Nov 10, 2017
@zwwill zwwill modified the milestone: Y-2017 Nov 10, 2017
@zwwill zwwill changed the title 我在同步 ajax 的 cookie 上栽了个"无语"的跟头 【Ajax】我在同步 ajax 的 cookie 上栽了个"无语"的跟头 Dec 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant