普通用户下载图片时只需一个「右键另存为」操作即可完成,但当我们做在线编辑器、整个 UI 都被自定义实现时,如何解决不同域问题并实现页面中图片资源的安全下载呢?本文就解决该问题过程中所涉及的正则表达式、Web API 和 canvas 操作进行记录。

本文分为以下七个部分:

  1. 利用 <a> 标签下载任意资源
  2. 解析 DOM 获取图片链接
  3. 分情况处理图片链接
  4. 工具函数中的正则表达式完善
  5. canvas 绘制图片资源并转 Data URLs 返回
  6. 实际使用与总结
  7. 参考资料

以下开始正文。

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[]) || '';

注:关于 <img> 标签有 <img> 和 <img /> 两种形式的讨论,本文不做讨论,详情可以移步 StackOverflow。

 

2. 分情况处理图片链接

拿到 src 即图片链接后我们来分情况讨论下,处理逻辑应该分这几步(本文中 Data URLs 特指 base64 形式图片 URL,以下不再额外说明):

  1. 同域图片或者 Data URLs 图片直接返回
  2. 跨域图片转 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--+.]+(;[a-z-]+=[a-z0--]+)?)?(;base64)?,([a-z0-!$&',()*+;=\-._~:@\/?%\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[]}` : 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[]) || '');
}

至此,不论图片资源是否跨域,我们都可以利用 <a> + canvas 的方式将其安全地下载下来,并保留图片的原始格式。这其中涉及不少 Web API 与概念,包含 canvas, <a> download 属性, Data URLs, MIME 以及人见人爱的正则表达式,这些都是可以细细探究的方面,欢迎深入学习。

组合 a 标签与 canvas 实现图片资源的安全下载的方法与技巧的更多相关文章

  1. a标签点击不跳转的几种方法

    a标签点击不跳转的几种方法 1.onclick事件中返回false <a href="http://www.baidu.com" onclick="return f ...

  2. HTML5 Canvas中绘制椭圆的几种方法

    1.canvas自带的绘制椭圆的方法 ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)是后来 ...

  3. PHP《将画布(canvas)图像保存成本地图片的方法》

    用PHP将网页上的Canvas图像保存到服务器上的方法 2014年6月27日 歪脖骇客 发表回复 8 在几年前HTML5还没有流行的时候,我们的项目经理曾经向我提出这样一个需求:让项目评审专家们在评审 ...

  4. 转载:将画布(canvas)图像保存成本地图片的方法

    之前我曾介绍过如何将HTML5画布(canvas)内容转变成图片形式,方法十分简单.但后来我发现只将canvas内容转变成图片输出还不够,如何能将转变后的图片保存到本地呢? 其实,这个方法也是非常简单 ...

  5. HTML5<canvas>标签:使用canvas元素在网页上绘制线条和圆(1)

    什么是 Canvas? HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像. 画布是一个矩形区域,您可以控制其每一像素. canvas 拥有多种绘制路径.矩形.圆形.字符以 ...

  6. HTML5中video标签与canvas绘图的使用

    video标签的使用 video标签定义视频, 它是html5中的新标签, 它的属性如下(参考自文档): domo01 <!DOCTYPE html> <html lang=&quo ...

  7. HTML5<canvas>标签:使用canvas元素在网页上绘制四分之一圆(3)

    前几天自己做了个四分之一的圆,放到手机里面测试.效果不是很好.于是今天通过查资料,找到了canvas.自己研究了一天,发现可以使用canvas画圆.代码如下: <!doctype html> ...

  8. HTML5<canvas>标签:使用canvas元素在网页上绘制渐变和图像(2)

    详细解释HTML5 Canvas中渐进填充的参数设置与使用,Canvas中透明度的设置与使用,结合渐进填充与透明度支持,实现图像的Mask效果. 一:渐进填充(Gradient Fill) Canva ...

  9. js动态新增组合Input标签

    var x = 1; function addlink() { var linkdiv = document.getElementById("add1_0"); if (linkd ...

随机推荐

  1. sublime配置C++编译环境

    配置C++编译命令 { "file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$", "workin ...

  2. Unit1-窝窝初体验

    全文共3179字,推荐阅读时间10~15分钟. 文章共分四个部分: 作业分析 评测相关 重构策略 初体验感受 作业分析 第一次作业 第一次作业要求我们实现一个简单的幂函数求导工具,没有乘积和复合的情况 ...

  3. JavaScript触发器

    感谢:链接(视频讲解很详细) JavaScript触发器 一.功能 顾名思义就是操控鼠标或键盘触发(实现)一些特定功能. 二.功能实现 <script type="text/javas ...

  4. Random Point in Triangle【随机数解决期望值问题】

    Random Point in Triangle 题目链接(点击) 题目描述 Bobo has a triangle ABC with A(x1,y1),B(x2,y2)A(x1,y1),B(x2,y ...

  5. (三)利用@DataProvider传递参数

    具体实现如下: @DataProvider(name="couponListData") public Object[][] couponListData(){ //自己定义Obj ...

  6. APP——python——自动化环境搭建01

    前提:python以及pycharm安装完成. ---------------------------------------------------------------------------- ...

  7. Windows 程序设计(4) MFC-02 基本控件-下

    1. TabCtrl 标签控件 1.1 创建主窗口 1)CMFCTabControlDlg,拖拽标签控件 2)增加变量 CTabCtrl m_tabCtrl 3)设置相关成员变量和处理函数 CFile ...

  8. MFC时间简单比较方法

    MFC//时间简单比较方法 void CMFCsaveListTofileDlg::OnBnClickedButton6()//时间简单比较方法 { // TODO: 在此添加控件通知处理程序代码 C ...

  9. Jenkins中agent的使用

    [前言] 很多小伙伴都已经会搭建Jenkins环境了,都想要用Jenkins来运行自动化接口,可我们的Jenkins在linux服务器上.服务器上默认的python包是2.6的这样不是很好,那么这边就 ...

  10. JAVA相关基础知识

    JAVA相关基础知识 1.面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面.抽象并不打算了解全部问题,而只是选择其中的一部分, ...