本組件基于vuejs框架, 使用ES6基本語法, css預編譯采用的scss, 圖片裁剪模塊基于cropperjs,拍照時的圖片信息獲取使用exif, 圖片上傳使用XMLHttpRequest
該組件已單獨部署上線, 線上地址:http://upload-img.sufaith.com/, 圖片最終是傳至我個人的七牛云, 獲取七牛云上傳憑證token的接口是我單獨做的一個nodejs服務, 可在PC或移動端打開測試下效果.
涉及到的知識點整理如下:
vuejs介紹 — Vue.js
scssSass世界上最成熟、穩定和強大的CSS擴展語言 | Sass中文網
cropperjshttps://github.com/fengyuanchen/cropperjs
exifhttps://github.com/exif-js/exif-js
XMLHttpRequest?XMLHttpRequest()
整體項目分成3個文件:
1. uploadAvator.vue (父組件,用于選擇圖片,接收crop回調,執行上傳)
2. crop.vue (裁剪組件, 用于裁剪,壓縮,回調裁剪結果給uploadAvator.vue)
3. image.js (封裝了基本的base64轉換blob、獲取圖片url、xhr上傳、圖片壓縮等方法)
整體流程如下:
input選擇圖片
調用cropperjs裁剪
修正方向, 壓縮
上傳
具體實現步驟:
一. 實現input選擇文件
1. 定義一個隱形樣式的輸入框,用于選擇圖片文件 (imgUrl初始化為默認圖片地址)
<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選擇圖片做一些優化
(1) 每次點擊input選擇圖片時, 彈出選擇文件的彈窗很慢,有些延遲
解決方案:?明確定義input的accept屬性對應的圖片類型
<input type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>
(2) 在ios設備下input若含有capture屬性, 則只能調起相冊,而安卓設備下input若不含capture屬性,則只能調起相冊
解決方案:?判斷是否為ios設備, 創建對應屬性的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=""/>
<!-- 安卓設備保留capture屬性 -->
<input v-else @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>?
(3) 再次點擊input選擇圖片時, 若選擇的圖片和上一次選擇的圖片相同時,則不會觸發onchange事件
解決方案:?在每次接收到onchange事件時先銷毀當前input, 再重新創建一個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=""/>
<!-- 安卓設備保留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元素, 解決在第二次和第一次選擇的文件相同時不觸發onchange事件的問題
? ? ? isIos: isIos // 是否為ios設備
? ? }
? },
二. 調用cropperjs裁剪
1.?獲取選擇的圖片的url (用于裁剪)
2. 獲取拍照時的Orientation信息,解決拍出來的照片旋轉問題
3.顯示裁剪組件并初始化
4. 取消裁剪和開始裁剪
三. 修正方向, 壓縮并將base64回調給父組件
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)
? }
? // 修復ios上傳圖片的時候 被旋轉的問題
? if (Orientation && Orientation !== '' && Orientation !== 1) {
? ? switch (Orientation) {
? ? ? case 6: // 需要順時針(向左)90度旋轉
? ? ? ? image.rotateImg(img, 'left', canvas)
? ? ? ? break
? ? ? case 8: // 需要逆時針(向右)90度旋轉
? ? ? ? image.rotateImg(img, 'right', canvas)
? ? ? ? break
? ? ? case 3: // 需要180度旋轉
? ? ? ? image.rotateImg(img, 'right', canvas) // 轉兩次
? ? ? ? image.rotateImg(img, 'right', canvas)
? ? ? ? break
? ? }
? }
? // 設置jpegs圖片的質量
? 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) {
? // 圖片旋轉
? // 最小與最大旋轉方向,圖片旋轉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++
? ? // 旋轉到原位置,即超過最大值
? ? step > maxStep && (step = minStep)
? } else {
? ? step--
? ? step < minStep && (step = maxStep)
? }
? // 旋轉角度以弧度值為參數
? 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轉換為文件
2. XMLHttpRequest上傳
3. 定義上傳狀態的樣式,包括上傳進度和上傳失敗的標識
4.父組件接收到裁剪組件的回調的base64后,執行上傳
<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=""/>
? ? <!-- 安卓設備保留capture屬性 -->
? ? <input v-if="!isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>
? ? <!-- 上傳狀態 -->
? ? <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>