如何做Antd Upload单个图片的校验和上传?

5,514 阅读6分钟

背景

在日常工作中,我们经常遇到图片上传的需求,同时对上传的图片尺寸、大小和格式有要求,比如如下:

目标效果图

根据上图我们来拆分下功能:

  1. 只允许选择单个图片;
  2. 校验图片的格式、大小和尺寸;
  3. 实现图片上传到后端服务器;

等会儿我们也就按照这个功能步骤来逐步实现我们的功能~~

工欲善其事,必先利其器。我们先去Upload组件文档里学习一下它的API,大概看下它能实现什么功能~
其实就是主要瞄一瞄,咱们的需求是否都能实现:),看了眼,嗯~ 我们要的功能貌似都满足呢,那我们就开始吧~~

Step 1:只允许选择单个图片

在文档中看到,fileList代表已经上传的文件列表,我们可以通过控制fileList的长度为1来达到只能选择单个图片。不过有一点需要注意,fileList一用,文件列表就受控了,我们还需要添加onChange事件:

const UploadImage = () => {
  let [fileList, setFileList] = useState([]);

  const handleChange = ({ file }) => {
    setFileList([file])
  }

  return (
    <Upload
      listType="picture-card"
      fileList={fileList}
      onChange={handleChange}
    >
      {
        fileList.length === 0 ? <div>
          <PlusOutlined />
          <div style={{ marginTop: 8 }}>上传图片</div>
        </div> : null
      }
    </Upload>
  )
}

如上,我们就可以点击选择图片了~ 这里有个小技巧,当fileList有内容的时候,我们就渲染了null,没有上传入口了,这样也一样做到了只能上传一张图片:)

单选图片效果图

鼠标hover图片,可以看到有个删除按钮,我们换个图片试试。 点击一下删除,呀,删不了!咋回事呢?那是因为文件列表已受控,我们需要自己加删除时的逻辑。

handleChange里面断点一看,发现点击删除的时候,filestatus会是removed,修改下handleChange

  const handleChange = ({ file }) => {
    if (file.status === 'removed') {
      setFileList([]);
    } else {
      setFileList([file]);
    }
  }

运行下,嗯~ 可以删除了,从无到有的选择图片已经work了,我们可以继续加砖啦~~

Step 2: 校验图片的格式、大小和尺寸

1. 校验图片格式

方法一:用accept配置

Upload组件文档里有个API accept,用于接受上传的文件类型,和input accept属性一样的用法。在Upload组建上添加如下属性:

accept="image/png"

如果允许多个格式用“,”隔开即可,这种在选择文件的时候有个优点,不符合格式要求的直接是不可选的状态,没法选中,是不是很easy?

方法二:使用beforeUpload方法

在上面提到的Upload组件文档里,页面右边第一个demo就是一个限制上传功能,使用的是beforeUpload,我们可以依葫芦画瓢了,哈~
添加beforeUpload方法:

  const beforeUpload = (file) => {
    //校验格式
    const isValidType = file.type === 'image/png';
    if (!isValidType) {
      message.error('只能上传格式为PNG的图片');
      return false;
    }
  }

限制尺寸的还没有做,先测试一下咱们的格式和大小是不是运行良好~ 于是我特意选了一个不是png的文件上传,然后我看到了这个?!

测试效果图.png

错误报了,文件也上传到页面了?这是拦截了啥? 带着疑问重回文档看了看beforeUpload API,上面这么写的:

上传文件之前的钩子,参数为上传的文件,若返回 false 则停止上传。支持返回一个 Promise 对象,Promise 对象 reject 时则停止上传,resolve 时开始上传( resolve 传入 File 或 Blob 对象则上传 resolve 传入对象);也可以返回 Upload.LIST_IGNORE,此时列表中将不展示此文件。 注意:IE9 不支持该方法

返回false则停止上传,表示没懂呀,于是再去跑到文档里,看看有没有啥补充说明,是不是漏了些啥。 突然看到文档的一个demo(左第5个demo),下面有段话这么说的:

beforeUpload 返回 false 或 Promise.reject 时,只用于拦截上传行为,不会阻止文件进入上传列表(原因)。如果需要阻止列表展现,可以通过返回 Upload.LIST_IGNORE 实现。

只用于拦截上传行为,不会阻止文件进入上传列表!! 原来停止上传这个意思。
点开demo看了下人家的代码,人家用的是Upload.LIST_IGNORE来做的,我们的文件列表都受控了,没法用这个来处理。既然能被显示到上传列表,说明肯定被setFileList过,整个代码就只有handleChange里面做过setFileList,到handleChange里打印看看拦截时的file有没有啥不同。

