# 从图片剪裁聊前端文件上传相关的API

# FileReader

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。

图片剪裁首先要上传图片,这个时候可以利用html5<input type="file">来上传图片,然后利用FileReader来实现异步读取文件机制。这个时候File对象可以是来自用户在一个<input>元素上选择文件后返回的FileList对象

<input type="file" name="avatar" id="file" @change="selectAvatar" :accept="acceptType">
1
 selectAvatar(e) {
    const file = e.target.files[0]
    let reader = new FileReader()
    reader.onload = (e) => {
        console.log(e)
        let data
        if(e.target.result) {
            data = e.target.result
        }
    }
    reader.readAsDataURL(file)
}
1
2
3
4
5
6
7
8
9
10
11
12

img

通过 FileReader 我们可以将图片文件转化成 DataURL,就是以 data:image/png;base64开头的一种URL,然后可以直接放在 image.src 里,这样本地图片就显示出来了。

# 方法

方法名 描述
abort 中止读取操作
readAsArrayBuffer 异步按字节读取文件内容,结果用ArrayBuffer对象表示
readAsBinaryString 异步按字节读取文件内容,结果用文件的原始二进制串表示
readAsDataURL 异步读取文件内容,结果用data:URL的字符串形式表示
readAsText 异步按字符读取文件内容,结果用字符串形式表示

# 事件

事件名 描述
onabort 中断时触发
onerror 出错时触发
onload 文件读取成功完成时触发
onloadend 读取完成触发(无论成功或失败)
onloadstart 读取开始时触发
onprogress 读取中

# 二进制数组

先来介绍ArrayBuffer,是因为 FileReader 有个 readAsArrayBuffer()的方法,如果被读的文件是二进制数据,那用这个方法去读应该是最合适的,读出来的数据,就是一个 Arraybuffer 对象

ArrayBuffer对象,TypedArray视图,DataView视图是JavaScript操作二进制数据的一个接口。它们都以数组的语法处理二进制数据,所以统称二进制数组。

  • ArrayBuffer对象:代表内存中的一段二进制数据,可以通过视图进行操作。可以用数组的方法操作内存。
  • TypedArray视图:有9种类型的视图,如Uint8Array(无符号8位整数)数组视图,Int16Array16位整数数组视图等。数组成员都是相同的数据类型
  • DataView视图:可以自定义复合格式的视图,每个字节的类型都可以自定义的。意思就是数组成员可以是不同的数据类型。

一言以蔽之,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用于读/写简单类型的二进制数据,DataView视图用于读/写复杂类型的二进制数据。二进制数组不是真正的数据,而是类数组

# ArrayBuffer对象

代表存储二进制数据的一段内存,不能直接读/写,只能通过另外两个视图对象读/写,视图的作用是以指定格式解读二进制数据。

var buf = new ArrayBuffer(32)

console.log(buf.byteLength) // ArrayBuffer 对象有实例属性 byteLength ,表示当前实例占用的内存字节长度(单位字节)
1
2
3

上面代码生成一段32字节的内存区域,每个字节的值默认都是0。该对象的参数是所需要的内存大小(单位字节)

TIP

  • ArrayBuffer.prototype.slice() 该方法允许将内存区域的一部分复制生成一个新的ArrayBuffer对象。
var buffer = new ArrayBuffer(8)
var newBuffer = buffer.slice(0,3)
1
2

上面代码复制了buffer对象的前3个字节,生成newBuffer一个新的ArrayBuffer对象。该方法其实包含了分配一段新内存,将原来的ArrayBuffer对象复制过去两步骤。该方法如果省略了第二个参数,则默认到原始内存对象的结尾。

  • ArrayBuffer.isView() 返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。
var buffer = new ArrayBuffer(8)
ArrayBuffer.isView(buffer) // false

var v = new Int32Array(buffer)
ArrayBuffer.isView(v) // true
1
2
3
4
5

ArrayBuffer对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做视图。

# DataView视图

为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。

var dataView = new DataView(buf)
dataView.getUint8(0) // 0  因为原始内存ArrayBuffer对象默认所有位都是0
1
2

# TypedArray视图

img TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

var buffer = new ArrayBuffer(12)

var x1 = new Int32Array(buffer) // 32位带符号整数
x1[0] = 1;

var x2 = new Uint8Array(buffer) // 8位不带符号整数
x2[0] = 2

x1[0] // 2
1
2
3
4
5
6
7
8
9

由于两个视图对应同一段内存,因此一个视图修改底层内存会影响到另一个视图。TypedArray视图还可以接收普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,同时完成这段内存的赋值。

const imageData = this.getDataURL()
const b64 = imageData.replace('data:image/png;base64,', '')
const binary = atob(b64) // 如图乱码所以需要下一步取编码
const array = []
for (let i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i))
}
let newBlob = new Blob([new Uint8Array(array)], {type: 'image/png'})
1
2
3
4
5
6
7
8

上面的自己封装的头像剪裁组件例子中array就是普通数组,新建了一个不带符号的8位整数视图,对底层内存的赋值也同时完成。使用Uint8Array是因为b64字符串在atob解码后一一获得具体字符的Unicode编码的值范围在0-255

img

  • 普通数组和TypedArray数组差异:
    • 后者数组成员连续,不会有空位
    • 后者数组成员默认值为0
    • 后者数组所有成员都是同一种类型 除此之外,所有在数组上使用的方法都能在其上使用。
