今天遇到个很经典的问题,涉及范围从 浏览器缓存 到 网络,所以记录下,以备之后遇到的时候可以快速定位。
bug发生背景:
有一个这样的需求,如下图所示,页面可以预览到一个加密过的图片的列表(这里的重点是,图片和页面是不同源,也就是我们常说的跨域了),查看图片的时候ok的,显示完美,但是呢,一旦点击右上角的“下载文件”就有问题了,会报跨域的错误(下图二)。
这里有个前提是关于图片下载的实现:(可点击到我的另一篇博文查看详情:https://blog.csdn.net/Echo601/article/details/102508976)
1. 前端实现图片下载的方式: 由于a标签的href赋值为图片地址,只能起到预览的效果不能下载到本地,所以要用到canvas.drawImage的方法将地址转化成base64格式,然后赋值给a标签后再点击。
function downloadIamge (imgsrc, name) {
// 下载图片地址和图片名
let image = new Image();
// 解决跨域 Canvas 污染问题
image.setAttribute('crossOrigin', 'anonymous');
image.onload = function () {
let canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
let context = canvas.getContext('2d');
context.drawImage(image, 0, 0, image.width, image.height);
let _dataURL = canvas.toDataURL('image/png'); // 得到图片的base64编码数据
let blob_ = dataURLtoBlob(_dataURL); // 用到Blob是因为图片文件过大时,在一部风浏览器上会下载失败,而Blob就不会
let url = {
name: name || '图片', // 图片名称不需要加.png后缀名
src: blob_
};
// 异步生成一个a标签,通过a标签的download属性下载图片
// 由于a标签的href赋值为图片地址,只能起到预览的效果不能下载到本地,所以要用到canvas.drawImage的方法将地址转化成base64格式,然后赋值给a标签后再点击
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(url.src, url.name); // filename文件名包括扩展名,下载路径为浏览器默认路径
} else {
let link = document.createElement('a');
link.setAttribute('referrer', 'origin');
link.setAttribute('href', window.URL.createObjectURL(url.src));
link.setAttribute('download', url.name + '.png');
document.body.appendChild(link);
link.click();
}
};
image.src = imgsrc;
function dataURLtoBlob (dataurl) {
let arr = dataurl.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let bstr = atob(arr[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
}
好了,背景介绍完了。
那我们来说下这个bug是怎么解决的:
首先这个问题的报错跨域了,第一反应就是是找后端查看跨域设置(Access-Control-Allow-Origin)这个属性是否配置了我当前页面域名的白名单。得到的答案是肯定的,所以后端的嫌疑基本排除。
第二,通过分析下载图片的时候控制台的请求以及抓包工具的监控,发现这个下载图片的请求根本就没有发出去!!页面请求如下图所示:
So,根据我敏锐的直觉,以及在查找资料时发现如下提示:
下图提示的意思,当我们在进行下载图片时进行的canvas重绘步骤,这个步骤是会跨域的,但是!正常浏览图片是不需要跨域!有了这个思路,我查看了下同一张图片在进行浏览时的请求信息:
我们在这里request header可以发现是不需要跨域的,并且不需要发送origin参数(可以再次看下上面请求失败的头,需要origin参数)。
到这里整个问题就比较清晰了,同一张图片第一次浏览的时候没有问题,第二次下载的时候需要跨域就出现了问题(这里的前提是后端以及配置好了Access-Control-Allow-Origin)。
so,解答来了,第一次我们看到图片的时候,图片其实是以及在浏览器中已经缓存好了,所以在第二次发送请求的时候,浏览器就是默认用缓存好的图片去发了请求,而这个缓存的图片是不需要跨域的!所以造成了请求头部信息的缺失。
而我们这里的解决方式就是把这个已经浏览过的图片当成一张全新的图片去请求,这样就会带上跨域请求需要的origin参数。
解决方法如下~:
我们只需要在请求下载的图片链接后面加上一个随机数,这样浏览器就会认为这是一张全新的图咯,请求就可以成功啦,所以上面下载图片的的方法在最开始我们可以这样修改:
imgsrc = imgsrc + '×temp=' + Math.floor(Math.random() * 1000);
function downloadIamge (imgsrc, name) {
// 给图片加上一个随机数的参数
imgsrc = imgsrc + '×temp=' + Math.floor(Math.random() * 1000);
// 下载图片地址和图片名
let image = new Image();
// 解决跨域 Canvas 污染问题
image.setAttribute('crossOrigin', 'anonymous');
...
...
和之前一样,不重复写。
}
这样就好啦~~
那接下来呢,我们就来分析下这个bug中涉及到的知识点
1. 什么时候我们需要跨域请求:
2. 上面提到drawImage支持我们使用跨域请求,那它是如何实现的呢?
答案就是这个属性 :
image.setAttribute('crossOrigin', 'anonymous');
image对象可以设置一个允许跨域访问的属性,解决跨域 Canvas 污染问题。所以我们下载图片,讲图片转成canvas之前,这个步骤必不可少。
好啦~ 知识点总结完毕,总之这次的问题还是比较有记录价值的,需要我们很多方面的只是都有所了解。所以说 路漫漫其修远兮~