【JS设计模式】牛郎织女——代理模式

146 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

代理模式(Proxy): 由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这两个对象之间起到 中介 作用。

跨域

由于JavaScript对 安全访问因素 的考虑, 不允许 跨域(域名)调用其他页面。

域名可以理解为我们常说的“网址”,比如说百度的域名是 http://www.baidu.com,而淘宝的域名是 http://www.taobao.com 。不同域名下的页面是 不能直接调用的。 这是一种JavaScript中 因同源策略 所定义的限制。除了这种限制,JavaScript还对以下几种情况也做了限制:

限制情况示例
同域名不同端口号http://www.baidu.com:8081http://www.baidu.com:8082
同域名不同协议http://www.baidu.comhttps://www.baidu.com
域名和域名对应的IPhttp://www.baidu.comhttp://61.135.169.125
主域和子域http://www.baidu.comhttp://b.*.com
子域和子域http://tieba.baidu.comhttp://fanyi.baidu.com

上面这些情况都会造成 跨域 问题。一般我们可以把跨域两端抽象成两个对象,牛郎和织女,他们中间隔着一条银河,让他们无法直接相见,只能通过鹊桥才能见面谈情说爱。同样的,跨域两端的对象想要实现通信, 同样需要找个代理对象(鹊桥)来帮助他们。

站长统计

代理对象有很多种,比如说 <img> 标签可以通过 src 属性向其他域下的服务器发送请求。 不过 这类请求是 get 请求,而且是单向的,不会有响应数据的。 就像我们放孔明灯,在灯上写着我们想传递的消息,但是能不能被别人收到我们就不知道了。

这种代理对象常见于一些站长平台,其原理就是 在你的页面触发一些动作的时候,向站长平台发送这类img的get请求,然后站长平台对你发的请求做统计。

// 统计代理
const Count = (function(){
    const img = new Image() // 缓存图片
    // 返回统计函数
    return function(params){
        let str = 'http://www.****.com.a.gif?' // 接口地址
        for(const key in params){
            str += (key + '=' + params[key])
        }
        // 发送统计请求
        img.src = str
    }
})();
Count({num:10}) // 测试:统计num

JSONP

还有一种代理对象是通过 <script> 标签,比如我们在CDN上获取JQuery的源码,就通过这种方式引入的。 这种方式获取到的script内容是不变的。 但是,我们可以在src指向的地址上添加一些字段,然后服务器获取到这些字段后再相应的生成一份内容。

<script>
    // 回调函数
    function jsonpCallBack(res,req){
        console.log(res,req)
    }
</script>
<!-- JSONP -->
<script src="http://uqiwehfkj/test.php?callback=jsonpCallBack&data=getJsonPData"></script>
<!-- 另一个域下的服务器请求接口 -->
<?php
    $data = $_GET['data']; // 获取数据
    $callBack = $_GET['callback']; 
    echo $callBack."('success','".$data."')" // 等价于 jsonpCallBack('success',$data)
>

这种方式需要其他域下的服务器端和我们前端协同开发,而且,还要求其他域具有一定的可靠性,不然很容易被人家攻击。就像我们写信告诉别人我家里有什么什么,家里钥匙藏在哪里呀,如果是亲人还好,如果是坏人直接就偷家了。

代理模板

代理模板的解决思路: 既然不同域之间相互调用对方的页面是有限制的,那么自己域中的两个页面相互之间的调用是可以的,也就是说,代理页面B调用被代理的页面A中对象的方式是可以的。那么,要实现这种方式,我们只需要在被访问的域中,请求返回的Header重定向到代理页面,并且在代理页面中处理被代理的页面A就可以了。

什么意思呢?

就是,我们称自己的域为X域,别人的域为Y域,正常情况下,X域访问Y域是会产生跨域错误的。但是现在X域有一个页面A,这个A称为被代理页面。A页面有 3个部分, 分别是 1. 可以发送请求的模块,比如说form表单提交,这个模块负责向Y域发送请求,并且提供额外的两组数据(执行的回调函数的名称;X域中代理模板所在路径,并将target执行iframe);2.一个内联框架,即iframe,负责提供第一部分form表单的响应目标target的执行,并将嵌入在X域中的代理页面作为子页面,也就是页面B。3. 一个回调函数,负责处理返回的数据。

<!--X域中被代理的页面A-->
<script>
    function callBack(data) {
        console.log('成功接收数据', data);
    }
</script>
<iframe src="" name="proxyIframe" id="proxyIframe"></iframe>
<!--form表单-->
<form action="http://localhost:8081/test/proxy.php" method="post" target="proxyIframe">
    <input type="text" name="callBack" value="callBack">
    <input type="text" name="proxy" value="http://localhost:8080/proxy.html">
    <input type="submit" value="提交">
</form>

form标签的target属性规定了在何处打开action的URL。

除了上面的页面A,我们在X域中还需要一个代理页面B,这个页面B主要 负责将自己页面的URL中query部分的数据解析出来,将数据重新组装好,接着调用A页面的回调函数,将组装好的数据作为参数传入A页面中定义的回调函数中并执行。

// 页面B加载后执行
window.onload = function () {
    if (top === self) return  // 不是在A页面就不执行
    // 获取并解析query中的数据
    const arr = location.search.substr(1).split('&')
    let fn, args;
    for (let i = 0, len = arr.length, item; i < len; i++) {
        // 解析query中的每组数据
        item = arr[i].split('=')
        if (item[0] === 'callback') {
            fn = item[1] // 设置回调函数
        }
        // 判断是否为参数集
        else if (item[0] === 'arg') {
            args = item[1]
        }
    }
    try {
        // 执行A页面中的回调
        eval('top.' + fn + '("' + args + '")')
    } catch (e) {        }
}

最后便是Y域中的接口文件C了, 它的主要工作是将X域过来的请求的数据解析并获取回调函数字符与代理模板路径字段数据,并打包含返回,同时还要将自己的Header重定向为B页面所在路径。

<?php
    $proxy = $_POST['proxy'];
    $callback = $_POST['callback'];
    header("Location:".$proxy."?callback=".$callback."&arg=success");
?>

代理模板这个里感觉讲起来很复杂,其实归纳一下也很好理解。首先是A页面,通过form表单向其他域的C文件发起请求,这个请求中,将B页面在X域中所在的路径也传递了过去,然后C文件巴拉巴拉一同操作,本来是应该将结果直接返回给A页面的,但是这样不行,于是C通过重定向的方式(因为有B页面的路径),跳转到B页面,而B页面因为form表单的target属性,会在iframe中打开,所以A是B的父页面,然后B页面中通过eval函数,执行A页面中的回调函数。这里面的B就像鹊桥,A和C文件就像牛郎织女。