前言
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 提倡的是代码提取和重用逻辑,但我个人觉得我还没做到这点,以后要加强。
写在最后
-
感谢能花费自己宝贵的时间看完这篇文章的读者们。
-
我的代码不优秀,但希望能一起在代码这条路上努力~
最后别忘了点赞噢 谢谢~