上传时获取图片和视频宽高(onload和Promise配合使用)

JavaScript如何获取图片和视频的尺寸呢?本文很详细,一步一步来,循序渐进.

方法1:得到图片的src属性,是否可以读到图片的宽高?
方法2:得到图片的DOM元素,是否可以读取图片的宽高?

下面我们一起验证一下吧~

获取图片原始的真实宽高

首先我们考虑入参,图片的src跟图片的DOM,接着我们如何读取宽高,先来看看图片的DOM元素有没有什么属性吧

随便搞张图片测试一下
在这里插入图片描述

我们看到有四个属性,width,height, naturalHeight, naturalWidth
在这里插入图片描述

那到底使用那种属性更合适呢?看看MDN文档怎么说

naturalWidth和naturalHeight—[(MDN文档)](HTMLImageElement.naturalHeight - Web API 接口参考 | MDN (mozilla.org))
在这里插入图片描述

width和height—:图像嵌入元素 - HTML(超文本标记语言) | MDN (mozilla.org)

在这里插入图片描述

看完之后,还是选择naturalWidth和naturalHeight更合适些

选择naturalWidth和naturalHeight

然后开始干

function getImgRealSize(img) {
    
    
  // img是dom元素
    if (img instanceof HTMLElement) {
    
    
      // 拥有真实宽高属性的dom元素
      if (
        img.naturalWidth !== null && img.naturalWidth !== undefined
      ) {
    
    
         return {
    
    
            naturalWidth: img.naturalWidth,
            naturalHeight: img.naturalHeight
          }
        
      } else {
    
    
        // 没有真实宽高属性的dom元素
        ....
      }
    } else {
    
    
      // img是图片url字符串,通过创建图片dom获取真实宽高
      ....
    }
}

图片url转换为Image对象

如果是一张图片的url,如何读取,我知道有一个Image对象Image() - Web API 接口参考 | MDN (mozilla.org)

**Image()**函数将会创建一个新的HTMLImageElement实例。

看到这句话,我就知道怎么读取啦~

const fn = imgSrc => {
    
    
  if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
    return reject(new Error('img url not found'))
  }
  let imgDom = new Image()
  imgDom.src = imgSrc
  console.log(imgDom.naturalWidth, imgDom.naturalHeight);
  console.log(imgDom);
}

咦?怎么回事,脑子快速运转中~~~

图片没有加载好,就已经直接去读取图片的属性啦,所以读到的属性值是0,那为啥console.dir输出的是有值的DOM数据呢?
console.log - Web API 接口参考 | MDN (mozilla.org)

在这里插入图片描述

console读取对象的引用

哈哈,console.dir输出的是对象的引用,在控制台输出的时候,已经有DOM值了

在这里插入图片描述

那怎么解决呢???

window.onload事件,大家肯定都听过吧,Window - Web API 接口参考 | MDN (mozilla.org)

DOM完全加载完毕(onload)

DOM完全加载完毕,看到这句话,就知道怎么做啦!

当图像装载完毕时调用

在这里插入图片描述

const fn = imgSrc => {
    
    
  if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
    return reject(new Error('img url not found'))
  }
  let imgDom = new Image()
  imgDom.src = imgSrc
  imgDom.onload = () => {
    
    
    console.log(imgDom.naturalWidth, imgDom.naturalHeight);
  }
}

哈哈哈,perfect

在这里插入图片描述

一顿操作猛如虎,赶紧试试

    const fn = imgSrc => {
    
    
      if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
        return reject(new Error('img url not found'))
      }
      let imgDom = new Image()
      imgDom.src = imgSrc
      imgDom.onload = () => {
    
    
        return {
    
    
          naturalWidth: imgDom.naturalWidth,
          naturalHeight: imgDom.naturalHeight
        }
      }
    }

    function getImgRealSize(img) {
    
    
      // img是dom元素
      if (img instanceof HTMLElement) {
    
    
        // 拥有真实宽高属性的dom元素
        if (
          img.naturalWidth !== null && img.naturalWidth !== undefined
        ) {
    
    
          return {
    
    
            naturalWidth: img.naturalWidth,
            naturalHeight: img.naturalHeight
          }
        } else {
    
    
          // 没有真实宽高属性的dom元素
          fn(img.src)
        }
      } else {
    
    
        // img是图片url字符串,通过创建图片dom获取真实宽高
        fn(img)
      }
    }

