前言
nice to meet you~ 認(rèn)識一下
大家好~ 很高興在這里寫下了自己的第一篇文章。
之前一直寫的是php,由于公司業(yè)務(wù),機(jī)緣巧合之下也兼顧了前端的工作,后來發(fā)現(xiàn)自己對前端還是挺有熱枕的...就一直研究下去了,前端這幾年變化的不是一般的大呀,前端的孩子們還學(xué)得動嗎?
哪些寫得不好不要吐槽,哈哈 ~ 純屬個人分享學(xué)習(xí),如果能幫到你會是我的榮幸。
里面有關(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)秀,但希望能一起在代碼這條路上努力~
最后別忘了點贊噢謝謝
最后別忘了點贊噢謝謝
最后別忘了點贊噢謝謝