We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前两天项目中有个小需求:前端下载后台小哥返回的二进制流文件。
起初接到这个需求时,我感觉这很简单啊(虽然我不会,但可以百度啊,,,,)
然后就写出了如下的代码:
let blob = new Blob([res.data]); let fileName = `Cosen.csv`; if (window.navigator.msSaveOrOpenBlob) { navigator.msSaveBlob(blob, fileName); } else { let link = document.createElement("a"); let evt = document.createEvent("HTMLEvents"); evt.initEvent("click", false, false); link.href = URL.createObjectURL(blob); link.download = fileName; link.style.display = "none"; document.body.appendChild(link); link.click(); window.URL.revokeObjectURL(link.href); }
这一段代码,我大概强行解释一下:
首先判断window.navigator.msSaveOrOpenBlob是为了兼容IE(谁要兼容这 xxIE!!)
window.navigator.msSaveOrOpenBlob
IE
然后非IE的通过URL.createObjectURL()将Blob(Blob是啥?不知道?没关系,我下面会具体装逼讲解的)构建为一个object URL对象、指定文件名&文件类型、创建a链接模拟点击实现下载,最后通过URL.revokeObjectURL释放创建的对象。
URL.createObjectURL()
Blob
object URL
a
URL.revokeObjectURL
功能虽然实现了,但其实我是似懂非懂的~
没过几天,产品又给我提了一个需求:图片裁剪上传及预览。
虽然听过类似的需求,但自己手写还真的没写过,然后我就开始了网上冲浪时光(各种搜索,,,)。但这次,没有想象中那么简单了~~
网上看到的都是诸如FileReader、canvas、ArrayBuffer、FormData、Blob这些名词。我彻底懵了,这些平时都只是听过啊,用的也不多啊。经过了一番学习,我发现这些都属于前端二进制的知识范畴,所以在搞业务前,我准备先把涉及到的前端二进制梳理一遍,正所谓:底层基础决定上层建筑嘛 🙈
FileReader
canvas
ArrayBuffer
FormData
HTML5定义了FileReader作为文件API的重要成员用于读取文件,根据W3C的定义,FileReader接口提供了读取文件的方法和包含读取结果的事件模型。
HTML5
API
W3C
var reader = new FileReader();
abort
readAsArrayBuffer
readAsBinaryString
readAsDataURL
data:url
readAsText
onabort
onerror
onload
onloadend
onloadstart
onprogress
下面我们尝试把一个文件的内容通过字符串的方式读取出来:
<input type="file" id='upload' /> document.getElementById('upload').addEventListener('change', function (e) { var file = this.files[0]; const reader = new FileReader(); reader.onload = function () { const result = reader.result; console.log(result); } reader.readAsText(file); }, false);
TypedArray
DataView 对象
先来看下ArrayBuffer的功能:
先来介绍ArrayBuffer ,是因为 FileReader 有个 readAsArrayBuffer()的方法,如果被读的文件是二进制数据,那用这个方法去读应该是最合适的,读出来的数据,就是一个 Arraybuffer 对象,来看下定义:
readAsArrayBuffer()
Arraybuffer
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区.ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容.
DataView
ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
const buffer = new ArrayBuffer(8); // ArrayBuffer 对象有实例属性 byteLength ,表示当前实例占用的内存字节长度(单位字节) console.log(buffer.byteLength);
由于无法对 Arraybuffer 直接进行操作,所以我们需要借助其他对象来操作. 所有就有了 TypedArray(类型数组对象)和 DataView对象。
上面代码生成了一段 8 字节的内存区域,每个字节的值默认都是 0。
为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。
DataView视图是一个可以从二进制ArrayBuffer对象中读写多种数值类型的底层接口。
setint8()
byte
byteOffset
8-bit
getint8()
new DataView(buffer, [, byteOffset [, byteLength]])
let buffer = new ArrayBuffer(2); console.log(buffer.byteLength); // 2 let dataView = new DataView(buffer); dataView.setInt(0, 1); dataView.setInt(1, 2); console.log(dataView.getInt8(0)); // 1 console.log(dataView.getInt8(1)); // 2 console.log(dataView.getInt16(0)); // 258
另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
TypedArray对象描述了一个底层的二进制数据缓存区(binary data buffer)的一个类数组视图(view)。
binary data buffer
view
但它本身不可以被实例化,甚至无法访问,你可以把它理解为接口,它有很多的实现。
const buffer = new ArrayBuffer(8); console.log(buffer.byteLength); // 8 const int8Array = new Int8Array(buffer); console.log(int8Array.length); // 8 const int16Array = new Int16Array(buffer); console.log(int16Array.length); // 4
Blob是用来支持文件操作的。简单的说:在JS中,有两个构造函数 File 和 Blob, 而File继承了所有Blob的属性。
JS
File
所以在我们看来,File对象可以看作一种特殊的Blob对象。
上面说了,File对象是一种特殊的Blob对象,那么它自然就可以直接调用Blob对象的方法。让我们看一看Blob具体有哪些方法,以及能够用它们实现哪些功能:
是的,我们这里更加倾向于实战中的应用~
关于Blob的更具体介绍可以参考Blob
atob
btoa
base64 相信大家都不会陌生吧(不知道的看这里),最常用的操作可能就是图片转 base64 了吧?
base64
在之前要在字符串跟base64之间互转,我们可能需要去网上拷一个别人的方法,而且大部分情况下,你没有时间去验证这个方法是不是真的可靠,有没有bug。
bug
从IE10+浏览器开始,所有浏览器就原生提供了Base64编码解码方法。
IE10+
Base64
var decodedData = window.atob(encodedData);
var encodedData = window.btoa(stringToEncode);
Canvas
ImageData
关于Canvas,这里我就不做过多介绍了,具体可参考canvas 文档
今天主要说一下Canvas中的ImageData对象(也是为下面的那个图片裁剪的项目做一些基础知识的铺垫~)
ImageData对象中存储着canvas对象真实的像素数据,它包含以下几个只读属性:
width
height
data
Uint8ClampedArray
RGBA
使用createImageData() 方法去创建一个新的,空白的ImageData对象。
createImageData()
var myImageData = ctx.createImageData(width, height);
上面代码创建了一个新的具体特定尺寸的ImageData对象。所有像素被预设为透明黑。
为了获得一个包含画布场景像素数据的ImageData对象,你可以用getImageData()方法:
getImageData()
var myImageData = ctx.getImageData(left, top, width, height);
你可以用putImageData()方法去对场景进行像素数据的写入。
putImageData()
ctx.putImageData(myImageData, dx, dy);
toDataURL
data URI
有如下<canvas>元素:
<canvas>
<canvas id="canvas" width="5" height="5"></canvas>
可以用下面的方式获取一个data-URL
data-URL
var canvas = document.getElementById("canvas"); var dataURL = canvas.toDataURL(); console.log(dataURL); // "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby // blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"
到这里,二进制相关的基础知识我已经铺垫完了。下面让我们回到文章开头提到的那个产品的“没那么简单”的新需求:图片裁剪上传及预览。
其实,像图片裁剪上传这种社区已经有非常成熟的解决方案了,如vue-cropper。这里,我选择手写一个简易的图片裁剪的目的是因为这其中用到了上文提及的大量的二进制知识,可以很好的将理论与实践结合。
图片裁剪上传
话不多说,开 Giao!!
先来看下最终的效果:
这里贴下完成后的代码地址
另外,我用一张图梳理了以上提到的前端二进制模块的关系,这对于下面需求的开发会有很大的帮助:
整个需求分以下四步:
1、获取文件并读取文件。
2、获取裁剪坐标。
3、裁剪图片。
4、读取裁剪后的图片预览并上传。
首先来看下上面第一步提到的获取文件。对应就是给input绑定的handleChange事件:
input
handleChange
handleChange = (event) => { let file = event.target.files[0]; let fileReader = new FileReader(); fileReader.onload = (event) => { this.setState({ file, dataURL: event.target.result, }); this.imageRef.current.onload = () => this.drawImage(); }; fileReader.readAsDataURL(file); };
HTML5 支持从 input[type=file] 元素中直接获取文件信息,也可以读取文件内容。
input[type=file]
这里就需要用到了 FileReader ,这个类是专门用来读取本地文件的。纯文本或者二进制都可以读取,但是本地文件必须是经过用户允许才能读取,也就是说用户要在input[type=file]中选择了这个文件,你才能读取到它。
通过 FileReader 我们可以将图片文件转化成 DataURL,就是以 data:image/png;base64开头的一种URL,然后可以直接放在 image.src 里,这样本地图片就显示出来了。
DataURL
data:image/png;base64
URL
image.src
这里主要是mousedown、mousemove、mouseup事件的结合使用。
mousedown
mousemove
mouseup
鼠标按下事件。这里要记录下鼠标按下时的开始坐标,即startX与startY,同时要将标志位startDrag设为true,标识鼠标开始移动。
startX
startY
startDrag
true
handleMouseDown = (event) => { this.setState({ startX: event.clientX, startY: event.clientY, startDrag: true, }); };
鼠标移动事件。判断startDrag为true(即鼠标开始移动),然后记录对应移动的距离。
handleMouseMove = (event) => { if (this.state.startDrag) { this.drawImage( event.clientX - this.state.startX + this.state.lastX, event.clientY - this.state.startY + this.state.lastY ); } };
鼠标弹起事件。这里要记录下最终鼠标的落点坐标,对应就是lastX与lastY。
lastX
lastY
handleMouseUp = (event) => { this.setState({ lastX: event.clientX - this.state.startX + this.state.lastX, lastY: event.clientY - this.state.startY + this.state.lastY, startDrag: false, }); };
这个时候我们就需要用到canvas了,canvas和图片一样,所以新建canvas时就要确定其高宽。
将图片放置入canvas时需要调用drawImage:
drawImage
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
具体API使用参考MDN上的drawImage
MDN
drawImage = (left = this.state.lastX, top = this.state.lastY) => { let image = this.imageRef.current; let canvas = this.canvasRef.current; let ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); let imageWidth = image.width; let imageHeight = image.height; if (imageWidth > imageHeight) { let scale = canvas.width / canvas.height; imageWidth = canvas.width * this.state.times; imageHeight = imageHeight * scale * this.state.times; } else { let scale = canvas.height / canvas.width; imageHeight = canvas.height * this.state.times; imageWidth = imageWidth * scale * this.state.times; } ctx.drawImage( image, (canvas.width - imageWidth) / 2 + left, (canvas.height - imageHeight) / 2 + top, imageWidth, imageHeight ); };
其中这里面我们还加入了scale,这个变量是用来实现图片放大、缩小效果的。
scale
放大
缩小
而且会判断图片的宽、高的大小关系,从而实现图片在canvas中对应的适配。
这时我们要获取canvas中图片的信息,用toDataURL就可以转换成上面用到的DataURL。
confirm = () => { let canvas = this.canvasRef.current; let ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(100, 100, 100, 100); let avatarCanvas = document.createElement("canvas"); avatarCanvas.width = 100; avatarCanvas.height = 100; let avatarCtx = avatarCanvas.getContext("2d"); avatarCtx.putImageData(imageData, 0, 0); let avatarDataUrl = avatarCanvas.toDataURL(); this.setState({ avatarDataUrl }); this.avatarRef.current.src = avatarDataUrl; };
然后取出其中base64信息,再用window.atob转换成由二进制字符串。但window.atob转换后的结果仍然是字符串,直接给Blob还是会出错。所以又要用Uint8Array转换一下。
window.atob
Uint8Array
这时候裁剪后的文件就储存在blob里了,我们可以把它当作是普通文件一样,加入到FormData里,并上传至服务器了。
blob
upload = (event) => { // console.log("文件url", this.state.avatarDataUrl); let bytes = atob(this.state.avatarDataUrl.split(",")[1]); console.log("bytes", bytes); let arrayBuffer = new ArrayBuffer(bytes.length); let uInt8Array = new Uint8Array(); for (let i = 0; i < bytes.length; i++) { uInt8Array[i] = bytes.charCodeAt[i]; } let blob = new Blob([arrayBuffer], { type: "image/png" }); let xhr = new XMLHttpRequest(); let formData = new FormData(); formData.append("avatar", blob); xhr.open("POST", "/upload", true); xhr.send(formData); };
https://es6.ruanyifeng.com/#docs/arraybuffer
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~
2.关注公众号前端森林,定期为你推送新鲜干货好文。
3.特殊阶段,带好口罩,做好个人防护。
4.添加微信fs1263215592,拉你进技术交流群一起学习 🍻
The text was updated successfully, but these errors were encountered:
No branches or pull requests
写在最前面(不看也不会少一个亿)
最开始的一个小需求
前两天项目中有个小需求:前端下载后台小哥返回的二进制流文件。
起初接到这个需求时,我感觉这很简单啊(虽然我不会,但可以百度啊,,,,)
然后就写出了如下的代码:
这一段代码,我大概
强行解释一下:首先判断
window.navigator.msSaveOrOpenBlob
是为了兼容IE
(谁要兼容这 xxIE
!!)然后非
IE
的通过URL.createObjectURL()
将Blob
(Blob
是啥?不知道?没关系,我下面会具体装逼讲解的)构建为一个object URL
对象、指定文件名&文件类型、创建a
链接模拟点击实现下载,最后通过URL.revokeObjectURL
释放创建的对象。功能虽然实现了,但其实我是似懂非懂的~
紧接着 一个不那么简单的需求
没过几天,产品又给我提了一个需求:图片裁剪上传及预览。
虽然听过类似的需求,但自己手写还真的没写过,然后我就开始了网上冲浪时光(各种搜索,,,)。但这次,没有想象中那么简单了~~
网上看到的都是诸如
FileReader
、canvas
、ArrayBuffer
、FormData
、Blob
这些名词。我彻底懵了,这些平时都只是听过啊,用的也不多啊。经过了一番学习,我发现这些都属于前端二进制的知识范畴,所以在搞业务前,我准备先把涉及到的前端二进制梳理一遍,正所谓:底层基础决定上层建筑嘛 🙈FileReader
HTML5
定义了FileReader
作为文件API
的重要成员用于读取文件,根据W3C
的定义,FileReader
接口提供了读取文件的方法和包含读取结果的事件模型。创建实例
方法
abort
readAsArrayBuffer
ArrayBuffer
对象表示readAsBinaryString
readAsDataURL
data:url
的字符串形式表示readAsText
事件
onabort
onerror
onload
onloadend
onloadstart
onprogress
示例
下面我们尝试把一个文件的内容通过字符串的方式读取出来:
ArrayBuffer
/TypedArray
/DataView 对象
ArrayBuffer
先来看下
ArrayBuffer
的功能:先来介绍
ArrayBuffer
,是因为FileReader
有个readAsArrayBuffer()
的方法,如果被读的文件是二进制数据,那用这个方法去读应该是最合适的,读出来的数据,就是一个Arraybuffer
对象,来看下定义:ArrayBuffer
也是一个构造函数,可以分配一段可以存放数据的连续内存区域。由于无法对
Arraybuffer
直接进行操作,所以我们需要借助其他对象来操作. 所有就有了TypedArray
(类型数组对象)和DataView
对象。DataView 对象
上面代码生成了一段 8 字节的内存区域,每个字节的值默认都是 0。
为了读写这段内容,需要为它指定视图。
DataView
视图的创建,需要提供ArrayBuffer
对象实例作为参数。DataView
视图是一个可以从二进制ArrayBuffer
对象中读写多种数值类型的底层接口。setint8()
从DataView
起始位置以byte
为计数的指定偏移量(byteOffset
)处存储一个8-bit
数(一个字节)getint8()
从DataView
起始位置以byte
为计数的指定偏移量(byteOffset
)处获取一个8-bit
数(一个字节)调用
示例
TypedArray
另一种
TypedArray
视图,与DataView
视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。TypedArray
对象描述了一个底层的二进制数据缓存区(binary data buffer
)的一个类数组视图(view
)。但它本身不可以被实例化,甚至无法访问,你可以把它理解为接口,它有很多的实现。
实现方法
示例
Blob
Blob
是用来支持文件操作的。简单的说:在JS
中,有两个构造函数File
和Blob
, 而File
继承了所有Blob
的属性。所以在我们看来,
File
对象可以看作一种特殊的Blob
对象。上面说了,
File
对象是一种特殊的Blob
对象,那么它自然就可以直接调用Blob
对象的方法。让我们看一看Blob
具体有哪些方法,以及能够用它们实现哪些功能:是的,我们这里更加倾向于实战中的应用~
关于
Blob
的更具体介绍可以参考Blobatob
和btoa
base64
相信大家都不会陌生吧(不知道的看这里),最常用的操作可能就是图片转base64
了吧?在之前要在字符串跟
base64
之间互转,我们可能需要去网上拷一个别人的方法,而且大部分情况下,你没有时间去验证这个方法是不是真的可靠,有没有bug
。从
IE10+
浏览器开始,所有浏览器就原生提供了Base64
编码解码方法。Base64 解码
Base64 编码
Canvas
中的ImageData
对象关于
Canvas
,这里我就不做过多介绍了,具体可参考canvas 文档今天主要说一下
Canvas
中的ImageData
对象(也是为下面的那个图片裁剪的项目做一些基础知识的铺垫~)ImageData
对象中存储着canvas
对象真实的像素数据,它包含以下几个只读属性:width
:图片宽度,单位是像素height
:图片高度,单位是像素data
:Uint8ClampedArray
类型的一维数组,包含着RGBA
格式的整型数据,范围在 0 至 255 之间(包括 255)。创建一个
ImageData
对象使用
createImageData()
方法去创建一个新的,空白的ImageData
对象。上面代码创建了一个新的具体特定尺寸的
ImageData
对象。所有像素被预设为透明黑。得到场景像素数据
为了获得一个包含画布场景像素数据的
ImageData
对象,你可以用getImageData()
方法:在场景中写入像素数据
你可以用
putImageData()
方法去对场景进行像素数据的写入。toDataURL
将canvas
转为data URI
格式有如下
<canvas>
元素:可以用下面的方式获取一个
data-URL
到这里,二进制相关的基础知识我已经铺垫完了。下面让我们回到文章开头提到的那个产品的“没那么简单”的新需求:图片裁剪上传及预览。
其实,像
图片裁剪上传
这种社区已经有非常成熟的解决方案了,如vue-cropper。这里,我选择手写一个简易的图片裁剪的目的是因为这其中用到了上文提及的大量的二进制知识,可以很好的将理论与实践结合。话不多说,开 Giao!!
需求开发 Giao Giao!
先来看下最终的效果:
这里贴下完成后的代码地址
另外,我用一张图梳理了以上提到的前端二进制模块的关系,这对于下面需求的开发会有很大的帮助:
整个需求分以下四步:
1、获取文件并读取文件。
2、获取裁剪坐标。
3、裁剪图片。
4、读取裁剪后的图片预览并上传。
获取文件并读取文件
首先来看下上面第一步提到的获取文件。对应就是给
input
绑定的handleChange
事件:HTML5
支持从input[type=file]
元素中直接获取文件信息,也可以读取文件内容。这里就需要用到了
FileReader
,这个类是专门用来读取本地文件的。纯文本或者二进制都可以读取,但是本地文件必须是经过用户允许才能读取,也就是说用户要在input[type=file]
中选择了这个文件,你才能读取到它。通过
FileReader
我们可以将图片文件转化成DataURL
,就是以data:image/png;base64
开头的一种URL
,然后可以直接放在image.src
里,这样本地图片就显示出来了。获取裁剪坐标
这里主要是
mousedown
、mousemove
、mouseup
事件的结合使用。mousedown
鼠标按下事件。这里要记录下鼠标按下时的开始坐标,即
startX
与startY
,同时要将标志位startDrag
设为true
,标识鼠标开始移动。mousemove
鼠标移动事件。判断
startDrag
为true
(即鼠标开始移动),然后记录对应移动的距离。mouseup
鼠标弹起事件。这里要记录下最终鼠标的落点坐标,对应就是
lastX
与lastY
。裁剪图片
这个时候我们就需要用到
canvas
了,canvas
和图片一样,所以新建canvas
时就要确定其高宽。将图片放置入
canvas
时需要调用drawImage
:具体
API
使用参考MDN
上的drawImage其中这里面我们还加入了
scale
,这个变量是用来实现图片放大
、缩小
效果的。而且会判断图片的宽、高的大小关系,从而实现图片在
canvas
中对应的适配。读取裁剪后的图片并上传
这时我们要获取
canvas
中图片的信息,用toDataURL
就可以转换成上面用到的DataURL
。然后取出其中
base64
信息,再用window.atob
转换成由二进制字符串。但window.atob
转换后的结果仍然是字符串,直接给Blob
还是会出错。所以又要用Uint8Array
转换一下。这时候裁剪后的文件就储存在
blob
里了,我们可以把它当作是普通文件一样,加入到FormData
里,并上传至服务器了。参考
https://es6.ruanyifeng.com/#docs/arraybuffer
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
❤️ 爱心三连击
1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~
2.关注公众号前端森林,定期为你推送新鲜干货好文。
3.特殊阶段,带好口罩,做好个人防护。
4.添加微信fs1263215592,拉你进技术交流群一起学习 🍻
The text was updated successfully, but these errors were encountered: