整理 Ajax 相關內容
(原文連結請點選這裡)
(示例程式碼請點選這裡)
關於 ajax 的相關內容,幾乎一直是面試中的必考題,在看了慕課網中關於 ajax 跨域的全面講解之後,加深了自己對 ajax 跨域的理解,結合自己以往的經驗,做出了下面學習筆記。視訊連線請點選這裡。再次感謝曉風清老師的講解。
Ajax
Ajax 不是一種程式語言,而是一種在無需重新載入整個網頁的情況下,能夠更新部分網頁的技術。它的出現大大優化了使用者體驗。所有現代瀏覽器(IE7+、Firefox、Chrome、Safari 以及 Opera)均內建 XMLHttpRequest 物件(IE5 和 IE6 使用 ActiveXObject)。
使用 Ajax 與後臺服務建立通訊大致可分為以下三步:
1、例項化 XMLHttpRequest(或 ActiveXObject)物件
let xhr;
if (window.XMLHttpRequest){
xhr = new XMLHttpRequest();
}else{
xhr = new ActiveXObject('Microsoft.XMLHTTP')
}
複製程式碼
2、傳送請求
if (options.type == 'GET'){
xhr.open("GET",options.url + '?' + options.data ,options.async);
xhr.send(null)
}else if (options.type == 'POST'){
xhr.open('POST',options.url,options.async);
// POST請求設定請求頭
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send(options.data);
}
複製程式碼
open 方法規定請求的型別、URL 以及是否非同步處理請求等資訊。
send 方法來向後臺傳送一個請求,在 post 請求中,我們將請求資料作為 send 方法的引數傳送個後臺。
setRequestHeader 方法用來設定請求頭資訊。
3、監聽狀態碼,請求結束後通過回撥函式來處理返回資料。
xhr.onreadystatechange = function () {
if (xhr.readyState == 4){
var status = xhr.status;
if (status >= 200 && status < 300 ){
options.success && options.success(JSON.parse(xhr.responseText));
}else{
options.fail && options.fail(status);
}
}
}
複製程式碼
以上,便完成了前後端的資料互動,在這裡,我做了一個簡易的 ajax 封裝。
Ajax 跨域
跨域安全問題的產生
關於 ajax 跨域安全問題的產生,主要是由於瀏覽器的同源策略:
同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。
關於源的定義:
如果兩個頁面的協議,埠(如果有指定)和域名都相同,則兩個頁面具有相同的源。
由上可知,當兩個頁面的協議,埠(如果有指定)和域名都不相同時,便產生了跨域問題。
基於以上原因,當我們的 http 請求出現跨域時,便會遭到瀏覽器的限制(注意:是瀏覽器的限制,在一段時間內,我曾一度以為,跨域是由於後臺服務的限制!)。
由此,我們可以得出跨域安全問題產生的條件:
1、瀏覽器限制。
2、遵循同源策略。(XMLHttpRequest遵循同源策略)
以上條件必須同時滿足,才會產生跨域安全問題。
解決跨域的思路
既然跨域安全問題的產生必須同時滿足以上兩個條件,那麼我們便可以從下面這兩個方面來解決跨域安全問題:
1、解決瀏覽器限制問題
1)我們可以在開啟瀏覽器時,手動關閉瀏覽器的限制,但是這個解決方法幾乎毫無用處,我們不可能要求每個使用者每次訪問網站時都來進行手動操作。
2)代理轉發。我們可以在同源下利用代理伺服器來轉發請求,避過瀏覽器限制。
3)通過在後端配置請求頭資訊,來告訴瀏覽器,允許對方跨域訪問。(CORS)
2、採取非同源策略限制下的其他手段。(JSONP)
具體實現
一、首先我們來聊一聊廣為人知的解決手段 JSONP
我們可以通過動態來建立一個 script 標籤,來向後臺請求資料資訊,在請求引數裡帶入前後端約定的回撥函式名稱,來實現在返回 script 檔案中呼叫資料處理函式。
const head = document.getElementsByTagName('head')[0];
const s = document.createElement('script');
let jsonpCallback = `?callback=${callbackName}`;
s.src = url + jsonpCallback;
head.appendChild(s);
複製程式碼
但這個方法存在一些弊端:
1、首先,因為回掉函式名是通過 url 帶入的引數,所以 JSONP 只支援 get 請求。
2、其次,這個方法需要前後端約定好,當後臺服務非本公司或無法改動時,則無法實現 JSONP。
在這裡我做了一個簡易的 JSONP 封裝。
二、解決瀏覽器的限制
手動關閉瀏覽器限制的方法,因其實用性不大,這裡不做贅述。
1、通過代理轉發
1)node 轉發。
我們可以在本域下建立 node 代理服務,來轉發請求:
node 服務程式碼 詳細程式碼示例,請看這裡
const express = require('express');
const request = require('request');
express()
.get('/proxy',(req,res) => {
request(req.query.proxy, function (error, response, body) {
res.send(body);
});
})
.listen(8081)
複製程式碼
前端 js 程式碼 詳細程式碼示例,請看這裡
$('#proxyBtn').addEventListener('click',(e) => {
ajax({
url: proxyUrl + 'proxy',
proxy: serverUrl + 'proxy',
type: 'GET',
async: true,
success: function(res,data){
$('#proxy').innerText = res.info;
},
})
})
複製程式碼
- Nginx 轉發(關於 Nginx 我所知不多,描述不對之處,望批評指正!)
這裡需要大家安裝 Nginx ,關於 Nginx 的安裝,網上教程很多,這裡不做贅述。(下面是我阿里雲的 Nginx 配置截圖)
在安裝完 Nginx 後,我們開啟 Nginx 的配置檔案:
在配置檔案中我們可以找到上圖紅框中的一句話,表明 Nginx 會從該路徑載入 Nginx 的配置。我們可以在該資料夾下新增配置檔案,來轉發請求:
其中 :
server_name 來設定我們監聽的同源請求路徑。
porxy_pass 來設定需要轉發的請求地址。
然後重新啟動 Nginx 來重新載入配置項,完成請求的代理轉發。
2、後端配置請求頭(CORS)
- 通過設定 Access-Control-Allow-Origin 來允許跨域
首先我們來看幾張截圖:
通過上圖,我們可以看到,跨域請求已經成功,後臺資料也已經返回,而控制檯卻依舊報跨域安全錯誤,更加充分的說明了,跨域安全錯誤是瀏覽器多管閒事,並非是後臺服務的限制。在圖中,我們還可以還看到,跨域請求的請求頭中比非跨域請求多出了一個 Origin 引數,表明了請求來源。我們將跨域請求的請求頭列印出來觀察,如下圖:
因此,我們可以通過在請求頭中新增 Access-Control-Allow-Origin 屬性,來告訴瀏覽器:允許該來源的跨域請求。
.get('/all',(req,res) => {
res.set('Access-Control-Allow-Origin',req.headers.origin);
res.send({ info : '通過在請求頭中設定 Access-Control-Allow-Origin 來告訴瀏覽器:允許所有的跨域請求(當帶有 cookie)'})
})
複製程式碼
詳細示例程式碼在這裡
- 使用下列方法之一:
GET
HEAD
POST- Fetch 規範定義了對 CORS 安全的首部欄位集合,不得人為設定該集合之外的其他首部欄位。該集合為:
Accept
Accept-Language
Content-Language
Content-Type (需要注意額外的限制)
DPR
Downlink
Save-Data Viewport-Width
Width- Content-Type 的值僅限於下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
我們在實際工作中,有時會在請求中新增自定義請求頭來滿足業務需求,例如:新增 token 做身份校驗。此時,我們便發出了一條非簡單請求。如下圖:
觀察圖片資訊,我們可以看到,當我們新增自定義請求頭時,瀏覽器會先傳送 OPTION 預檢請求,來和後臺進行確信,當後臺響應頭中不存在我們新增的自定義頭時,便會報錯。 我們可以在後臺服務中做以下處理:
.all('*',(req,res,next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'token');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (req.method == 'OPTIONS') {
res.send(200);
} else {
next();
}
})
複製程式碼
在響應頭中新增自定義的請求頭,讓瀏覽器通過預檢請求,發出真正的請求資訊。
next 引數的作用在於讓程式碼往下執行,因為 * 號可以匹配所有的路由,如果不呼叫 next ,則下面所有的請求都會被攔截下來。
詳細示例程式碼在這裡
雖然解決了自定義請求頭的跨域問題,但是這裡還有一個問題,如圖:
觀察圖片我們看到:每次的跨域請求,瀏覽器都會傳送預檢資訊。
這個問題我們可以通過設定 Access-Control-Max-age 快取預檢命令結果來進行優化。