var v3 = new Int16Array(b,2,2)// 创建一个指向b的Int16视图,开始于字节2,长度为2
1

视图的构造函数可以接受3个参数:

  • 第一个参数(必选):视图对应的底层ArrayBuffer对象。
  • 第二个参数(可选):视图开始的字节序号,默认从0开始。必须与所建立的数据类型一致,否则报错
  • 第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束。
var buffer = new ArrayBuffer(8)
var i16 = new Int16Array(buffer, 1)
// Uncaught RangeError: start offset of Int16Array
// should be a multiple of 2
1
2
3
4

上面例子新生成一个8个字节的原始内存对象,然后在这个对象的第一个字节建立带符号的16位整数视图,结果报错。因为带符号的16位整数需要2个字节,所以这就是数据类型不一致引起的报错。

# Blob对象

Blob是用来支持文件操作的。简单的说:在JS中,有两个构造函数 FileBlob, 而File继承了所有Blob的属性。 Blob具体实现的功能: img

要从其他非blob对象和数据构造一个 Blob,请使用 Blob()构造函数。

Blob(blobParts[,options])
1

返回一个新创建的Blob对象,其内容由参数中给定定数组串联组成。

  • 属性
    • Blob.size:只读,Blob对象中包含数据定大小(字节)
    • Blob.type:只读,一个字符串,表示该Blob对象所包含数据的MIME类型。如果类型未知,则为空字符串。
let newBlob = new Blob([new Uint8Array(array)], {type: 'image/png'})
console.log(newBlob)
1
2

img

  • 方法
    • Blob.slice([start[,end[,contentType]]]):返回一个新的Blob对象,包含了源Blob对象中指定范围内的数据。用于文件分片上传
    • Blob.stream():返回一个能读取blob内容的ReadableStream
    • Blob.text():返回一个promise且包含blob所有内容的UTF-8格式的 USVString
    • Blob.arrayBuffer():返回一个promise且包含blob所有内容的二进制格式的 ArrayBuffer

# base64

img

其实就是Data URI,前缀为data:协议的URL,其允许内容创建者向文档嵌入小文件。Data URI 由四个部分组成:前缀(data:)、指示数据类型的MIME类型、如果非文本则为可选的base64标记、数据本身:

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

TIP

如果数据是文本类型,你可以直接将文本嵌入 (根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行base64编码之后再进行嵌入。

data:,Hello%2C%20World! # 简单的 text/plain 类型数据

data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D # 上一条示例的 base64 编码版本
1
2
3

在 JavaScript 中,有两个函数被分别用来处理解码和编码base64字符串:atob()btoa()

atob() 函数能够解码通过base-64编码的字符串数据。相反地,btoa() 函数能够从二进制数据“字符串”创建一个base-64编码的ASCII字符串。

然后取出其中base64信息,再用window.atob转换成由二进制字符串。但window.atob转换后的结果仍然是字符串,直接给Blob还是会出错。所以又要用Uint8Array转换一下。

TIP

如果不仔细看,真的会误把data URI看成data URL,然后用URL的方式去理解URI,其实不然!URLuniform resource locator的缩写,在web中的每一个可访问资源都有一个URL地址,例如图片,HTML文件,js文件以及style sheet文件,我们可以通过这个地址去download这个资源。其实URLURI的子集,URIuniform resource identifier的缩写。URI是用于获取资源,包括其附加的信息的一种协议。附加信息可能是地址,也可能不是地址,如果是地址,那么这时URI就变成URL了。注意的是data URI不是URL,因为它并不包含资源的公共地址。

// base64转换成buffer
    base64ToBuffer(base64Url) {
      let parts = base64Url.split(';base64,');
      // let contentType = parts[0].split(':')[1];
      let raw = window.atob(parts[1]);
      let rawLength = raw.length;
      let uInt8Array = new Uint8Array(rawLength);
      for(let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
      }
      return uInt8Array
    }
1
2
3
4
5
6
7
8
9
10
11
12

# canvas

const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.drawImage(this.img, 0,0, sw, sh, dx, dy, dw, dh) // sw/sh 被剪裁图像的宽高。 dx/dy 在画布上放置的位置。 width 要使用的图像宽度
const imageData = canvas.toDataURL()
1
2
3
4

toDataURL方法返回一个包含图片展示的data URI

canvas.toDataURL(type, encoderOptions);
1
  • 参数说明:

    • type可选:图片格式,默认为image/png
    • encoderOptions可选:在指定图片格式为 image/jpegimage/webp的情况下,可以从 01 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
  • 需要注意的点:

    • 如果画布的高度或宽度是0,那么会返回字符串“data:,”
    • 如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。
    • Chrome支持“image/webp”类型。

# FormData

提交请求有两种方式:一种是Ajax,另外一种就是通过表单提交 通过File API能够访问到文件内容,利用这一点就可以通过XHR直接把文件上传到服务器。当然啦,把文件内容放到send()方法中,再通过POST请求,的确很容易就能实现上传。但这样做传递的是文件内容,因而服务器必须收集提交的内容,然后再把它们保存到另一个文件中。其实更好的做法是以表单提交的方式来上传文件。

这时候裁剪后的文件就储存在blob里了,我们可以把它当作是普通文件一样,加入到FormData里,并上传至服务器了。

let fd = new FormData()
fd.append('file',file)

xhr.send(fd)
1
2
3
4