前端自动生成图片并下载(不到60行代码)

5,934 阅读3分钟

前端自动生成图片并下载(不到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保证错误能被拿到。