产品说(你在系统中添加一个全局文件上传)
在平时工作过程中,文件上传是一个再平常不过的功能了。如果使用UI
框架的情况下通常使用已经封装好的功能组件,但是难免有的时候组件无法完全满足我们的需求。
背景事情是这个样子的,在某天早起,兴冲冲的来到工作,刚刚准备摸鱼,鼠标刚刚点开心爱的网站,产品经理搬着小板凳坐在了我旁边.就开始说:现在咱们这个系统,上传文件的时候文件太大需要用户等待,弹出层的遮罩遮住了整个页面,用户无法进行任何操作,只能等,文件上传完成之后才能继续操作。我当时大脑飞速运转,弹出层去掉就可以了。产品说:No,我不想要这样的,能不能在用户上传的时候,用户能看到进度,还能做其他的操作,当文件上传完成之后还能有后续的操作,上传文件的时候可以批量上传。内心已经1W只羊驼在奔腾。这还没有完,产品继续:用户在
A
模块上传的文件和B
模块上传的文件,进度都可以看到,无奈最终还是接下需求,开始构思。程序规划项目整体是使用的是
现有功能
Vue2.0 + Element-ui
为基础搭建的系统框架,查看现有上传文件功能,现有内容是依赖于el-upload
组件完成的上传功能,现有功能是将上传到后台服务器而非阿里OSS,考虑到后期可能会使用OSS
使用分片上传,所以上传这部分打算自己做不再依赖于el-upload
自身方便以后程序容易修改。文件选择部分仍然使用el-upload
其余部分全部重做。需求整理对于产品经理所提出的需求,其中最主要的分为了一下几个重点内容:
- 用户上传文件时可以进行其他操作,无需等待结果
- 上传文件时用户可以实时查看进度
- 文件上传成功可以进行后续操作
- 支持批量上传
- 上传文件以任务为单位
文章图片
通过流程图程序已经有了大体的轮廓,接下来就是通过程序实现找个功能。
功能实现关于进度条部分使用
el-progress
,事件总线使用则是Vue
的EventBus
本打算自己封装,无奈时间紧任务重。首先要定义
MyUpload
组件,因为需要在打开任何一个模块的时候都需要看到,把组件放到了系统首页的根页面中,这样除了首页之外的页面就无法在看到该组件了。
{{ item.moduleName }}{{ fileInfo.name }}
{{ ["等待中","上传中","上传成功","上传失败","文件错误"][fileInfo.status || 0] }}
整体结构就是这样的了,样式这里就不做展示了,对于一个合格前端来说,样式无处不在,我与样式融为一体,哈哈哈。既然结构已经出来了,接下来就是对现有内容添加逻辑。
方便程序能够正常的进行和编写,这里需要先完成,发送上传任务,也就是上传组件那部分内容,就不写相关的
HTML
结构了,相关内容大家可以参考Element-ui
相关组件。export default {
methods: {
onUploadFile(){
const { actions } = this;
const { uploadFiles } = this.$refs.upload;
//不再保留组件内中的文件数据
this.$refs.upload.clearFiles();
this.$bus.$emit("upFile",{
files: [...uploadFiles],//需要上传文件的列表
actions,//上传地址
moduleId: "模块id",
moduleName: "模块名称",
content: {} //携带参数
});
}
}
}
el-upload
中可以通过组件实例中的uploadFiles
获取到所需要上传的文件列表,为了避免二次选择文件的时候,第一次选择的文件仍然保存在组件中,需要调用组件实例的clearFiles
方法,清空现有组件中缓存的文件列表。export default {
created(){
this.$bus.$on("upFile", this.handleUploadFiles);
},
destroyed(){
this.$bus.$off("upFile", this.handleUploadFiles);
}
}
当
MyUpload
组件初始化时订阅一下对应的事件方便接收参数,当组件销毁的时候销毁一下对应的事件。通过Bus
现在可以很容易的得到所需要上传的文件以及上传文件中对应所需要的参数。export default {
data(){
return {
//是否展示上传列表
visible: false,
//上传文件任务列表
filesList: [],
//显示进度条
isShowProgress: false,
//进度条进度
percentage: 0,
//定时器
timer: null,
//是否全部上传完成
isSuccess: false,
//是否有文件正在上传
isUpLoading: false,
//正在上传的文件名称
currentUploadFileName: ""
}
},
methods: {
async handleUploadFiles(data){
//唯一消息
const messageId = this.getUUID();
data.messageId = messageId;
this.filesList.push(data);
//整理文件上传列表展示
this.uResetUploadList(data);
this.isSuccess = false;
//如果有文件正在上传则不进行下面操作
if(this.isUpLoading) return;
//显示进度条
this.isShowProgress = true;
//记录当亲
this.isUpLoading = true;
await this.upLoadFile();
this.isSuccess = true;
this.isUpLoading = false;
this.delyHideProgress();
},
getUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
}
}
由于文件上传任务是分批次的,所以应该为每一个消息设置一个独立的
id
,这样的话,即使是同一个模块上传的文件也不会发生消息的混乱和破坏了。接下来就是渲染一下上传任务的列表,这里考虑的是,当文件上传的时候,消息列表中不应该再存储
File
对象的相关内容了,而且消息列表需要再对应模块中获取到上传列表中的内容,所以需要把上传展示列表存放到Vuex
中。import { mapState } from "vuex";
export default {
computed: {
...mapState("upload",{
upList: (state) => {
return state.upList;
}
})
},
methods: {
uResetUploadList(data){
//上传展示任务列表
const { upList } = this;
//模块名称,模块id,文件列表,上传地址,携带参数,消息id
const { moduleName, moduleId, files = [], actions, content, messageId } = data;
const uplistItem = {
moduleName,
moduleId,
actions,
content,
messageId,
isDealWith: false,//消息是否已处理
isUpload: false,//是否上传完成
children: files.map(el => ({//文件上传结果
name: el.name,
status: 0,
result: {}
}))
};
this.$store.commit("upload/addUpload",[...upList, uplistItem]);
},
}
}
【产品说(你在系统中添加一个全局文件上传)】当上传文件列表展示完成之后,接下来需要处理的就是整个组件的核心内容上传文件,由于上传文件时时已任务为节点,当一个任务完成才能继续执行下一个任务。
import ajax from "@/utils/ajax";
export default {
methods: {
async upLoadFile(){
//执行循环
while(true){
//取出上传任务
const fileRaw = this.filesList.shift();
const { actions, files,messageId, content, moduleId } = fileRaw;
const { upList, onProgress } = this;
//取出对应展示列表中的对应信息
const upListItem = upList.find(el => el.messageId === messageId);
//循环需要上传的文件列表
for(let i = 0,file;
file = files[i++];
){
//如果对应示列表中的对应信息不存在,跳过当前循环
if(!upListItem) continue;
//设置状态为 上传中
upListItem.children[i - 1].status = 1;
try{
//执行上传
const result = await this.post(file, { actions, content, onProgress });
if(result.code === 200){
//设置状态为上传成功
upListItem.children[i - 1].status = 2;
}else{
//上传失败
upListItem.children[i - 1].status = 4;
}
//存储上传结果
upListItem.children[i - 1].result = result;
}catch(err){
//上传错误
upListItem.children[i - 1].status = 3;
upListItem.children[i - 1].result = err;
}
}
//设置上传成功
upListItem.isUpload = true;
//更新展示列表
this.$store.commit("upload/addUpload",[...upList]);
//任务完成,发送消息,已模块名称为事件名称
this.$bus.$emit(moduleId,{ messageId });
//没有上传任务,跳出循环
if(!this.filesList.length){
break;
}
}
},
async post(file, config){
const { actions, content = {}, onProgress } = config;
//上传文件
const result = await ajax({
action: actions,
file: file.raw,
data: content,
onProgress
});
return result;
},
onProgress(event,){
//上传进度
const { percent = 100 } = event;
this.percentage = parseInt(percent);
},
delyHideProgress(){
//延时隐藏进度
this.timer = setTimeout(() => {
this.isShowProgress = false;
this.visible = false;
this.percentage = 0;
},3000);
}
}
}
到这里除了上传文件
ajax
部分,任务执行已经文件上传的具体内容已经完成了,关于ajax
部分可以直接使用axios
进行文件上传也是可以的,这里为了方便以后更好的功能拓展,所以采用了手动封装的形式。function getError(action, option, xhr) {
let msg;
if (xhr.response) {
msg = `${xhr.response.error || xhr.response}`;
} else if (xhr.responseText) {
msg = `${xhr.responseText}`;
} else {
msg = `fail to post ${action} ${xhr.status}`;
}const err = new Error(msg);
err.status = xhr.status;
err.method = 'post';
err.url = action;
return err;
}function getBody(xhr) {
const text = xhr.responseText || xhr.response;
if (!text) {
return text;
}try {
return JSON.parse(text);
} catch (e) {
return text;
}
}function upload(option) {
return new Promise((resovle, reject) => {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
const xhr = new XMLHttpRequest();
const action = option.action;
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
option.onProgress && option.onProgress(e);
};
}const formData = https://www.it610.com/article/new FormData();
if (option.data) {
Object.keys(option.data).forEach(key => {
formData.append(key, option.data[key]);
});
}formData.append("file", option.file, option.file.name);
for(let attr in option.data){
formData.append(attr, option.data[attr]);
}xhr.onerror = function error(e) {
option.onError(e);
};
xhr.onload = function onload() {
if (xhr.status < 200 || xhr.status >= 300) {
option.onError && option.onError(getBody(xhr));
reject(getError(action, option, xhr));
}
option.onSuccess && option.onSuccess(getBody(xhr));
};
xhr.open('post', action, true);
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true;
}const headers = option.headers || {};
for (let item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item]);
}
}
xhr.send(formData);
})
}export default (option) => {return new Promise((resolve,reject) => {
upload({
...option,
onSuccess(res){
resolve(res.data);
},
onError(err){
reject(err);
}
})
})}
接下来就是完善细节部分了,当所有任务完成用户想要查看上传列表的时候,忽然隐藏了这样就不太好了,这里使用事件进行限制。还有就是点击的进度条的时候需要把上传列表展示出来。
export default {
methods: {
async onUpFileProgressClick(){
await this.$nextTick();
this.visible = !this.visible;
},
onProgressMouseLeave(){
if(this.isUpLoading) return;
this.delyHideProgress();
},
onProgressMouseEnter(){
if(this.isUpLoading) return;
clearTimeout(this.timer);
}
}
}
作为一名合格的前端来说,当然要给自己加需求,这样才完美,为了当上传进度出现时不遮挡页面上的数据,所以需要给其添加拖拽,解决这个问题这里使用的时,自定义指令完成的元素的拖拽,这样用以后拓展起来相对来说会方便很多。
expor default {
directives:{
progressDrag:{
inserted(el, binding, vnode,oldVnode){
let { offsetLeft: RootL, offsetTop: RootT } = el;
el.addEventListener("mousedown", (event) => {
const { pageX, pageY } = event;
const { offsetTop, offsetLeft } = el;
const topPoor = pageY - offsetTop;
const leftPoor = pageX - offsetLeft;
const mousemoveFn = (event)=> {
const left = event.pageX - leftPoor;
const top = event.pageY - topPoor;
RootT = top;
if(RootT <= 0) RootT = 0;
if(RootT )
el.style.cssText = `left:${left}px;
top: ${top}px;
`;
}
const mouseupFn = () => {
if(el.offsetLeft !== RootL){
el.style.cssText = `left:${RootL}px;
top: ${RootT}px;
transition: all .35s;
`;
}
document.removeEventListener("mousemove",mousemoveFn);
document.removeEventListener("mouseup", mouseupFn);
}document.addEventListener("mousemove",mousemoveFn);
document.addEventListener("mouseup", mouseupFn);
});
let { clientHeight: oldHeight, clientWidth:oldWidth } = document.documentElement;
const winResize = () => {
let { clientHeight, clientWidth } = document.documentElement;
let maxT = (clientHeight - el.offsetTop);
RootL += (clientWidth - oldWidth);
RootT += (clientHeight - oldHeight);
if(RootT <= 0) RootT = 0;
if(RootT >= clientHeight) RootT = maxT;
oldHeight = clientHeight;
oldWidth = clientWidth;
el.style.cssText = `left:${RootL}px;
top: ${RootT}px;
transition: all .35s;
`;
};
window.addEventListener("resize",winResize);
}
}
}
}
关于上传文件的组件,基本已经接近尾声了,接下来就是对接到业务方,功能实现之后,对接业务方就简单很多了,毕竟组件是自己写的对功能一清二楚,不需要再看什么文档了。
export default {
methods:{
// messageId 消息id用于过滤消息
handleUploadFiles({ messageId }){
//业务逻辑
},
uReadUploadTask(){
//用户关闭模块是无法获取到事件通知的
// 重新打开,重新检测任务
}
},
async mounted(){
//事件名称写死,和模块id相同
this.$bus.$on("事件名称", this.handleUploadFiles);
await this.$nextTick();
this.uReadUploadTask();
},
destroyed(){
this.$bus.$off("事件名称", this.handleUploadFiles);
}
}
整个组件就已经完成了,从最开始的事件触发,以及整个上传的过程,到最后组件的对接。虽然整个组件来说是一个全局组件,对于一个全局组件来说不应该使用
vuex
对于复用性来说不是特别的优雅,目前来说还没有找到一个更好的解决方案。如果有小伙伴有想法的话,可以在评论区里讨论。组件整体代码:
{{ item.moduleName }}{{ fileInfo.name }}
{{ ["等待中","上传中","上传成功","上传失败","文件错误"][fileInfo.status || 0] }}
感谢大家阅读这篇文章,文章中如果有什么问题,大家在下方留言我会及时做出改正。
推荐阅读
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 尽力
- 一个人的碎碎念
- 时间老了
- 午门传说
- 我们重新了解付费。
- 说的真好
- “不完美,才美”01(190410)
- 父母越不讲道理,孩子反而越优秀!说的是你吗()
- 我和你之前距离