最新更新时间:2019年10月31日15:33:32
《猛戳-查看我的博客地图-总有你意想不到的惊喜》
本文内容:在移动端实现图片拍摄、压缩、预览、裁剪、上传的五大功能,看起来是一套很复杂的业务逻辑组合,实际上每个模块可以单独开发,细分并拆分业务模块是常见复杂业务形态开发的基本方案。
概述
在移动端做开发永远越不过的两个障碍或技术瓶颈,兼容性和性能。
兼容性,某些HTML元素的默认样式在不同浏览器下显示效果不一;CSS样式的兼容性,移动端常见的是同一样式在不同OS和不同机型下显示效果不一;原生事件交互的兼容性,比如拍照和键盘输入场景下,Android和iOS系统表现的形式不一;性能,主要表现在硬件设备系统内存容量的受限,比如视频播放、图片连续拍摄都是高功耗的应用场景,处理不当容易造成内存泄漏导致浏览器crash。
本文中的技术方案,瓶颈在于连续拍摄照片有数量限制,实测过程中iPhone X等高性能手机连续拍摄几十张照片的时候,容易导致浏览器crash,这个问题经过长期探索和研究,手动实现了实时垃圾回收,以及图片压缩比例调整和压缩时机控制,性能有所提高和改善,从二十张左右的数量提升到了四十张左右的数量,但部分机型无限连续拍摄图片出现崩溃的场景终究没有解决方案,究其本质原因,受限于手机系统、可用内存容量等硬件。
本方案,对于大部分机型连续拍摄照片,并实施压缩预览裁剪上传功能,无数量限制。最终向服务器上传的是base64数据格式的图片数据。
技术方案的实现
DOM布局如下:
import React
from 'react'
import Cropper
from 'react-cropper';
import 'cropperjs/dist/cropper.css';
let styles
= .contianer
{
.cropModal
{
position
: absolute
;
width
: 100%;
height
: 100%;
top
: 0;
left
: 0;
background
: #
000000;
display
: flex
;
flex
-direction
: column
;
justify
-content
: center
;
align
-items
: center
;
z
-index
: 3;
.crop
{
}
.btn
{
display
: flex
;
flex
-direction
: row
;
justify
-content
: space
-between
;
position
: absolute
;
bottom
: 0;
left
: 50%;
transform
: translateX(-50%);
width
: 285px
;
height
: 60px
;
.cropperBtn
{
width
: 60px
;
height
: 60px
;
line
-height
: 30px
;
color
: #
FFFFFF;
font
-size
: 14px
;
text
-align
: center
;
img
{
width
: 23px
;
height
: 22px
;
vertical
-align
: top
;
position
: relative
;
top
: 50%;
transform
: translateY(-50%);
}
}
}
.cropTips
{
position
: absolute
;
top
: 22px
;
font
-size
: 11px
;
line
-height
: 15px
;
color
: #
B8B8B8;
padding
: 0 26px
;
letter
-spacing
: 0.5px
;
}
}
}
export default class TakePhoto extends React.Component {
constructor(props
) {
super(props
);
this.state
= {
displayLoading
: false,
cropperData
:'',
showCropModal
: false
};
this.fReader
= new FileReader();
this.closureTime
= 0;
}
render() {
return <div id
='testPage' className
={styles
.contianer
}>
{}
{
this.state
.showCropModal
? <div className
={styles
.cropModal
} id
='cropModal'>
<Cropper
className
={styles
.crop
}
ref
='cropper'
src
={this.state
.cropperData
}
style
={{maxHeight
: '78%', width
: '100%'}}
viewMode
={2}
dragMode
='none'
minCanvasWidth
={285}
background
={false}
guides
={true}
center
={false}
rotatable
={true}
scalable
={true}
/>
<div className
={styles
.btn
}>
<div className
={styles
.cropperBtn
} onClick
={this.cancelCrop
}>取消
</div
>
<div className
={styles
.cropperBtn
} onClick
={this.confirmCrop
}>确认
</div
>
<div className
={styles
.cropperBtn
} onClick
={this.rotateCrop
}>旋转
</div
>
</div
>
</div
> : null
}
{this.state
.displayLoading
? <Loading
></Loading
> : null}
<input
type
="file"
onChange
={(e
)=>{this.onChange(e
)}}
className
={styles
.getImg
}
title
={this.state
.title
}
id
="fileinput"
ref
='onChange'
accept
="image/*"
/>
</div
>
}
}
input元素onChange事件调起相机和相册的功能代码如下:
onChange(e
){
let _this
= this;
this.openLoading()
let file
= e
.currentTarget
.files
[0];
if(file
== undefined
){
return
}
this.fReader
= new FileReader();
let tempTimer
= setTimeout(function(){
_this
.fReader
.readAsDataURL(file
);
_this
.fReader
.onload=function(e
) {
this.zip(this.result
);
}
file
= null;
tempTimer
= null;
},500)
}
openLoading(){
this.setState({
displayLoading
: true
})
}
图片压缩
zip(base64
){
let img
= new Image();
let canvas
= document
.createElement("canvas");
let ctx
= canvas
.getContext("2d");
let compressionRatio
= 0.5
let orientation
= this.getOrientation(this.base64ToArrayBuffer(base64
));
img
.src
= base64
img
.onload = function () {
let width
= img
.width
, height
= img
.height
;
if(orientation
== 3){
canvas
.width
= width
;
canvas
.height
= height
;
ctx
.rotate(Math
.PI)
ctx
.drawImage(img
, -width
, -height
, width
, height
)
}else if(orientation
== 6){
canvas
.width
= height
;
canvas
.height
= width
;
ctx
.rotate(Math
.PI / 2)
ctx
.drawImage(img
, 0, -height
, width
, height
)
}else if(orientation
== 8){
canvas
.width
= height
;
canvas
.height
= width
;
ctx
.rotate(-Math
.PI / 2)
ctx
.drawImage(img
, -width
, 0, width
, height
)
}else{
canvas
.width
= width
;
canvas
.height
= height
;
ctx
.drawImage(img
, 0, 0, width
, height
);
}
this.setCropperDate(canvas
.toDataURL('image/jpeg', compressionRatio
));
};
}
setCropperDate = (imgDataBase64
) => {
let _this
= this;
this.state
.cropperData
= imgDataBase64
;
let tempTimer
= setTimeout(function(){
_this
.setState({
displayLoading
: false,
showCropModal
: true
})
clearTimeout(tempTimer
)
},300)
}
获取图片的旋转角度
base64ToArrayBuffer(base64
) {
base64
= base64
.replace(/^data\:([^\;]+)\;base64,/gmi, '');
var binary
= atob(base64
);
var len
= binary
.length
;
var buffer
= new ArrayBuffer(len
);
var view
= new Uint8Array(buffer
);
for (var i
= 0; i
< len
; i
++) {
view
[i
] = binary
.charCodeAt(i
);
}
return buffer
;
}
getOrientation(arrayBuffer
) {
var dataView
= new DataView(arrayBuffer
);
var length
= dataView
.byteLength
;
var orientation
;
var exifIDCode
;
var tiffOffset
;
var firstIFDOffset
;
var littleEndian
;
var endianness
;
var app1Start
;
var ifdStart
;
var offset
;
var i
;
if (dataView
.getUint8(0) === 0xFF && dataView
.getUint8(1) === 0xD8) {
offset
= 2;
while (offset
< length
) {
if (dataView
.getUint8(offset
) === 0xFF && dataView
.getUint8(offset
+ 1) === 0xE1)
app1Start
= offset
;
break;
}
offset
++;
}
}
if (app1Start
) {
exifIDCode
= app1Start
+ 4;
tiffOffset
= app1Start
+ 10;
if (getStringFromCharCode(dataView
, exifIDCode
, 4) === 'Exif') {
endianness
= dataView
.getUint16(tiffOffset
);
littleEndian
= endianness
=== 0x4949;
if (littleEndian
|| endianness
=== 0x4D4D ) {
if (dataView
.getUint16(tiffOffset
+ 2, littleEndian
) === 0x002A) {
firstIFDOffset
= dataView
.getUint32(tiffOffset
+ 4, littleEndian
);
if (firstIFDOffset
>= 0x00000008) {
ifdStart
= tiffOffset
+ firstIFDOffset
;
}
}
}
}
}
if (ifdStart
) {
length
= dataView
.getUint16(ifdStart
, littleEndian
);
for (i
= 0; i
< length
; i
++) {
offset
= ifdStart
+ i
* 12 + 2;
if (dataView
.getUint16(offset
, littleEndian
) === 0x0112 ) {
offset
+= 8;
orientation
= dataView
.getUint16(offset
, littleEndian
);
if (true) {
dataView
.setUint16(offset
, 1, littleEndian
);
}
break;
}
}
}
return orientation
;
}
getStringFromCharCode(dataView
, start
, length
) {
var str
= '';
var i
;
for (i
= start
, length
+= start
; i
< length
; i
++) {
str
+= String
.fromCharCode(dataView
.getUint8(i
));
}
return str
;
}
Cropper组件的取消、裁剪、旋转的三个方法:
rotateCrop(){
this.refs
.cropper
.rotate(-90);
}
confirmCrop(){
let _this
= this;
if(Date
.now() - this.closureTime
< 2000){
return
}
this.closureTime
= Date
.now()
document
.getElementById('cropModal').style
.visibility
= 'hidden';
this.setState({
displayLoading
: true,
})
let tempTimer
= setTimeout(function(){
let compressionRatio
= 0.5;
let cropperData
= _this
.refs
.cropper
.getCroppedCanvas().toDataURL("image/jpeg", compressionRatio
)
while(cropperData
.length
/1024 > 500 && compressionRatio
> 0.1){
compressionRatio
-= 0.1;
cropperData
= _this
.refs
.cropper
.getCroppedCanvas().toDataURL("image/jpeg", compressionRatio
)
}
_this
.state
.cropperData
= null;
_this
.refs
.cropper
.clear();
_this
.upload(cropperData
);
cropperData
= null;
_this
.setState({showCropModal
: false})
clearTimeout(tempTimer
)
},300)
}
cancelCrop(){
this.state
.cropperData
= null;
this.refs
.cropper
.clear()
this.setState({
showCropModal
: false
})
}
参考资料
无
感谢阅读,欢迎评论^-^
打赏我吧^-^