實(shí)現(xiàn)一個(gè)Vue版Upload組件
前言
之前對(duì)一些主流手機(jī)拍出的照片大小做過(guò)對(duì)比,華為P30拍出的照片3M左右,同事的小米9不知開(kāi)啟了什么模式拍出了10M以上的照片。照片太大了對(duì)服務(wù)端上傳文件造成了不小的壓力,對(duì)此,后端對(duì)前端提出了圖片上傳前對(duì)圖片進(jìn)行壓縮。我們目前所用的UI庫(kù)Upload組件并不支持對(duì)上傳的圖片進(jìn)行壓縮,所以花了一點(diǎn)時(shí)間自己寫了上傳的組件。
今天分享我的第N個(gè)Vue組件,Upload
1.組件設(shè)計(jì)
- 只有圖片裁進(jìn)行壓縮,文件沒(méi)法壓縮。
- 超過(guò)規(guī)定大小的圖片裁進(jìn)行壓縮,不應(yīng)該是個(gè)圖片就壓縮,內(nèi)存過(guò)下的圖片沒(méi)必要壓縮。
- 自行定義圖片壓縮的的寬度,高度按比例自動(dòng)適配。
Upload
組件的禁用,該有的基本功能。- 文件上傳進(jìn)度。
- 可接管文件上傳。
- 等等圍繞以上幾個(gè)點(diǎn)開(kāi)始擴(kuò)展。
組件實(shí)現(xiàn)
1.mixins
export default { props: { icon: { //上傳組件的占位圖 type: String, default: "iconcamera" }, size: { //圖片超過(guò)指定大小不讓上傳 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: { //是否開(kāi)啟圖片壓縮 type: Boolean, default: true, }, compressSize: { //超過(guò)大小的圖片壓縮 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ù)來(lái)判斷傳入的文件是否為圖片的方式總感覺(jué)很傻。但是當(dāng)網(wǎng)絡(luò)資源的url沒(méi)有文件后綴時(shí)沒(méi)法分辨出來(lái)是圖片還是文件,各位大佬有木有好的解決方式。
效果圖
圖片壓縮前后大小對(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 文件上傳組件開(kāi)發(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)過(guò)一段時(shí)間的探索,前端后端都有大致的樣子了,下面就是部署到服務(wù)器,這篇文章主要給大家介紹了關(guān)于將VUE項(xiàng)目部署到服務(wù)器的詳細(xì)步驟,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08vue checkbox 全選 數(shù)據(jù)的綁定及獲取和計(jì)算方法
下面小編就為大家分享一篇vue checkbox 全選 數(shù)據(jù)的綁定及獲取和計(jì)算方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02vue如何實(shí)現(xiàn)簡(jiǎn)易的彈出框
這篇文章主要介紹了vue如何實(shí)現(xiàn)簡(jiǎn)易的彈出框,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04vue開(kāi)發(fā)之LogicFlow自定義業(yè)務(wù)節(jié)點(diǎn)
這篇文章主要為大家介紹了vue開(kāi)發(fā)之LogicFlow自定義業(yè)務(wù)節(jié)點(diǎn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01詳解基于vue的移動(dòng)web app頁(yè)面緩存解決方案
這篇文章主要介紹了詳解基于vue的移動(dòng)web app頁(yè)面緩存解決方案,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08關(guān)于Vue源碼vm.$watch()內(nèi)部原理詳解
這篇文章主要介紹了關(guān)于Vue源碼vm.$watch()內(nèi)部原理詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04