The product says: You add a global file upload in the system

Aaron
中文

In the process of normal work, file upload is an ordinary function. If you use the UI framework, you usually use already encapsulated functional components, but it is inevitable that sometimes the components cannot fully meet our needs.

background

Things are like this. One day, I got up early and came to work excitedly. I was just about to fish. I just clicked on my favorite website. The product manager took a small bench and sat next to me. He started to say: Now let’s do this In the system, when uploading a file, the file is too large and the user needs to wait. The pop-up layer mask covers the entire page, and the user cannot perform any operations. They can only wait until the file upload is completed before proceeding. My brain was spinning very fast, and it was enough to remove the pop-up layer. The product says: No, I don’t want this. Can the user see the progress when the user uploads it, and can do other operations? After the file upload is complete, there can be follow-up operations. When uploading the file, you can batch Upload. There are already 1W alpacas running in my heart. This is not over yet, the product continues: A module and the files uploaded by the B module, the progress can be seen, but I finally took the demand and started thinking.

Program planning

Existing function

The overall project uses the Vue2.0 + Element-ui to view the existing upload file function. The existing content is dependent on el-upload component. The existing function is to upload to the backend server instead of Ali OSS, considering In the later period, you may use OSS upload in fragments, so I plan to do this part of the upload by myself and no longer rely on el-upload itself for convenience and easy modification of the program in the future. The file selection part still uses el-upload rest is completely redone.

Demand finishing

For the requirements put forward by the product manager, the most important ones are divided into the following key contents:

  1. Users can perform other operations while uploading files without waiting for the results
  2. Users can view the progress in real time while uploading files
  3. After the file is uploaded successfully, follow-up operations can be performed
  4. Support batch upload
  5. Upload files in units of tasks

Draw a flowchart for the above points, and the planning process is ready to start:

The program has a general outline through the flowchart, and the next step is to find a function through the program.

Function realization

Regarding the progress bar part using el-progress , the event bus using Vue is EventBus originally intended to encapsulate it, but time is tight and the task is heavy.

First define the MyUpload component, because you need to see it when you open any module. Put the component on the root page of the system homepage, so that pages other than the homepage cannot see the component.

<!--进度条显示与隐藏动画-->
<transition name="slide">
    <!--外层-->
    <div class="index-upfile-progress"
          v-progressDrag
          v-if="isShowProgress"
          @click.native.stop="onUpFileProgressClick"
          :title="currentUploadFileName">
      <!--展示上传列表-->
      <el-popover v-model="visible">
          <div class="up-file-list">
            <div v-for="(item,index) of upList"
                :key="index"
                :title="item.name"
                class="module-warp">
              <h5 class="module-title">{{ item.moduleName }}</h5>
              <div>
                <div v-for="(fileInfo,j) of item.children"
                    :key="j"
                    class="up-file-item">
                  <p class="file-name">{{ fileInfo.name }}</p>
                  <p class="status">
                    {{ ["等待中","上传中","上传成功","上传失败","文件错误"][fileInfo.status || 0] }}
                </p>
                </div>
              </div>
            </div>
          </div>
          <template slot="reference">
            <!--展示上传进度-->
            <el-progress type="circle"
                        :percentage="percentage" 
                        width="45"
                        :status="isSuccess?'success':''"
                        @mouseleave.native="onProgressMouseLeave"
                        @mouseenter.native="onProgressMouseEnter"></el-progress>
          </template>
      </el-popover>
    </div>
</transition>

The overall structure is like this, the style will not be shown here. For a qualified front-end, the style is everywhere, I blend with the style, hahaha. Now that the structure is out, the next step is to add logic to the existing content.

It is convenient for the program to be carried out and written normally. Here you need to complete it first. Send the upload task, that is, the part of the upload component. The related HTML structure is not written. For related content, you can refer to the Element-ui related components.

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 may be provided through component instance uploadFiles get to the list of files to upload, select the file in order to avoid the second time, the first selected file is still saved in the component, call the component instance clearFiles method, emptied existing The list of files cached in the component.

export default {
  created(){
    this.$bus.$on("upFile", this.handleUploadFiles);
  },
  destroyed(){
    this.$bus.$off("upFile", this.handleUploadFiles);
  }
}

When the MyUpload component is initialized, subscribe to the corresponding event to facilitate receiving parameters, and destroy the corresponding event when the component is destroyed. Through Bus you can now easily get the files you need to upload and the corresponding parameters in the uploaded files.

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)
            })
        }
    }
}

Since the file upload task is divided into batches, an independent id should be set for each message. In this way, even the files uploaded by the same module will not be messed up and destroyed.

The next step is to render the list of upload tasks. The consideration here is that when the file is uploaded, File object should not be stored in the message list, and the message list needs to be obtained in the corresponding module to obtain the content in the upload list. , So you need to store the upload display list in 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]);
        },
    }
}

