今天遇到個很經典的問題,涉及範圍從 瀏覽器緩存 到 網絡,所以記錄下,以備之後遇到的時候可以快速定位。
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之前,這個步驟必不可少。
好啦~ 知識點總結完畢,總之這次的問題還是比較有記錄價值的,需要我們很多方面的只是都有所瞭解。所以說 路漫漫其修遠兮~