美文网首页前端小栈Web前端之路程序员
我在同步 ajax 的 cookie 上栽了个"无语&

我在同步 ajax 的 cookie 上栽了个"无语&

作者: 木羽zwwill | 来源:发表于2017-10-19 01:48 被阅读775次

    前言

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

    仅以此文记录如此尴尬无奈的一天。拿来替大伙儿解闷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 的同步操作,很多机制并不会像我们自以为是的那样理所应当。

    相关文章

      网友评论

      • 小坏蛋_:我前阵子也遇到类似的问题,我用了方案2……

      本文标题:我在同步 ajax 的 cookie 上栽了个"无语&

      本文链接:https://www.haomeiwen.com/subject/dxbyuxtx.html