遇到這種問題實屬無奈,前端的瀏覽器兼容性一直是一個讓人頭痛的問題javascript
僅以此文記錄如此尷尬無奈的一天。拿來替大夥兒解悶T_Thtml
同事:快來!快來!線上出問題了!!
我:神馬?! 咩?! WHAT?! なに?!
同事:是此次發佈形成的嗎?
我:回滾!回滾!(爲何要在快吃飯的時候掉鏈子!顧不上肚子了!快查吧)
......前端
一通混亂的對話後只能靜下心來「掃雷」了。java
回滾、代理、抓包、對比、單因子排查。。。node
一套組合拳打完,大概一炷香的時間,終於找到了破綻,居然是 ajax 同步回調的問題!不合理啊!不該該啊!還有這種操做?!react
使用 ajax 作「同步」請求,此請求會返回一個 cookie,在
success
回調中讀取此目標cookie 失敗!ajax執行結束後document.cookie
纔會被更新jquery
PC 端和 Android 端影響範圍小,屬於偶現。ios
IOS 端是重災區,出來 Chrome 和 Safari 瀏覽器外的絕大多說瀏覽器都會出現此問題,而且 App 內置的 Webview 環境一樣不能倖免。程序員
在本同步請求回調內預讀取本請求返回的 cookie 會產生問題。ajax
半壁江山都淪陷了,我要這鐵棒有何用!
小範圍的兼容問題我姑且能夠饒你,奈何你如此猖狂,怎能任你瞞天過海!
排除一些干擾項,還原其本質,咱們分別用框架nej
,jQuery
和js
寫幾個相同功能的「同步」 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複製代碼
咱們依次執行以下操做,
【nej.html】
【jquery.html】
【js.html】
咦?結果不同!使用 nej 的第二次加載讀取到了第一次 cookie。其餘的兩次均爲獲取到。
nej 依賴框架的加載是異步的,當同步請求發起時,dom 已經加載完畢,回調相應時,document.cookie
已經呈「ready」狀態,可讀可寫。但請求依然獲取不到自身返回攜帶的 cookie。
而其餘兩種加載的機制阻塞了 dom 的加載,致使同步請求發起時,dom 還沒有加載完成,回調相應時,document.cookie
依然不可寫。
咱們將以上幾個 html 文件的邏輯作下修改。
將同步請求推遲到 document 點擊觸發時再發起。
以下
$('document').click(function () {
// TODO 發起同步請求
});複製代碼
依然是上面的執行步驟,來看看這次的結果
【nej.html】
【jquery.html】
【js.html】
結果和預期同樣,本次請求沒法獲取本期返回的目標 cookie,請求回調執行後,目標cookie纔會更新到document.cookie
上。
在執行以上操做是,發現,【jquery.html】的執行結果時不時會有兩種結果
一言不合看源碼
咱們在 jquery 的源碼中看到,jquery 的success
回調綁定在了 onload
事件上
code.jquery.com/jquery-3.2.… :9533行
而我本身實現的和 nej 的實現均是將success
回調綁定在了 onreadystatechange
事件上,惟一的區別就在於此
一個正向的 ajax 請求,會先觸發兩次onreadystatechange
,在觸發onload
,或許緣由在於document.cookie
的同步有概率在onload
事件觸發前完成??I'm not sure.
只有問題沒有方案的都是在耍流氓!
將回調方法中的 cookie 獲取方法轉化爲異步操做。
_$ajax({
url: '/api',
async: false,
type: 'POST',
success: function (result) {
setTimeout(function(){
// do something 在此處獲取 cookie 操做是安全的
},0)
}
});複製代碼
沒有把握的方案,咱們是要斟酌着實施的。
若是你不能100%卻被操做的安全性,那並不建議你強行使用 ajax 的同步操做,不少機制並不會像咱們自覺得是的那樣理所應當。
善讀書能夠醫愚
給你們推薦一本好書
《JavaScript框架設計(第2版)》
做者: 司徒正美
此書全面講解了JavaScript框架設計及相關的知識,主要內容包括種子模塊、語言模塊、瀏覽器嗅探與特徵偵測、類工廠、選擇器引擎、節點模塊、數據緩存模塊、樣式模塊、屬性模塊、PC端和移動端的事件系統、jQuery的事件系統、異步模型、數據交互模塊、動畫引擎、MVVM、前端模板(靜態模板)、MVVM的動態模板、性能牆與複雜牆、組件、jQuery時代的組件方案、avalon2的組件方案、react的組件方案等。
本書適合前端設計人員、JavaScript開發者、移動UI設計者、程序員和項目經理閱讀,也可做爲相關專業學習用書和培訓學校教材。