實踐是檢驗程序員的唯一標準02:用戶不想跟你說話並向你扔出一張圖片 - 圖片上傳組件開發【開發篇】

溫馨提示:這裏除了一些幼稚的小組件啥也沒有

寫在前面

距離寫完上一篇實踐是檢驗程序員的唯一標準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);
        border-radius: 2px;
        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方法完成了以下幾個步驟

  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;
        }
        //獲取組件起點座標
        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) {
        //在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>

寫在後面

第一次寫相對獨立的組件
從有想法到完全實現成一個能用的組件,中間還是有很多路的,而且功能還簡單的令人髮質,怎麼說呢感覺自己可以進步的空間還很大啊
不過令人欣慰的是這個組件已經用在單位的一個項目中了,可喜可賀
雖然拖了很久,不過還是有成就感的,希望能繼續下去,誰知道能走到哪呢

歡迎大家挑錯提意見,雖然不情願,但是接受

能看到這的,功能應該都實現了把?!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章