# 多种方式实现前端图片下载
图片下载的功能,相信大家或多或少都有接触过.刚好这两天在回看以前项目中的代码时看见有这么一个需求,就是生成一张图片并下载.因为是个管理后台项目,并没有很多人使用,因此当时的代码写的也比较简陋.正好趁空点的时间,优化下代码,把有关下载图片的这块内容捋一下.
# 通过a标签download属性下载
.container {
display: flex;
}
figure {
display: flex;
flex-direction: column;
}
img {
width: 200px;
}
<div class="container">
<figure>
<figcaption>本地图片:</figcaption>
<img src="images/gao.jpg" alt="" />
<a href="images/gao.jpg">
下载图片1
</a>
</figure>
<figure>
<figcaption>远程图片:</figcaption>
<img src="https://ssyerv1.oss-cn-hangzhou.aliyuncs.com/picture/337f923e0cbd4b7b9a328135dec89c62.jpg?x-oss-process=image/resize,w_400" alt="" />
<a href="https://ssyerv1.oss-cn-hangzhou.aliyuncs.com/picture/337f923e0cbd4b7b9a328135dec89c62.jpg?x-oss-process=image/resize,w_400">
下载图片2
</a>
</figure>
</div>
css和html代码如上,首先我们以File
协议打开页面,就是我们平时最常用的双击一个html文件自动打开的协议格式,类似于 file:///Users/xxx/Desktop/index.html
这种形式的.
点击下载图片就会直接在浏览器打开图片并显示,如下图:
同样的,假如我们启动一个http
服务器,以http
协议来打开页面,比如 vscode
安装了 Live Server
等插件,url格式类似于http://192.168.104.72:8080/index.html
这种形式的.
再次点击两个下载图片结果还是一样的,都是直接在浏览器中打开了.
那么我们有没有什么办法通过a标签来实现直接下载图片呢?有的,在MDN文档中提到了,a标签具有一个叫做 download
的属性.这个属性会指示浏览器去下载URL而不是导航到它.如果这个属性有一个值的话,比如 download="test.png"
,那么这个test.png
将作为下载下来的图片的图片名.但是这个属性仅适用于同源URL
.至于同源URL
是啥,这个就不在这里做详细的解释了,不清楚的同学去MDN 看看就明白了,说白了就是浏览器的同源策略.
到现在我们明白了,HTML5中a标签给我们提供了download
属性,就是让我们用来下载的.下面我们将此属性加入进去.修改后的代码如下,其余部分同上面保持一致,这里就不写出来了.
<a href="images/gao.jpg" download>
下载图片1
</a>
<a href="https://ssyerv1.oss-cn-hangzhou.aliyuncs.com/picture/337f923e0cbd4b7b9a328135dec89c62.jpg?x-oss-process=image/resize,w_400" download>
下载图片2
</a>
修改后的代码,我们再次点击两个页面(同一个页面,一个以file协议打开,另一个以http协议打开)的下载,发现此时以http协议打开的页面的下载图片1
已经可以实现下载图片的功能了.即是在同源的情况下,download
属性确实实现了下载的功能.那么对于跨域的图片,我们又该怎么下载呢?因为在实际的生产环境,我们很有可能是把图片放到oss上,通过cdn访问图片.
# 通过js触发a标签下载
# 直接生成a标签下载
这个方法的本质就是使用js生成一个a标签,然后添加 download
属性实现下载,和上面介绍的直接使用a标签下载并没有什么本质的区别.
<div class="container">
<figure>
<figcaption>本地图片:</figcaption>
<img id="img1" src="images/ball_scroll.gif" alt="" />
<button id="btn1">下载图片</button>
</figure>
</div>
let btn1 = document.querySelector('#btn1')
btn1.onclick = function(){
download('images/ball_scroll.gif', 'ball_scroll_copy.gif')
}
function download(link, filename){
let a = document.createElement('a')
a.href = link
a.download = filename || 'default.png'
a.dispatchEvent(new MouseEvent('click'))
}
# 将图片转为base64
到这里我们就开始介绍该怎么下载跨域的图片了.首先介绍的第一个方法是将图片资源请求到之后转为 canvas
,再转为 base64
,最后生成图片下载的这么一个流程.
<div class="container">
<figure>
<figcaption>本地图片:</figcaption>
<img src="images/gao.jpg" alt="" />
<button id="btn1">下载图片</button>
</figure>
</div>
let btn1 = document.querySelector('#btn1')
btn1.onclick = function(){
download('images/gao.jpg', '888.png')
}
function download(link, picName){
let img = new Image()
img.setAttribute('crossOrigin', 'Anonymous')
img.onload = function(){
let canvas = document.createElement('canvas')
let context = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
context.drawImage(img, 0, 0, img.width, img.height)
let url = canvas.toDataURL('images/png')
let a = document.createElement('a')
let event = new MouseEvent('click')
a.download = picName || 'default.png'
a.href = url
a.dispatchEvent(event)
}
img.src = link + '?v=' + Date.now()
}
其中toDataURL
方法返回一个包含图片展示的 data URI
,即前缀为data:
协议的URL.我们可以将图片转为base64
,也可以将 base64
转为图片.点击工具网站,自行上传一张图片或者一段 base64
编码,可以查看效果.
在上面的代码中,我们下载的还是本地的资源.若是我们去下载一个oss上的图片,极有可能还是会出现跨域的报错.那么为了解决这个问题,这里我们需要做的事情有两个.第一个就是去我们的oss上配置跨域访问CORS的规则,让它能够允许访问.我今天花了不少时间在解决这个跨域问题,后来才发现原来我在oss上并没有配置规则.至于配置规则,每个厂商都有自己的方式,不过基本大同小异.因为我现在使用的企鹅的 COS
,所以就贴一下我的配置规则.
而在开启CORS规则之后,我们去下载图片,还是会有可能报跨域的错误,这也太蛋疼了吧.这是因为cdn缓存的缘故,当我们请求一张图片的时候,cdn会先判断是否有这个资源.若没有的话则会去源站请求资源,若有的话则会直接将此资源返回.而cdn的配置则有可能是缓存了我们之前未设置CORS时候的规则.所以这时我们可以给请求的图片路径最后拼接上一个时间戳来解决这个问题.大家有兴趣的话,可以将代码中最后添加的时间戳去掉看一下效果.
# 将图片转为blob
let btn1 = document.querySelector('#btn1')
btn1.onclick = function(){
download('images/gao.jpg')
}
function download(link, picName){
let img = new Image()
img.setAttribute('crossOrigin', 'Anonymous')
img.onload = function(){
let canvas = document.createElement('canvas')
let context = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
context.drawImage(img, 0, 0, img.width, img.height)
canvas.toBlob(blob => {
let url = URL.createObjectURL(blob)
let a = document.createElement('a')
let event = new MouseEvent('click')
a.download = picName || 'default.png'
a.href = url
a.dispatchEvent(event)
URL.revokeObjectURL(url) // 内存管理,将这句代码注释掉,则将以 blob:http 开头的url复制到浏览器地址栏有效,否则无效.
})
}
img.src = link + '?v=' + Date.now()
}
其中toBlob
方法可以创建 Blob
对象,用以展示canvas上的图片,这个图片文件可以被缓存或保存到本地.此方法可以传入三个参数,第一个就是上面的回调函数,第二个是图片格式,默认为image/png
,第三个参数是介于0与1之间的一个数字,表示图片的质量,数值越高,质量越好.但是只有当第二个参数是image/jpeg
或 image/webp
的时候才有效.而 createObjectURL
方法则可以创建一个 DOMString
,其中包含了一个表示参数中给出的对象的URL.这个url大概长这样:blob:http://192.168.104.72:8080/39f8b0f7-b946-4fc5-af70-d0562469a0f4
,将其复制到浏览器的地址栏中可以看到图片,注意看地址哦.
但是这个url的生命周期和创建它的窗口中的 document
是绑定的,这又是什么意思呢? 意思就是只要我之前的那个html页面还在,那这个url就有效.要是我把那个html页面关掉了或者刷新了,那么这个url也就跟着失效了.这里需要注意一点的是,在每次调用 createObjectURL
方法的时候,都会创建一个新的URL对象,当我们不需要这些URL对象的时候,通过 URL.revokeObjectURL()
来释放一个之前已经存在的对象,以便更好的内存管理.
# XMLHttpRequest配合blob下载gif
但是上面两个通过 canvas
转 base64
或 blob
的方法都有个问题,就是只支持 png
和 jpeg
格式的图片.如果是 gif
格式的话,就只会显示第一帧的内容,因为 canvas
就画了一幅画呀.这里提供一种思路,就是通过一个 XMLHttpRequest
对象去请求一张图片,获取返回的 blob
. 在 download.js
中使用的就是这样的思路, github地址,大概在第51行左右开始.
<div class="container">
<figure>
<figcaption>本地图片:</figcaption>
<img id="img1" src="images/ball_scroll.gif" alt="" />
<button id="btn1">下载图片</button>
</figure>
</div>
let btn1 = document.querySelector('#btn1')
btn1.onclick = function(){
download('images/ball_scroll.gif', 'ball_scroll.gif')
}
function download(link, filename){
let xhr = new XMLHttpRequest()
xhr.open('get', link, true)
xhr.responseType = 'blob'
xhr.onload = function(){
let url = URL.createObjectURL(xhr.response)
let a = document.createElement('a')
let event = new MouseEvent('click')
a.href = link
a.download = filename || 'default.png'
a.dispatchEvent(event)
URL.revokeObjectURL(url)
}
xhr.send()
}
这样下载 gif
格式的就是一张动图,不再是一张只有第一帧的图片了.若是上面通过XMLHttpRequest请求的结果报跨域错误,那就让后端设置下 Access-Control-Allow-Origin
为 *
或者指定域即可.
# JS下载这么麻烦,为啥还要用JS
相信对比上面的代码后,大家都发现了直接使用a标签加上download多方便,为啥还要那么麻烦的用JS去做下载功能.其实使用JS的原理无非还是使用a标签的download属性,只是在JS中我们可以做更多的逻辑判断.比如这个用户是否有权限下载这张图片,或者这张图片是不是付费下载等.所以说,在啥逻辑都不用管,只管下载的场景下,我们直接使用a标签就完事了.但是在有复杂逻辑判断的场景下,我们还是需要使用JS来实现下载功能的.
# 补充:文本信息的下载
将文本或者JS字符串通过 Blob
转换成二进制下载
function downloadTxt(str, filename){
let a = document.createElement('a')
a.download = filename
a.style.display = 'none'
let blob = new Blob([str])
a.href = URL.createObjectURL(blob)
document.body.appendChild(a)
a.dispatchEvent(new MouseEvent('click'))
document.body.removeChild(a)
}
# 总结
图片下载功能在日常开发中还是比较常见的.其中有单纯前端的下载,也有前端和后端配合的下载.至于采用哪种解决方案就要视具体情况而定了.
参考链接:
- https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
- https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toDataURL
- https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
- https://developer.mozilla.org/zh-CN/docs/Web/API/URL/revokeObjectURL
- https://www.jianshu.com/p/dfe9c351b898