前端常用的几种跨域通信方式实践:jsonp&cors&postMessage

什么是“ 前端 ”“ 跨域 ” ?

常见跨域方式有这么几种: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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章