前言

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

仅以此文记录如此尴尬无奈的一天。拿来替大伙儿解闷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,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',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',next) {
    fs.readFile("./nej.html",function (err,data) {
        res.end(data);
    });
});

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

router.get('/test3',next) {
    fs.readFile("./js.html",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',success: function (result) {
        setTimeout(function(){
            // do something 在此处获取 cookie 操作是安全的
        },0)
    }
});

方案2 - 不抵抗政策

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

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

我在同步 ajax 的 cookie 上栽了个"无语"的跟头的更多相关文章

  1. HTML5 Web缓存和运用程序缓存(cookie,session)

    这篇文章主要介绍了HTML5 Web缓存和运用程序缓存(cookie,session),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. 关于h5中的fetch方法解读(小结)

    这篇文章主要介绍了关于h5中的fetch方法解读(小结),fetch身为H5中的一个新对象,他的诞生,是为了取代ajax的存在而出现,有兴趣的可以了解一下

  3. ios – 如何在Swift中手动为UIWebView设置Cookie

    我需要在swift中为webview设置一个cookie.我找到了一个解决方案,但它是针对objective-c的.如何在Swift中做到这一点?

  4. 通过在iOS中处理cookie来维护会话信息

    我是iOS开发的新手.我正在使用NSURLSession来管理会话信息.下面是我用来调用任何服务器API的示例代码,我的申请流程是,如果没有登录–>登录(呼叫登录api)Else转到主屏幕并调用其他API.我的问题是,一旦从内存中删除应用程序,会话信息就不会被维护,我不得不再次调用Login.我的要求就像Facebook一样,用户只需登录一次,并且在下次应用程序启动时保持会话.编辑:我想我必须通过

  5. ios – 以http无效的自定义URL方案开头

    我在应用程序中使用了自定义URL方案.我成功地从safari重定向到我的应用程序.就像我已经制作了URL方案“appname”.请检查http://prntscr.com/2cjx0p.我需要使用像iosurlredirectfrommailtoapp这样的解决方案,但我不确定如何设置cookie.我发现我必须首先在我的应用程序中为服务器“http://myappname.com”设置一个cook

  6. 在iOS 8.4上清除WKWebView的Cookie

    另一个想法是使用自定义的WKProcesspool子类手动管理cookie.我相信这是Firefox如何管理任何cookie问题.

  7. ios – 在302重定向之后,AVPlayer无法为同一个域发送cookie

    有没有办法解决这个问题?请分享你的想法.解决方法制作一个演示该问题的示例,提交错误报告.然后创建一个提到雷达的DTS事件,并且您正在寻找一个解决方案,以便您认为是一个错误.这打开了两种可能性:>工程师可以确认错误–如果不知道他们会修复它,如果它是重复的,您可以请求原始状态信息>DTS具有足够的知识,有能力查询实际的AV工程师如何解决这个问题这样你就可以花更少的时间找到可能不会得到任何其他答案的地方.

  8. ios – AFNetworking自动持久化Cookie

    在ThisQuestion中说,AFNetworking在后台自动处理cookies,但是在Previous问题中,我遇到麻烦,在登录时在PHP服务器上保持会话.一旦关闭的应用程序,并回到会议已经没了.答案是保持像这样的cookie来解决问题:当我尝试做这样的事情时,这给我一个应用程序崩溃.当我登录时,我设置了NSUserDefault:这是错误的使用方法吗?谢谢.解决方法使用以下内容,这是保存和加载适用于我的Cookie的正确方法:希望有帮助!

  9. ios – WKWebView Cookie存储位置

    希望苹果能够在最新的更新中解决这个问题.

  10. ios – SFSafariViewController cookie

    据我所知,从iOS9开始,您应该能够使用SFSafariViewController读取cookie.如果我使用以下内容在JS中的页面上设置cookie:如果我做:cookiesArray总是空的.如果我使用传统的UIWebView我得到了我期待的cookie.我有什么想法可能做错了吗?解决方法SFSafariViewController基本上是一个Safari进程,在你的应用程序之外运行.您的应

