温馨提示:这里除了一些幼稚的小组件啥也没有
写在前面
距离写完上一篇实践是检验程序员的唯一标准 01:用户不想跟你说话并向你扔出一张图片 – 图片上传组件开发【思路篇】过去了大半年,才开始写开发篇真的是令人悲哀,不过有句话说的好,开始做一件事最好的时间是大半年前,其次是现在上一篇偏设计和尝试技术能否实现,这一篇会在工程层面实现,并且保证他能被(轻易)引用!上一篇文章的评论里好多同学(差不多 3 个人)希望我传到 git 上。好吧,本文最终的劳动成果会放上去的,不过那是下一篇文章干的事了,不过这里我已经把全部源码贴上来了 - –
功能完善
在之前那篇文章中,又习惯性的做了很多无用的设计,你就是一个上传图片的组件,低调点谢谢,所以最终我搞成了这样子
state-1: 初始状态
state-2: 完成载入状态
state-3: 图片截取
总体来说,把能剩的按钮都省了,本体就是个框,适合放在任何地方,此外为了防止破坏页面的整体性,组件不再自带截图预览功能,而是通过事件的方式将所截取的图像的 DataURL 实时穿给父组件,方便父组件自由使用(图中的展示区就是在父组件中写的)
组件设计
在一开始设计组件的时候简直就是父母给孩子报课外班的心情,希望能尽可能的满足各种需求,但转头想想先把最基本的功能(做个好人)做好别的都是可以慢慢加上的(懒)
要保证基本功能能(好)用,大概以下这几点:1. 要让其大小可控,方便应用于不同场景,所以组件的宽高有必要成为参数 2. 对于被裁出的部分,在原图中看和拎出来单独看视觉上差别还挺大的,所以一个可以实时单独展现所截取内容的功能就挺重要的 3. 在大多数情况下,裁剪区域的选定可能是有固定比例的,所以要将是否限制比例以及按照什么比例作为参数,根据适用场景决定
所以组件的参数和事件大概也就这么几个了
参数名:inputWidth 说明:组件宽度类型:Number 默认值:200px
参数名:inputHeight 说明:组件高度类型:Number 默认值:200px
参数名:cuttingRatio 说明:裁剪比例,限定比例为宽 / 高,为空时没有比例限制类型:Number 默认值:0
事件名:getImageData 说明:框选完成后鼠标抬起时触发,返回选定区域的图像数据参数:blobData 参数格式:Blob 对象
事件名:getImageDataURL 说明:鼠标拖动的每一帧触发,返回选定区域的图像数据,可用于预览区域展示参数:dataURL 参数格式:dataURL
代码实现
HTML 框架搭建
由于功能很单一,HTML 的布局也就很简单大概结构如下
< 根标签 >
< 提示信息 />// 绝对定位,位于组件下方,初始状态不可见,载入图片后出现
< 重新选择按钮 />// 绝对定位,位于组件右上角,初始状态不可见,载入图片后出现
< 初始及载入层 />// 绝对定位,位于画布上方,大小与画布完全相同
< 画布 />//canvas
< 隐藏的 input 标签 />// 不可见
</ 根标签 >
HTML 代码如下
<template>
<div class=”inputArea” :style=”{height:inputHeight+’px’,width:inputWidth+’px’}”>
<!– 提示区域 –>
<div class=”notice” :class=”{showNotice:noticeFlag}”>
{{notice}}
<div class=”close-notice” @click=”closeNotice”>X</div>
</div>
<!– 重新选择按钮 –>
<div class=”reloadBtn” @click=”openWindow”>
重新选择
</div>
<!– 初始及载入层 –>
<div class=”blankMask” @click=”openWindow” v-if=”loadFlag!=2″>
<img v-if=”loadFlag==0″ src=”../assets/img.png” />
<img v-if=”loadFlag==1″ src=”../assets/loading.png” />
<div class=”text”>{{loadFlag == 0?’ 点击浏览图片 ’:’ 加载中 ’}}</div>
</div>
<!– 画布 –>
<div class=”canvasArea”>
<canvas id=”inputAreaCanvas” @mousedown=”setStartPoint” @mousemove=”drawArea” @mouseup=”reset”>
</canvas>
</div>
<!– 隐藏的 input 标签 –>
<input id=”input” type=”file” @change=”loadImg” />
</div>
</template>
对应的 css 如下
<style>
.inputArea {
position: relative;
background: #000;
}
.inputArea .notice {
height: 30px;
line-height: 30px;
text-align: center;
background: #FFF;
color: #2C3E50;
font-size: 12px;
text-align: center;
position: absolute;
width: 90%;
margin-left: 5%;
left: 0px;
transition: all .5s;
bottom: -30px;
opacity: 0;
box-shadow: 0px 0px 5px rgba(0,0,0,0.3);
border-radius: 2px;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.inputArea .notice.showNotice {
bottom: 0px;
opacity: 1;
}
.inputArea .notice .close-notice {
position: absolute;
right: 10px;
top: 0px;
height: 30px;
line-height: 30px;
cursor: pointer;
}
.inputArea .reloadBtn {
height: 20px;
padding: 2px 5px 2px 5px;
text-align: center;
line-height: 20px;
font-size: 12px;
background: #FFFFFF;
box-shadow: 0px 0px 5px rgba(0,0,0,0.3);
color: #2C3E50;
position: absolute;
top: 5px;
right: 5px;
cursor: pointer;
transition: all 0.5s;
}
.inputArea .reloadBtn:hover {
box-shadow: 0px 0px 8px rgba(0,0,0,0.5);
}
.inputArea .blankMask {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: flex;
color: gainsboro;
border-radius: 2px;
background: #FFF;
cursor: pointer;
flex-direction: column;
-ms-flex-direction: column;
justify-content: center;
-webkit-justify-content: center;
align-items: center;
-webkit-align-items: center;
transition: all 0.5s;
z-index: 2;
}
.inputArea .blankMask:hover {
background: #F6F6F6;
}
.inputArea .blankMask .text {
margin-top: 10px;
font-size: 16px;
font-weight: bold;
}
.inputArea .blankMask img {
height: 40px;
width: 40px;
}
.inputArea .canvasArea {
display: flex;
align-items: center;
-webkit-align-items: center;
justify-content: center;
-webkit-justify-content: center;
height: 100%;
width: 100%;
}
#input {
display: none;
}
</style>
参数及变量定义以及对象初始化
props:{
inputWidth:{
type:Number,
default:200
},
inputHeight:{
type:Number,
default:200
},
cuttingRatio:{
type:Number,
default:0
}
},
data() {
return {
mouseDownFlag: false,// 记录鼠标点击状态用标记
loadFlag: 0,// 记录图像家在状态用标记
resultImgData: {},// 被截取数据
input: {},// 输入框对象
imgObj: new Image(),// 图片对象
inputAreaCanvas: {},// 主体 canvas 对象
inputArea2D: {},// 主体 CanvasRenderingContext2D 对象
notice: “ 拖拽鼠标框选所需要的区域 ”,// 提示区域文本
noticeFlag: false,// 提示区域展示状态标记
dataURL:””,// 被截取 dataURL
tempCanvas:{},// 存放截取结果用 canvas 对象
tempCanvas2D:{},// 存放截取结果用 CanvasRenderingContext2D 对象
resetX:0,// 组件起点横坐标
resetY:0,// 组件起点纵坐标
startX:0,// 截取开始点横坐标
startY:0,// 截取开始点纵坐标
resultX:0,// 截取结束点横坐标
resultY:0,// 截取结束点纵坐标
}
},
mounted: function() {
// 对象初始化
this.input = document.getElementById(‘input’)
this.inputAreaCanvas = document.getElementById(“inputAreaCanvas”);
this.inputArea2D = this.inputAreaCanvas.getContext(“2d”);
this.tempCanvas = document.createElement(‘canvas’);
this.tempCanvas2D = this.tempCanvas.getContext(‘2d’);
},
图片的读取
此部分开始放在 methods 对象下
图片读取的功能主要设计两个方法:
openWindow 方法主要用于触发隐藏的 <input> 标签的文件读取功能
// 打开文件选择窗口
openWindow() {
this.input.click();
},
loadImg 方法完成了以下几个步骤
新建一个 FileReader 对象用来读取选中的图片文件
将原有的被选中的 dataURL 变量清空
将读取的图片文件转为 dataURL 格式
将 dataURL 赋给一个创建的 image 对象
计算 image 对象的长宽比决定图片渲染方式
获取 canvas 起点坐标
将 image 对象中的图像数据赋给 canvas
// 载入图片方法,当图片被选中后,input 的 value 发生改变时触发
loadImg() {
let vm = this;
let reader = new FileReader();
// 每次载入后传给父组件的 dataURL 清空
this.dataURL = ”;
// 文件为空时返回
if(this.input.files[0] == null) {
return
}
// 开始载入图片,并将数据通过 dataURL 的方式读取,展现载入层信息
this.loadFlag = 1;
reader.readAsDataURL(this.input.files[0]);
// 读取完成后将图像的 dataURL 数据赋给 image 对象的 src 的属性,使其加载图像
reader.onload = function(e) {
vm.imgObj.src = e.target.result;
}
// 图像加载完成,利用 drawImage 将 image 对象渲染至 canvas
this.imgObj.onload = function() {
vm.loadFlag = 2;
vm.noticeFlag = true;
// 计算载入图像的长宽比,决定图片显示方式
let ratioHW = (vm.imgObj.height/vm.imgObj.width)
// 每张图片根据比例不同,总有一个方向占满显示区域
if(ratioHW > 1) {
vm.inputAreaCanvas.height = vm.inputHeight;
vm.inputAreaCanvas.width = vm.inputHeight / ratioHW;
} else {
vm.inputAreaCanvas.width = vm.inputWidth;
vm.inputAreaCanvas.height = vm.inputWidth * ratioHW;
}
// 获取组件起点坐标
vm.resetX = vm.inputAreaCanvas.getBoundingClientRect().left;
vm.resetY = vm.inputAreaCanvas.getBoundingClientRect().top;
// 将获取的图像数据选在至 canvas
vm.inputArea2D.clearRect(0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height);
vm.inputArea2D.drawImage(vm.imgObj, 0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height);
vm.inputArea2D.fillStyle = ‘rgba(0,0,0,0.5)’; // 设定为半透明的黑色
vm.inputArea2D.fillRect(0, 0, vm.inputWidth, vm.inputHeight); // 矩形 A
}
},
图像的截取
图像截取功能包含四个方法:
setStartPoint 方法用于获取截取范围的起点以及更改点击状态
// 获取截取范围起始坐标,当鼠标在 canvas 标签上点击时触发
setStartPoint(e) {
this.mouseDownFlag = true; // 改变标记状态,置为点击状态
this.startX = e.offsetX // 获得起始点横坐标
this.startY = e.offsetY // 获得起始点纵坐标
},
drawArea 方法通过以下步骤实现了选定区域的展现和截取功能:
取得实时鼠标坐标作为截取区域的终点
在被选择区域外绘制半透明蒙版
获取将所选区域图像对应 imageData 数据
利用新建的 canvas 对象将 imageData 转为 dataURL
// 选择截取范围,当鼠标被拖动时触发
drawArea(e) {
// 当鼠标被拖动时触发(处于按下状态且移动)
if(this.mouseDownFlag) {
// 在 canvas 标签上范围的终点横坐标
this.resultX = parseInt(e.clientX – this.resetX);
// 在 canvas 标签上范围的终点纵坐标,根据比例参数决定
if(this.cuttingRatio != 0) {
// 根据一定比例截取
this.resultY = this.startY + parseInt((1 / this.cuttingRatio) * (this.resultX – this.startX))
} else {
// 自由截取
this.resultY = parseInt(e.clientY – this.resetY);
}
// 所选区域外阴影部分
this.inputArea2D.clearRect(0, 0, this.inputWidth, this.inputHeight); // 清空整个画面
this.inputArea2D.drawImage(this.imgObj, 0, 0, this.inputAreaCanvas.width, this.inputAreaCanvas.height); // 重新绘制图片
this.inputArea2D.fillStyle = ‘rgba(0,0,0,0.5)’; // 设定为半透明的白色
this.inputArea2D.fillRect(0, 0, this.resultX, this.startY); // 矩形 A
this.inputArea2D.fillRect(this.resultX, 0, this.inputWidth, this.resultY); // 矩形 B
this.inputArea2D.fillRect(this.startX, this.resultY, this.inputWidth – this.startX, this.inputHeight – this.resultY); // 矩形 C
this.inputArea2D.fillRect(0, this.startY, this.startX, this.inputHeight – this.startY); // 矩形 D
// 当选择区域大于 0 时,将所选范围内的图像数据实时返回
if(this.resultX – this.startX > 0 && this.resultY – this.startY > 0) {
this.resultImgData = this.inputArea2D.getImageData(this.startX, this.startY, this.resultX – this.startX, this.resultY – this.startY);
//canvas to DataURL
this.tempCanvas.width = this.resultImgData.width;
this.tempCanvas.height = this.resultImgData.height;
this.tempCanvas2D.putImageData(this.resultImgData, 0, 0)
this.dataURL = this.tempCanvas.toDataURL(‘image/jpeg’, 1.0);
}
}
},
reset 方法用于重制鼠标点击状态,并获取 blob 格式的所截图像数据,触发 getImageData 事件将数据专递给父组件
// 结束选择截取范围,返回所选范围的数据,重制鼠标状态,当鼠标点击结束时触发
reset() {
this.mouseDownFlag = false; // 将标志置为已抬起状态
let blob = this.dataURLtoBlob(this.dataURL)
this.$emit(‘getImageData’, blob);
},
dataURLtoBlob 方法的作用是将 dataURL 对象转化为 Blob 对象,来自 Blob/DataURL/canvas/image 的相互转换 -Lorem 由于在 IE 中并不支持 Canvas.toBlob,所以需要这里走个弯路,自己写一下这个方法
//DataURL to Blob,兼容 IE
dataURLtoBlob(dataurl) {
let arr = dataurl.split(‘,’)
let mime = arr[0].match(/:(.*?);/)[1]
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n)
while(n–) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], {
type: mime
});
}
其他方法
// 关闭提示信息
closeNotice() {
this.noticeFlag = false
},
通过监听 dataURL 的变化,将结果实时返回给父组件以达到预览的目的
watch:{
dataURL:function(newVal,oldVal){
this.$emit(‘getImageDataUrl’, this.dataURL)// 将所截图的 dataURL 返回给父组件,共预览使用
}
},
应用方式
用起来嘛,就很简单了
html
<template>
<div id=”app”>
<MainBlock
@getImageData=”getImageData”
@getImageDataUrl=”getImageDataUrl”
:inputHeight=’300′
:inputWidth=’300′
></MainBlock>
<img :src=”src”/>
</div>
</template>
javascript
<script>
import MainBlock from ‘./components/mainBlock’
export default {
name: ‘App’,
components: {
MainBlock,
},
data() {
return {
imageData: ”,
src: “”
}
},
methods: {
getImageData(imageData) {
this.imageData = imageData
console.log(this.imageData)
},
getImageDataUrl(dataUrl) {
this.src = dataUrl
}
},
}
</script>
写在后面
第一次写相对独立的组件从有想法到完全实现成一个能用的组件,中间还是有很多路的,而且功能还简单的令人发质,怎么说呢感觉自己可以进步的空间还很大啊不过令人欣慰的是这个组件已经用在单位的一个项目中了,可喜可贺虽然拖了很久,不过还是有成就感的,希望能继续下去,谁知道能走到哪呢
欢迎大家挑错提意见,虽然不情愿,但是接受
能看到这的,功能应该都实现了把?!