實(shí)現(xiàn)一個(gè)Vue版Upload組件
前言
之前對(duì)一些主流手機(jī)拍出的照片大小做過對(duì)比,華為P30拍出的照片3M左右,同事的小米9不知開啟了什么模式拍出了10M以上的照片。照片太大了對(duì)服務(wù)端上傳文件造成了不小的壓力,對(duì)此,后端對(duì)前端提出了圖片上傳前對(duì)圖片進(jìn)行壓縮。我們目前所用的UI庫Upload組件并不支持對(duì)上傳的圖片進(jìn)行壓縮,所以花了一點(diǎn)時(shí)間自己寫了上傳的組件。
今天分享我的第N個(gè)Vue組件,Upload
1.組件設(shè)計(jì)
- 只有圖片裁進(jìn)行壓縮,文件沒法壓縮。
- 超過規(guī)定大小的圖片裁進(jìn)行壓縮,不應(yīng)該是個(gè)圖片就壓縮,內(nèi)存過下的圖片沒必要壓縮。
- 自行定義圖片壓縮的的寬度,高度按比例自動(dòng)適配。
Upload組件的禁用,該有的基本功能。- 文件上傳進(jìn)度。
- 可接管文件上傳。
- 等等圍繞以上幾個(gè)點(diǎn)開始擴(kuò)展。
組件實(shí)現(xiàn)
1.mixins
export default {
props: {
icon: { //上傳組件的占位圖
type: String,
default: "iconcamera"
},
size: { //圖片超過指定大小不讓上傳
type: Number,
default: 3072
},
disabled: { //禁止上傳
type: Boolean
},
iconSize: { //占位icon的大小
type: Number,
default: 24
},
name: { //input的原生屬性
type: String,
default: 'file'
},
accept: { //接受上傳的文件類型
type: Array,
default() {
return [];
}
},
acceptErrorMessage: { //文件類型錯(cuò)誤的提示內(nèi)容
type: String,
default: '文件類型錯(cuò)誤'
},
compress: { //是否開啟圖片壓縮
type: Boolean,
default: true,
},
compressSize: { //超過大小的圖片壓縮
type: Number,
default: 512,
},
data: { //上傳附帶的內(nèi)容
type: Object,
default() {
return {};
},
},
action: { //上傳地址
type: String,
default: '',
},
headers: { //設(shè)置上傳的請(qǐng)求頭部
type: Object,
default() {
return {};
},
},
imgWidth: { //圖片壓縮時(shí)指定壓縮的圖片寬度
type: [Number, Boolean],
default: 800,
},
quality: { //圖片壓縮的質(zhì)量
type: Number,
default: 1,
},
beforeUpload: { //上傳文件之前的鉤子
type: Function
},
onSuccess: { //上傳成功的鉤子
type: Function
},
onError: { //上傳失敗的鉤子
type: Function
},
onLoadend: { //文件上傳成功或者失敗都會(huì)執(zhí)行的鉤子
type: Function
},
onProgress: { //文件上傳進(jìn)度的鉤子
type: Function
},
onSuccessText: { //上傳成功的提示內(nèi)容
type: String,
default: '上傳成功'
},
onErrorText: { //上傳失敗的提示內(nèi)容
type: String,
default: '上傳失敗'
},
beforeRemove: { //刪除文件的鉤子
type: Function
},
showRemove: { //是否展示刪除icon
type: Boolean,
default: true
},
type: { //單文件上傳還是多文件上傳
type: String,
default: 'single',
validator: function (value) {
return ["single", "multiple"].includes(value);
}
},
maxNumber: { //多文件上傳最多上傳的個(gè)數(shù)
type: Number
},
isImage: { //文件是否為圖片
type: Boolean,
default: true
}
}
}2. 上傳組件的實(shí)現(xiàn)
template
<template>
<div class="g7-Upload-single">
<!-- 占位內(nèi)容,圖片展示,文件展示的處理 -->
<div class="g7-Upload-default-icon">
<template v-if="!value">
<slot>
<Icon :size="iconSize" :icon="icon" />
</slot>
</template>
<template v-else>
<template v-if="isImage">
<img class="g7-Upload-img" :src="value" />
</template>
<template v-else>
<Icon :size="34" icon="iconicon-" />
</template>
<span @click.stop="onRemove" v-if="showRemove" class="g7-Upload-removeImg">
<Icon :size="14" icon="iconcuowu" color="#fff" />
</span>
</template>
</div>
<input
class="g7-Upload-input"
@change="change"
:disabled="computedDisabled"
:name="name"
type="file"
ref="input"
/>
<!-- 圖片壓縮需要用到的canvas -->
<canvas hidden="hidden" v-if="compress" ref="canvas"></canvas>
<!-- 進(jìn)度條 -->
<div v-if="progress > 0" class="g7-Upload-progress">
<div :style="{width:`${progress}%`}" class="g7-Upload-progress-bar"></div>
</div>
</div>
</template>文件壓縮實(shí)現(xiàn):
canvasDataURL(base) {
const img = new Image();
img.src = base;
const that = this;
function ImgOnload() {
/**
* 計(jì)算生成圖片的寬高
*/
const scale = this.width / this.height;
const width =
that.imgWidth === false || this.width <= that.imgWidth
? this.width
: that.imgWidth;
const height = width / scale;
const canvas = that.$refs.canvas;
canvas.width = width;
canvas.height = height;
//利用canvas繪制壓縮的圖片并生成新的圖片
const context = canvas.getContext("2d");
context.drawImage(this, 0, 0, width, height);
canvas.toBlob(
blob => {
that.file = blob;
that.upload(blob);
that.$emit("on-change", blob, that.options);
},
"image/png",
that.quality
);
/**
* 使用完的createObjectURL需要釋放內(nèi)存
*/
window.URL.revokeObjectURL(this.src);
}
img.onload = ImgOnload;
}上傳文件的實(shí)現(xiàn):
export function fetch(options, file) {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
const xhr = new XMLHttpRequest();
const action = options.action;
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
options.uploadProgress(e);
};
}
const formData = new FormData();
formData.append(options.name, file, options.fileName);
for (const key in options.data) {
formData.append(key, options.data[key]);
}
// 成功回調(diào)
xhr.onload = (e) => {
const response = e.target.response;
if (xhr.status < 200 || xhr.status >= 300) {
options.uploadError(response);
return;
}
options.onload(response);
};
// 出錯(cuò)回調(diào)
xhr.onerror = (e) => {
const response = e.target.response;
options.uploadError(response);
};
// 請(qǐng)求結(jié)束
xhr.onloadend = (e) => {
const response = e.target.response;
options.uploadLoadend(response);
};
xhr.open('post', action, true);
const headers = options.headers;
for (const key in headers) {
if (headers[key] !== null) {
xhr.setRequestHeader(key, headers[key]);
}
}
xhr.send(formData);
}3. 完整的代碼
上傳組件:
<!-- components/upload.vue -->
<template>
<div class="g7-Upload-single">
<div class="g7-Upload-default-icon">
<template v-if="!value">
<slot>
<Icon :size="iconSize" :icon="icon" />
</slot>
</template>
<template v-else>
<template v-if="isImage">
<img class="g7-Upload-img" :src="value" />
</template>
<template v-else>
<Icon :size="34" icon="iconicon-" />
</template>
<span @click.stop="onRemove" v-if="showRemove" class="g7-Upload-removeImg">
<Icon :size="14" icon="iconcuowu" color="#fff" />
</span>
</template>
</div>
<input
class="g7-Upload-input"
@change="change"
:disabled="computedDisabled"
:name="name"
type="file"
ref="input"
/>
<!-- 圖片壓縮需要用到的canvas -->
<canvas hidden="hidden" v-if="compress" ref="canvas"></canvas>
<!-- 進(jìn)度條 -->
<div v-if="progress > 0" class="g7-Upload-progress">
<div :style="{width:`${progress}%`}" class="g7-Upload-progress-bar"></div>
</div>
</div>
</template>
<script>
import Icon from "../../Icon"; //自定義組件
import mixins from "./mixins";
import { getType, fetch } from "./utils";
import Toast from "../../~Toast"; //自定義組件
const compressList = ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG"];
export default {
components: { Icon },
mixins: [mixins],
props: {
value: {
type: String
}
},
data() {
return {
file: "",
progress: 0,
src: ""
};
},
computed: {
computedDisabled() {
return this.disabled || this.progress !== 0;
}
},
methods: {
change(e) {
if (this.disabled) {
return;
}
const file = e.target.files[0];
if (!file) {
return;
}
const type = getType(file.name);
if (this.accept.length) {
if (!this.accept.includes(type)) {
Toast.info(this.acceptErrorMessage);
return;
}
}
const size = Math.round((file.size / 1024) * 100) / 100;
if (size > this.size) {
Toast.info(`請(qǐng)上傳小于${this.size / 1024}M的文件`);
return;
}
if (this.isCompress(type, size)) {
this.canvasDataURL(URL.createObjectURL(file));
return;
}
this.$emit("on-change");
this.file = file;
this.upload(file);
},
/**
* 判斷是否滿足壓縮條件
*/
isCompress(type, size) {
return (
this.compress && compressList.includes(type) && size > this.compressSize
);
},
canvasDataURL(base) {
const img = new Image();
img.src = base;
const that = this;
function ImgOnload() {
/**
* 計(jì)算生成圖片的寬高
*/
const scale = this.width / this.height;
const width =
that.imgWidth === false || this.width <= that.imgWidth
? this.width
: that.imgWidth;
const height = width / scale;
const canvas = that.$refs.canvas;
canvas.width = width;
canvas.height = height;
//利用canvas繪制壓縮的圖片并生成新的blob
const context = canvas.getContext("2d");
context.drawImage(this, 0, 0, width, height);
canvas.toBlob(
blob => {
that.file = blob;
that.upload(blob);
that.$emit("on-change", blob, that.options);
},
"image/png",
that.quality
);
/**
* 使用完的createObjectURL需要釋放內(nèi)存
*/
window.URL.revokeObjectURL(this.src);
}
img.onload = ImgOnload;
},
/**
* 上傳成功
*/
onload(e) {
this.progress = 0;
this.$emit("input", e);
if (this.onSuccess) {
this.onSuccess(this.file, e);
return;
}
Toast.info(this.onSuccessText);
},
/**
* 上傳進(jìn)度
*/
uploadProgress(e) {
this.progress = e.percent;
if (this.onProgress) {
this.onProgress(this.file, e);
}
},
/**
* 上傳失敗
*/
uploadError(e) {
this.progress = 0;
if (this.onError) {
this.onSuccess(this.file, e);
return;
}
Toast.info(this.onErrorText);
},
/**
* 請(qǐng)求結(jié)束
*/
uploadLoadend(e) {
this.clearInput();
if (this.onloadend) {
this.onloadend(this.file, e);
}
},
/**
* 上傳
*/
upload(file) {
this.clearInput();
if (!this.beforeUpload) {
fetch(this, file);
return;
}
const before = this.beforeUpload(file);
if (before && before.then) {
before.then(res => {
if (res !== false) {
fetch(this, file);
}
});
return;
}
if (before !== false) {
fetch(this, file);
}
},
/**
* 刪除文件
*/
onRemove() {
this.clearInput();
if (this.type === "single") {
if (!this.beforeRemove) {
this.$emit("input", "");
return;
}
const before = this.beforeRemove(this.file, this.value);
if (before && before.then) {
before.then(res => {
if (res !== false) {
this.$emit("input", "");
}
});
return;
}
if (before !== false) {
this.$emit("input", "");
}
return;
}
this.$emit("on-remove");
},
clearInput() {
this.$refs.input.value = null;
}
}
};
</script>utils.js:獲取文件的后綴
/**
* 獲取文件的后綴
* @param {*} file
*/
export const getType = file => file.substr(file.lastIndexOf('.') + 1);
/**
* 請(qǐng)求封裝
* @param {*} options
* @param {*} file
*/
export function fetch(options, file) {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
const xhr = new XMLHttpRequest();
const action = options.action;
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
options.uploadProgress(e);
};
}
const formData = new FormData();
formData.append(options.name, file, options.fileName);
for (const key in options.data) {
formData.append(key, options.data[key]);
}
// 成功回調(diào)
xhr.onload = (e) => {
const response = e.target.response;
if (xhr.status < 200 || xhr.status >= 300) {
options.uploadError(response);
return;
}
options.onload(response);
};
// 出錯(cuò)回調(diào)
xhr.onerror = (e) => {
const response = e.target.response;
options.uploadError(response);
};
// 請(qǐng)求結(jié)束
xhr.onloadend = (e) => {
const response = e.target.response;
options.uploadLoadend(response);
};
xhr.open('post', action, true);
const headers = options.headers;
for (const key in headers) {
if (headers[key] !== null) {
xhr.setRequestHeader(key, headers[key]);
}
}
xhr.send(formData);
}整個(gè)Upload組件的對(duì)外暴露組件:
<template>
<div class="g7-Upload">
<template v-if="type === 'single'">
<Upload
:icon="icon"
:size="size"
:accept="accept"
:name="name"
:acceptErrorMessage="acceptErrorMessage"
:compress="compress"
:iconSize="iconSize"
:compressSize="compressSize"
:imgWidth="imgWidth"
:response="response"
:showLabel="showLabel"
:headers="headers"
:action="action"
:data="data"
:quality="quality"
:beforeRemove="beforeRemove"
:beforeUpload="beforeUpload"
:onSuccess="onSuccess"
:onSuccessText="onSuccessText"
:onError="onError"
:onProgress="onProgress"
:onLoadend="onLoadend"
:onErrorText="onErrorText"
:disabled="disabled"
:showRemove="showRemove"
v-model="currentValue"
@input="input"
:type="type"
:maxNumber="maxNumber"
:isImage="isImage"
>
<slot></slot>
</Upload>
</template>
</div>
</template>
<script>
import Upload from "./components/upload";
import mixins from "./components/mixins";
export default {
name: "G-Upload",
components: { Upload },
mixins: [mixins],
props: {
value: {
type: [String, Array]
}
},
data() {
return {
currentValue: this.value
};
},
watch: {
value(val) {
this.currentValue = val;
}
},
methods: {
input(val) {
this.$emit("input", val);
},
//接管文件上傳時(shí),自定義文件上傳進(jìn)度
onProgress(percent) {
this.$refs.upload.uploadProgress(percent);
}
}
};
</script>因?yàn)閳D片和文件展示的占位圖不一樣,所以用一個(gè)isImage的參數(shù)來判斷傳入的文件是否為圖片的方式總感覺很傻。但是當(dāng)網(wǎng)絡(luò)資源的url沒有文件后綴時(shí)沒法分辨出來是圖片還是文件,各位大佬有木有好的解決方式。
效果圖

