Vue圖片裁剪上傳組件

本組件基于vuejs框架, 使用ES6基本語法, css預(yù)編譯采用的scss, 圖片裁剪模塊基于cropperjs,拍照時的圖片信息獲取使用exif, 圖片上傳使用XMLHttpRequest

該組件已單獨部署上線, 線上地址:http://upload-img.sufaith.com/, 圖片最終是傳至我個人的七牛云, 獲取七牛云上傳憑證token的接口是我單獨做的一個nodejs服務(wù), 可在PC或移動端打開測試下效果.

涉及到的知識點整理如下:

vuejs介紹 — Vue.js

scssSass世界上最成熟、穩(wěn)定和強大的CSS擴(kuò)展語言 | Sass中文網(wǎng)

cropperjshttps://github.com/fengyuanchen/cropperjs

exifhttps://github.com/exif-js/exif-js

XMLHttpRequest?XMLHttpRequest()

整體項目分成3個文件:

1. uploadAvator.vue (父組件,用于選擇圖片,接收crop回調(diào),執(zhí)行上傳)

2. crop.vue (裁剪組件, 用于裁剪,壓縮,回調(diào)裁剪結(jié)果給uploadAvator.vue)

3. image.js (封裝了基本的base64轉(zhuǎn)換blob、獲取圖片url、xhr上傳、圖片壓縮等方法)

整體流程如下:

input選擇圖片

調(diào)用cropperjs裁剪

修正方向, 壓縮

上傳

具體實現(xiàn)步驟:

一. 實現(xiàn)input選擇文件

1. 定義一個隱形樣式的輸入框,用于選擇圖片文件 (imgUrl初始化為默認(rèn)圖片地址)

<template>

? <div class="upload-wrapper" :style="{backgroundImage: 'url(' + imgUrl + ')'}">

? ? <input @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>

? </div>

</template>

<style lang="scss" scoped>

.upload-wrapper {

? position: relative;

? width: 77px;

? height: 77px;

? background-size: cover;

? border: 0;

? border-radius: 50%;

? margin: 20px auto;

? .input {

? ? position: absolute;

? ? z-index: 1;

? ? top: 0;

? ? left: 0;

? ? width: 100%;

? ? height: 100%;

? ? opacity: 0;

? ? -webkit-tap-highlight-color: rgba(0,0,0,0);

? }

}

</style>

2. 對input選擇圖片做一些優(yōu)化

(1) 每次點擊input選擇圖片時, 彈出選擇文件的彈窗很慢,有些延遲

解決方案:?明確定義input的accept屬性對應(yīng)的圖片類型

<input type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>

(2) 在ios設(shè)備下input若含有capture屬性, 則只能調(diào)起相冊,而安卓設(shè)備下input若不含capture屬性,則只能調(diào)起相冊

解決方案:?判斷是否為ios設(shè)備, 創(chuàng)建對應(yīng)屬性的input

const UA = navigator.userAgent

const isIpad = /(iPad).*OS\s([\d_]+)/.test(UA)

const isIpod = /(iPod)(.*OS\s([\d_]+))?/.test(UA)

const isIphone = !isIpad && /(iPhone\sOS)\s([\d_]+)/.test(UA)

const isIos = isIpad || isIpod || isIphone

<input v-if="isIos" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>

<!-- 安卓設(shè)備保留capture屬性 -->

<input v-else @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>?

(3) 再次點擊input選擇圖片時, 若選擇的圖片和上一次選擇的圖片相同時,則不會觸發(fā)onchange事件

解決方案:?在每次接收到onchange事件時先銷毀當(dāng)前input, 再重新創(chuàng)建一個input, 此時可利用vue的v-if指令,輕松銷毀或重建

<input v-if="isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>

<!-- 安卓設(shè)備保留capture屬性 -->

<input v-if="!isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>? ?

data() {

? ? return {

? ? ? destroyInput: false, // 是否銷毀input元素, 解決在第二次和第一次選擇的文件相同時不觸發(fā)onchange事件的問題

? ? ? isIos: isIos // 是否為ios設(shè)備

? ? }

? },

二. 調(diào)用cropperjs裁剪

1.?獲取選擇的圖片的url (用于裁剪)

2. 獲取拍照時的Orientation信息,解決拍出來的照片旋轉(zhuǎn)問題

3.顯示裁剪組件并初始化

4. 取消裁剪和開始裁剪

三. 修正方向, 壓縮并將base64回調(diào)給父組件

const image = {}

