組合 a 標籤與 canvas 實現圖片資源的安全下載
普通使用者下載圖片時只需一個「右鍵另存為」操作即可完成,但當我們做線上編輯器、整個 UI 都被自定義實現時,如何解決不同域問題並實現頁面中圖片資源的安全下載呢?本文就解決該問題過程中所涉及的正則表示式、Web API 和 canvas 操作進行記錄。
本文分為以下七個部分:
- 利用<a>標籤下載任意資源
- 解析 DOM 獲取圖片連結
- 分情況處理圖片連結
- 工具函式中的正則表示式完善
- canvas 繪製圖片資源並轉 Data URLs 返回
- 實際使用與總結
- 參考資料
以下開始正文。
廣州vi設計公司 http://www.maiqicn.com 我的007辦公資源網 https://www.wode007.com
0. 利用<a>標籤下載任意資源
最簡單的辦法,當然是利用<a>標籤。根據MDN描述,<a>標籤有一個屬性叫download,此屬性指示瀏覽器下載 URL 而不是導航到它,因此將提示使用者將其儲存為本地檔案。如果我們再給該屬性賦值,那麼此值將在下載儲存過程中作為預填充的檔名。
所以我們可以將需要資源連結附在一個帶download屬性的<a>標籤上,以此實現下載的功能,例如:
<a
href="http://hijiangtao.github.io/README.md"
download="default"
>
下載 README
</a>
但需要注意的是,此屬性僅適用於同源 URL,如果我們給<a>標籤塞入一個跨域圖片,那麼在 chrome 中點選的效果將會是在一個頁面中開啟並展示這張圖片,而沒有下載行為。所以,面對跨域圖片資源時,我們該怎麼辦呢?
我們都知道<img>載入圖片資源時是不受跨域限制的,而canvas畫布可以繪製任意圖片資源,並將自身轉換為 Data URLs。是的,按照這個思路,我們來一步步來解決問題。
1. 解析 DOM 獲取圖片連結
首先從 DOM 中找到<img>標籤並提取圖片資源連結,如果你可以通過選擇器直接取到<img>物件,那麼直接取 src 屬性便可,例如:
const {src} = document.getElementById("hijiangtao");
如果你拿到的是一串html字串,那麼你將會用到如下一條正則表示式,用於匹配<img>標籤並提取其中 src 內容:
// @Input - rawhtml
const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
const matchArray = re.exec(rawHTML);
const src = matchArray && matchArray[1]) || '';
注:關於<img>標籤有<img>和<img />兩種形式的討論,本文不做討論,詳情可以移步StackOverflow。
2. 分情況處理圖片連結
拿到 src 即圖片連結後我們來分情況討論下,處理邏輯應該分這幾步(本文中 Data URLs 特指 base64 形式圖片 URL,以下不再額外說明):
- 同域圖片或者 Data URLs 圖片直接返回
- 跨域圖片轉 Data URLs 返回
故我們的程式碼應該長成這樣,考慮到 img 標籤完成資源下載時需要回調,我們用一個 Promise 將函式結果包住:
/**
* 獲取可安全下載的圖片地址
* @param src
*/
export const getDownloadSafeImgSrc = (src: string): Promise<string> => {
return new Promise(resolve => {
// 0. 無效 src 直接返回
if (!src) {
resolve(src);
}
// 1. 同域或 base64 形式 src 直接返回
if (isValidDataUrl(src) || isSameOrigin(src)) {
resolve(src);
}
// 2. 跨域圖片轉 base64 返回
getImgToBase64(src, resolve);
});
};
注:關於 base64 格式的編碼和解碼本文不做過多解釋,Web APIs 已經有對 base64 進行編碼解碼的方法:,詳情可移步Base64 encoding and decoding檢視更多。
3.工具函式中的正則表示式完善
上例中我們新增了很多處理函式,在這裡我們把他們一一實現,首先來看看判斷圖片是否為 base64 格式的函式實現。
base64 格式是 Data URLs 的一種。Data URLs,即字首為data:協議的URL,其允許內容建立者向文件中嵌入小檔案。它由四個部分組成:字首data:、指示資料型別的MIME型別、如果非文字則為可選的base64標記、資料本身:
data:[<mediatype>][;base64],<data>
其中標記部分可選,字首和資料必選,MIME 我們後文再繼續介紹。那麼,知道了 Data URLs 的組成,我們便可以把判斷 URL 是否為有效 Data URLs 的正則匹配方法寫成這樣:
/**
* 判斷給定 URL 是否為 Data URLs
* @param s
*/
export const isValidDataUrl = (s: string): boolean => {
const rg = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)\s*$/i;
return rg.test(s);
};
關於跨域問題,我在文章《前端跨域請求解決方案彙總》中已有更詳細的說明,這裡我們直接用一個不夠完美但基本可用的字串方法來解決跨域判斷:
/**
* 判斷給定 URL 是否與當前頁面同源
* @param s
*/
export const isSameOrigin = (s: string): boolean => {
return s.includes(location.origin)
}
這裡我們再來說說 MIME,這個在我們完善 canvas 轉 Data URLs 方法時會用上。MIME,全稱 Multipurpose Internet Mail Extensions,我們通常說的 MIME 型別也稱為媒體型別,它是一種用來表示文件、檔案或位元組流的性質和格式的標準。
對於圖片資源來說,Web 頁面中廣泛支援的 MIME 型別包含以下幾種:
MIME 型別 | 圖片型別 |
---|---|
image/gif | GIF 圖片 (無損耗壓縮方面被PNG所替代) |
image/jpeg | JPEG 圖片 |
image/png | PNG 圖片 |
image/svg+xml | SVG圖片 (向量圖) |
如果不考慮 webp 以及 icon 等格式,我們想要從一個資源 URL 中提取出 MIME 格式便可以這樣做:
/**
* 根據資源連結地址獲取 MIME 型別
* 預設返回 'image/png'
* @param src
*/
export const getImgMIMEType = (src: string): string => {
const PNG_MIME = 'image/png';
// 找到檔案字尾
let type = src.replace(/.+\./, '').toLowerCase();
// 處理特殊各種對應 MIME 關係
type = type.replace(/jpg/i, 'jpeg').replace(/svg/i, 'svg+xml');
if (!type) {
return PNG_MIME;
} else {
const matchedFix = type.match(/png|jpeg|bmp|gif|svg\+xml/);
return matchedFix ? `image/${matchedFix[0]}` : PNG_MIME;
}
};
啟用了 CORS 的圖片瞭解更多。
注2: 由於編碼格式有所差別,Blob URL 比起 Data URLs 所佔的空間資源更少,效能也更好。經網友指明,Blob URL 效能會好於 Data Urls,感興趣的話可以嘗試。
5. 實際使用與總結
以 Angular 為例,我們的 HTML程式碼可能要增加這麼一段:
<a
*ngIf="downloadImageUrl"
href=""
download="image"
class="context-menu-link"
>
儲存圖片至本地
</a>
而對於 TypeScript 指令碼,除了引入 getDownloadSafeImgSrc 實現外,我們需要在某一個流更新所通知到的方法中增加如下引用:
import { getDownloadSafeImgSrc } from './utils.ts';
// ...
// 某一個流更新所通知到的方法
function updateDownloadImgState(editors: any[]) {
// 假設 editors 裡面存有各類選中的 DOM HTML
const rawHTML = editors.getSelectionInnerHTML();
const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
const matchArray = re.exec(rawHTML);
this.downloadImageUrl = await getDownloadSafeImgSrc((matchArray && matchArray[1]) || '');
}
至此,不論圖片資源是否跨域,我們都可以利用<a>+canvas的方式將其安全地下載下來,並保留圖片的原始格式。這其中涉及不少 Web API 與概念,包含canvas,<a>download 屬性, Data URLs, MIME 以及人見人愛的正則表示式,這些都是可以細細探究的方面,歡迎深入學習。