前言
遇到這種問題實(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
,jQuery
和js
寫幾個(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í)行如下操作,
- 使用 ios 端 QQ 瀏覽器忌怎,清空所有緩存
- 加載其中一個(gè)頁面籍滴,觀察是否有目標(biāo) cookie 輸出
- 執(zhí)行刷新操作,觀察是否有目標(biāo) cookie 輸出榴啸,比較 cookie 輸出的時(shí)間戳孽惰,確認(rèn)是否為上次 cookie 的同步結(jié)果而非本次請(qǐng)求獲取的 cookie,
- 清空所有緩存鸥印,切換目標(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é)論
- 在 PC 端夜只,Android 端,IOS 端Chrome蒜魄、Safari 瀏覽器環(huán)境下扔亥,ajax 的同步請(qǐng)求的回調(diào)方法中,取到本請(qǐng)求返回的 cookie 失敗幾率低
- 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)嘹朗。