组合 a 标签与 canvas 实现图片资源的安全下载

更新日期: 2020-03-08阅读: 2.3k标签: 标签

普通用户下载图片时只需一个「右键另存为」操作即可完成,但当我们做在线编辑器、整个 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[1]) || '';

注:关于 <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-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/gifGIF 图片 (无损耗压缩方面被PNG所替代)
image/jpegJPEG 图片
image/pngPNG 图片
image/svg+xmlSVG图片 (矢量图)

如果不考虑 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 以及人见人爱的正则表达式,这些都是可以细细探究的方面,欢迎深入学习。

原文:https://hijiangtao.github.io/2020/02/15/Safely-Download-Image-Resource-With-JavaScript/


链接: https://www.fly63.com/article/detial/8883

Html中a标签用法总结:创建email,电话,描点链接等。以及防止链接被搜索引擎收录

a标签是我们常用的一个标签之一,这篇文章主要讲解如何使用a来创建email,电话,描点链接等。以及防止链接被搜索引擎收录。

web标签语义化的理解_web语义化是什么意思

Web语义化,使用语义恰当的标签,可以让页面具有良好的结构,页面元素具有良好的含义,从而让人和机器都能快速理解。语义化的web页面一方面可以让机器在更少的人类干预情况下收集并研究网页的信息,从而可以读懂网页的内容

HTML中<base>标签的正确使用

<base> 标签必须位于 <head> 元素内部,在一个文档中,最多能使用一个 <base> 标签,使用了<base>标签的链接后,其他链接必须在<base>标签的链接里面,不然将无法找到。

使用Map标签指定点击区域时的兼容性问题

因为map标签用的较少,而常用的label标签中的for属性,是不需要加#的,加上了反而会有错。当然,label和map中的用法也不同,不论如何,引以为戒

html 标签优化_如何优化网站HTML标签代码优化

如何优化网站HTML标签代码优化?选用合适的标签、htm5提供了很多类型的input的type类型、使用html5语义化标签等

js动态添加html标签和属性_手动插入meta、script、div、img等标签

web网页是由 html标签一层层组成的,js也可以动态添加对应的标签,比如mate标签、script标签、div标签、img标签等,动态创建的方法基本都差不多,下面将简单介绍下如何实现

head里两个重要标签base和meta

<base>我们并不常用的一个标签,但是一旦用得不当会带来灾难性的影响。meta标签日常开发中我们用得更多的是meta标签,分为3类:具有charset属性的meta 、带有http-equiv 属性的 meta

JSTL学习总结

在mvnrepository中,直接搜索jstl,,找到下载量最高的,下载1.2版本。下载完导入到项目里面就好了。jstl是apache对EL表达式的扩展,使用需导jar包

<script>标签的属性

src定义引用外部脚本的URI,这可以用来代替直接在文档中嵌入脚本。指定了 src 属性的script元素标签内不应该再有嵌入的脚本。type该属性定义script元素包含或src引用的脚本语言。

10 个不常用但你有必要知道的 html 标签

HTML 也被称为超文本记语言( hyper text markup language )是为网页创建和其他可在网页浏览器中看到的信息设计的一种标记语言。HTML 被用来结构化信息,如标题、段落和列表等,也可用来在一定程度上描述文档的外观和语义。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!