20

温馨提示:这里除了一些幼稚的小组件啥也没有

写在前面

距离写完上一篇实践是检验程序员的唯一标准01:用户不想跟你说话并向你扔出一张图片 - 图片上传组件开发【思路篇】过去了大半年,才开始写开发篇真的是令人悲哀,不过有句话说的好,开始做一件事最好的时间是大半年前,其次是现在
上一篇偏设计和尝试技术能否实现,这一篇会在工程层面实现,并且保证他能被(轻易)引用!
上一篇文章的评论里好多同学(差不多3个人)希望我传到git上。好吧,本文最终的劳动成果会放上去的,不过那是下一篇文章干的事了,不过这里我已经把全部源码贴上来了- -

上传到了github上,觉得好的给星哦!l-imgupload //181119

功能完善

在之前那篇文章中,又习惯性的做了很多无用的设计,你就是一个上传图片的组件,低调点谢谢,所以最终我搞成了这样子

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,//组件起点纵坐标
        /*
        181031改:其实并不用这两个变量
        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方法完成了以下几个步骤

  1. 新建一个FileReader对象用来读取选中的图片文件
  2. 将原有的被选中的dataURL变量清空
  3. 将读取的图片文件转为dataURL格式
  4. 将dataURL赋给一个创建的image对象
  5. 计算image对象的长宽比决定图片渲染方式
  6. 获取canvas起点坐标
  7. 将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;
        }

        /*
        181031改:其实并不用这两个变量,直接用offset属性即可
        //获取组件起点坐标
        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方法通过以下步骤实现了选定区域的展现和截取功能:

  1. 取得实时鼠标坐标作为截取区域的终点
  2. 在被选择区域外绘制半透明蒙版
  3. 获取将所选区域图像对应imageData数据
  4. 利用新建的canvas对象将imageData转为dataURL
//选择截取范围,当鼠标被拖动时触发
drawArea(e) {
     //当鼠标被拖动时触发(处于按下状态且移动)
    if(this.mouseDownFlag) {

        /*181031改:结束坐标的获取方式进行了优化,请忽略此处
        //在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);
        }
        */
        
        //在canvas标签上范围的终点横坐标
        this.resultX = e.offsetX;
        //在canvas标签上范围的终点纵坐标,根据比例参数决定
        if(this.cuttingRatio != 0) {
            //根据一定比例截取
            this.resultY = this.startY + parseInt((1 / this.cuttingRatio) * (this.resultX - this.startX))
        } else {
            //自由截取
            this.resultY = e.offsetX;
        }
        //所选区域外阴影部分
        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>

写在后面

第一次写相对独立的组件
从有想法到完全实现成一个能用的组件,中间还是有很多路的,而且功能还简单的令人发质,怎么说呢感觉自己可以进步的空间还很大啊
不过令人欣慰的是这个组件已经用在单位的一个项目中了,可喜可贺
虽然拖了很久,不过还是有成就感的,希望能继续下去,谁知道能走到哪呢

欢迎大家挑错提意见,虽然不情愿,但是接受

能看到这的,功能应该都实现了把?!

181031修改

之前应该是脑子抽了,其实在drawArea方法中,e.offsetX和e.offsetY就是

parseInt(e.clientX - this.resetX)
parseInt(e.clientY - this.resetY)

181119修改

增加了选框拖拽功能,并且上传到了github上,觉得好的给星哦!l-imgupload


lskrat
478 声望43 粉丝

能不能借我个谱靠靠