我在同步 ajax 的 cookie 上栽了个"无语"的跟头
前言遇到这种问题实属无奈,前端的浏览器兼容性一直是一个让人头痛的问问题 仅以此文记录如此尴尬无奈的一天。拿来替大伙儿解闷T_T 场景再现
一通混乱的对话后只能静下心来“扫雷”了。 回滚、代理、抓包、对比、单因子排查。。。 一套组合拳打完,大概一炷香的时间,终于找到了破绽,竟然是 ajax 同步回调的问题!不合理啊!不应该啊!还有这种操作?! 问题复现一句话概括问题
影响范围PC 端和 Android 端影响范围小,属于偶现。 IOS 端是重灾区,出来 Chrome 和 Safari 浏览器外的绝大多说浏览器都会出现此问题,并且 App 内置的 Webview 环境同样不能幸免。 在本同步请求回调内预读取本请求返回的 cookie 会产生问题。
追因溯果
纵向对比排除一些干扰项,还原其本质,我们分别用框架 【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,在回调中将 下面使用 node 实现这个可写 cookie 的服务。 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 操作我们依次执行如下操作,
结果【nej.html】
【jquery.html】
【js.html】
咦?结果不一样!使用 nej 的第二次加载读取到了第一次 cookie。其他的两次均为获取到。 原因nej 依赖框架的加载是异步的,当同步请求发起时,dom 已经加载完毕,回调相应时,
而其他两种加载的机制阻塞了 dom 的加载,导致同步请求发起时,dom 尚未加载完成,回调相应时, 单因子对照我们将以上几个 html 文件的逻辑做下修改。 $('document').click(function () { // TODO 发起同步请求 }); 依然是上面的执行步骤,来看看此次的结果 结果【nej.html】
【jquery.html】
【js.html】
结果和预期一样,本次请求无法获取本期返回的目标 cookie,请求回调执行后,目标cookie才会更新到 特例在执行以上操作是,发现,【jquery.html】的执行结果时不时会有两种结果
产生原因
我们在 jquery 的源码中看到,jquery 的 https://code.jquery.com/jquery-3.2.1.js :9533行
而我自己实现的和 nej 的实现均是将 一个正向的 ajax 请求,会先触发两次 问题结论
解决方案
方案1 - 明修栈道暗度陈仓将回调方法中的 cookie 获取方法转化为异步操作。 _$ajax({ url: '/api',success: function (result) { setTimeout(function(){ // do something 在此处获取 cookie 操作是安全的 },0) } }); 方案2 - 不抵抗政策没有把握的方案,我们是要斟酌着实施的。 如果你不能100%却被操作的安全性,那并不建议你强行使用 ajax 的同步操作,很多机制并不会像我们自以为是的那样理所应当。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |