vue3.0/vite/typescript 重写upload组件__Vue.js
发布于 4 年前 作者 banyungong 1731 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

nice to meet you~ 认识一下

大家好~ 很高兴在这里写下了自己的第一篇文章。

之前一直写的是php,由于公司业务,机缘巧合之下也兼顾了前端的工作,后来发现自己对前端还是挺有热枕的…就一直研究下去了,前端这几年变化的不是一般的大呀,前端的孩子们还学得动吗?

哪些写得不好不要吐槽,哈哈 ~ 纯属个人分享学习,如果能帮到你会是我的荣幸。

github地址

里面有关于这个组件的完整代码,还有一些todo、发布订阅、观察者等相关代码。

移动端上传通用组件

编写目的

  • 记录vue3.0尝鲜的开发过程

  • 一边体验Compositon API一边学习typescript

关注组件

通常开发一个组件,我们需要问自己两个问题:

1、这个组件是解决什么问题?

2、组件颗粒化需要达到什么程度?

回答如下:

1、提高复用性、提升开发效率、解耦等等。

2、像上传组件,我们需要考虑自身项目及业务了,这里我这边的需求比较简单,大概是满足上传->预览/删除->数据回调即可。

满足以下需求:

    - [x] 调用手机相机、相册

    - [x] 获取图片并渲染到浏览器

    - [x] 解决图片EXIF旋转

    - [x] 预览图片

    - [x] 删除图片

    - [x] 支持上传图片配置

    - [x] 支持多选

    回调方法:

    @on-change="onChange"

    @on-success="onSuccess"

    @on-error="onError"

vue3.0、vite搭建

  $ yarn create vite-app <project-name>

  $ cd <project-name>

  $ yarn

  $ yarn dev

集成 typescript

  $yarn add --dev typescript

集成 sass

  $yarn add sass

安装sass时,你会发现控制台报错,解决方法:

1. 打开package.json

2. 把dependencies里的sass这一行,移到devDependencies

3. 重新运行yarn install

编写代码

<template>
  <div>
    <h1>Vue3.0-ts-upload</h1>
    <k-uploader
      :files="fileList"
      title="vue3.0_ts_组件上传"
      @on-change="onChange"
      @on-success="onSuccess"
      @on-error="onError"
    ></k-uploader>
  </div>
</template>

<script lang="ts">
import { reactive, ref } from "vue";
import KUploader from "../components/Uploader/Uploader.vue";

// 附件对象接口
interface IFile {
  url: string;
}
export default {
  components: {
    KUploader,
  },
  setup() {
    const activeId = ref<number | null>(null);
    // 默认附件数据
    const fileList = reactive<Array<IFile>>([
      {
        url: "https://ossweb-img.qq.com/images/lol/web201310/skin/big84000.jpg",
      },
      {
        url: "https://ossweb-img.qq.com/images/lol/web201310/skin/big37006.jpg",
      },
      {
        url: "https://ossweb-img.qq.com/images/lol/web201310/skin/big39000.jpg",
      },
    ]);

    const onSuccess = (res: IFile) => {
      console.log(res);
      console.log("success");
    };
    const onError = (res: IFile) => {
      console.log(res);
      console.log("error");
    };
    const onChange = (res: IFile[]) => {
      console.log(res);
      console.log("change");
    };
    return {
      fileList,
      activeId,
      onSuccess,
      onError,
      onChange,
    };
  },
};
</script>

<style>
</style>

**uploader组件 **

