普通用户下载图片时只需一个「右键另存为」操作即可完成,但当我们做在线编辑器、整个 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. Java实现 洛谷 P1328 生活大爆炸版石头剪刀布

    import java.util.Scanner; public class Main{ private static int[] duel(int playerA, int playerB){ in ...

  2. java实现扑克牌移动

    /* 下面代码模拟了一套扑克牌(初始排序 A~K,共 13 张)的操作过程. 操作过程是: 手里拿着这套扑克牌,从前面拿一张放在后面,再从前面拿一张放桌子上,再从前面拿一张放在后面,.... 如此循环 ...

  3. java实现第五届蓝桥杯生物芯片

    生物芯片 X博士正在研究一种生物芯片,其逻辑密集度.容量都远远高于普通的半导体芯片. 博士在芯片中设计了 n 个微型光源,每个光源操作一次就会改变其状态,即:点亮转为关闭,或关闭转为点亮. 这些光源的 ...

  4. 美女面试官问我Python如何优雅的创建临时文件,我的回答....

    [摘要] 本故事纯属虚构,如有巧合,他们故事里的美女面试官也肯定没有我的美,请自行脑补... 小P像多数Python自学者一样,苦心钻研小半年,一朝出师投简历. 这不,一家招聘初级Python开发工程 ...

  5. Hive的压缩存储和简单优化

    一.Hive的压缩和存储 1,MapReduce支持的压缩编码 压缩格式 工具 算法 文件扩展名 是否可切分 对应的编码/解码器 DEFLATE 无 DEFLATE .deflate 否 org.ap ...

  6. HashMap(三)之源码分析

    通过分析HashMap来学习源码,那么通过此过程我们要带着这几个问题去一起探索 为什么要学习源码 怎么去学习 0.1 为什么要学习源码 这个问题,直接给出结论,学习源码肯定是有好处的,比如: 学习优秀 ...

  7. DRY原则的一个简单实践

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文出处:https://dzone.com/articles/dry-dont-repeat-yourse ...

  8. Windows环境下PHP安装pthreads多线程扩展

    一.判断PHP是ts还是nts版 通过phpinfo(); 查看其中的 Thread Safety 项,这个项目就是查看是否是线程安全,如果是:enabled,一般来说应该是ts版,否则是nts版. ...

  9. Firefox中input元素,不能重新获取焦点函数focus()

    js校验输入框的函数: function is_number(feild) { var strRegExp = /^\d+(\.\d{1,2})?$/; if (!strRegExp.test(fei ...

  10. (四)log4j同配置下多个进程写日志

    原文链接:https://blog.csdn.net/voiceofwind/article/details/51966361 由于起了两个不同的任务,log4j中用的是一套配置,写入的是同一个路径, ...