结果…额…果然没有这么简单
在这里插入图片描述

没事,继续优化啦

首先分析一下为什么会是0???
想想刚刚那个Image对象,难道需要加onload事件,让DOM加载完成

加点辅助的输出,看看怎么回事

在这里插入图片描述

控制台输出如下:
在这里插入图片描述

哦哦,找到原因啦,开心~

onload事件执行需要时间,是个异步执行,导致同步已经返回了undefined,所以怎么办呢???

返回promise

大家知道await和async关键字吧, 那肯定知道await就能够等待事件执行完毕,再继续执行后续操作,其实这就是promise微任务加载啦,可以利用promise的resolve和reject解决

const fn = imgSrc => {
    
    
  if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
    return reject(new Error('img url not found'))
  }
  let imgDom = new Image()
  imgDom.src = imgSrc
  imgDom.onload = () => {
    
    
    return {
    
    
      naturalWidth: imgDom.naturalWidth,
      naturalHeight: imgDom.naturalHeight
    }
  }
}
function getImgRealSize(img) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const rs = {
    
     naturalWidth: 0, naturalHeight: 0 }
    // img是dom元素
    if (img instanceof HTMLElement) {
    
    
      // 拥有真实宽高属性的dom元素
      if (
        img.naturalWidth !== null && img.naturalWidth !== undefined
      ) {
    
    
        img.onload = () => {
    
    
          const {
    
     naturalWidth, naturalHeight } = img
          Object.assign(rs, {
    
     naturalWidth: naturalWidth, naturalHeight: naturalHeight })
          resolve(rs)
        }
      } else {
    
    
        // 没有真实宽高属性的dom元素
        fn(img.src)
      }
    } else {
    
    
      // img是图片url字符串,通过创建图片dom获取真实宽高
      fn(img)
    }
  })
}
// 既然返回了一个promise,那就需要使用.then接收啦
getImgRealSize(document.querySelector("#imgDom")).then(res => {
    
    
  console.log(res, 11)
}) 

在这里插入图片描述

这下子就调用成功啦,哈哈

然后看看传入参数是url的时候,会是什么样子吧
在这里插入图片描述
在这里插入图片描述

没有输出,怎么回事呢,有了上面的经验,大家应该知道了吧,fn函数需要优化,不然就会返回undefined啦,注意fn函数里面需要使用到promise的resolve和reject,所以我们需要把fn写入到promise里面,形成一个闭包环境.

function getImgRealSize(img) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const rs = {
    
     naturalWidth: 0, naturalHeight: 0 }
    const fn = imgSrc => {
    
    
      if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
        return reject(new Error('img url not found'))
      }
      let imgDom = new Image()
      imgDom.src = imgSrc
      imgDom.onload = () => {
    
    
        const {
    
     width, height } = imgDom // 如果imgDom存在naturalWidth属性,那么它的值等于width
        Object.assign(rs, {
    
     naturalWidth: width, naturalHeight: height })
        imgDom = null;
        resolve(rs)
      }
    }
    // img是dom元素
    if (img instanceof HTMLElement) {
    
    
      // 拥有真实宽高属性的dom元素
      if (
        img.naturalWidth !== null && img.naturalWidth !== undefined
      ) {
    
    
        img.onload = () => {
    
    
          const {
    
     naturalWidth, naturalHeight } = img
          Object.assign(rs, {
    
     naturalWidth: naturalWidth, naturalHeight: naturalHeight })
          resolve(rs)
        }
      } else {
    
    
        // 没有真实宽高属性的dom元素
        fn(img.src)
      }
    } else {
    
    
      // img是图片url字符串,通过创建图片dom获取真实宽高
      fn(img)
    }
  })
}
    
getImgRealSize(document.querySelector("#imgDom")).then(res => {
    
    
  console.log(res, 11)
})
getImgRealSize("https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb510bf31a7b4d3692c22b390f157f32~tplv-k3u1fbpfcp-zoom-mark-crop-v2:0:0:1812:480.awebp?").then(res => {
    
    
  console.log(res, 22)
}) 

