File uploads are a common feature in everyday work. When using a UI framework, it is common to use packaged functional components, but there are inevitably times when components do not fully meet our needs.

background

One day, I got up early and came to work excitedly. I was just ready to touch fish. When I clicked the happy website with my mouse, the product manager sat beside me with a small bench. He began to say: “Now in our system, when uploading files, the file is too big and users need to wait. The mask of the pop-up layer covers the whole page, and users can not carry out any operation, but can only continue to operate after the file is uploaded. My mind was racing, and I just had to remove the layer. The product said: No, I don’t want this. Can users see the progress and do other operations when uploading files? Can there be follow-up operations after uploading files? 10, 000 alpacas are galloping inside. This is not finished, the product continues: the progress of the files uploaded by users in module A and module B can be seen, but I finally accepted the requirements and began to conceive.

Program planning

Existing functionality

As a whole, the project uses the system framework based on Vue2.0 + Element-UI. Check the existing file uploading function. The existing content is the uploading function completed by the el-Upload component, and the existing function is uploading to the background server instead of Ali OSS. Therefore, we plan to upload this part by ourselves instead of relying on el-upload itself for the convenience of easy modification of the program in the future. The selection part of the file will still use el-upload and the rest will be completely redone.

Need to sort out

The requirements put forward by the product manager are mainly divided into the following key contents:

  1. Users can perform other operations when uploading files without waiting for the result
  2. When uploading files, users can view the progress in real time
  3. Follow-up operations can be performed if the file is successfully uploaded
  4. Batch upload
  5. Upload files by task

According to the above points draw flow chart, planning procedures ready to start:

Now that we have a general outline of the flow chart program, the next step is to find a function through the program implementation.

Function implementation

El-progress is used for the progress bar, and Vue EventBus is used for the EventBus.

First, we need to define the MyUpload component, because it needs to be seen when opening any module. We put the component in the root page of the system home page, so that other pages can not see the component.

<! Progress bar show and hide animation -->
<transition name="slide">
    <! - the outer - >
    <div class="index-upfile-progress"
          v-progressDrag
          v-if="isShowProgress"
          @click.native.stop="onUpFileProgressClick"
          :title="currentUploadFileName">
      <! -- Display upload list -->
      <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">{{[" wait ", "cross", "uploaded successfully," "upload failed", "file error"] [the fileInfo. Status | | 0]}}</p>
                </div>
              </div>
            </div>
          </div>
          <template slot="reference">
            <! -- Display upload progress -->
            <el-progress type="circle"
                        :percentage="percentage" 
                        width="45"
                        :status="isSuccess? 'success':''"
                        @mouseleave.native="onProgressMouseLeave"
                        @mouseenter.native="onProgressMouseEnter"></el-progress>
          </template>
      </el-popover>
    </div>
</transition>
Copy the code

The overall structure is like this, style is not shown here, for a qualified front end, style is everywhere, I blend with style, ha ha ha. Now that the structure is out, it’s time to add logic to the existing content.

The convenience program can be normally carried out and written, here needs to be completed first, send upload task, that is, upload component part of the content, do not write the relevant HTML structure, related content you can refer to the element-UI related components.

