我在同步 ajax 的 cookie 上栽了個(gè)"無語"的跟頭

前言

遇到這種問題實(shí)屬無奈,前端的瀏覽器兼容性一直是一個(gè)讓人頭痛的問題

僅以此文記錄如此尷尬無奈的一天惦费。拿來替大伙兒解悶T_T

場(chǎng)景再現(xiàn)

同事:快來趟脂!快來泰讽!線上出問題了!昔期!
我:神馬?! 咩?! WHAT?! なに?!
同事:是這次發(fā)布造成的嗎已卸?
我:回滾!回滾E鹨弧(為什么要在快吃飯的時(shí)候掉鏈子累澡!顧不上肚子了!快查吧)
......

一通混亂的對(duì)話后只能靜下心來“掃雷”了般贼。

回滾愧哟、代理、抓包哼蛆、對(duì)比蕊梧、單因子排查。腮介。肥矢。

一套組合拳打完,大概一炷香的時(shí)間叠洗,終于找到了破綻橄抹,竟然是 ajax 同步回調(diào)的問題!不合理疤栉丁楼誓!不應(yīng)該啊名挥!還有這種操作疟羹?!

問題復(fù)現(xiàn)

一句話概括問題

使用 ajax 做“同步”請(qǐng)求,此請(qǐng)求會(huì)返回一個(gè) cookie榄融,在success回調(diào)中讀取此目標(biāo)cookie 失敳我!ajax執(zhí)行結(jié)束后 document.cookie 才會(huì)被更新

影響范圍

PC 端和 Android 端影響范圍小愧杯,屬于偶現(xiàn)涎才。

IOS 端是重災(zāi)區(qū),出來 Chrome 和 Safari 瀏覽器外的絕大多說瀏覽器都會(huì)出現(xiàn)此問題力九,并且 App 內(nèi)置的 Webview 環(huán)境同樣不能幸免耍铜。

在本同步請(qǐng)求回調(diào)內(nèi)預(yù)讀取本請(qǐng)求返回的 cookie 會(huì)產(chǎn)生問題。

半壁江山都淪陷了跌前,我要這鐵棒有何用棕兼!

追因溯果

小范圍的兼容問題我姑且可以饒你,奈何你如此猖狂抵乓,怎能任你瞞天過海伴挚!

縱向?qū)Ρ?/h2>

排除一些干擾項(xiàng),還原其本質(zhì)灾炭,我們分別用框架nej,jQueryjs寫幾個(gè)相同功能的“同步” 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】自己實(shí)現(xiàn)的 ajax 請(qǐng)求函數(shù)

<!DOCTYPE html>
<html>
<head>
    <title>JS</title>
    <meta charset="utf-8" />
</head>
<body>
    js
    <script>
        var _$ajax = (function () {
            /**
            * 生產(chǎn)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("您的系統(tǒng)或?yàn)g覽器不支持XHR對(duì)象败徊!");
                }
            };
            /**
            * 將JSON格式轉(zhuǎn)化為字符串
            */
            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("&");
            };
            /**
            * 字符串轉(zhuǎn)換為JSON對(duì)象,兼容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)
                };
            })();

            /**
            * 回調(diào)函數(shù)
            */
            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();
                    }
                    //清空狀態(tài)
                    this.xhr = null;
                    clearTimeout(options.reqTimeout);
                } else if (!options.requestDone) {
                    //設(shè)置超時(shí)
                    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;
                //創(chuàng)建 - 第一步
                var xhr = createXHR();
                //接收 - 第三步
                xhr.onreadystatechange = function () {
                    callBack(xhr, options);
                };
                //連接 和 發(fā)送 - 第二步
                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);
                    //設(shè)置表單提交時(shí)的內(nèi)容類型
                    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>

三個(gè)文件都是一樣的掏缎,在html 加載完之后發(fā)起一個(gè)同步請(qǐng)求皱蹦,該請(qǐng)求會(huì)返回一個(gè) cookie,在回調(diào)中將document.cookie打印出來眷蜈,檢測(cè)是否已經(jīng)在回調(diào)時(shí)寫入的了 cookie沪哺。

