環境:
打包工具: Webpack;
整合軟件包: WAMP;
編輯器:VsCode;
Webpack虛擬出的端口號是8080,本地Apache的端口號是80
問題重現:
JS代碼,使用FormData對象作為傳輸數據的格式:
function postData() { var formData = new FormData(); formData.append("data", JSON.stringify({name:'xxx', age:20})); function xhrRequest(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('POST', _this.gRequestPhpUrl); xhr.setRequestHeader("cache-control","no-cache"); //② xhr.onreadystatechange = function() { if (this.readyState === 4 && this.status === 200){ var dataResp = JSON.parse(this.responseText); resolve(dataResp, this); } else { reject('網絡錯誤:' + this.status, this); } } xhr.send(formData); } var sendReq = new Promise(xhrRequest); return sendReq; }
PHP代碼,使用跨域接收他域傳來的訪問請求:
<?php header('Access-Control-Allow-Origin:*'); header('Access-Control-Allow-Headers: cache-control'); //① if ( !isset($_POST['data'])){ return; } file_put_contents('./rec.txt', json_encdoe($_POST['data'])); $objResp = new class{}; $objResp->data = 'this is from server'; echo json_encode($objResp); ?>
問題:
XMLHttpRequest請求代碼在Webpack虛擬的環境中發出,請求PHP文本資源。調試PHP時發現JS發起請求后,$_POST數組處於—— 一次請求數組為空、下一次請求數組有元素、再下一次請求數組又為空,這樣有序的循環中。且每次請求file_get_contents("php://input", "r")中也沒有值。
奇怪的是當$_POST數組為空時前端竟然收到了正確的JSON格式消息。
問題排查:
覺得奇怪的是前台用FormData包裹的數據,后台$_POST數組卻為空,且從‘php://input’中讀不到值。
1. 懷疑JS發起請求的請求頭‘Content-Type‘值不對。Server通過這個字段得到請求頭的類型,然后進行解析。[1] 如果解析不對,$_POST數據將出錯或沒有數據。 ——》 嘗試各種’Content-Type‘,沒有解決這個問題;
2. 注意到瀏覽器控制台有OPTIONS類型的請求,即某次請求發出,控制台出現一次OPTIONS請求,
發現該請求里沒有請求數據,此時調試PHP,發現$_POST為空的情況。隨即在沒有手動發送請求情況下控制台自動發出一次POST請求,這時請求里包含請求數據。
解決方案:
不讓瀏覽器發送OPTIONS請求,而是只發送一次POST請求。
查閱資料后了解OPTIONS請求是一個預檢請求,xhr判斷請求頭里的URL路徑是否能被訪問到,若不能的話就返回一個CORS策略問題(
上圖是注釋掉①處代碼會出現的問題,因為跨域時PHP需要設置能夠接收包含’cache-control‘的請求頭),若可以的話就會再次發送原來的POST請求。
如果在跨域時不想要發送OPTIONS請求,需滿足下列條件:[2]
1. 不要發送下列方法的請求:
PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH
2. 除請求頭自帶的設置,不要手動設置除下列的其它字段:
Accept、Accept-Language、Content-Language、Content-Type (but note the additional requirements below)、DPR、Downlink、Save-Data、Viewport-Width、Width
3. 上述2中的’Content-Type‘字段不可以設置除以下的值:
application/x-www-form-urlencoded、multipart/form-data、text/plain
綜上所述,只要把代碼中的①和②處注釋掉就可以了。
如果請求頭必須帶上指定其它類型字段,可以為請求頭添加“Access-Control-Max-Age”字段,該字段指明訪問某個跨域URL的預檢請求緩存時間。設置完畢后,第一次訪問某個跨域URL會發送OPTIONS預檢請求,隨后瀏覽器緩存該預檢請求,在隨后的緩存時間內,再次訪問該URL將不會發送預檢請求。
參考:
[1] https://blog.csdn.net/qq_27845259/article/details/83106391 —— ’Content-Type‘字段類型及簡介
[2] https://juejin.im/entry/58eaf351a22b9d0058a8e35c —— 淺談 AJAX 跨域請求時的 OPTIONS 方法
https://segmentfault.com/a/1190000016040998 —— 發送兩次請求,其中有個是OPTIONS請求