调试校验图片

哈哈,找到不同点了,被beforeUpload返回false拦截的文件没有status属性,我们来改改handleChange方法:

  const handleChange = ({ file }) => {
    let curFile;

    switch (file.status) {
      case 'uploading':
      case 'done':
        curFile = [file];
        break;

      case 'removed':
      default:
        curFile = [];
        break;
    }

    setFileList([...curFile]);
  }

再跑一把,good,这回不满足要求的不会显示了,并且弹出错误提醒~

方法三:通过读取文件的二进制数据来判断文件类型

一般情况下,通过文件的后缀名或者文件的MIME类型来判断文件的类型,可以满足大部分的需求。为了测试上面的格式判断起效,我特意把一个JPEG格式的图片改成.png后缀,发现图片就成功“越狱”了!

如果要杜绝这种“越狱”的话,我们需要通过读取文件的二进制数据,通过魔数来判断。
PNG的魔数是 0x89 50 4E 47 0D 0A 1A 0A,我们可以通过读取文件二进制的前8个字节来一一比对来判定文件的类型:

const getBuffers = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(file.slice(0, 8));
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.onerror = reject;
  })
};

const beforeUpload = (file) => {
  //校验格式
  const isValidType = file => {
    return new Promise((resolve, reject) => {
      //PNG的魔数
      const pngBufferValues = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 
      getBuffers(file).then(buffers => {
        const fileBufferValues = new Uint8Array(buffers);
        const isPNG = pngBufferValues.join(',') === fileBufferValues.join(',');
        if (!isPNG) {
          message.error('只能上传格式为PNG的图片');
          reject();
        } else {
          resolve(file);
        }
      })
    })
  }
  return isValidType(file);

这么一来,即使把JPEG格式的图片改成.png后缀,这个图片也会被拦截住,完美~~

2. 校验图片大小

上面格式已经可以work了,大小瞬间变得好容易,在beforeUpload函数里面添加图片大小限制的逻辑即可:

  const beforeUpload = (file) => {
    //校验尺寸.....

    //校验大小
    const isValidVolume = file.size / 1024 < 300;
    if (!isValidVolume) {
      message.error('只能上传大小小于300k的图片');
      return false;
    }
  }

测一把,嗯,大小的限制也okay了~~

3. 校验图片尺寸

思路:图片再加载好后(onload),可以获得到它的宽度和高度。
直接上代码,在beforeUpload函数里添加校验图片尺寸:

  const beforeUpload = (file) => {
    //校验格式......

    //校验大小......

    //校验尺寸......
    const isSize = file => {
      return new Promise((resolve, reject) => {
        let width = 800;
        let height = 600;
        let _URL = window.URL || window.webkitURL;
        let img = new Image();
        img.onload = function () {
          _URL.revokeObjectURL(this.src);
          let valid = img.width <= width && img.height <= height;
          valid ? resolve() : reject();
        };
        img.src = _URL.createObjectURL(file);
      }).then(
        () => {
          return file;
        },
        () => {
          message.error('只能上传尺寸为800*600的图片');
          return Promise.reject();
        }
      );
    };
    return isSize(file);
  }

跑跑看,嗯~尺寸限制也运行良好

Step 3:实现图片上传到后端服务器

Upload的API中,我们可以看到一个API,action 上传的地址,这里,只要把我们后端的接口url配置在这就可以了,如果你上传还需要额外的参数,那就配置在data这个API里。

<Upload
  listType="picture-card"
  fileList={fileList}
  onChange={handleChange}
  beforeUpload={beforeUpload}
  action="/api/file/upload"
  data={{ platform: 'pc' }}
>
  {
    fileList.length === 0 ? <div>
      <PlusOutlined />
      <div style={{ marginTop: 8 }}>上传图片</div>
    </div> : null
  }
</Upload>

也可以参考文档右倒数第三个demo(使用阿里云 OSS 上传示例)。
不过有点需要注意,现在已经走了接口上传,那么接口就可能会成功,也会失败。这么一来,我们handleChange里面file.status是done的情况,需要添加接口的返回逻辑判断:

case 'uploading':
  curFile = [file];
  break;

case 'done':
  if (file.response.status === 0) {
    curFile = [file];
  } else {
    curFile = [];
  }
  break;

到这里,图片在选择完成,通过我们的校验后就被成功上传到后端接口啦~~

最后的话

Antd Upload单个图片的校验和上传内容到这里结束咯~
感谢你的阅读,希望对在读的你有所帮助:)