eJayYoung / blog

逸杰的个人博客
MIT License
11 stars 0 forks source link

H5拍照上传填坑汇总 #2

Open eJayYoung opened 6 years ago

eJayYoung commented 6 years ago

前言

最近工作一直在使用vue+vux做移动端项目,有一个拍照上传照片的需求,发现vux里并没有实现,调研过非官方的vux-uploader后,感觉还不是很理想。

其实网上已经可以找到很多已经实现的成熟方案,但是在调研这个需求的时候,我发现在各种实现方案中也有一些puzzle的知识点,因此自己动手撸了一个轮子vux-uploader-component,并记录一二。

需求

组件的交互功能要求如下:

  1. html5调用手机相机
  2. 渲染图片为缩略图
  3. 前端压缩图片
  4. 预览大图
  5. 删除当前图片
  6. 自动上传

部分关键技术点的实现方案

使用html media capture调用手机端的相机

   <input type="file" accept="image/*"  capture />

既然是在HTML5规范中,那最关心的问题肯定是兼容性了

html-media-capture-caniuse

html-media-capture

可以看到,在大部分的主流平台,兼容性还可以接受,andriod2-4 都支持,只是在ios 6-10支持不太好。

感兴趣的可以在自己的手机上测测兼容性

html-media-capture-demo

html-media-capture demo

使用ULR.createObjectURL获取图片地址

   blobURL = ULR.createObjectURL(object)

object参数可以为FileBlobMediaSource

在这一块可以衍生出好几个问题:

使用canvas来压缩图片

可以从两个方面可以进行压缩:

使用FormData来上传

   const formData = new FormData()
   formData.append('file', blob)

这是XHR Level2的产物,可以方便的以键值对的形式插入。

最大的优势是可以通过XMLHttpRequest.send()来异步提交二进制文件。

后期还可以通过Blobslice来扩展分片上传功能。

知识点剖析

FileReader和URL.createObjectURL的区别

关于FileReaderURL.createObjectURL的用法就不详细介绍了,感兴趣的自行google。

我们现在只需要知道

既然这两个API都可以满足我们获取图片地址的需求,那它们之间的区别在哪呢?

1、执行时机

2、内存使用

3、兼容性

从上面答案不难看出,两者的优劣势

参考

相机拍照的图片会旋转

从上面的createObjectURL获取到图片的地址后,我们可以插入到页面元素的background-image属性展示这个图片,PC端模拟器展示没有问题,但手机真机拍照得到的图片会有逆时针的90°旋转。

为什么从相机拍照获取的图片会旋转呢?

是因为从相机拍照获取的图片的EXIF(Exchangeable image file format)会默认设置一个orientation tag

目前只有jpeg格式的图片会有

orientation

:point_up_2:上图就是orientation tag与图片旋转角度的对应关系

如何解决这个问题呢?

1、获取图片的orientation

2、根据图片的orientation做对应的旋转

 switch (orientation) {
    case 2:
      // horizontal flip
      ctx.translate(width, 0);
      ctx.scale(-1, 1);
      break;
    case 3:
      // 180 rotate left
      ctx.translate(width, height);
      ctx.rotate(Math.PI);
      break;
    case 4:
      // vertical flip
      ctx.translate(0, height);
      ctx.scale(1, -1);
      break;
    case 5:
      // vertical flip + 90 rotate right
      ctx.rotate(0.5 * Math.PI);
      ctx.scale(1, -1);
      break;
    case 6:
      // 90 rotate right
      ctx.rotate(0.5 * Math.PI);
      ctx.translate(0, -height);
      break;
    case 7:
      // horizontal flip + 90 rotate right
      ctx.rotate(0.5 * Math.PI);
      ctx.translate(width, -height);
      ctx.scale(-1, 1);
      break;
    case 8:
      // 90 rotate left
      ctx.rotate(-0.5 * Math.PI);
      ctx.translate(-width, 0);
      break;

参考

File和Blob的关系?Blob Url和DataURL的区别?DataURL如何转成Blob?

File和Blob的关系

input onchange中返回的图片对象其实就是一个File对象。

Blob对象是一个用来包装二进制文件的容器,File继承于Blob

FileReader是用来读取内存中的文件的API,支持FileBlob两种格式。

Blob Url和Data URLs的区别

Blob Url只能在浏览器中通过URL.createObjectURL(blob)创建,当不使用的时候,需要URL.revokeObjectURL(blobURL)来进行释放。

可以简单理解为对应浏览器内存文件中的软链接。该链接只能存在于浏览器单一实例或对应会话中(例如:页面的生命周期)

blobURL = URL.createObjectURL(blob)

// blob:http://localhost:8000/xxxxxxxx

Data URLs可以获取文件的base64

data:[<mediatype>][;base64],<data>

mediatype是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII

可以通过FileReader.readAsDataURL获取

const reader = new FileReader();
reader.addEventListener("load", e => {
    const dataURL = e.target.result;
})
reader.readAsDataURL(blob);

DataURL如何转成Blob?

function dataURItoBlob(dataURI) {
  // convert base64 to raw binary data held in a string
  // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
  var byteString = atob(dataURI.split(',')[1]);

  // separate out the mime component
  var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]

  // write the bytes of the string to an ArrayBuffer
  var ab = new ArrayBuffer(byteString.length);

  // create a view into the buffer
  var ia = new Uint8Array(ab);

  // set the bytes of the buffer to the correct values
  for (var i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i);
  }

  // write the ArrayBuffer to a blob, and you're done
  var blob = new Blob([ab], {type: mimeString});
  return blob;

}

参考

上传进度条从一开始上传就是100%,为什么?

众所周知,目前监听上传文件进度的主流方式是使用XHR的onprogress事件来实现,但是为什么在我本地调试上传的时候,onprogress只被调用了一次呢?

在XHR2中有一个事件对象ProgressEvent,以下几种监听事件都可以获取到这个对象:

事件名称 触发时机
loadstart 请求发起
progress 传递数据
abort 请求被中止(例如,通过abort()方法来触发)
error 请求失败
load 请求成功完成后
timeout 在指定时间内,请求超时时触发
loadend 请求完成后(不论请求成功还是失败)

ProgressEvent的事件循环如下:

  1. 每个请求发起后先触发loadstart,请求完成的flagfalse

  2. 在请求完成的flag设置为true之前,以50ms的间隔来轮询触发progress事件

  3. 当请求完成时,请求完成的flagtrue,根据请求完成的结果状态,触发abort,error,load,timeout其中之一。

  4. 请求完成后触发loadend

至此,我们就很清楚的知道了,为什么就算我们在本地上传,并在progress的回调里console.log也只执行一次的原因了:本地上传请求事件小于50ms

只要将network调成slow 3G,并换一张高清像素的大图片进行上传,就可以看到progress事件会在上传完成之前以50ms的间隔调用。

参考

最后

小小推广一波基于weui风格实现的移动端vue图片上传组件vux-uploader-component

vux-uploader-component

欢迎扫码体验,Star, Issue, PR:)

CurtisTong commented 6 years ago

666