下面使用 node 實(shí)現(xiàn)這個(gè)可寫 cookie 的服務(wù)。
【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

操作

我們依次執(zhí)行如下操作,

  1. 使用 ios 端 QQ 瀏覽器忌怎,清空所有緩存
  2. 加載其中一個(gè)頁面籍滴,觀察是否有目標(biāo) cookie 輸出
  3. 執(zhí)行刷新操作,觀察是否有目標(biāo) cookie 輸出榴啸,比較 cookie 輸出的時(shí)間戳孽惰,確認(rèn)是否為上次 cookie 的同步結(jié)果而非本次請(qǐng)求獲取的 cookie,
  4. 清空所有緩存鸥印,切換目標(biāo) html 文件勋功,循環(huán)執(zhí)行2坦报,3,4步驟

結(jié)果

【nej.html】

  • 純凈環(huán)境加載狂鞋,未讀取到目標(biāo) cookie
  • 刷新加載片择,讀取到上一次請(qǐng)求返回的 cookie

【jquery.html】

  • 純凈環(huán)境加載,未讀取到目標(biāo) cookie
  • 刷新加載骚揍,未讀取到目標(biāo) cookie

【js.html】

  • 純凈環(huán)境加載字管,未讀取到目標(biāo) cookie
  • 刷新加載,未讀取到目標(biāo) cookie

咦信不?結(jié)果不一樣嘲叔!使用 nej 的第二次加載讀取到了第一次 cookie。其他的兩次均為獲取到浑塞。

原因

nej 依賴框架的加載是異步的,當(dāng)同步請(qǐng)求發(fā)起時(shí)政己,dom 已經(jīng)加載完畢酌壕,回調(diào)相應(yīng)時(shí),document.cookie已經(jīng)呈“ready”狀態(tài)歇由,可讀可寫卵牍。但請(qǐng)求依然獲取不到自身返回?cái)y帶的 cookie。

而其他兩種加載的機(jī)制阻塞了 dom 的加載沦泌,導(dǎo)致同步請(qǐng)求發(fā)起時(shí)糊昙,dom 尚未加載完成,回調(diào)相應(yīng)時(shí)谢谦,document.cookie依然不可寫释牺。

單因子對(duì)照

我們將以上幾個(gè) html 文件的邏輯做下修改。
將同步請(qǐng)求推遲到 document 點(diǎn)擊觸發(fā)時(shí)再發(fā)起回挽。
如下

$('document').click(function () {
    // TODO 發(fā)起同步請(qǐng)求
});

依然是上面的執(zhí)行步驟没咙,來看看此次的結(jié)果

結(jié)果

【nej.html】

  • 純凈環(huán)境加載,未讀取到目標(biāo) cookie
  • 刷新加載千劈,讀取到上一次請(qǐng)求返回的 cookie

【jquery.html】

  • 純凈環(huán)境加載祭刚,未讀取到目標(biāo) cookie
  • 刷新加載,讀取到上一次請(qǐng)求返回的 cookie

【js.html】

  • 純凈環(huán)境加載墙牌,未讀取到目標(biāo) cookie
  • 刷新加載涡驮,讀取到上一次請(qǐng)求返回的 cookie

結(jié)果和預(yù)期一樣,本次請(qǐng)求無法獲取本期返回的目標(biāo) cookie喜滨,請(qǐng)求回調(diào)執(zhí)行后捉捅,目標(biāo)cookie才會(huì)更新到document.cookie上。

特例

在執(zhí)行以上操作是虽风,發(fā)現(xiàn)锯梁,【jquery.html】的執(zhí)行結(jié)果時(shí)不時(shí)會(huì)有兩種結(jié)果

  • 純凈環(huán)境加載即碗,未讀取到目標(biāo) cookie
  • 刷新加載,讀取到上一次請(qǐng)求返回的 cookie
    另外一種幾率較小陌凳,但也會(huì)出現(xiàn)
  • 純凈環(huán)境加載剥懒,讀取到目標(biāo) cookie
  • 刷新加載,讀取到目標(biāo) cookie

產(chǎn)生原因

一言不合看源碼

我們?cè)?jquery 的源碼中看到合敦,jquery 的success回調(diào)綁定在了 onload 事件上

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