<script lang="ts">
import { ref, reactive, watchEffect } from "vue";
import { handleFile, transformCoordinate, dataURItoBlob } from "./utils";
// 文件信息接口
interface IFile {
  url: string;
}
interface IFileItem {
  url: string;
  blob: any;
}
// InputEvent接口
interface HTMLInputEvent extends Event {
  target: HTMLInputElement & EventTarget;
}
export default {
  name: "Uploader",
  props: {
    title: {
      type: String,
      default: "图片上传",
    },
    files: {
      type: Array, //初始化数据源
      default: () => [],
    },
    limit: {
      type: Number, //限制上传图片个数
      default: 9,
    },
    capture: {
      type: Boolean, //是否只选择调用相机
      default: false,
    },
    enableCompress: {
      type: Boolean, //是否压缩
      default: true,
    },
    maxWidth: {
      type: Number, //图片压缩最大宽度
      default: 1024,
    },
    quality: {
      type: Number, //图片压缩率
      default: 0.9,
    },
    url: {
      type: String, //上传服务器url
      default: "",
    },
    params: {
      type: Object, //上传文件时携带的自定义参数
      default: () => {},
    },
    name: {
      type: String, //上传文件时FormData的Key,默认为file
      default: "file",
    },
    autoUpload: {
      type: Boolean, //是否自动开启上传
      default: true,
    },
    multiple: {
      type: Boolean, //是否支持多选, `false`为不支持
      default: "",
    },
    readonly: {
      type: Boolean, //只读模式(隐藏添加和删除按钮)
      default: false,
    },
  },
  setup(props, { emit }) {
    // 待上传文件
    let fileList = reactive<any[]>(props.files);
    //fileList = files;
    // 预览开关
    let previewVisible = ref<Boolean>(false);
    // 当前预览的图片序号
    let currentIndex = ref(0);
    // 定义当前预览图片img
    let currentImg = ref<string | null>("");
    let inputValue = ref<string | null>("");

    watchEffect(()=>{
      
    })


    // 文件变更操作
    const handleChange = (event: HTMLInputEvent): void => {
      const { enableCompress, maxWidth, quality, autoUpload } = props;
      const target = event.target || event.srcElement;
      const inputChangeFiles: [] | any = target.files;
      // console.log("files", inputChangeFiles);
      if (inputChangeFiles.length <= 0) {
        // 调用取消
        return;
      }
      const fileCount = fileList.length + inputChangeFiles.length;
      if (fileCount > props.limit) {
        alert(`不能上传超过${props.limit}张图片`);
        return;
      }
      // console.log("handleFile");
      // 执行操作
      Promise.all(
        Array.prototype.map.call(inputChangeFiles, (file) => {
          return handleFile(file, {
            maxWidth,
            quality,
            enableCompress,
          }).then((blob) => {
            const blobURL = URL.createObjectURL(blob);
            const fileItem: any = <IFileItem>{
              url: blobURL,
              blob,
            };
            for (let key in file) {
              if (["slice", "webkitRelativePath"].indexOf(key) === -1) {
                fileItem[key] = file[key];
              }
            }
            if (autoUpload) {
              uploadFile(blob, fileItem)
                .then((result) => {
                  fileList.push(fileItem);
                  // 回调方法
                  // vue2.x写法 :this.$emit('on-change', fileList);
                  emit("on-change", fileList);
                  console.log("success");
                })
                .catch((e) => {
                  fileList.push(fileItem);
                });
            } else {
            }
          });
        })
      ).then(() => {
        inputValue.value = "";
      });
    };

    // 上传文件
    const uploadFile = (blob: string, fileItem: any) => {
      return new Promise((resolve, reject) => {
        // 暂时resolve 模拟返回 正式使用请删掉
        const result = {
          status: 1,
          msg: "上传成功",
          data: {
            filename: "图片名字",
            url:
              "https://ossweb-img.qq.com/images/lol/web201310/skin/big84000.jpg",
          },
        };
        resolve(result);
        emit("on-success", result);
        return;

        const me = this;
        const { url, params, name } = props;
        const formData = new FormData();
        const xhr = new XMLHttpRequest();

        formData.append(name, blob);
        if (params) {
          for (let key in params) {
            formData.append(key, params[key]);
          }
        }
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 1) {
            if (localStorage.getItem("token")) {
              const accessToken: any = localStorage.getItem("token");
              xhr.setRequestHeader("Authorization", accessToken);
            }
          }
          if (xhr.readyState === 4) {
            if (xhr.status === 200) {
              const result = JSON.parse(xhr.responseText);
              // 回调父页面on-success
              // vue2.x写法 this.$emit("on-success", result, fileItem);
              emit("on-success", result, fileItem);
              resolve(result);
            } else {
              // 回调父页面on-error
              // vue2.x写法 this.$emit("on-error", xhr);
              emit("on-error", xhr);
              reject(xhr);
            }
          }
        };
        xhr.upload.addEventListener(
          "progress",
          function (evt) {
            if (evt.lengthComputable) {
              const precent = Math.ceil((evt.loaded / evt.total) * 100);
              // 上传进度
            }
          },
          false
        );
        xhr.open("POST", url, true);
        xhr.send(formData);
      });
    };

    // 预览图片、删除图片
    const handleFileClick = (
      e: MouseEvent,
      item: IFile,
      index: number
    ): void => {
      showPreviewer();
      currentImg.value = item.url;
      currentIndex.value = index;
    };

    // 显示预览
    const showPreviewer = () => {
      previewVisible.value = true;
    };

    // 隐藏预览
    const handleHide = () => {
      previewVisible.value = false;
    };

    // 删除图片
    const handleDelete = () => {
      const delFn = () => {
        handleHide();
        fileList.splice(currentIndex.value, 1);
        emit("on-change", fileList);
      };
      delFn();
    };

    return {
      fileList,
      previewVisible,
      currentImg,
      inputValue,
      handleChange,
      handleFileClick,
      handleHide,
      handleDelete,
    };
  },
};
</script>

不足之处 / 一些想法

  • props传参时,是否应使用如下代码:
interface IProps{
        title:string,
        limit:number,
        ...
}
props:[title,limit],
setup(props:IProps,context){
}

将 props 独立出来作为第一个参数,可以让 TypeScript 对 props 单独做类型推导,不会和上下文中的其他属性相混淆。这也使得 setup 、 render 和其他使用了 TSX 的函数式组件的签名保持一致。
  • composition api 提倡的是代码提取和重用逻辑,但我个人觉得我还没做到这点,以后要加强。

写在最后

  • 感谢能花费自己宝贵的时间看完这篇文章的读者们。

  • 我的代码不优秀,但希望能一起在代码这条路上努力~

最后别忘了点赞噢~谢谢~

最后别忘了点赞噢~谢谢~

最后别忘了点赞噢~谢谢~

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 最后一只熊大m 原文链接:https://juejin.im/post/6867716120087003144

回到顶部