背景
公司静态资源存储采用的是阿里云的oss服务,会返回一个如下的静态资源链接:http://static.xxxxxxx.com/5b7fe45205691500062a1c59/3315E3869EFD673014AAB086D9C2CD9B?x-oss-process=image/format,jpg
需要提供一个点击下载图片的按钮,如下图所示,点击可以下载该图片。
踩坑过程
1、通过window.open(url)下载的方式
因为看到下载excel是采用的这种方法,起初尝试这种方式,直接赋值图片的url,结果是不会触发下载,只会在当前页面打开图片,这种方式是无效的,对于文件像是excel、pdf是可以下载的,不支持图片的下载。
2、通过 <a> 的 download 属性
function download(url, name) {
const aLink = document.createElement('a')
aLink.download = name
aLink.href = url
aLink.dispatchEvent(new MouseEvent('click', {}))
}
这种方法是最常想到的,也是网上最常见的方法,当图片地址是同源的,可以采用这种方式下载图片,但是如果图片的地址是远端跨域的,点击下载效果也是在当前页面打开图片,这种方式对于需要将远端图片下载到本地是无效的。
3、通过url 转 base64,利用canvas.toDataURL的API转化成base64
urlToBase64(url) {
return new Promise ((resolve,reject) => {
let image = new Image();
image.onload = function() {
let canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
// 将图片插入画布并开始绘制
canvas.getContext('2d').drawImage(image, 0, 0);
// result
let result = canvas.toDataURL('image/png')
resolve(result);
};
// CORS 策略,会存在跨域问题https://stackoverflow.com/questions/20424279/canvas-todataurl-securityerror
image.setAttribute("crossOrigin",'Anonymous');
image.src = url;
// 图片加载失败的错误处理
image.onerror = () => {
reject(new Error('图片流异常'));
};
}
这种方式经过测试,同样会存在跨域问题,虽然已经设置了允许跨域,但是浏览器对此却是拒绝的。
现在的主要问题是不能解决跨域问题,如果图片链接是跨域的,浏览器会禁用download,只允许打开图片而不允许下载,需要想办法生成图片本地的下载地址,我想到的是把远端图片的二进制信息或者是base64信息返回给前端,在前端生成本地的地址,因为我们前端团队在用node做中间层,我开始考虑在中间层做处理,在中间层根据图片的链接地址获取到图片的流信息。(下面的的步骤提供了一个思路,不一定非要在中间层做,联系后台开发同事提供图片流信息的接口也是一样的)。
4、中间层获取远端图片流信息
req.requestImage = (url) => new Promise((resolve, reject) => {
request.get({ url, encoding: null }, (error, response) => {
if (error) {
return reject(error)
}
return resolve(response)
})
})
注意这里encoding: null需要设置,默认采用的utf-8, 而我们需要的是二进制流。
把图片的url地址传给中间层
function downloadImage(req) {
return req.requestImage(req.body.imgUrl)
}
获取到图片的流信息后返回给前端,response.body就是图片的二进制流
router.post('/v1/house/download', (req, res, next) => {
House.downloadImage(req).then((response) => {
res.writeHead(200, { 'Content-Type': response.headers['content-type'] })
res.end(response.body)
}).catch(next)
})
这里也可以处理成base64
request.get({url: 'http://image.baidu.com/search/detail?ct=503316480&z=undefined&tn=baiduimagedetail&ipn=d&word=%E7%99%BE%E5%BA%A6&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=undefined&hd=undefined&latest=undefined©right=undefined&cs=2909213714,147280373&os=1055424764,46736385&simid=3326701403,130035821&pn=0&rn=1&di=183480&ln=1116&fr=&fmq=1571394702128_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=0&objurl=http%3A%2F%2Fwww.kuaishang.cn%2Fassets%2Fjs%2Fupfiles%2Fimages%2F160419091958808762rbaph.png&rpstart=0&rpnum=0&adpicid=0&force=undefined', encoding: null }, function (error, response, body) {
const type = response.headers["content-type"]
const prefix = "data:" + type + ";base64,"
const data = response.body.toString('base64');
const base64 = prefix + data
res.end(base64)
})
修改axios的配置信息 responseType: 'blob',axios默认是json,会出现乱码问题
downloadImage({ commit }, data) {
return this.$http({
method: 'post',
url: '/api/v1/house/download',
data,
responseType: 'blob',
})
},
这时候前端就可以获取到图片的流信息,URL.createObjectURL()具有处理二进制流的能力,可以生成一个本地的地址blob:http://192.168.x.xx:3000/093e08bc-0c0b-436a-b24c-c065b78303d2,该地址可以用于将资源下载到本地。
downloadImage(imgUrl) {
if (!imgUrl) return
const imgName = imgUrl.split('//')[1].split('/')[1]
this.$store.dispatch('houses/downloadImage', { imgUrl }).then((res) => {
const aLink = document.createElement('a')
aLink.download = imgName
aLink.href = URL.createObjectURL(res.data)
aLink.dispatchEvent(new MouseEvent('click', {}))
})
},
5、用Content-disposition
方法4是有效的,但并不是优雅的解决方法,前端拿到图片的流信息后,需要做进一步转化处理,将流信息转化成一个本地的下载地址,这对前端是一种性能上的消耗,其实这一过程是多余的。HTTP协议响应头Content-disposition可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名,文件直接在浏览器上显示或者在访问时弹出文件下载对话框。
在中间层开个通用的下载接口,可以支持任意类型的文件下载
router.get('/common/download', (req, res, next) => {
const url = req.query.url
const fileName = req.query.fileName
request.get({ url, encoding: null }, (error, response, body) => {
if (error) {
next(error)
return
}
const fileType = response.headers['content-type'].split('/')[1]
res.setHeader('Content-disposition', getContentDisposition(fileName, fileType))
res.setHeader('Content-type', response.headers['content-type'])
res.send(body)
})
})
Content-disposition的设置需要注意兼容firefox以及IE浏览器
function getContentDisposition(fileName, fileType) {
return `attachment; filename=${encodeURIComponent(fileName)}.${fileType}; filename*=utf-8''${encodeURIComponent(fileName)}.${fileType};`
}
感悟
逐渐意识到node做中间层给前端团队带来的好处。
1、基于设配器模式的思想,有了中间层可以对后台返回的数据进行拦截,转化,过滤,可以实现由前端定义数据结构,生成适合UI组件库的数据结构而不需要放到浏览器做转化。
2、可以在中间层隐藏一些用户的关键信息,避免http明文传输,用户信息被轻易的抓包。
3、中间层与静态资源天生同源,不需要考虑浏览器与后台的跨域问题。
4、承接部分后台任务,让后台专注于提供数据,而前后端的通讯都交由前端管理,实现更为彻底的前后端分离。