vue3.0+vite+typescript 重寫upload組件

前言

nice to meet you~ 認(rèn)識一下

大家好~ 很高興在這里寫下了自己的第一篇文章。

之前一直寫的是php,由于公司業(yè)務(wù),機(jī)緣巧合之下也兼顧了前端的工作,后來發(fā)現(xiàn)自己對前端還是挺有熱枕的...就一直研究下去了,前端這幾年變化的不是一般的大呀,前端的孩子們還學(xué)得動嗎?

哪些寫得不好不要吐槽,哈哈 ~ 純屬個人分享學(xué)習(xí),如果能幫到你會是我的榮幸。

github地址

里面有關(guān)于這個組件的完整代碼,還有一些todo、發(fā)布訂閱、觀察者等相關(guān)代碼。

移動端上傳通用組件

編寫目的

  • 記錄vue3.0嘗鮮的開發(fā)過程

  • 一邊體驗Compositon API一邊學(xué)習(xí)typescript

關(guān)注組件

通常開發(fā)一個組件,我們需要問自己兩個問題:

1、這個組件是解決什么問題?

2、組件顆?;枰_(dá)到什么程度?

回答如下:

1、提高復(fù)用性、提升開發(fā)效率、解耦等等。

2、像上傳組件,我們需要考慮自身項目及業(yè)務(wù)了,這里我這邊的需求比較簡單,大概是滿足上傳->預(yù)覽/刪除->數(shù)據(jù)回調(diào)即可。

滿足以下需求:

    - [x] 調(diào)用手機(jī)相機(jī)、相冊

    - [x] 獲取圖片并渲染到瀏覽器

    - [x] 解決圖片EXIF旋轉(zhuǎn)

    - [x] 預(yù)覽圖片

    - [x] 刪除圖片

    - [x] 支持上傳圖片配置

    - [x] 支持多選

    回調(diào)方法:

    @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時,你會發(fā)現(xiàn)控制臺報錯,解決方法:

1. 打開package.json

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

3. 重新運(yùn)行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);
    // 默認(rèn)附件數(shù)據(jù)
    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, //初始化數(shù)據(jù)源
      default: () => [],
    },
    limit: {
      type: Number, //限制上傳圖片個數(shù)
      default: 9,
    },
    capture: {
      type: Boolean, //是否只選擇調(diào)用相機(jī)
      default: false,
    },
    enableCompress: {
      type: Boolean, //是否壓縮
      default: true,
    },
    maxWidth: {
      type: Number, //圖片壓縮最大寬度
      default: 1024,
    },
    quality: {
      type: Number, //圖片壓縮率
      default: 0.9,
    },
    url: {
      type: String, //上傳服務(wù)器url
      default: "",
    },
    params: {
      type: Object, //上傳文件時攜帶的自定義參數(shù)
      default: () => {},
    },
    name: {
      type: String, //上傳文件時FormData的Key,默認(rèn)為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;
    // 預(yù)覽開關(guān)
    let previewVisible = ref<Boolean>(false);
    // 當(dāng)前預(yù)覽的圖片序號
    let currentIndex = ref(0);
    // 定義當(dāng)前預(yù)覽圖片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) {
        // 調(diào)用取消
        return;
      }
      const fileCount = fileList.length + inputChangeFiles.length;
      if (fileCount > props.limit) {
        alert(`不能上傳超過${props.limit}張圖片`);
        return;
      }
      // console.log("handleFile");
      // 執(zhí)行操作
      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);
                  // 回調(diào)方法
                  // 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);
              // 回調(diào)父頁面on-success
              // vue2.x寫法 this.$emit("on-success", result, fileItem);
              emit("on-success", result, fileItem);
              resolve(result);
            } else {
              // 回調(diào)父頁面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);
              // 上傳進(jìn)度
            }
          },
          false
        );
        xhr.open("POST", url, true);
        xhr.send(formData);
      });
    };

    // 預(yù)覽圖片、刪除圖片
    const handleFileClick = (
      e: MouseEvent,
      item: IFile,
      index: number
    ): void => {
      showPreviewer();
      currentImg.value = item.url;
      currentIndex.value = index;
    };

    // 顯示預(yù)覽
    const showPreviewer = () => {
      previewVisible.value = true;
    };

    // 隱藏預(yù)覽
    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傳參時,是否應(yīng)使用如下代碼:
interface IProps{
        title:string,
        limit:number,
        ...
}
props:[title,limit],
setup(props:IProps,context){
}

將 props 獨立出來作為第一個參數(shù),可以讓 TypeScript 對 props 單獨做類型推導(dǎo),不會和上下文中的其他屬性相混淆。這也使得 setup 、 render 和其他使用了 TSX 的函數(shù)式組件的簽名保持一致。
  • composition api 提倡的是代碼提取和重用邏輯,但我個人覺得我還沒做到這點,以后要加強(qiáng)。

寫在最后

  • 感謝能花費自己寶貴的時間看完這篇文章的讀者們。

  • 我的代碼不優(yōu)秀,但希望能一起在代碼這條路上努力~

最后別忘了點贊噢謝謝

最后別忘了點贊噢謝謝

最后別忘了點贊噢謝謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容