vue3.0+vite+ts重写手机端upload组件

tech2022-09-14  87

前言

nice to meet you~ 认识一下

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

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 提倡的是代码提取和重用逻辑,但我个人觉得我还没做到这点,以后要加强。

写在最后

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

希望能一起在代码这条路上努力~

最后别忘了点赞噢 谢谢~

最新回复(0)