什麼是“ 前端 ”“ 跨域 ” ?
常見跨域方式有這麼幾種:jsonp、cors,iframe+domain跨域、以及nginx反向代理,還有就是postMessage。
相比之“基於”前端的其餘幾種方法,iframe+domain和cors的方式不太常用 —— 沒錯,cors一般來講是後端設置,但是完全可以讓前端“一力以擔之”。
前文推薦(不瞭解“跨域”的可以先看這裏):
https://blog.csdn.net/qq_43624878/article/details/95853615
jsonp方式解決跨域問題
jsonp是打破第一重限制,(因爲)用了XMLHttpRequest就跨域,那不用這種方式了,我們來看一段jquery的帶jsonp的ajax請求:
$.ajax({
type : "GET",
url : "http://api.map.baidu.com/geocoder/v2/",
data:"address=河南",
dataType:"jsonp",
jsonp:"callback", //回調函數名默認是callback,可以自定義回調函數名字,#但是必須和後臺保持一致#
jsonpCallback:"showLocation", //數據返回成功之後,回調函數的名字是隨機生成的,如jquery0122526({....}) 在這裏自己指定一個(會被替換掉——這一行可以不寫)
success : function(data){
alert("成功");
},
error : function(data){
alert("失敗");
}
});
使用這種類型的話,會創建一個查詢字符串參數 callback=? ,這個參數會加在請求的URL後面。服務器端應當在JSON數據前加上回調函數名,以便完成一個有效的JSONP請求 。
上面代碼看似用了ajax請求,其實內部完全不是那麼回事,多了jsonp和jsonpCallback選項,它內部將代碼翻譯並把頁面上的dom操作成這樣:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type='text/javascript'>
// 後端返回直接執行的方法,相當於執行這個方法,由於後端把返回的數據放在方法的參數裏,所以這裏能拿到res。
window.showLocation = function (res) {
console.log(res)
//執行ajax回調
}
</script>
<script src='http://api.map.baidu.com/geocoder/v2/?address=河南&callback=showLocation' type='text/javascript'></script>
</body>
</html>
這個時候,html頁面的script src標籤回去訪問api.map.baidu.com的服務端,由於script,img這種標籤是不受瀏覽器xmlhttprequest限制的,可以隨意訪問,這個時候對應的後端代碼取得address等參數,然後根據雙方約定好的callback參數,返回一個被包裝後的json。
cors解決跨域
前面說了,這種方式一般是後端設置的:在後臺添加允許跨域:“跨域資源共享”(Cross-origin resource sharing)。它允許瀏覽器向跨源(協議 + 域名 + 端口)服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
前面也說了,這種方式完全可以前端獨自完成:
CORS——跨域資源共享
我們可以通過添加 共享站 :www.corsproxy.com(我們也叫作“請求中轉站”)
這主要是通過設置 Access-Control-Allow-Origin 來進行的。
使用方法: 在“共享站”後面加上url即可!
比如我們上面請求的:http://api.map.baidu.com/geocoder/v2/
現在我們這樣來寫:http://www.corsproxy.com/ api.map.baidu.com/geocoder/v2/
postMessage實現跨域通信
這常被用在比如“聊天機器人”上(至少筆者是這麼幹的…)
window.postMessage()方法可以安全地實現跨源通信。通常,對於兩個不同頁面的腳本,只有當執行它們的頁面位於具有相同協議、端口號以及主機(即兩個頁面的模數Document.domain設置爲相同的值)時,這兩個腳本才能互相通信。
window.postMessage()方法提供了一種受控機制來規避此限制,只要正確的使用,就很安全。
本質上說,postMessage()是基於消息事件機制來實現跨域通信,它隸屬於消息窗體本身,比如window以及window內嵌的frame的window,基本使用形式如下(通常被用在“發送方”頁面中):
someWindow.postMessage(message,targetOrigin,[transfer]);
- someWindow :窗口的一個引用(一般是新窗口),比如 iframe的contentWindow屬性、執行window.open返回的窗口對象,後者是命名過或數值索引的window.frames
- message :將要發送到其他window的數據。——不受格式限制,無需自己序列化
- targetOrigin :通過窗口的origin屬性指定哪些窗口能接收到消息事件,此值可以是字符串“*”(表示無限制)
- transfer :(可選)是一串和message同時傳遞的Transferable對象
我們可以通過如下方式監聽message(這通常被用在“接收方”頁面中):
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
let origin = event.origin || event.originalEvent.origin;
if (origin !== "http://aaa:8080")
return;
// ...
console.log(event.data)
}
// 派發消息的頁面
winB.postMessage(_({text: '休息休息'}), origin)
其中,event中有幾個核心屬性需要注意 如下:
- data :從其他window中傳遞過來的對象
- origin :調用postMessage時消息發送方窗口的origin。這個字符串由“協議、😕/、域名、:端口號”拼接而成
- source :對發送消息的窗口對象的引用
跨域實踐:聊天機器人
此demo只展示核心部分,使用postMessage完成。
我們分別有兩個HTML:a.html和b.html,然後用node分別代理兩個不同頁面,設置不同端口:
//依賴一個http模塊,相當於java中的import
// a.js
var http = require('http');
var fs = require('fs');
var { resolve } = require('path');
//創建一個服務器對象
server = http.createServer(function (req, res) {
//設置請求成功時響應頭部的MIME爲純文本
res.writeHeader(200, {"Content-Type": "text/html"});
//向客戶端(頁面)輸出字符
let data = fs.readFileSync(resolve(__dirname, './a.html'))
res.end(data);
});
//讓服務器監聽本地8000端口開始運行
server.listen(8000,'127.0.0.1');
console.log('http://127.0.0.1:8000')
// b.js
// ...
server.listen(8001,'127.0.0.1');
我們將a.html代理在8000端口下,將b.html代理在8001端口下。
搭建頁面層級:這裏將b頁面以iframe的形式嵌入到a頁面:
(a頁面)
<body>
<div class="wrap">
<iframe src="http://127.0.0.1:8001" frameborder="0" id="b"></iframe>
<div class="control">
<input type="text" placeholder="請輸入內容" id="ipt">
<span id="send">發送</span>
</div>
</div>
<script>
window.onload = function() {
let origin = 'http://127.0.0.1:8001';
let _ = (data) => JSON.stringify(data); //對象函數 & es6箭頭表達式
let winB = document.querySelector('#b').contentWindow;
let sendBtn = document.querySelector('#send');
sendBtn.addEventListener('click', (e) => {
let text = document.querySelector('#ipt');
winB.postMessage(_({text: text.value}), origin)
text.value = '';
}, false)
winB.postMessage(_({text: ''}), origin)
}
</script>
</body>
通過iframe的contentWindow來拿到b頁面窗體的引用,然後在發送按鈕的點擊事件中觸發postMessage將數據發送給B。
(b頁面)
<body>
<div class="content">
<h4>Mxc只能機器人</h4>
<div class="content-inner"></div>
</div>
<script>
// 語料庫-略
const pool = [];
window.addEventListener("message", receiveMessage, false);
let content = document.querySelector('.content-inner');
let initContentH = content.scrollHeight;
let _ = (data) => JSON.stringify(data);
function createChat(type, mes) {
let dialog = document.createElement('div');
dialog.className = type === 0 ? 'dialog robot' : 'dialog user';
let content = type === 0 ? `
<span class="tx">${type === 0 ? 'lab' : 'user'}</span>
<span class="mes">${mes}</span>
` : `
<span class="mes">${mes}</span>
<span class="tx">${type === 0 ? 'lab' : 'user'}</span>
`;
dialog.innerHTML = content;
return dialog
}
function scrollTop(el, h) {
if(el.scrollHeight !== h) {
el.scrollTop = h + 100;
}
}
function receiveMessage(event){
// 兼容其他瀏覽器
let origin = event.origin || event.originalEvent.origin;
if(origin === 'http://127.0.0.1:8000') {
let data = JSON.parse(event.data);
if(data && !data.text) {
mes = { text: '你好,我是機器人Lab,請問有什麼可以幫到您的嗎?' };
event.source.postMessage(_(mes), event.origin)
content.appendChild(createChat(0, mes.text))
}else {
content.appendChild(createChat(1, data.text))
scrollTop(content, initContentH)
setTimeout(() => {
content.appendChild(createChat(0, '正在解決'))
scrollTop(content, initContentH)
}, 2000);
}
}
}
</script>
</body>