Ajax 跨域
我們先從這么一個問題來引入我們本章節的學習 —— 什么是跨域請求?
1.跨域請求
簡單來說,跨域請求就是一個域下的資源請求另外一個域下的資源。
同一個域,指的是,協議名、域名、端口號都一致。 舉個例子來說,假如 “http://www.a.com
” 下的 JavaScript 腳本發起 Ajax 請求 “http://www.a.com/ajax
” ,由于 協議名 http 、域名 www.a.com
和 端口號(默認都是 80)三者都是一致的,因此都屬于同一個域,不造成跨域請求。而假如其中任一元素不相同,則造成跨域請求。與此同時,瀏覽器出于安全考慮,基于同源策略則會做一定的限制:比方說:
- 無法獲取不同域的 Cookie、LocalStorage 等等。
- 無法獲取不同域的 DOM 對象。
- 無法向不同域發送 Ajax 請求。
2.正文須知
本章節不考慮不同域文檔之間的跨域交互,主要講 Ajax 造成的跨域的解決方法。
開始講解 Ajax 造成的跨域問題如何解決之前,我們思考一下:
假如我們要從山的一邊 A 到山的另一邊 B,這座山無疑就是個障礙,那么我們有幾種解決辦法?
我想,要么我們就直接穿過去,要么我們就“曲線救國”,繞個道也未嘗不可。沒錯,接下來我們要講的 Ajax 跨域也是從這兩方面來講,既然跨域有這樣那樣的一些限制,那我們要么就直面去解決,要么就耍個機靈,同樣能夠解決。
3.曲線救國法
3.1 JSONP
JSONP 是一個非常經典的解決跨域的方法。我們知道,在 HTML 中,一些資源的引用事實上是不會受到跨域限制的,比如 script 標簽。瀏覽器在解析 HTML 的時候,解析到了 script 標簽,會把相應的資源下載下來。我們可以利用這一點,來實現前后端信息的交互。
3.1.1 JSONP 原理
- 定義好回調函數,比方說命名為 callback ,并將函數名作為 url 的參數;
- 添加 script 標簽,指定的資源為目標域的方法,也就是上面的 url ;
- 后端接收 GET 請求,返回
callback(responseData)
格式數據,把要返回的數據 responseData 傳到callback()
中; - 前端接收 javaScript 內容,執行了后端返回的
callback(responseData)
,這樣就完成了一次前后端交互了。
3.1.2 具體例子
假如 HTML 有一個容器為 container,我們要通過 JSONP 的方式來為 container 插入一條內容,那么,我們可以這么做:
3.1.2.1 HTML 關鍵代碼
<div id="container">
</div>
3.1.2.2 javaScript 關鍵代碼
// jsonp
// 定義一個添加內容的回調函數
window.addContent = function (content) {
document.getElementById('container').innerHTML = content;
}
/**
* 發送 JSONP 請求的函數
* cb 為回調函數的函數名
*/
function sendJsonPRequest (cb) {
// 創建 script 標簽
const body = document.getElementsByTagName('body')[0];
const script = document.createElement('script');
script.type = 'text/javascript';
// 指定標簽的 url ,callback 參數為回調函數的函數名
script.src = `http://localhost:8082/jsonp/get?callback=${cb}`;
body.appendChild(script); // 添加到 body 最后面
}
sendJsonPRequest('addContent') // 執行發送 JSONP 請求
顯而易見,前端我們會創建一個 script 標簽,并且附帶定義好的回調函數的函數名傳給服務端。與此同時,我們需要在服務端進行 JSONP 請求的響應。
3.1.2.3 服務端關鍵代碼
router.get("/jsonp/get", function(req, res) {
const cb = req.query.callback; // 讀取請求附帶的參數 callback
const resData = '這是一條服務端返回的內容';
res.send(`${cb}(${JSON.stringify(resData)})`); // 返回 callback(resData) 格式的數據
});
3.1.2.4 效果
從右邊控制臺可以看出來,我們成功創建了 JSONP 的請求,并且結果正如我們預期的執行了 addContent('這是一條服務端返回的內容')
,界面上展示出插入的內容。
3.1.3 JSONP 小結
使用 JSONP 的方式,我們可以通過 script 標簽繞過瀏覽器的跨域限制,進行前后端數據交互。不過另一方面,這種方法也很有局限性,我們只能夠發送 GET 請求,無法滿足更加復雜業務的需求。一般我們也不會推薦直接使用 JSONP 的方式來解決跨域問題。
3.2 服務端代理
接下來講到的一種是服務端代理的方式。要問為什么采取服務端代理的方式呢?很簡單,因為瀏覽器端 Ajax 請求有跨域的限制,那我們就把請求不同域的操作放在服務端好了,畢竟服務端是沒有跨域限制這一說的。
3.2.1 服務端代理原理
- 瀏覽器端發送請求到同域的服務端;
- 服務端接收到請求之后,進行轉發,請求不同域的另外一個服務端;
- 服務端間進行交互數據后,同域服務端返回數據給瀏覽器端。
3.2.2 具體例子
舉一個服務端代理的例子,這里我使用了一個 Express 的中間件,叫做 express-http-proxy 。當然同學們也可以在同域服務端接收到請求的時候,發起 http 請求訪問不同域的服務端來模擬這一代理行為。前端方面我使用了 jQuery 的 Ajax 方法。
3.2.2.1 javaScript 關鍵代碼
$.ajax({
url: '/proxy/proxy_get',
method: 'GET',
data: {
a: '123',
b: '234'
}
}).done(data => {
console.log(data)
})
很簡單,我們就是向同域的服務器發送了一個請求。
3.2.2.2 同域服務器關鍵代碼
const proxy = require('express-http-proxy'); // 引入代理中間件
// ... 一些代碼
app.use('/proxy', proxy('http://localhost:8082/')); // 注冊,之后 /proxy 都會代理到 http://localhost:8082/ 上
3.2.2.3 不同域的服務器關鍵代碼
router.get("/proxy_get", function(req, res) {
const {a, b} = req.query
res.send(`參數是:${a} 和 ${b}`)
});
這是目標服務器的響應方法,返回一個 處理后的字符串。
3.2.2.4 效果
3.2.3 服務端代理小結
服務端代理通過服務端和服務端之間的交互來避免瀏覽器和不同域的服務端之間直接進行交互,從而避免了跨域的問題。當然這種方法要求我們有一個中間服務器的存在。
4.面對疾風法
舉了兩個繞過跨域限制的方法,接下來我們要談談常規解決的情況。既然有跨域限制了,我們就來老老實實解決這個問題。
接下來我要講的,是 CORS 。
4.1 CORS
首先展開一下 CORS 的全稱:
Cross-origin resource sharing
意思是跨域資源共享,這是一個 W3C 標準,從字面意思來看不難理解,它允許瀏覽器向跨域的資源發送請求,并且獲得結果數據。
4.1.1 CORS 原理
跨域資源共享標準新增了一組 HTTP 首部的字段,使得我們能夠通過這些字段來跨域獲取到我們所需要的資源。而要實現這一功能,我們需要前后端的配合,只有當后端實現了 CORS 功能,我們才能夠通過瀏覽器直接訪問資源。為此,我們先來看看接下來的幾個首部字段:
- Access-Control-Allow-Origin :表示服務端允許的請求源的域,如果是 * 表示允許所有域訪問,一般我們不建議使用 *;
- Access-Control-Allow-Headers: 表示預檢測中,列出了將會在正式請求的 Access-Control-Request-Headers 字段中出現的首部信息;
- Access-Control-Allow-Methods: 表示服務端允許的請求方法;
- Access-Control-Allow-Credentials: 表示服務端是否允許發送cookie。當然前端也需要設置對應的
xhr.withCredentials
來進行配合; - Access-Control-Expose-Headers: 列出了可以作為響應的一部分暴露在外的頭部信息。
其中,我們更為重要的當屬 Access-Control-Allow-Origin 字段,因為這個字段直接關系到你是否能夠跨域訪問資源的權限了。通常情況下,為了解決跨域問題,后端同學會設置 Access-Control-Allow-Origin 指定為我們的請求源的域,而前端代碼基本無感。
4.1.2 簡單請求和非簡單請求
關于 CORS ,HTTP 請求上會有一些小小區別,最直觀的區別就是會不會觸發多一次 OPTIONS 預檢測請求。我們把一些不會觸發預檢測請求的請求,稱為簡單請求,而相反,會觸發預檢測的請求則是非簡單請求。
而關于如何區分簡單請求和非簡單請求,這里我就不再累贅,有興趣的同學可以讀一下 HTTP 控制訪問 。在實際的工作過程中,使用到 CORS 來解決跨域限制是非常常見的,這里我們注意一下簡單請求和非簡單請求的直觀區別即可,并在以后的工作中留意一下,而不至于懵逼于為什么多了一次 OPTIONS 請求。
4.1.3 具體例子
4.1.3.1 服務端核心代碼
// 全局設置請求過濾
app.all('*',function (req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); // 設置 Access-Control-Allow-Origin
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With'); // 設置 Access-Control-Allow-Headers
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); // 設置 Access-Control-Allow-Methods
next()
});
// 注冊一個簡單的路由
router.get("/simple/get", function(req, res) {
const {a} = req.query
res.send(`參數值是${a}`)
});
后端要做的工作就是實現 CORS 功能。正如上方代碼,我們規定了一系列 HTTP 請求頭首部字段,使得 http://localhost:8080 這個域的前端腳本擁有向服務端發起請求并取得資源的權限。
4.1.3.2 前端核心代碼
$.ajax({
url: 'http://localhost:8083/simple/get',
method: 'GET',
data : {
a: 1
}
}).done(data => {
console.log(data)
})
4.1.3.3 效果
可見,通過 CORS ,前端成功拿到了不同域的服務端的返回內容。
4.1.4 CORS 小結
CORS 是一個 W3C 的標準。使用 CORS ,我們可以使用使用常規的方式來解決前后端跨域訪問的問題。并且,大多數的工作其實也是放在了服務端上,對于前端而言,基本上可以說是無感的。
當然, CORS 也是存在著一些弊端。正因為它是 W3C 中一個比較新的方案,導致了各大瀏覽器引擎沒有對其做嚴格規格的實現,由此可能產生一些不一致的情況。
5.本章最后
跨域遠不止 Ajax 跨域,而解決 Ajax 跨域的方法也不只有本章中提到的這三種。
說跨域遠不止 Ajax 跨域,打個比方,不同域的網頁之間的通信也是屬于跨域范疇。但由于本章的主題是 Ajax 跨域,因此我們不做過多的討論。有興趣的同學,可以深入去探究一下。
而解決 Ajax 跨域的方法,本章提及 3 種方法,從兩個方面來闡述。對于遇見的問題,解決的方法要么就是繞個道走,要么就是穿過去走。無論你使用哪一種方法,肯定也都有利有弊。而實際的應用中,我們到底要采用何種方法來解決 Ajax 跨域問題呢?我的建議是關注業務和場景,這就需要同學們在另外一個層面去進行深入的思考了。
本著鼓勵深入學習深入思考的原則,我希望同學們能夠在跨域的問題上,進行深入的研究,總結起來。