一、写在前面
代理模式(Proxy):由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这
两个对象之间起到中介的作用。
上述的简介,你能想到这个模式是怎么样的吗?
本系列文章,迟来的更新。
关注公众号“笔优站长”可阅读全部文章哟。
二、场景实例 —— 跨域
请求地址的更新变换
假如有这样的一个场景,由于用户相册模块上传的照片量越来越大,导致服务器端需要将图片上传模块重新部署到另外一个域(可理解为另一台服务器)中,这样对于前端来说,用户上传图片的请求路径发生变化,指向其他服务器,这就导致跨域问题。
假设地址如下:
原地址:https://upload-sc.biunav.com/
新域地址:https://upload-hz.biunav.com/
为什么我向咱们图片上传模块所在的服务器发送的请求,得不到数据呢?
$.ajax({
url: 'https://upload-hz.biunav.com',
success:function(res{
//无法获取返回的数据
}
})
这时候打开控制台,发现己经报错了,出现跨域问题了。
一切只因跨域
由于 JavaScript 对安全访问因素的考虑,不允许跨域调用其他页面,这里的域你可以想象成域名,
比如百度的域名http://www.baidu.com,
淘宝的域名 http://www.taobao.com,
不同域名下的页面是不能直接调用的。这样百度域名下的页面是不允许直接调用淘宝页面。
这也是一种 JavaScript 中因同源策略所定义的限制,不过仅此一点限制还不够,JavaScript 还对同一城名不同的端口号、同-城名不同协议、域名和域名对应的IP、主城与子域、子域与子域等
做了限制,都不能直接调用。
浏览器的同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源的定义
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:
三、代理对象
以上面的相册为例子,相册页面与图片上传模块所在的服务器之间你可抽象成两个对象,那么现在的问题是,它们之间被一条河隔开了,就像天河两端的牛郎织女,只能远远观望而不能相聚一见。
他们的情感感动万物,所以才有那么多需求为他们搭桥。同样你想让跨域两端的对象之间实现通信,你就需要找个代理对象来实现他们之间的通信。
虽然他们之间分开了,但是我们可以找一个代理对象来实现相互之间的通信。
代理对象有很多,简单一点的如 img 之类的标签通过 src 属性可以向其他域下的服务器发送请求。不过这类请求是get 请求,并且是单向的,它不会有响应数据,就好比你站在河的一边向另一边发消息,却又不想让别人听见,所以你可以将你的消息写在纸上放在口袋里,然后扔过去,不过河对岸有没有人按收到你的消息就不得而知了。
1、站长统计
站长平台会有对于你的页面的统计项,其实现原理就是在页面触发一些动作的时候向站长平台发送这类 img 的get 请求,然后他们会对你发的请求做统计,然而你并不知道统计的相关消息。
//统计代理
var Count = (function () {
//缓存图片
var _img = new Image();
//返回统计函数
return function (param) {
//统计请求
var str = 'http://www.***.com/a.gif';
//拼接请求字符串
for (const key in param) {
str += key + '=' + param[key];
}
//发送统计请求
_img.src = str;
}
})();
//测试用例: 统计number
Count({ number: 10 });
上面是一个简单的站长统计,就是使用了img的src来说实现的。
2、JSONP
第二种代理对象形式是通过 script 标签。比如我们在 CDN(内容分发网络,一种更接近用户的网络架构,是用户可以就近获取内容)上更快速地获取 jQuery 文件时,用
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
来获取,然而这种获取方式获取的 script 内容是不变的。而我们需要的代理对象,是对页面与浏览器间通信的,显然上面的方式还不能满足我们的需求,不过我们知道通过 src 属性可实现 get 请求,因此我们可以在src 指向的 url(请求地址)上面添加一些字段信息,然后服务器端获取这些字段,再相应地生成一份内容。
如下:
//前端浏览器页面
<script type="text/javascript">
//回调函数,打印出请求的数据与响应的数据
function jsonpCallBack(res,req){
console.log(res,req)
}
</script>
<script type="text/javascript" src="http://localhost/test/jsonp.php?callback=jsonpCallBack&data=getJsonPData"></script>
//另一个域下服务器请求接口
<?
// 后端获取的请求字段数据,并返回内容
$data = $_GET['data'];
$callback = $_GET['callback'];
echo $callback."('success','".$data."')";
?>
这种方式,你可以想象成河里面的一只小船,通过小船将你的请求发送给对岸,然后对岸的人们将数据放在小船里为你带回来。
3、代理模板
当然,这种方式还要求其他域要有一定可靠性。否则将会攻击到你的网站。当然这种方式也被人称之为 JSONP 方案,有时我们还会通过一个方法来动态生成需要的 JSONP 中的script标签。
与之类似的还有另外一种方案是被称之为代理模板的方案,解决思路是这样的,既然不同城之间相互调用对方的页面是有限制的,那么自己域中的两个页面相互之间的调用是可以的,即代理页面 B 调用被代理的页面 A 中对象的方式是可以的。
那么要实现这种方式我们只需要在被访问的域中,请求返回的 Header 重定向到代理页面,并在代理页面中处理被代理的页面 A就可以了。
X域中被代理页面 A
<script type="text/javascript">
//回调函数,打印出请求的数据与响应的数据
function jsonpCallBack(data){
console.log(data)
}
</script>
<iframe name="proxyIframe" id="proxyIframe" src="" frameborder="0"></iframe>
<form action="http://localhost/test/proxy.php" method="POST" target="proxyIframe">
<input type="text" name="callback" value="callback">
<input type="text" name="callback" value="http://localhost:8080/proxy.html">
<input type="submit" value="提交">
</form>
其次在X域中我们也要有一个代理页面,主要负责将自己页面 URL 中 searcher 部分的数据解析出来,如 http://www.*.com?type=1&title=aa 这个url 中 searcher 部分指的就是?type=1&title=aa。
将数据重新组裝好,调用 A 页面里的回调函数,将组装好的数据作为参数传入父页面中定义的回调函数中并执行。
X域中被代理页面 B
<script type="text/javascript">
// 页面加载后智校
window.onload = function () {
//如果不在A页面中返回,不执行
if (top == self) return;
// 获取解析searcher中的数据
var arr = location.search.substr(1).split("&"),
// 预定义函数名称及参数
fn, args;
for (var i = 0, len = arr.length, item; i < len; i++) {
// 解析searcher中的每组数据
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 (error) {
console.error(error)
}
}
</script>
最后是Y域中的被请求的接口文件C,它的主要工作是将从X域过来的请求的数据解析并获取回调函数字段与代理模板路径字段数据,并打包返回,并将自己的Header 重定向为X域的代理模板 B 所在路径。
<?
$proxy = $_POST['proxy'];
$callback = $_POST['callback'];
header("Location: ".$proxy."?callback=".$backcall."&arg=success")
?>
代理模式章节到站下车,本章内容比较详细,文字较多,整理了很久。有兴趣的朋友可以仔细阅读,相信你会有一个不错的收获。
四、总结
通过几种代理模式对跨域问题的解决方案,我们可以看到代理对象可以完全解决被代理对象与外界对象之间的耦合,当然从对被代理的页面角度来看是一种保护代理,然而从服务器角度来看又是一种远程代理。
除了在跨域问题中有很多应用外,有时对 对象的实例化对资源的开销很大,如页面加载初期加载文件有很多,此时能够延迟加载一些图片对页面首屏加载时间收益是很大的;再比如图片预览页面,页面中有很多图片,面对这么多的图片如果一加载对资源的开销也是很可怕的,所以通常是当用户点击某张图片时加载这张图片。
但如果该图片源文件也很大,此时我们常用的做法是先代理加载一张预览图片,然后再将原图片替换这张预览图片,这种代理有时也称为虚拟代理。
由此可见代理模式可以解决系统之间的耦合度以及系统资源开销大的问题,通过代理对象可保护被代理对象,使被代理对象拓展性不受外界的影响。
也可通过代理对象解决某一交互或者某一需求中造成的大量系统开销。当然无论代理模式在处理系统、对象之间的耦合度问题还是在解决系统资源开销问题,他都将构建出一个复杂的代理对象,增加系统的复杂度,同时也增加了一定的系统开销,当然有时对于这种开销往往是可接受的在JavaScript中,它的执行常常依托于浏览器,所以代理模式解决问题的思想有时也为我们提供了一些解决问题的方案。
留个彩蛋
文中提到的动态加载 script 标签的方法,查查资料看看你能否实现。对于图片预览这种代理模式,新建一个页面试着去实现它。
下章剧透 :在不改变原对象的基础上,对其进行扩展(添加属性或者方法),该如何解决呢?
五、写在后面
上面就是结构型设计模式中的——代理模式的全部内容了,你学废了吗?
有问题请留言或者@博主,谢谢支持o( ̄︶ ̄)o~
感谢您的阅读,如果此文章或项目对您有帮助,请扫个二维码点个关注吧,若可以的话再给个一键三连吧!
公众号阅读的朋友可以点一下右下角的在看和分享哦。
GitHub有开源项目,需要的小伙伴可以顺手star一下!
GitHub: https://github.com/langyuxiansheng
更多信息请关注公众号: “笔优站长”