vue3提供了compostion api,一方面给JS开发提供了更多的灵活性,另一方面对开发出稳定性高,扩展性好,易阅读维护成本低的代码提出了新的挑战,在使用vue3的过程中,我们应该如何开发自己的业务组件,读完本篇文章你将会获得答案。
如果把逻辑都放到vue模版中,setup函数将会非常臃肿,因此,首先要对自己的业务组件逻辑部分进行拆分(模板部分拆分不在本次讨论范围),以音频文件上传功能为例。
文件上传分为:
1、选择文件
2、对文件进行试听、增加、删除
3、上传文件、上传进度、上传状态
4、上传成功、上传失败的处理
我们把1、2分为一类,挑选文件的处理;把3、4分为一类,文件上传的处理;
1、choise-audio.ts:挑选文件的处理
2、upload-audio.ts:文件上传的处理
实现文件上传的核心功能点,但具体功能细节不在这里
import uploadAudio from './upload-audio';
import choiseAudio from './choise-audio';
setup() {
const route = useRoute();
// TODO:
const fileListInit: FileListType[] = [];
const fileList = ref(fileListInit);
const teacherId = route.query.id;
const uploadData = {
teacherId
};
const {
handleUnit,
handleChange,
handleAudioPlay,
handleAudioPause,
handleRemove
} = choiseAudio(fileList);
const {
// handleTips,
beforeUpload,
handleUploadAgain,
handleSuccess,
handleProgress,
handleError,
handleSubmit
} = uploadAudio(fileList);
return {
handleUnit,
fileList,
uploadData,
handleError,
handleRemove,
handleAudioPlay,
handleAudioPause,
handleChange,
handleProgress,
handleSuccess,
handleUploadAgain,
beforeUpload,
handleSubmit
};
}
1、编写choise-audio.ts
编写选择文件的主要逻辑:试听、删除、选择、文件类型判断等:
export default function choiseAudio(fileList: any): ChoiseType {
const ctx = getCurrentInstance();
const {
proxy: { $confirm, $message }
} = ctx;
// 处理文件大小
const handleUnit = (val: number) => {
return caculateFileSize(val);
};
// 从上传队列中移除
const handleDelItem = (uid: number) => {
fileList.value = ChoiseAudio.deleteFile(fileList.value, uid);
};
// 文件类型判断
const handleGetFileType = (file: FileListType) => {
const type = ChoiseAudio.getFileType(file);
if (type !== 'wav' && type !== 'aac') {
handleDelItem(file.uid);
// handleTips(file.uid, '不支持文件类型,请重新上传', false);
}
};
// 文件过大的提示
const handleOversize = (file: any) => {
// 文件不得大于 100M
if (file.size > 100 * 1024 * 1024) {
handleDelItem(file.uid);
// handleTips(file.uid, '文件大于100M,请重新选择', false);
}
};
// 播放音频
const handleAudioPlay = (file: any, index: number) => {
if (!el) {
el = document.createElement('audio');
}
ChoiseAudio.handleAudioPlay(currentIndex, fileList, el, file, index);
currentIndex = index;
};
// 暂停音频
const handleAudioPause = (file: FileListType, index: number) => {
if (!el) return;
ChoiseAudio.handleAudioPause(el, fileList, index);
};
// 处理变化
const handleChange = (file: FileListType) => {
if (file.status === 'ready') {
ChoiseAudio.fileListDataInit(file, fileList);
handleGetFileType(file); // 校验文件类型
// handleAudioUrl(file); // 获取音频 url 和时长
handleOversize(file); // 文件大小校验
}
};
// 删除语音
const handleRemove = (file: FileListType, index: number) => {
};
return {
handleUnit,
handleRemove,
// handleOversize,
// handleDelItem,
// handleGetFileType,
handleAudioPlay,
handleChange,
handleAudioPause
};
}
2、编写upload-audio.ts
编写上传的逻辑部分:上传成功、上传失败、上传进度、上传状态等逻辑部分
export default function uploadAudio(fileList: any): UploadAudioType {
const ctx = getCurrentInstance();
const {
proxy: { $message }
} = ctx;
// 设置提示
const handleTips = (uid: any, tip: string, flag: boolean) => {
fileList.value = UploadAudio.handleFileUploadTips(
uid,
tip,
flag,
fileList.value
);
};
// 上传失败
const handleError = (err: any, file: FileListType) => {
handleTips(file.uid, '上传失败', false);
};
// 展示上传进度
const handleProgress = (event: any, file: FileListType) => {
fileList.value = UploadAudio.handleProgress(file, fileList.value, false);
};
// 处理上传成功
const handleSuccess = (res: any, file: FileListType) => {
};
// 处理提交
const handleSubmit = () => {
(ctx as any).refs.upload.submit();
};
// 上传之前
const beforeUpload = (file: FileListType) => {
};
// 重新上传
const handleUploadAgain = (file: FileListType, index: number) => {
};
return {
handleTips,
beforeUpload,
handleUploadAgain,
handleSuccess,
handleProgress,
handleError,
handleSubmit
};
}
在复杂业务场景中,我们可以把业务组件逻辑代码进行进一步拆分:视图逻辑与非视图业务逻辑
1、我们把文件选择的非视图逻辑抽象为类ChoiseAudio
在选择文件过程中对数据的计算处理、文件的处理等
class ChoiseAudio {
/**
*
* @param file
* @param fileList
* @description 初始化音视频数据
*/
static fileListDataInit(file: FileListType, fileList: any): void {
file.type = '1';
file.playStat = 'pause';
fileList.value.push(file);
}
/**
*
* @param file
* @returns
* @description 创建audioElement
*/
static createAudio(file: FileListType): HTMLMediaElement {
const url = URL.createObjectURL(file.raw);
const audioElement = new Audio(url);
audioElement.setAttribute('src', url);
return audioElement;
}
/**
*
* @param fileList
* @description 删除
*/
static deleteFile(fileList: any, uid: number): FileListType[] {
fileList.forEach((item: FileListType, index: number) => {
if (item.uid === uid) {
fileList.splice(index, 1);
}
});
return fileList;
}
/**
*
* @param audioElement
* @param fileList
* @param file
* @description audio 标签加载音频文件并获取音频文件时长
*
*/
static handleAudioUrl(
audioElement: HTMLMediaElement,
fileList: any,
file: FileListType
): any {
}
/**
*
* @param file
* @returns
* @description 获取文件类型
*/
static getFileType(file: FileListType): string {
const startIndex = file.name.lastIndexOf('.');
const type = file.name
.substring(startIndex + 1, file.name.length)
.toLowerCase();
return type;
}
/**
*
* @param currentIndex
* @param fileList
* @param el
* @param file
* @param index
* @description 播放
*/
static handleAudioPlay(
currentIndex: number,
fileList: any,
el: HTMLMediaElement,
file: FileListType,
index: number
): void {
}
/**
*
* @param el
* @param fileList
* @param index
* @description 暂停
*/
static handleAudioPause(
el: HTMLMediaElement,
fileList: any,
index: number
): void {
el.pause();
(fileList.value as FileListType[])[index].playStat = 'pause';
}
}
2、我们把文件上传的非视图逻辑抽象为类UploadAudio
在上传的过程中数据处理:
class UploadAudio {
/**
*
* @param uid
* @param tip
* @param flag
* @param fileList
* @returns
* @description 处理fileList数据,进行提示操作
*/
static handleFileUploadTips(
uid: number,
tip: string,
flag: boolean,
fileList: any
): FileListType[] {
const newFileList = fileList.map((val: FileListType, index: number) => {
if (val.uid === uid) {
fileList[index].tip = tip;
if (flag) {
fileList[index].class = 'fail';
} else {
fileList[index].class = '';
}
}
return val;
});
return newFileList;
}
/**
*
* @param file
* @param fileList
* @param flag
* @returns
* @description 处理进度条
*/
static handleProgress(
file: FileListType,
fileList: any,
flag: boolean
): FileListType[] {
const fileItem = fileList.find(
(item: FileListType) => item.uid === file.uid
);
// console.log(fileItem.percentage, file.percentage);
if (fileItem) {
fileItem.status = flag ? 'success' : 'uploading';
fileItem.percentage = flag ? 100 : file.percentage;
}
return fileList;
}
}
业务开发中要遵循单一职责、逻辑关注点分离、逻辑视图分离原则,这样子开发出来的代码也不会那么乱,质量自然会高很多,而且如此设计对单元测试来说真的不能再友好了(测试视图复杂,测试逻辑还是简单多的),如果每一个函数都有返回值那就更好了。
以上是我的项目总结,如果你对vue3有更好的实践,欢迎留言。
vue官网中关于逻辑关注点分离:https://vue3js.cn/docs/zh/guide/composition-api-introduction.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BB%84%E5%90%88%E5%BC%8F-api
vue组件分离UI与业务逻辑:https://cloud.tencent.com/developer/article/1694043