After the upload file list is displayed, the next thing that needs to be processed is the core content upload file of the entire component. Since the upload file is already a node, the next task can only be executed when one task is completed.

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);
        }
    }
}

In addition to uploading the file ajax , the specific content of the task execution and file upload has been completed. For the ajax part, it is also possible to directly use axios for file upload. Here, in order to facilitate better function expansion in the future, manual packaging is adopted. form.

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 = 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);
      }
    })
  })

}

The next step is to improve the details. When all the tasks are completed, the user wants to view the upload list, and suddenly hides it, which is not good. Here we use events to limit it. There is also a need to display the upload list when the progress bar is clicked.

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);
        }
    }
}

As a qualified front-end, of course, you have to add requirements to yourself. This is perfect. In order not to obscure the data on the page when the upload progress appears, you need to add drag and drop to solve this problem. Define the drag and drop of the elements completed by the instruction, so that it will be relatively convenient to expand in the future.

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);
      }
    }
  }
}

Regarding the components of uploading files, it is almost over. The next step is to dock with the business side. After the function is implemented, the docking with the business side is much simpler. After all, the components are written by themselves and the functions are clear, and there is no need to read any documents. NS.

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);
    }
}

The entire component has been completed, from the initial event triggering, and the entire upload process, to the final component docking. vuex should not be used for a global component. It is not particularly elegant for reusability, and a better solution has not yet been found. If any friends have ideas, they can discuss them in the comment area.

The overall code of the component:

<template>
  <transition name="slide">
    <div class="index-upfile-progress"
          v-progressDrag
          v-if="isShowProgress"
          @click.native.stop="onUpFileProgressClick"
          :title="currentUploadFileName">
      <el-popover v-model="visible">
          <div class="up-file-list">
            <div v-for="(item,index) of upList"
                :key="index"
                :title="item.name"
                class="module-warp">
              <h5 class="module-title">{{ item.moduleName }}</h5>
              <div>
                <div v-for="(fileInfo,j) of item.children"
                    :key="j"
                    class="up-file-item">
                  <p class="file-name">{{ fileInfo.name }}</p>
                  <p class="status">{{ ["等待中","上传中","上传成功","上传失败","文件错误"][fileInfo.status || 0] }}</p>
                </div>
              </div>
            </div>
          </div>
          <template slot="reference">
            <el-progress type="circle"
                        :percentage="percentage" 
                        width="45"
                        :status="isSuccess?'success':''"
                        @mouseleave.native="onProgressMouseLeave"
                        @mouseenter.native="onProgressMouseEnter"></el-progress>
          </template>
      </el-popover>
    </div>
  </transition>
</template>

<script>
import ajax from '@/utils/upFileAjax';
import { mapState } from "vuex";

export 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);
      }
    }
  },
  computed: {
    ...mapState("upload",{
      upList: (state) => {
        return state.upList;
      }
    })
  },
  data(){
    return {
      visible: false,
      filesList: [],
      isShowProgress: false,
      percentage: 0,
      timer: null,
      isSuccess: false,
      isUpLoading: false,
      currentUploadFileName: ""
    }
  },
  methods: {
    async onUpFileProgressClick(){
      setTimeout(() => {
        this.visible = !this.visible;
      }, 400)
    },
    onProgressMouseLeave(){
      if(this.isUpLoading) return;
      this.delyHideProgress();
    },
    onProgressMouseEnter(){
      if(this.isUpLoading) return;
      clearTimeout(this.timer);
    },
    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();
      await this.$nextTick();
      this.isSuccess = true;
      this.isUpLoading = false;
      this.delyHideProgress();
    },
    uResetUploadList(data){
      const { upList } = this;
      const { moduleName, moduleId, files = [], actions, content, messageId } = data;
      const uplistItem = {
        moduleName,
        moduleId,
        actions,
        content,
        messageId,
        isDealWith: false,
        isUpload: false,
        business: false,
        children: files.map(el => ({
          name: el.name,
          status: 0,
          result: {}
        }))
      };
      this.$store.commit("upload/addUpload",[...upList, uplistItem]);
    },
    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);
    },
    getUUID () {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
      })
    }
  },
  mounted(){
    this.$bus.$on("upFile", this.handleUploadFiles);
  },
  destroyed(){
    this.$bus.$off("upFile", this.handleUploadFiles);
  }
}
</script>

Thank you everyone for reading this article. If there is any problem in the article, I will make corrections in time if you leave a message below.

阅读 593

Easy life, happy elimination of bugs.

3.9k 声望
6.1k 粉丝
0 条评论
你知道吗?

Easy life, happy elimination of bugs.

3.9k 声望
6.1k 粉丝
文章目录
宣传栏