前端自动生成图片并下载(不到60行代码)
预览流程
需求
由于一些简单的图片拼合需要处理(大概8000张),但是又没有找到合适的傻瓜软件能够很好地解决需求,同时也很令人头疼的是nodejs里面图片处理库,基本上没几个好用,且安装费时费劲 (当然我还是装了)。作为一个有追求的前端,如果能不依靠其他的东西,直接在前端页面上实现它不香么?于是就有了这个小尝试。其实非常简单,仅以此文记录一下,怕以后不用忘记。
前奏
1.由于canvas的安全限制,我们必须保证资源和网页在同一个域名下进行,否则画入是没问题的,但是导出就会报“画布被污染”的错误。所以我们一共有多个解决办法:
- nodejs搭建一个本地服务器,所有资源都在同域下。特点:适合部署上线使用,但麻烦
- 本地解除chrome跨域限制(利用chrome启动参数)。特点:简单粗暴,本地快速解决问题,但只适合自己使用
- img标签增加
img.setAttribute('crossOrigin', 'anonymous')。特点:能解决目前的问题,但是有隐患 - 使用new FileReader();将请求到的blob数据读取成base64地址。特点:代码增加,其他没发现缺点
- 使用URL.createObjectURL(res) 将请求到的blob数据读取成Blob类型的地址。特点:代码增加,同时要记得
URL.revokeObjectURL取消掉关联,其他没发现缺点。
以上是我个人探索的五种解决办法,分别适合不同场景下的需求。
前两种例子不便于展示,直接下面是三种办法的对应代码例子:
let can = document.getElementById('canvas');
let ctx = can.getContext('2d');
fetch('https://img.alicdn.com/bao/uploaded/i1/446338500/O1CN01npzdZ52Cf3A4cx8JG_!!0-item_pic.jpg_240x240.jpg').then(
res => res.blob())
.then(res => {
let fr = new FileReader();//https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
fr.onload = function (e) {
console.log(e)
console.log(e.target.result)
let img = new Image();
//第一种办法
// img.setAttribute('crossOrigin', 'anonymous');
// img.src =
// 'https://img.alicdn.com/bao/uploaded/i1/446338500/O1CN01npzdZ52Cf3A4cx8JG_!!0-item_pic.jpg_240x240.jpg'
//第二种办法
// img.src = e.target.result; //转成base64
//第三种办法
// img.src = URL.createObjectURL(res);//转成Blob地址ObjectUrl
img.onload = function () {
ctx.drawImage(img, 0, 0)
console.log(can.toDataURL())//导出成dataurl
// canvas.toBlob(function (blob) {//导出成blob地址
// var url = URL.createObjectURL(blob);
// console.log(url)
// });
}
}
fr.onerror = function () {
console.log('读取错误!')
}
fr.readAsDataURL(res)//如果是转文字,第二个参数可以使用编码
})
核心
这里的整个流程核心在于:利用 base64 地址或者 createObjectURL 虚拟地址来往画布填内容,这样的话,就不会是说污染的画布了。
完善导出
有了之前的前奏流程,接下来我们可以开始实现下载,利用a标签的download即可。同时要注意,a标签download属性不支持跨域,不过在我们这里面,不会涉及到跨域(canvas数据就是从这页面里产生的)。
如何批量导出?
在这里要注意,由于我们存在一些异步环节,我们要使用Async await来实现对它们的顺序执行。这样的话,就会按照顺序执行,另外浏览器记得打开允许自动下载(本地环境可能会老有提示,但是如果拿一个localhost托管就不会有问题了)
例子:
(async () => {
let result = []
let arr = []
for (let i = 0; i < arr.length; i++) {
await new Promise((res, rej) => {
setTimeout(() => {
res()
}, 10)
})
try {
await createImg(arr[i]).catch(err => {
result.push(arr[i])
console.log(arr[i], '服务器端响应出错,无法获取图片')
})
} catch (e) {
result.push(arr[i])
console.log(arr[i], '程序处理时无法处理图片', '原因:', e)
}
}
document.body.innerText = result.join("")
})()
function createImg(article) {
return new Promise((res, rej) => {
let temp = new Image()
temp.src = 'http://XXXXXX/getpic?type=touming&article=' + article;
temp.onerror = function () {
rej(article)
}
temp.onload = function () {
ctx.drawImage(temp, 940, 90, 650, 650);
var bloburl = canvas.toDataURL('image/jpeg');
var anchor = document.createElement('a');
anchor.href = bloburl;
anchor.download = article;
anchor.click()
ctx.rect(0, 0, 2000, 800);
ctx.fillStyle = "rgb(236,237,239)";
ctx.fill();
res()
}
})
}
踩坑:
这里要注意,我定义了Image对象的onerror事件,因为async await等待的Promise不知道捕获到服务器错误的(如果服务器宕机了一下,脚本会直接停止报错),所以我们需要在onerror里面引入,然后reject掉,promise就可以拿到错误信息了,之后再catch就好了。同时为了防止出现复杂的报错停止,加入了try catch保证错误能被拿到。