温馨提示:这里除了一些幼稚的小组件啥也没有写在前面距离写完上一篇实践是检验程序员的唯一标准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,兼容IEdataURLtoBlob(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>写在后面第一次写相对独立的组件从有想法到完全实现成一个能用的组件,中间还是有很多路的,而且功能还简单的令人发质,怎么说呢感觉自己可以进步的空间还很大啊不过令人欣慰的是这个组件已经用在单位的一个项目中了,可喜可贺虽然拖了很久,不过还是有成就感的,希望能继续下去,谁知道能走到哪呢欢迎大家挑错提意见,虽然不情愿,但是接受能看到这的,功能应该都实现了把?!