この種の問題に遭遇すると、本当にどうしようもないです。
この記事は、そのような恥ずかしくてどうしようもない日を記録するだけです。皆様の退屈解消にご利用くださいT_T
混乱した会話の後、私は落ち着いて「地雷を除去することしかできません」 」。 ロールバック、プロキシ、パケットキャプチャ、比較、単一要素のトラブルシューティング。 。 。 一連のコンボパンチを終えた後、最終的に欠陥を見つけるのに線香一本ほどかかりました。それは、ajax 同期コールバックに問題があることが判明しました。理不尽!そんなはずはありません!そのような操作はありますか? ! 問題の再発問題を一文で要約同僚:さあ!来て!オンラインで問題が発生しました! !
私: 一体何ですか?! ナナ?! 同僚: このリリースが原因ですか?
私: ロールバック!ロールバック! (なぜ食べようとしているのにチェーンを落とすのですか! 胃の世話ができません! すぐに確認してください)
...
ajax を使用して「同期」リクエストを行うと、このリクエストは
success
でこのターゲット Cookie を返します。 > コールバックが失敗しました!document.cookie
は、ajax の実行が完了するまで更新されません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
刷新加载,未读取到目标 cookie
咦?结果不一样!使用 nej 的第二次加载读取到了第一次 cookie。其他的两次均为获取到。
nej 依赖框架的加载是异步的,当同步请求发起时,dom 已经加载完毕,回调相应时,document.cookie
已经呈“ready”状态,可读可写。但请求依然获取不到自身返回携带的 cookie。
而其他两种加载的机制阻塞了 dom 的加载,导致同步请求发起时,dom 尚未加载完成,回调相应时,document.cookie
iOS は最も影響を受けやすい領域です。Chrome と Safari を除くほとんどのブラウザではこの問題が発生し、アプリの組み込み Webview 環境も影響を受けません。
小規模な互換性の問題については許せますが、あなたはとても傲慢です、どうやってそれを隠すことができますか?
nej
、jQuery
、および js
を使用して、いくつかのフレームワークを作成します。同一 「同期」機能のデモです。様子を見てみましょう。 。 $('document').click(function () { // TODO 发起同步请求 });
_$ajax({ url: '/api', async: false, type: 'POST', success: function (result) { setTimeout(function(){ // do something 在此处获取 cookie 操作是安全的 },0) } });
rrreee
htmlで読み込んだ後の3つのファイルは同じです次に、同期リクエストを開始します。これにより、コールバック内で Cookie が返され、コールバック中に Cookie が書き込まれたかどうかを確認します。【serve.js】
rrreee操作を実行します
document.cookie
はすでに「準備完了」になっています。状態であり、読み書き可能です。ただし、リクエスト自体は依然として返される Cookie を取得できません。 🎜🎜🎜 🎜他の 2 つのロード メカニズムは DOM のロードをブロックするため、同期リクエストが開始されたときに DOM はロードされず、document.cookie
はまだ書き込み不可能です。 🎜🎜単一要素制御🎜🎜上記のHTMLファイルのロジックを修正していきます。 🎜ドキュメントがクリックされてトリガーされるまで同期リクエストを遅らせます。 🎜以下🎜rrreee🎜はまだ上記の実行手順です。結果を見てみましょう🎜🎜結果🎜🎜[nej.html]🎜🎜🎜🎜純粋な環境の読み込み、ターゲットのCookieは読み取られませんでした🎜🎜🎜🎜読み込みを更新します、前のリクエストによって返された Cookie を読み取ります🎜🎜🎜🎜[jquery.html]🎜🎜🎜🎜純粋な環境の読み込み、ターゲットの Cookie は読み取られません🎜🎜🎜🎜ロードを更新し、前のリクエストによって返された Cookie を読み取ります🎜🎜 🎜🎜【js.html】🎜🎜🎜🎜純粋な環境読み込み、ターゲットCookieは読み込まれません🎜刷新加载,读取到上一次请求返回的 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 中国語 Web サイトの他の関連記事を参照してください。