随机推荐

  1. xe-ajax-mock 前端虚拟服务

    最新版本见Github,点击查看历史版本基于XEAjax扩展的Mock虚拟服务插件;对于前后端分离的开发模式,ajax+mock使前端不再依赖后端接口开发效率更高。CDN使用script方式安装,XEAjaxMock会定义为全局变量生产环境请使用xe-ajax-mock.min.js,更小的压缩版本,可以带来更快的速度体验。

  2. vue 使用 xe-ajax

    安装完成后自动挂载在vue实例this.$ajaxCDN安装使用script方式安装,VXEAjax会定义为全局变量生产环境请使用vxe-ajax.min.js,更小的压缩版本,可以带来更快的速度体验。cdnjs获取最新版本点击浏览已发布的所有npm包源码unpkg获取最新版本点击浏览已发布的所有npm包源码AMD安装require.js安装示例ES6Module安装通过Vue.use()来全局安装示例./Home.vue

  3. AJAX POST数据中文乱码解决

    前端使用encodeURI进行编码后台java.net.URLDecoder进行解码编解码工具

  4. Koa2框架利用CORS完成跨域ajax请求

    实现跨域ajax请求的方式有很多,其中一个是利用CORS,而这个方法关键是在服务器端进行配置。本文仅对能够完成正常跨域ajax响应的,最基本的配置进行说明。这样OPTIONS请求就能够通过了。至此为止,相当于仅仅完成了预检,还没发送真正的请求呢。

  5. form提交时,ajax上传文件并更新到&lt;input&gt;中的value字段

  6. ajax的cache作用

    filePath="+escape;},error:{alert;}});解决方案:1.加cache:false2.url加随机数正常代码:网上高人解读:cache的作用就是第一次请求完毕之后,如果再次去请求,可以直接从缓存里面读取而不是再到服务器端读取。

  7. 浅谈ajax上传文件属性contentType = false

    默认值为contentType="application/x-www-form-urlencoded".在默认情况下,内容编码类型满足大多数情况。在这里,我们主要谈谈contentType=false.在使用ajax上传文件时:在其中先封装了一个formData对象,然后使用post方法将文件传给服务器。说到这,我们发现在JQueryajax()方法中我们使contentType=false,这不是冲突了吗?这就是因为当我们在form标签中设置了enctype=“multipart/form-data”,

  8. 909422229_ajaxFileUpload上传文件

    ajaxFileUpload.js很多同名的,因为做出来一个很容易。我上github搜AjaxFileUpload出来很多类似js。ajaxFileUpload是一个异步上传文件的jQuery插件传一个不知道什么版本的上来,以后不用到处找了。语法:$.ajaxFileUploadoptions参数说明:1、url上传处理程序地址。2,fileElementId需要上传的文件域的ID,即的ID。3,secureuri是否启用安全提交,默认为false。4,dataType服务器返回的数据类型。6,error

  9. AJAX-Cache:一款好用的Ajax缓存插件

    原文链接AJAX-Cache是什么Ajax是前端开发必不可少的数据获取手段,在频繁的异步请求业务中,我们往往需要利用“缓存”提升界面响应速度,减少网络资源占用。AJAX-Cache是一款jQuery缓存插件,可以为$.ajax()方法扩展缓存功能。

  10. jsf – Ajax update/render在已渲染属性的组件上不起作用

    我试图ajax更新一个有条件渲染的组件。我可以确保#{user}实际上是可用的。这是怎么引起的,我该如何解决呢?必须始终在ajax可以重新呈现之前呈现组件。Ajax正在使用JavaScriptdocument.getElementById()来查找需要更新的组件。但是如果JSF没有将组件放在第一位,那么JavaScript找不到要更新的内容。解决方案是简单地引用总是渲染的父组件。

返回
顶部