实现一个Vue版Upload组件

目录
  • 前言
  • 今天分享我的第N个Vue组件,Upload
    • 1.组件设计
  • 组件实现
    • 1.mixins
    • 2. 上传组件的实现
    • 3. 完整的代码
  • 效果图
  • 图片压缩前后大小对比

前言

之前对一些主流手机拍出的照片大小做过对比,华为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没有文件后缀时没法分辨出来是图片还是文件,各位大佬有木有好的解决方式。

效果图

图片压缩前后大小对比

到此这篇关于实现一个Vue版Upload组件的文章就介绍到这了,更多相关Vue Upload组件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue webuploader 文件上传组件开发

    最近项目中需要用到百度的webuploader大文件的分片上传,对接后端的fastdfs,于是着手写了这个文件上传的小插件,步骤很简单,但是其中猜到的坑也不少,详细如下: 一.封装组件 引入百度提供的webuploader.js.Uploader.swf css样式就直接写在组件里面了 <template> <div> <div id="list" class="uploader-list"></div> <di

  • vue element upload组件 file-list的动态绑定实现

    本文解决,upload组件 file-list的动态绑定list1,list2 ...,实现动态添加,相信很多电商后台管理系统都会遇到这个需求,例子如下 本例,我是使用的upload默认的上传地址(很多图片不能上传,你可以在本地截几张图片,进行测试),我可以上传多张活动图片,可以加相应的,名称,链接描述等,如果有多个活动,可以点击添加活动,在第二个活动又能添加相应的内容,保存完之后,可以实现回显,活动详情页可以看到添加的几个活动和相应的活动内容. 实现方法不为一,这是一种特别简单的.代码如下 <

  • vue2.0 使用element-ui里的upload组件实现图片预览效果方法

    1.首先我们在cli中引入element-ui 2.然后在具体的代码中放入uoload组件 <el-upload class="upload-demo" action="" :auto-upload='false' :on-change='changeUpload'> <el-button size="small" type="primary">点击上传</el-button> <di

  • vue-cli3.0+element-ui上传组件el-upload的使用

    最近项目中涉及很多文件上传的地方,然后文件上传又有很多限制.比如文件大小限制,文件个数限制,文件类型限制,文件上传后的列表样式自定义,包括上传进度条等问题.下面是我对element-ui的上传组件的一些改造, 点击查看源码. 我是自己维护了一个列表数据,再对这个列表数据进行一些操作,没用组件自带的.先看看我的组件模版 <template> <el-upload class="upload-demo" :limit="limit" :action=&

  • vue中element 的upload组件发送请求给后端操作

    1.用到了before-upload属性, 用于在上传文件前的校验,并且发送请求给后端,传输格式进行文件流传输 什么都不用设置,action属性随便设置,不能为空即可! 在before-upload属性的方法中的代码如下: var _this = this; debugger; // var files=file.target.files[0]; debugger; const isJPG = file.type === "image/jpeg"; const isLt2M = fil

  • 基于vue-upload-component封装一个图片上传组件的示例

    需求分析 业务要求,需要一个图片上传控件,需满足 多图上传 点击预览 图片前端压缩 支持初始化数据 相关功能及资源分析 基本功能 先到https://www.npmjs.com/search?q=vue+upload上搜索有关上传的控件,没有完全满足需求的组件,过滤后找到 vue-upload-component 组件,功能基本都有,自定义也比较灵活,就以以此进行二次开发. 预览 因为项目是基于 vant 做的,本身就提供了 ImagePreview 的预览组件,使用起来也简单,如果业务需求需要

  • 在vue项目中使用element-ui的Upload上传组件的示例

    本文介绍了vue项目中使用element-ui的Upload上传组件的示例,分享给大家,具体如下: <el-upload v-else class='ensure ensureButt' :action="importFileUrl" :data="upLoadData" name="importfile" :onError="uploadError" :onSuccess="uploadSuccess&quo

  • vue使用Element el-upload 组件踩坑记

    目录 一.基本使用 二.图片数量控制 三.图片格式限制/可以选中多张图片 补充:在vue项目中使用element-ui的Upload上传组件 一.基本使用 最近研究了一下 el-upload组件 踩了一些小坑  写起来大家学习一下 很经常的一件事情 经常会去直接拷贝 element的的组件去使用 但是用到上传组件时 就会遇到坑了 如果你直接去使用upload 你肯定会遇见这个错误 而且 上传的图片 可能会突然消失  这时候如果你没有接口  你是完全不知道为什么移除的  所以 无接口时 只能去猜测

  • Vue上传组件vue Simple Uploader的用法示例

    在日常开发中经常会遇到文件上传的需求,vue-simple-uploader 就是一个基于 simple-uploader.js 和 Vue 结合做的一个上传组件,自带 UI,可覆盖.自定义:先来张动图看看效果: 其主要特点就是: 支持文件.多文件.文件夹上传 支持拖拽文件.文件夹上传 统一对待文件和文件夹,方便操作管理 可暂停.继续上传 错误处理 支持"快传",通过文件判断服务端是否已存在从而实现"快传" 上传队列管理,支持最大并发上传 分块上传 支持进度.预估剩

  • 实现一个Vue版Upload组件

    目录 前言 今天分享我的第N个Vue组件,Upload 1.组件设计 组件实现 1.mixins 2. 上传组件的实现 3. 完整的代码 效果图 图片压缩前后大小对比 前言 之前对一些主流手机拍出的照片大小做过对比,华为P30拍出的照片3M左右,同事的小米9不知开启了什么模式拍出了10M以上的照片.照片太大了对服务端上传文件造成了不小的压力,对此,后端对前端提出了图片上传前对图片进行压缩.我们目前所用的UI库Upload组件并不支持对上传的图片进行压缩,所以花了一点时间自己写了上传的组件. 今天

  • vue版日历组件的实现方法

    开发背景 常用日历组件可能满足不了我们自定义的多种需求(比如样式),因此通常情况下我们可能需要自己手动开发款日历,先上图 开发流程 1. 根据常用日历样式,我们template部分可以分为三部分(上下月及当前月份展示:周日至周六展示:主体日期展示三部分) 1) template部分代码 <div class="date">     <div class="header">         <span class="pre_mo

  • vue设计一个倒计时秒杀的组件详解

    简介: 倒计时秒杀组件在电商网站中层出不穷  不过思路万变不离其踪,我自己根据其他资料设计了一个vue版的 核心思路: 1.时间不能是本地客户端的时间  必须是服务器的时间这里用一个settimeout代替 以为时间必须统一 2.开始时间,结束时间通过父组件传入,当服务器时间在这个开始时间和结束时间的范围内  参加活动按钮可以点击,并且参加过活动以后不能再参加, 3.在组件创建的时候 同步得到现在时间服务时间差,并且在这里边设置定时器,每秒都做判断看秒杀是否开始和结束, 4.在更新时间的函数中是

  • 详解vue为什么要求组件模板只能有一个根元素

    我是在知乎上看到的这个问题,转念一想,用了大半年的vue,好像真的没有了解过: '为什么只能有且只有一个根元素' 于是我花了二十多分钟去找了一下答案......竟然没有找到答案.... 好的现在我来说说我的理解,如果有不对的地方欢迎指出. 我觉得这个问题需要从两个方面来说起: 1.new Vue({el:'#app'}) 2.单文件组件中,template下的元素div 一.当我们实例化Vue的时候,填写一个el选项,来指定我们的SPA入口: let vm = new Vue({ el:'#ap

  • 从零开始在NPM上发布一个Vue组件的方法步骤

    TL;DR 本文细致讲解了在NPM上发布一个Vue组件的全过程,包括创建项目.编写组件.打包和发布四个环节. 创建项目 这里我们直接利用@vue/cli来生成项目.如果没有安装@vue/cli的话,可以使用以下方法进行安装: # 如果喜欢npm npm i -g @vue/cli # 如果喜欢yarn yarn global add @vue/cli 此外,如果安装了npx(高版本的nodejs发行版会自带这一工具)的话,还可以很方便地通过npx vue这一方式实现免安装使用. 接下来就可以创建

  • 使用Vue3实现一个Upload组件的示例代码

    通用上传组件开发 开发上传组件前我们需要了解: FormData上传文件所需API dragOver文件拖拽到区域时触发 dragLeave文件离开拖动区域 drop文件移动到有效目标时 首先实现一个最基本的上传流程: 基本上传流程,点击按钮选择,完成上传 代码如下: <template> <div class="app-container"> <!--使用change事件--> <input type="file" @ch

  • vue封装一个图案手势锁组件

    目录 说在前面 效果展示 预览地址 实现步骤 1.组件设计 2.组件分析 3.组件实现 4.组件使用 组件库引用 源码地址 组件文档 说在前面 现在很多人都喜欢使用图案手势锁,这里我使用vue来封装了一个可以直接使用的组件,在这里记录一下这个组件的开发步骤. 效果展示 组件实现效果如下图: 预览地址 http://jyeontu.xyz/jvuewheel/#/JAppsLock 实现步骤 完成一个组件需要几步? 1.组件设计 首先我们应该要知道我们要做怎样的组件,具备怎样的功能,这样才可以开始

  • 封装一个Vue文件上传组件案例详情

    目录 前言 1. 子组件 2 父组件使用 3.效果 4.总结 前言 在面向特定用户的项目中,引 其他ui组件库导致打包体积过大,首屏加载缓慢,还需要根据UI设计师设计的样式,重写大量的样式覆盖引入的组件库的样式.因此尝试自己封装一个自己的组件,代码参考了好多前辈的文章 1. 子组件 <template> <div class="digital_upload"> <input style="display: none" @change=&

随机推荐