而我自己實(shí)現(xiàn)的和 nej 的實(shí)現(xiàn)均是將success回調(diào)綁定在了 onreadystatechange 事件上初橘,唯一的區(qū)別就在于此

一個(gè)正向的 ajax 請(qǐng)求,會(huì)先觸發(fā)兩次onreadystatechange充岛,在觸發(fā)onload保檐,或許原因在于document.cookie的同步有幾率在onload事件觸發(fā)前完成?崔梗?I'm not sure.

問題結(jié)論

  1. 在 PC 端夜只,Android 端,IOS 端Chrome蒜魄、Safari 瀏覽器環(huán)境下扔亥,ajax 的同步請(qǐng)求的回調(diào)方法中,取到本請(qǐng)求返回的 cookie 失敗幾率低
  2. IOS 端谈为,QQ 瀏覽器旅挤、App 內(nèi)置Webview瀏覽器環(huán)境下,失敗率極高伞鲫。

解決方案

只有問題沒有方案的都是在耍流氓粘茄!

方案1 - 明修棧道暗度陳倉

將回調(diào)方法中的 cookie 獲取方法轉(zhuǎn)化為異步操作。

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

方案2 - 不抵抗政策

沒有把握的方案秕脓,我們是要斟酌著實(shí)施的柒瓣。

如果你不能100%卻被操作的安全性,那并不建議你強(qiáng)行使用 ajax 的同步操作吠架,很多機(jī)制并不會(huì)像我們自以為是的那樣理所應(yīng)當(dāng)嘹朗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市诵肛,隨后出現(xiàn)的幾起案子屹培,更是在濱河造成了極大的恐慌,老刑警劉巖怔檩,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褪秀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡薛训,警方通過查閱死者的電腦和手機(jī)媒吗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乙埃,“玉大人闸英,你說我怎么就攤上這事锯岖。” “怎么了甫何?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵出吹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我辙喂,道長(zhǎng)捶牢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任巍耗,我火速辦了婚禮秋麸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炬太。我一直安慰自己灸蟆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布亲族。 她就那樣靜靜地躺著炒考,像睡著了一般。 火紅的嫁衣襯著肌膚如雪孽水。 梳的紋絲不亂的頭發(fā)上票腰,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天城看,我揣著相機(jī)與錄音女气,去河邊找鬼。 笑死测柠,一個(gè)胖子當(dāng)著我的面吹牛炼鞠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播轰胁,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼谒主,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了赃阀?” 一聲冷哼從身側(cè)響起霎肯,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎榛斯,沒想到半個(gè)月后观游,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驮俗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年懂缕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片王凑。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搪柑,死狀恐怖聋丝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情工碾,我是刑警寧澤弱睦,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站倚喂,受9級(jí)特大地震影響每篷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜端圈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一焦读、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舱权,春花似錦矗晃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸵贬,卻和暖如春俗他,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阔逼。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工兆衅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嗜浮。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓羡亩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親危融。 傳聞我的和親對(duì)象是個(gè)殘疾皇子畏铆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 在線閱讀 http://interview.poetries.top[http://interview.poetr...
    程序員poetry閱讀 114,307評(píng)論 24 450
  • AJAX 原生js操作ajax 1.創(chuàng)建XMLHttpRequest對(duì)象 var xhr = new XMLHtt...
    碧玉含香閱讀 3,179評(píng)論 0 7
  • 1.幾種基本數(shù)據(jù)類型?復(fù)雜數(shù)據(jù)類型?值類型和引用數(shù)據(jù)類型?堆棧數(shù)據(jù)結(jié)構(gòu)? 基本數(shù)據(jù)類型:Undefined、Nul...
    極樂君閱讀 5,502評(píng)論 0 106
  • 很早之前就在看web前端面試題吉殃,一直想總結(jié)一個(gè)比較全面又詳細(xì)的面試題庫辞居,現(xiàn)在總結(jié)了一些,分享給大家蛋勺,以后還會(huì)持續(xù)更...
    櫻桃小丸子兒閱讀 85,475評(píng)論 32 691
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,732評(píng)論 25 707