遇到這種問題實屬無奈,前端的瀏覽器相容性一直是個讓人頭痛的問問題
僅以此文記錄如此尷尬無奈的一天。拿來替大夥兒解悶T_T
同事:快來!快來!線上出問題了! !
我:神馬?! 咩?! WHAT?! なに?!
同事:是這次發布造成的嗎?
我:回滾!復原! (為什麼要在快吃飯的時候掉鍊子!顧不上肚子了!快查吧)
......
一通混亂的對話後只能靜下心來“掃雷」了。
回滾、代理、抓包、對比、單因子排查。 。 。
一組組合拳頭打完,大概一炷香的時間,終於找到了破綻,竟然是 ajax 同步回調的問題!不合理啊!不應該啊!還有這種操作? !
使用ajax 做「同步」請求,此請求會傳回一個cookie,在
success
回呼中讀取此目標cookie 失敗! ajax執行結束後document.cookie
才會被更新
PC 端和 Android 端影響範圍小,屬於偶現。
IOS 端是重災區,出來 Chrome 和 Safari 瀏覽器外的絕大多說瀏覽器都會出現此問題,且 App 內建的 Webview 環境同樣不能倖免。
在本同步請求回呼內預先讀取本請求傳回的 cookie 會產生問題。
半壁江山都淪陷了,我要這鐵棒有何用!
小範圍的兼容問題我姑且可以饒你,奈何你如此猖獗,怎能任你瞞天過海!
排除一些幹擾項,還原其本質,我們分別用框架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
我們依序執行如下操作,
#使用ios 端QQ 瀏覽器,清空所有快取
載入其中一個頁面,觀察是否有目標cookie 輸出
執行刷新操作,觀察是否有目標cookie 輸出,比較cookie 輸出的時間戳,確認是否為上次cookie 的同步結果而非本次請求獲取的cookie,
清空所有緩存,切換目標html 文件,循環執行2,3,4步驟
【nej.html】
純淨環境加載,未讀取到目標cookie
刷新加載,讀取到上一次請求返回的cookie
【jquery. html】
純淨環境加載,未讀取到目標cookie
刷新加載,未讀取到目標cookie
【js.html】
純淨環境加載,未讀取到目標cookie
document.cookie已經呈現「ready ”狀態,可讀可寫。但請求依然取得不到自身回傳攜帶的 cookie。
而其他兩個載入的機制阻塞了dom 的加載,導致同步請求發起時,dom 尚未加載完成,回呼相應時,
document.cookie仍不可寫。
將同步請求延後到 document 點擊觸發時再發起。
如下
$('document').click(function () { // TODO 发起同步请求 });依然是上面的執行步驟,來看看這次的結果結果【nej.html】
刷新加载,读取到上一次请求返回的 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.
在 PC 端,Android 端,IOS 端Chrome、Safari 浏览器环境下,ajax 的同步请求的回调方法中,取到本请求返回的 cookie 失败几率低
IOS 端,QQ 浏览器、App 内置Webview浏览器环境下,失败率极高。
只有问题没有方案的都是在耍流氓!
将回调方法中的 cookie 获取方法转化为异步操作。
_$ajax({ url: '/api', async: false, type: 'POST', success: function (result) { setTimeout(function(){ // do something 在此处获取 cookie 操作是安全的 },0) } });
没有把握的方案,我们是要斟酌着实施的。
如果你不能100%却被操作的安全性,那并不建议你强行使用 ajax 的同步操作,很多机制并不会像我们自以为是的那样理所应当。
以上是關於同步 ajax 的 cookie問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!