export default {
    methods: {
        onUploadFile(){
            const { actions } = this;
            const { uploadFiles } = this.$refs.upload;
            // The file data in the component is no longer retained
            this.$refs.upload.clearFiles();
            this.$bus.$emit("upFile", {files: [...uploadFiles],    // List of files to upload
                actions,        // Upload the address
                moduleId: "Module id".moduleName: "Module name".content: {} // Carry parameters}); }}}Copy the code

The list of files to be uploaded can be obtained from the uploadFiles in the component instance in el-upload. In order to avoid secondary file selection, the file selected for the first time is still saved in the component, you need to call the clearFiles method of the component instance to clear the file list cached in the existing component.

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

Subscribe to events when the MyUpload component is initialized to receive parameters, and destroy events when the component is destroyed. Through Bus, it is now easy to get the files that need to be uploaded and the corresponding parameters in the uploaded files.

export default {
    data(){
        return {
            // Whether to display the upload list
            visible: false.// Upload file task list
            filesList: [].// Displays a progress bar
            isShowProgress: false.// Progress bar progress
            percentage: 0./ / timer
            timer: null.// Whether all uploads are complete
            isSuccess: false.// Whether a file is being uploaded
            isUpLoading: false.// The name of the file being uploaded
            currentUploadFileName: ""}},methods: {
        async handleUploadFiles(data){
            // Unique message
            const messageId = this.getUUID();
            data.messageId = messageId;
            this.filesList.push(data);
            // Organize file upload list display
            this.uResetUploadList(data);
            this.isSuccess = false;
            // Do not perform the following operations if a file is being uploaded
            if(this.isUpLoading) return;
            // Displays a progress bar
            this.isShowProgress = true;
            // Record when kiss
            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)})}}}Copy the code

Since the file upload task is batch, you should have a separate ID for each message, so that even files uploaded by the same module will not be messed and corrupted.

The next step is to render the list of upload tasks, considering that when the File is uploaded, the message list should not store the content of the File object, and the message list needs to be retrieved from the upload list in the corresponding module, so the upload display list needs to be stored in Vuex.

import { mapState } from "vuex";
export default {
    computed: {
        ...mapState("upload", {upList: (state) = > {
                returnstate.upList; }})},methods: {
        uResetUploadList(data){
            // Upload the display task list
            const { upList } = this;
            // Module name, module ID, file list, upload address, carry parameter, message ID
            const { moduleName, moduleId, files = [], actions, content, messageId } = data;
            const uplistItem = {
                moduleName,
                moduleId,
                actions,
                content,
                messageId,
                isDealWith: false.// Whether the message has been processed
                isUpload: false.// Whether the upload is complete
                children: files.map(el= > ({    // File upload result
                    name: el.name,
                    status: 0.result: {}}}));this.$store.commit("upload/addUpload",[...upList, uplistItem]); }}},Copy the code

After the list of uploaded files is displayed, the next step is to upload the core content of the entire component. Since the task is a node at the time of uploading files, the next task can be executed only after one task is completed.

import ajax from "@/utils/ajax";

export default {
    methods: {
        async upLoadFile(){
            // Execute the loop
            while(true) {// Retrieve the upload task
                const fileRaw = this.filesList.shift();
                const { actions, files,  messageId, content, moduleId } = fileRaw;
                const { upList, onProgress } = this;
                // Retrieve the corresponding information in the corresponding display list
                const upListItem = upList.find(el= > el.messageId === messageId);
                // Loop through the list of files to upload
                for(let i = 0,file; file = files[i++];) {// If the corresponding information in the corresponding list does not exist, skip the current loop
                    if(! upListItem)continue;
                    // Set the state to upload
                    upListItem.children[i - 1].status = 1;
                    try{
                        // Perform upload
                        const result = await this.post(file, { actions, content, onProgress });
                        if(result.code === 200) {// Set the status to upload succeeded
                            upListItem.children[i - 1].status = 2;
                        }else{
                            // Upload failed
                            upListItem.children[i - 1].status = 4;
                        }
                        // Store the upload result
                        upListItem.children[i - 1].result = result;
                    }catch(err){
                        // Upload error
                        upListItem.children[i - 1].status = 3;
                        upListItem.children[i - 1].result = err; }}// Setup upload succeeded
                upListItem.isUpload = true;
                // Update the display list
                this.$store.commit("upload/addUpload",[...upList]);
                // Task complete, send message, the module name is the event name
                this.$bus.$emit(moduleId,{ messageId });
                // No upload task, out of the loop
                if(!this.filesList.length){
                  break; }}},async post(file, config){
            const { actions, content = {}, onProgress } = config;
            // Upload the file
            const result = await ajax({
                action: actions,
                file: file.raw,
                data: content,
                onProgress
            });
            return result;
        },
        onProgress(event,){
            // Upload progress
            const { percent = 100 } = event;
            this.percentage = parseInt(percent);
        },
        delyHideProgress(){
            // Delay to hide progress
            this.timer = setTimeout(() = > {
                this.isShowProgress = false;
                this.visible = false;
                this.percentage = 0;
            },3000); }}}Copy the code

Except for the ajax part of uploading files, the specific content of uploading files has been completed in task execution. For the Ajax part, it is also possible to upload files directly using AXIOS. In order to facilitate better function expansion in the future, manual encapsulation is adopted here.

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) {
    returntext; }}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); }})})}Copy the code

The next step is to refine the details, when all the tasks are done and the user wants to see the list of uploads, it’s not a good idea to suddenly hide them, so there’s an event limit. Also, click on the progress bar to display the list of uploads.

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); }}}Copy the code

As a qualified for the front, to give yourself a demand, of course, this only then to be perfect, in order to when there is an upload progress doesn’t keep out the data on the page, so you need to add drag and drop, when used to solve the problem here, custom instructions to complete the elements of the drag and drop, so in later will be relatively easy for you to expand a lot.

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); }}}}Copy the code

About the component that uploads the file, basically already close to the end, next is to receive the business side, after the function realizes, the docking business side is much simpler, after all, the component is written by oneself to the function is clear, do not need to see what document.

export default {
    methods: {// messageId messageId used to filter messages
        handleUploadFiles({ messageId }){
            // Business logic
        },
        uReadUploadTask(){
            // The user cannot get the event notification when closing the module
            // Re-open and re-test the task}},async mounted(){
        // The event name is dead, the same as the module ID
        this.$bus.$on("Event Name".this.handleUploadFiles);
        await this.$nextTick();
        this.uReadUploadTask();
    },
    destroyed(){
      this.$bus.$off("Event Name".this.handleUploadFiles); }}Copy the code

The entire component is done, from the initial event firing and the entire upload process to the final component docking. Although the entire component is a global component, not using VuEX for a global component is not particularly elegant for reusability, and a better solution has not been found. If you have any ideas, feel free to discuss them in the comments section.

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">{{[" wait ", "cross", "uploaded successfully," "upload failed", "file error"] [the 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) = > {
        returnstate.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>
Copy the code

Thank you for reading this article. If you have any questions, please leave a comment below and I will correct them in time.