哈哈,看到返回结果啦,开心到飞起~~~

在这里插入图片描述

优化promise,Image的onerror

既然使用了promise,那我们就应该考虑什么时候使用reject呢,所以我们要考虑异常情况哟
我们前面使用到了Image对象的onload事件,但其实除了onload事件,Image也有onerror事件呢?
当图片加载出现异常的时候调用.

在这里插入图片描述

所以优化代码如下:

function getImgRealSize(img) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const rs = {
    
     naturalWidth: 0, naturalHeight: 0 }
    const fn = imgSrc => {
    
    
      if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
        return reject(new Error('img url not found'))
      }
      let imgDom = new Image()
      imgDom.src = imgSrc
      imgDom.onload = () => {
    
    
        const {
    
     width, height } = imgDom // 如果imgDom存在naturalWidth属性,那么它的值等于width
        Object.assign(rs, {
    
     naturalWidth: width, naturalHeight: height })
        imgDom = null;
        resolve(rs)
      }
      imgDom.onerror = (err) => {
    
    
        imgDom = null;
        reject(new Error('image onerror'))
      }
    }
    // img是dom元素
    if (img instanceof HTMLElement) {
    
    
      // 拥有真实宽高属性的dom元素
      if (
        img.naturalWidth !== null && img.naturalWidth !== undefined
      ) {
    
    
        img.onload = () => {
    
    
          const {
    
     naturalWidth, naturalHeight } = img
          Object.assign(rs, {
    
     naturalWidth: naturalWidth, naturalHeight: naturalHeight })
          resolve(rs)
        }
        img.onerror = (err) => {
    
    
          reject(err)
        }
      } else {
    
    
        // 没有真实宽高属性的dom元素
        fn(img.src)
      }
    } else {
    
    
      // img是图片url字符串,通过创建图片dom获取真实宽高
      fn(img)
    }
  })
}

哈哈,这样就可以了,但是既然都已经做到这里了,我们再多考虑一种情况,我需要上传文件的时候判断图片的尺寸,这里演示就使用elemment-ui的上传组件啦, el-upload

上传时获取图片,使用FileReader对象

el-upload上传组件的说明: https://element.eleme.cn/#/zh-CN/component/upload

在这里插入图片描述

在before-upload这个钩子这里,就能拿到文件对象啦~~~

开干,冲冲冲

// 上传时读到的文件,将文件转为img的src
readFiles(file) {
    
    
  return new Promise((resolve, reject) => {
    
    
    let readerFile = new FileReader();
    readerFile.onload = (evt) => {
    
    
      getImgRealSize(evt.target.result).then(res => {
    
    
        console.log('img的宽高 -->', res.naturalWidth, res.naturalHeight)
        if(res.naturalWidth / res.naturalHeight === 16 / 9) {
    
    
          resolve(true);
        } else {
    
    
          this.$message.error(`直播间只支持16:9比例文件,请重新选择文件`)
          reject(false)
        }
      });         
    }
    readerFile.onerror = (err) => {
    
    
      console.error(err);
      reject(err)
    }
    readerFile.readAsDataURL(file);
  })
},
// 文件上传前格式,大小和数量校验提示
async function beforeAvatarUpload(file) {
    
    
  let isPic = file.type === "image/jpg" || file.type === "image/png" || file.type === "image/jpeg";
  if (!isPic) {
    
    
    this.$message.error("不支持选择的上传格式,请重新选择上传格式为jpg或png")
    return false;
  } else if (file.size > this.exceedSize) {
    
    
    this.$message.error("只允许上传1M以内的文件")
    return false;
  } else if (this.imgfileList.length > this.limitNum) {
    
    
    this.$message.error(`文件数量已到限制`)
    return false;
  } else {
    
    
    return await this.readFiles(file)
  }
  return false;
}

看到了吗?调用了我们上面的getImgRealSize函数,首先我们拿到的是一个文件对象,如果是一张图片的话,直接是拿不到图片的url的,所以我们需要使用readFiles方法,取出图片的url.

