普通用户下载图片时只需一个「右键另存为」操作即可完成,但当我们做在线编辑器、整个 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实现第三届蓝桥杯填算式

    ** 填算式** [结果填空] (满分11分) 看这个算式: ☆☆☆ + ☆☆☆ = ☆☆☆ 如果每个五角星代表 1 ~ 9 的不同的数字. 这个算式有多少种可能的正确填写方法? 173 + 286 ...

  2. [apue] epoll 的一些不为人所注意的特性

    之前曾经使用 epoll 构建过一个轻量级的 tcp 服务框架: 一个工业级.跨平台.轻量级的 tcp 网络服务框架:gevent 在调试的过程中,发现一些 epoll 之前没怎么注意到的特性. a) ...

  3. Jmeter让压测随时做起来(转载)

    为什么要压测 这个问题问的其实挺没有必要的,做开发的同学应该都很清楚,压测的必要性,压力测试主要目的就是让我们在上线前能够了解到我们系统的承载能力,和当前.未来系统压力的提升情况,能够评估出当前系统的 ...

  4. 根据现有Bitmap生成相同图案指定大小的新Bitmap

    通过一张现有的Bitmap,画出一张同样的但是大小使我们指定的Bitmap 需求:直接createBitmap的话不允许生成的bitmap的宽高大于原始的,因此需要特定方法来将一张Bitmap的大小进 ...

  5. Python如何绘制可视化图?给你一段代码,你能自己做出来吗

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 喜欢的朋友欢迎关注小编,除了分享技术文章之外还有很多福利 没有数据生成的图 ...

  6. 搭建手机web服务器-----内网穿透(无需Root)

    搭建手机web服务器-----内网穿透(无需Root) 一.内网穿透部分 前言: 网上内网穿透的方法很多,像花生壳.Ngrok.Frp等等,但是大多都需要获取手机root权限 本文使用的软件是Term ...

  7. Vue使用js鼠标蜘蛛特效

    1. 在src下新建文件夹utils,里面新建文件canvas-nest.js,将代码复制进去.(可以自己定义存放路径) !function() { function n(n, e, t) { ret ...

  8. [computer graphics]世界坐标系->相机坐标系详细推导

    基变换 理论部分 在n维的线性空间中,任意n个线性无关的向量都可以作为线性空间的基,即空间基不唯一.对于不同的基,同一个向量的坐标一般是不同的.因为在计算机图形学中,主要研究三维的空间,所以可以简化问 ...

  9. 手写网页扫雷之css部分

    #ui{ text-align: center; } #saolei{ width: 500px; height: 500px; border: 1px solid #456345; margin: ...

  10. 如何解决在electron里无法使用puppeteer的evaluate函数

    报错如图,只需要注释掉 index.html 含有 http-equiv="Content-Security-Policy 的 meta 标签就可以了.