Vue Upload组件
Fone 人气:0前言
之前对一些主流手机拍出的照片大小做过对比,华为P30拍出的照片3M左右,同事的小米9不知开启了什么模式拍出了10M以上的照片。照片太大了对服务端上传文件造成了不小的压力,对此,后端对前端提出了图片上传前对图片进行压缩。我们目前所用的UI库Upload组件并不支持对上传的图片进行压缩,所以花了一点时间自己写了上传的组件。
今天分享我的第N个Vue组件,Upload
1.组件设计
- 只有图片裁进行压缩,文件没法压缩。
- 超过规定大小的图片裁进行压缩,不应该是个图片就压缩,内存过下的图片没必要压缩。
- 自行定义图片压缩的的宽度,高度按比例自动适配。
Upload
组件的禁用,该有的基本功能。- 文件上传进度。
- 可接管文件上传。
- 等等围绕以上几个点开始扩展。
组件实现
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: { //文件类型错误的提示内容 type: String, default: '文件类型错误' }, compress: { //是否开启图片压缩 type: Boolean, default: true, }, compressSize: { //超过大小的图片压缩 type: Number, default: 512, }, data: { //上传附带的内容 type: Object, default() { return {}; }, }, action: { //上传地址 type: String, default: '', }, headers: { //设置上传的请求头部 type: Object, default() { return {}; }, }, imgWidth: { //图片压缩时指定压缩的图片宽度 type: [Number, Boolean], default: 800, }, quality: { //图片压缩的质量 type: Number, default: 1, }, beforeUpload: { //上传文件之前的钩子 type: Function }, onSuccess: { //上传成功的钩子 type: Function }, onError: { //上传失败的钩子 type: Function }, onLoadend: { //文件上传成功或者失败都会执行的钩子 type: Function }, onProgress: { //文件上传进度的钩子 type: Function }, onSuccessText: { //上传成功的提示内容 type: String, default: '上传成功' }, onErrorText: { //上传失败的提示内容 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: { //多文件上传最多上传的个数 type: Number }, isImage: { //文件是否为图片 type: Boolean, default: true } } }
2. 上传组件的实现
template
<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> <!-- 进度条 --> <div v-if="progress > 0" class="g7-Upload-progress"> <div :style="{width:`${progress}%`}" class="g7-Upload-progress-bar"></div> </div> </div> </template>
文件压缩实现:
canvasDataURL(base) { const img = new Image(); img.src = base; const that = this; function ImgOnload() { /** * 计算生成图片的宽高 */ 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需要释放内存 */ window.URL.revokeObjectURL(this.src); } img.onload = ImgOnload; }
上传文件的实现:
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]); } // 成功回调 xhr.onload = (e) => { const response = e.target.response; if (xhr.status < 200 || xhr.status >= 300) { options.uploadError(response); return; } options.onload(response); }; // 出错回调 xhr.onerror = (e) => { const response = e.target.response; options.uploadError(response); }; // 请求结束 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> <!-- 进度条 --> <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(`请上传小于${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() { /** * 计算生成图片的宽高 */ 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需要释放内存 */ 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); }, /** * 上传进度 */ 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); }, /** * 请求结束 */ 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); /** * 请求封装 * @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]); } // 成功回调 xhr.onload = (e) => { const response = e.target.response; if (xhr.status < 200 || xhr.status >= 300) { options.uploadError(response); return; } options.onload(response); }; // 出错回调 xhr.onerror = (e) => { const response = e.target.response; options.uploadError(response); }; // 请求结束 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); }
整个Upload组件的对外暴露组件:
<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); }, //接管文件上传时,自定义文件上传进度 onProgress(percent) { this.$refs.upload.uploadProgress(percent); } } }; </script>
因为图片和文件展示的占位图不一样,所以用一个isImage
的参数来判断传入的文件是否为图片的方式总感觉很傻。但是当网络资源的url没有文件后缀时没法分辨出来是图片还是文件,各位大佬有木有好的解决方式。
效果图
图片压缩前后大小对比
加载全部内容