本元件基於vuejs框架, 使用ES6基本語法, css預編譯採用的scss, 圖片裁剪模組基於cropperjs,拍照時的圖片資訊獲取使用exif, 圖片上傳使用XMLHttpRequest
該元件已單獨部署上線, 線上地址: upload-img.sufaith.com/, 圖片最終是傳至我個人的七牛雲, 獲取七牛雲上傳憑證token的介面是我單獨做的一個nodejs服務, 可在PC或移動端開啟測試下效果.
涉及到的知識點整理如下:
vuejs 介紹 — Vue.js
scss Sass世界上最成熟、穩定和強大的CSS擴充套件語言 | Sass中文網
cropperjs github.com/fengyuanche…
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>
複製程式碼