就是我们需要使用FileReader对象去读取文件,然后使用readAsDataURL事件,读取到evt.target.result,这个就是图片的url了.

这里需要注意onload事件哟,相信经过上面的经历,并不需要再提醒了.

优化上传校验

使用了await函数呢,最好使用try…catch包裹一下await函数,因为这样可以捕获异常处理,如果await中的promise出现了问题,也能够及时把问题抛出来,同时又兼容不阻塞当前流程

使用try…catch…包裹await之后,又出现了新的问题

猜猜下面输入一张不是16:9的图片,会出现什么情况?

在这里插入图片描述

效果如下:没有拦截成功,还是上传了图片,很明显,走的是catch,但是err没有输出

在这里插入图片描述

why?try/catch 能捕获所有异常,并且把reject当成了异常.

在这里插入图片描述

知道原因了,那我们是不是可以直接将reject(false)变成resolve(false)呢?

在这里插入图片描述

突然发现使用了try…catch…竟然使得程序不正确了,按理说不应该阻塞上传流程了吗?

为此我们再仔细看看element-ui官网(element-ui官网)对el-upload组件的描述

在这里插入图片描述

为此写成reject(false)没啥问题,但是加上try…catch…之后就不正常了

这该如何是好???

返回了false,但就是正常上传了,没有停止,所以反方向想想,直接返回false,是不是不能停止上传了

在这里插入图片描述

此刻,是不是发现什么问题了.

没关系滴!一番恍然大悟,有async和await的函数,默认变成了promise函数,需要返回Promise.reject才可以停止上传咯!

在这里插入图片描述

那显然改了beforeAvatarUpload方法还不够,还需要改readFiles方法,因为这里resolve(false)显然不能停止上传了,我们需要reject(false),不然就得在beforeAvatarUpload方法中继续判断,如果返回false就需要return为Promise.reject()

在这里插入图片描述

完整代码

最后完整代码如下:

// 上传时读到的文件,将文件转为img的src
    readFiles(file) {
    
    
      return new Promise((resolve, reject) => {
    
    
        let readerFile = new FileReader();
        readerFile.onload = (evt) => {
    
    
          getImgRealSize(evt.target.result).then(res => {
    
    
            console.log('img的宽高 -->', res.naturalWidth, res.naturalHeight)
            if(res.naturalWidth / res.naturalHeight === 16 / 9) {
    
    
              resolve(true);
            } else {
    
    
              this.$message.error(`直播间只支持16:9比例文件,请重新选择文件`)
              reject(`直播间只支持16:9比例文件,请重新选择文件`)
            }
          });         
        }
        readerFile.onerror = (err) => {
    
    
          console.error(err);
          reject(err)
        }
        readerFile.readAsDataURL(file);
      })
    },
    // 文件上传前格式,大小和数量校验提示
    async beforeAvatarUpload(file) {
    
    
      let isPic = file.type === "image/jpg" || file.type === "image/png" || file.type === "image/jpeg";
      if (!isPic) {
    
    
        this.$message.error("不支持选择的上传格式,请重新选择上传格式为jpg或png")
        return Promise.reject();
      } else if (file.size > this.exceedSize) {
    
    
        this.$message.error("只允许上传1M以内的文件")
        return Promise.reject();
      } else if (this.imgfileList.length > this.limitNum) {
    
    
        this.$message.error(`文件数量已到限制`)
        return Promise.reject();
      } else {
    
    
        try {
    
    
          return await this.readFiles(file)
        } catch(err) {
    
    
          console.error(err)
          return Promise.reject();
        }
      }
      return Promise.reject();
    },
    
    
