在進行AJAX的時候會經常產生這樣一個報錯:
看紅字,這是瀏覽器的同源策略,使跨域進行的AJAX無效。注意,不是不發送AJAX請求(其實就是HTTP請求),而是請求了,也返回了,但瀏覽器‘咔擦’一聲,下面沒有了。對比下fiddler和瀏覽器抓的包的異同:
fiddler:
chrome:
簡而言之,瀏覽器這邊就是頭(response header)給看,身體(response body)不給看。
什么是同源策略?為什么會有同源策略?這一點在吳翰清老師著的《白帽子講Web安全》一書中由闡述,這里就不贅述了。下面要做的,就是使用JSONP讓上面的報錯消失,按正確的流程進行下去。
首先介紹下我這里的環境,兩個Web服務器,Tomcat監聽8081,Node監聽3000,Tomcat這邊實現一個處理AJAX的JSP文件,很簡單,返回一個JSON
<%@ page contentType="application/json; charset=utf-8" %> {"status": true}
Tomcat的頁面對這個URL發出AJAX請求,並打印出了返回值
但Node的頁面發出AJAX請求,則像上面那樣報錯了,因為AJAX有同源策略保護。怎么繞過這個保護呢?平時我們頁面引入的CSS、JS可能是從其他的服務器比如靜態服務器、CDN獲取內容,都在不同的域,可知頁面內的標簽引入JS是沒有同源策略一說的,而且也是進行request和處理response,於是我們把這個AJAX請求改為如下代碼:
var script = document.createElement('script'); script.src = 'http://localhost:8081/test/true.jsp'; document.body.insertBefore(script, document.body.lastChild);
但還是殘忍的報錯了
因為返回的JSON({"status": true})成為了一個獨立的js片段,而這個片段明顯是不符合語法的,如果返回的是符合語法規范的處理JSON的js片段而不僅僅是JSON就好了。比如我們將服務器端的代碼改成這樣:
<%@ page contentType="application/javascript; charset=utf-8" %> console.log({"status": true});
再在Node的頁面進行AJAX
目的是達到了,但問題是,這個AJAX的servlet不僅返回了數據,還返回了行為,難道我要把處理DOM的js寫在這里面嗎?頁面重構了又跑到這里來修改?問題太美不敢想,所以請求成功的方法必須寫在頁面的js里面,比如這樣
function callback(data) { console.log(data); } var script = document.createElement('script'); script.src = 'http://localhost:8081/test/true.jsp'; document.body.insertBefore(script, document.body.lastChild);
而服務器返回的js片段直接調用這個function就行了,這個就叫回調函數了
<%@ page contentType="application/javascript; charset=utf-8" %> callback({"status": true});
可以看到,這個方案比之前好多了,servlet和請求頁面的耦合度低了很多,但沒完全解決,比如callback這個回調函數的名字,如果把這個名字放在請求的parameter中,比如這樣
function callback(data) { console.log(data); } var script = document.createElement('script'); script.src = 'http://localhost:8081/test/true.jsp?cb=callback'; document.body.insertBefore(script, document.body.lastChild);
服務器對這個parameter進行處理
<%@ page contentType="application/javascript; charset=utf-8" %>
<%= request.getParameter("cb") %>({"status": true});
優化一下,對沒有cb參數的請求僅返回JSON
<% String callback = request.getParameter("cb"); if(null == callback) { response.setContentType("application/json; charset=utf-8"); %> {"status": true} <% }else { response.setContentType("application/javascript; charset=utf-8"); %> <%= callback %>({"status": true}) <% } %>
那么整個JSONP的功能就實現了。但還有一點瑕疵,代碼執行完html中留下了一個script標簽,強迫症能忍?處女座能忍?
解決方法:可以使用jQuery的方法,jQuery會清除掉留下的script標簽。
$.ajax({ url: 'http://localhost:8081/test/true.jsp', dataType: "jsonp", jsonp: "cb", success: function (data) { console.log(data) } });
也可以自己實現一個,我拋個磚,在js加載完成后刪除節點。
function callback(data) { console.log(data); } var script = document.createElement('script'); script.src = 'http://localhost:8081/test/true.jsp?cb=callback'; document.body.insertBefore(script, document.body.lastChild); script.onload = function(){ this.parentNode.removeChild(this); };
至此,不知道有人發現沒有,JSONP這種方式有一個致命的缺陷:就是由於它是通過引入script節點實現的,所以只支持GET方法。如果你任性,你無理取鬧,你一定要用post跨域,那么只能考慮使用CORS了。
JSONP的東西就到此結束了,其實做完才發現,實際上這是個很簡單的概念,取了個比較唬人的名字而已。