圖片壓縮前后大小對(duì)比

到此這篇關(guān)于實(shí)現(xiàn)一個(gè)Vue版Upload組件的文章就介紹到這了,更多相關(guān)Vue Upload組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue上傳組件vue Simple Uploader的用法示例
- vue webuploader 文件上傳組件開發(fā)
- 在vue項(xiàng)目中使用element-ui的Upload上傳組件的示例
- vue2.0 使用element-ui里的upload組件實(shí)現(xiàn)圖片預(yù)覽效果方法
- 基于vue-upload-component封裝一個(gè)圖片上傳組件的示例
- vue-cli3.0+element-ui上傳組件el-upload的使用
- vue element upload組件 file-list的動(dòng)態(tài)綁定實(shí)現(xiàn)
- vue中element 的upload組件發(fā)送請(qǐng)求給后端操作
- vue使用Element el-upload 組件踩坑記
相關(guān)文章
將VUE項(xiàng)目部署到服務(wù)器的詳細(xì)步驟
經(jīng)過一段時(shí)間的探索,前端后端都有大致的樣子了,下面就是部署到服務(wù)器,這篇文章主要給大家介紹了關(guān)于將VUE項(xiàng)目部署到服務(wù)器的詳細(xì)步驟,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
vue checkbox 全選 數(shù)據(jù)的綁定及獲取和計(jì)算方法
下面小編就為大家分享一篇vue checkbox 全選 數(shù)據(jù)的綁定及獲取和計(jì)算方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02
vue如何實(shí)現(xiàn)簡(jiǎn)易的彈出框
這篇文章主要介紹了vue如何實(shí)現(xiàn)簡(jiǎn)易的彈出框,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
vue開發(fā)之LogicFlow自定義業(yè)務(wù)節(jié)點(diǎn)
這篇文章主要為大家介紹了vue開發(fā)之LogicFlow自定義業(yè)務(wù)節(jié)點(diǎn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
詳解基于vue的移動(dòng)web app頁面緩存解決方案
這篇文章主要介紹了詳解基于vue的移動(dòng)web app頁面緩存解決方案,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08
關(guān)于Vue源碼vm.$watch()內(nèi)部原理詳解
這篇文章主要介紹了關(guān)于Vue源碼vm.$watch()內(nèi)部原理詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