image.compress = function(img, Orientation) {

? // 圖片壓縮

? // alert('圖片的朝向' + Orientation)

? let canvas = document.createElement('canvas')

? let ctx = canvas.getContext('2d')

? // 瓦片canvas

? let tCanvas = document.createElement('canvas')

? let tctx = tCanvas.getContext('2d')

? let initSize = img.src.length

? let width = img.width

? let height = img.height

? // 如果圖片大于四百萬像素,計算壓縮比并將大小壓至400萬以下

? let ratio

? if ((ratio = width * height / 4000000) > 1) {

? ? console.log('大于400萬像素')

? ? ratio = Math.sqrt(ratio)

? ? width /= ratio

? ? height /= ratio

? } else {

? ? ratio = 1

? }

? canvas.width = width

? canvas.height = height

? // 鋪底色

? ctx.fillStyle = '#fff'

? ctx.fillRect(0, 0, canvas.width, canvas.height)

? // 如果圖片像素大于100萬則使用瓦片繪制

? let count

? if ((count = width * height / 1000000) > 1) {

? ? count = ~~(Math.sqrt(count) + 1) // 計算要分成多少塊瓦片

? ? // 計算每塊瓦片的寬和高

? ? let nw = ~~(width / count)

? ? let nh = ~~(height / count)

? ? tCanvas.width = nw

? ? tCanvas.height = nh

? ? for (let i = 0; i < count; i++) {

? ? ? for (let j = 0; j < count; j++) {

? ? ? ? tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh)

? ? ? ? ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh)

? ? ? }

? ? }

? } else {

? ? ctx.drawImage(img, 0, 0, width, height)

? }

? // 修復(fù)ios上傳圖片的時候 被旋轉(zhuǎn)的問題

? if (Orientation && Orientation !== '' && Orientation !== 1) {

? ? switch (Orientation) {

? ? ? case 6: // 需要順時針(向左)90度旋轉(zhuǎn)

? ? ? ? image.rotateImg(img, 'left', canvas)

? ? ? ? break

? ? ? case 8: // 需要逆時針(向右)90度旋轉(zhuǎn)

? ? ? ? image.rotateImg(img, 'right', canvas)

? ? ? ? break

? ? ? case 3: // 需要180度旋轉(zhuǎn)

? ? ? ? image.rotateImg(img, 'right', canvas) // 轉(zhuǎn)兩次

? ? ? ? image.rotateImg(img, 'right', canvas)

? ? ? ? break

? ? }

? }

? // 設(shè)置jpegs圖片的質(zhì)量

? let ndata = canvas.toDataURL('image/jpeg', 1)

? console.log(`壓縮前:${initSize}`)

? console.log(`壓縮后:${ndata.length}`)

? console.log(`壓縮率:${~~(100 * (initSize - ndata.length) / initSize)}%`)

? tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0

? return ndata

}

image.rotateImg = function(img, direction, canvas) {

? // 圖片旋轉(zhuǎn)

? // 最小與最大旋轉(zhuǎn)方向,圖片旋轉(zhuǎn)4次后回到原方向

? const minStep = 0

? const maxStep = 3

? if (img == null) return

? // img的高度和寬度不能在img元素隱藏后獲取,否則會出錯

? let height = img.height

? let width = img.width

? let step = 2

? if (step == null) {

? ? step = minStep

? }

? if (direction === 'right') {

? ? step++

? ? // 旋轉(zhuǎn)到原位置,即超過最大值

? ? step > maxStep && (step = minStep)

? } else {

? ? step--

? ? step < minStep && (step = maxStep)

? }

? // 旋轉(zhuǎn)角度以弧度值為參數(shù)

? let degree = step * 90 * Math.PI / 180

? let ctx = canvas.getContext('2d')

? switch (step) {

? ? case 0:

? ? ? canvas.width = width

? ? ? canvas.height = height

? ? ? ctx.drawImage(img, 0, 0)

? ? ? break

? ? case 1:

? ? ? canvas.width = height

? ? ? canvas.height = width

? ? ? ctx.rotate(degree)

? ? ? ctx.drawImage(img, 0, -height)

? ? ? break

? ? case 2:

? ? ? canvas.width = width

? ? ? canvas.height = height

? ? ? ctx.rotate(degree)

? ? ? ctx.drawImage(img, -width, -height)

? ? ? break

? ? case 3:

? ? ? canvas.width = height

? ? ? canvas.height = width

? ? ? ctx.rotate(degree)

? ? ? ctx.drawImage(img, -width, 0)

? ? ? break

? }

}

export default image

四. 上傳圖片

1. base64轉(zhuǎn)換為文件

2. XMLHttpRequest上傳

3. 定義上傳狀態(tài)的樣式,包括上傳進(jìn)度和上傳失敗的標(biāo)識

4.父組件接收到裁剪組件的回調(diào)的base64后,執(zhí)行上傳

<template>

<div>

? <div class="upload-wrapper" :class="{'upload-status-bg': showStatusWrapper}" :style="{backgroundImage: 'url(' + imgUrl + ')'}">

? ? <input v-if="isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>

? ? <!-- 安卓設(shè)備保留capture屬性 -->

? ? <input v-if="!isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>

? ? <!-- 上傳狀態(tài) -->

? ? <div v-if="showStatusWrapper" class="upload-status-wrapper">

? ? ? <i class="fail" v-if="showStatusFail">!</i>

? ? ? <i v-else>{{procent}}%</i>

? ? </div>

? </div>

? <crop ref="cropWrapper" v-show="showCrop" @hide="showCrop=false" @finish="setUpload"></crop>

</div>

</template>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容