可以封装起来,变成公共方法
// 获取图片原始的真实宽高
// img参数:是图片dom元素或者图片url
export function getImgRealSize(img) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const rs = {
    
     naturalWidth: 0, naturalHeight: 0 }
    const fn = imgSrc => {
    
    
      if (!(typeof imgSrc === 'string' && !!imgSrc)) {
    
    
        return reject(new Error('img url not found'))
      }
      let imgDom = new Image()
      imgDom.src = imgSrc
      imgDom.onload = () => {
    
    
        const {
    
     width, height } = imgDom // 如果imgDom存在naturalWidth属性,那么它的值等于width
        Object.assign(rs, {
    
     naturalWidth: width, naturalHeight: height })
        imgDom = null;
        resolve(rs)
      }
      imgDom.onerror = (err) => {
    
    
        imgDom = null;
        reject(new Error('image onerror'))
      }
    }
    // img是dom元素
    if (img instanceof HTMLElement) {
    
    
      // 拥有真实宽高属性的dom元素
      if (
        img.naturalWidth !== null && img.naturalWidth !== undefined
      ) {
    
    
        img.onload = () => {
    
    
          const {
    
     naturalWidth, naturalHeight } = img
          Object.assign(rs, {
    
     naturalWidth: naturalWidth, naturalHeight: naturalHeight })
          resolve(rs)
        }
        img.onerror = (err) => {
    
    
          reject(err)
        }
      } else {
    
    
        // 没有真实宽高属性的dom元素
        fn(img.src)
      }
    } else {
    
    
      // img是图片url字符串,通过创建图片dom获取真实宽高
      fn(img)
    }
  })
}

上传时获取视频宽高比例

16/9 = 1.777777777…

// 判断视频宽高比例是否为16比9
export function getVideoRealSize(video) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const url = URL.createObjectURL(file)
    let video = document.createElement('video')
    video.onloadedmetadata = evt => {
    
    
      // Revoke when you don't need the url any more to release any reference
      URL.revokeObjectURL(url)
      console.log('video的宽高 -->', video.videoWidth, video.videoHeight)
      if(video.videoWidth / video.videoHeight === 16 / 9) {
    
    
          video = null;
          resolve(true);
        } else {
    
    
          video = null;
          this.$message.error(`图片比例需要为16 比 9`)
          reject(false)
        }
    }
    video.onerror = (err) => {
    
    
      console.error(err);
      video = null;
      reject(err)
    }
    video.src = url
    video.load() // fetches metadata
  })
}

总结

注意:

  1. 在有onload事件的函数中,有返回值,需要使用promise,借助reslove和reject,不然会先返回undefined,没有等到onload事件执行完成,就已经返回了值。
  2. DOM元素需要等待onload事件之后再去读取元素的值,比如元素的naturalWidth和width,不然会先读到0.
  3. 创建DOM元素之后,不需要再使用了就清空DOM元素哟~
  4. 使用了async和await关键字,最好使用try…catch…或.catch()捕获错误

try/catch和 .catch的区别

  1. .catch和try/catch都能捕获异步方法中reject错误,也能捕获其他错误
  2. 但是try/catch需要在async…await…函数中,不然会报错Use node --trace-warnings ... to show where the warning was created,这个报错会导致在catch中的输出阻塞,但是catch中的错误还是会抛出来。
  3. try/catch当捕获到第一个promise出现reject,或者出现异常时,会直接阻塞下面函数的执行与调用,当调用了fn1和fn2时,fn1出现了异常,fn2不会调用了
  4. 上面一种情况, .catch还会继续调用fn2,不会阻塞执行.
  5. 所以如果想要尽管出错了,也还要继续执行下面函数和判断,就使用.catch, 如果希望第一个promise出现异常,下面所有执行都不用了,就使用try…catch…
// 捕获异步中的错误1
const asyncFn1 = async () => {
    
    
  try {
    
    
      let result1 = await fn(false, 'hello')
      console.log('中间内容输出')
      let result2 = await fn(false, 'world')
      console.log('result1' + result1)
      console.log('result2' + result2)
  } catch (error) {
    
    
      console.log('catch:' + error) // 执行
  }
}
asyncFn1();
// catch:fail:hello

/*
// 捕获异步中的错误2
const asyncFn2 = async () => {
  try {
      await fn(false, 'hello').then(() => {}).catch(err => {
          console.log('result1:' + err)
      })
      console.log('中间内容输出')
      await fn(false, 'world').then(() => { }).catch(err => {
          console.log('result2:' + err)
      })
  } catch (error) {
      console.log('catch:' + error)
  }
}
asyncFn2();
result1:fail:hello
中间内容输出
result2:fail:world
*/

猜你喜欢

转载自blog.csdn.net/qq_44859233/article/details/127821675
今日推荐