請求跨域問題的產生原因是瀏覽器的同源策略(Same origin policy),這是有Netscape提出的一個著名的安全策略,對於瀏覽器而言,它提供了最基本也是最核心的安全功能。它所指的同源是域名、協議、端口都相同。
從wiki百科上截了個例子說明:
從圖中可以看出,三者只要任何一個不相同,都會導致Failure.
什么樣的姿勢跨域,才能成功的跨域?
貼上一份簡單的node代碼,用以說明服務端的情況:
var http = require('http'); var fs = require('fs'); var reg = /(^|\?|&)callback=\w*/; var MINE_TYPE = { 'css': 'text/css', 'html': 'text/html', 'js': 'text/javascript', 'txt':'text/plain' }; http.createServer(function(req,res){ var _url = req.url; var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("XMLHttpRequest is success")':'XMLHttpRequest is success'; res.writeHead(200,{'Content-Type':MINE_TYPE['txt'],'Access-Control-Allow-Origin':'http://192.168.1.162:9988'}); res.end(data); }).listen(3003,'192.168.1.162'); function _server(port){ http.createServer(function(req,res){ var pathname = req.url.substr(1); var _ext = pathname.split('.').pop(); fs.readFile(pathname,'utf-8',function(err,data){ res.writeHead(200,{'Content-Type':MINE_TYPE[_ext]}); res.end(data) }) }).listen(port,'192.168.1.162'); } _server(8899); _server(9988);
先以普通的姿勢來跨一次(本文調試使用端口條件處理同源):
html:
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script> <div class="xmlhttprequset">發送請求</div>
js:
$('.xmlhttprequset').click(function(){
$.get('http://192.168.1.162:3003',function(res){
console.log(res);
})
})
我們在瀏覽器url框輸入192.168.1.162:8899/test.html和192.168.1.162:9988/test.html打開兩個頁面,分別點擊頁面上的"發送請求",得到以下結果:
XMLHttpRequest cannot load http://192.168.1.162:3003/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.1.162:port' is therefore not allowed access.
看到這個錯誤,心里一如既往的爽...那么爽完之后,該解決錯誤了,最常見的是jsonp處理跨域問題:
js:
$('.xmlhttprequset').click(function(){
$.ajax({
type:'get',
dataType:'jsonp',
url:'http://192.168.1.162:3003',
success:function(res){
console.log(res);
}
});
})
於是,我們就能夠在控制台看到打印出來的東西了...
那么我不想使用jsonp呢,能否實現跨域呢?答案是肯定的。而且還可以允許9988端口跨域請求,對8899端口進行"丑拒",就是這么任性~ 辦法就是在response頭配置"Access-Control-Allow-Origin"參數的值來控制給不給跨,不給跨,給跨,跨... ←_← 這真的只是回聲.
於是我們將server.js代碼改了改:
http.createServer(function(req,res){ var _url = req.url; var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("success")':'success'; res.writeHead(200,{'Content-Type':MINE_TYPE['txt'],'Access-Control-Allow-Origin':'http://192.168.1.162:9988'}); res.end(data); }).listen(3003,'192.168.1.162');
然后將jsonp請求的代碼換回get請求的代碼,重啟這個js,切換到瀏覽器,刷新2個頁面,再次點擊請求,效果如下:
:9988
success
:8899
XMLHttpRequest cannot load http://192.168.1.162:3003/. The 'Access-Control-Allow-Origin' header has a value 'http://192.168.1.162:9988' that is not equal to the supplied origin. Origin 'http://192.168.1.162:8899' is therefore not allowed access. // 強行暴擊
那么現在姿勢明確了,兩個姿勢:正常請求+response設置允許域,jsonp。
jsonp 實現原理
jsonp通過添加一個<script>標簽,將該標簽的src指向請求資源的接口,並且需要在請求中帶上一個callback參數,script的src是不受瀏覽器的同源策略限制的,所以只要后端將數據包裝在這個callback的方法里返回即可,於是我們有了這段代碼:
server.js:
var data = _url.indexOf('callback') >=0 ? req.url.match(reg)[0].substr(10)+'("success")':'success'; // 如果包含callback參數,則將參數以callback的值為方法名包裝成一個執行函數,否則直接返回數據
jsonp在jQuery中的實現為:
var oldCallbacks = [], rjsonp = /(=)\?(?=&|$)|\?\?/; // 默認jsonp設置 jQuery.ajaxSetup({ jsonp: "callback", jsonpCallback: function () { var callback = oldCallbacks.pop() || (jQuery.expando + "_" + (nonce++)); this[callback] = true; return callback; } }); jQuery.ajaxPrefilter("json jsonp", function (s, originalSettings, jqXHR) { var callbackName, overwritten, responseContainer, jsonProp = s.jsonp !== false && (rjsonp.test(s.url) ? "url" : typeof s.data === "string" && (s.contentType || "") .indexOf("application/x-www-form-urlencoded") === 0 && rjsonp.test(s.data) && "data" ); // jsonp 判斷 if (jsonProp || s.dataTypes[0] === "jsonp") { // 生成 callback 名稱 callbackName = s.jsonpCallback = jQuery.isFunction(s.jsonpCallback) ? s.jsonpCallback() : s.jsonpCallback; // url處理 加入 callback if (jsonProp) { s[jsonProp] = s[jsonProp].replace(rjsonp, "$1" + callbackName); } else if (s.jsonp !== false) { s.url += (rquery.test(s.url) ? "&" : "?") + s.jsonp + "=" + callbackName; } // 方法執行后取數據 s.converters["script json"] = function () { if (!responseContainer) { jQuery.error(callbackName + " was not called"); } return responseContainer[0]; }; // Force json dataType s.dataTypes[0] = "json"; // 添加callback這個方法 overwritten = window[callbackName]; window[callbackName] = function () { responseContainer = arguments; }; // 毀屍滅跡處理 jqXHR.always(function () { // 如果之前不存在這個方法,刪除 if (overwritten === undefined) { jQuery(window).removeProp(callbackName); // 如果之前就存在這個方法,恢復 } else { window[callbackName] = overwritten; } // if (s[callbackName]) { // 確保安全處理,不影響其他項 s.jsonpCallback = originalSettings.jsonpCallback; // 預留着 oldCallbacks.push(callbackName); } // 如果是個函數,攜帶數據調用 if (responseContainer && jQuery.isFunction(overwritten)) { overwritten(responseContainer[0]); } responseContainer = overwritten = undefined; }); // 歸為script return "script"; } }); // jsonp實現,生成script及之后移除 jQuery.ajaxTransport("script", function (s) { // 這個函數指明了只處理跨域請求 if (s.crossDomain) { var script, callback; return { send: function (_, complete) { script = jQuery("<script>").prop({ charset: s.scriptCharset, src: s.url }).on( "load error", callback = function (evt) { script.remove(); callback = null; if (evt) { complete(evt.type === "error" ? 404 : 200, evt.type); } } ); // 使用原生DOM操作避免一些 domManip ajax 問題 document.head.appendChild(script[0]); }, abort: function () { if (callback) { callback(); } } }; } });
簡單過一遍,就是這樣了...然后扔出整理后的,不加各種判斷的,簡單的,只有20行代碼的實現,幫助理解上面這么長長的一堆代碼:
js:
function getInfo(url, callback, _callback){ url += url.indexOf('?')>=0 ? '&callback=' + callback : '?callback='+callback; // url處理 var overWriteContent; var script = document.createElement('script'); script.src = url; overWritten = window.dataBack; window[callback] = function(){ overWriteContent = arguments[0]; }; // 生成callback方法,掛在window下 document.head.appendChild(script); // 添加script標簽 script.onload = function(e){ document.head.removeChild(script); // 刪除script標簽 if(overWritten === undefined) delete window[callback]; // 銷毀window下的callback方法 if(e.type === 'load'){ _callback(overWriteContent); // 帶上數據執行回調 } else{ console.error('error:failed to load the resource'); } } }
如有不正之處,感謝指正,同時歡迎小伙伴們交流討論~
水平較渣,不喜勿噴,謝謝!
