The front-end implements batch packaging into compressed packages according to the file url

Question background

vue2 used by the project, there is a materialsList object in data, and there is a materialsObj in computed code> object (calculated according to materialsList), the data content is:

{<!-- -->
    "materialsList": [
      {<!-- -->"code":"CM","name":"A"},
      {<!-- -->"code":"EM","name":"B"},
      {<!-- -->"code":"LD","name":"C"},
      {<!-- -->"code":"MM","name":"D"},
      {<!-- -->"code":"NE","name":"E"},
      {<!-- -->"code":"OO","name":"F"},
      {<!-- -->"code":"PO","name":"G"},
      {<!-- -->"code":"RP","name":"H"},
      {<!-- -->"code":"WD","name":"I"},
      {<!-- -->"code":"WM","name":"J"}
    ],
  "materialsObj": {<!-- -->
    "CM":[],
    "EM":[{<!-- -->
      "attachment": {<!-- -->
        "url":"http://xxx.com/1640830559688.pdf"
      }
    }],
    "LD":[],
    "MM":[{<!-- -->
      "attachment": {<!-- -->
        "url":"http://xxx.com/1637292330025.pdf"
       }
    }],
    "NE":[{<!-- -->
      "attachment": {<!-- -->
        "url":"http://xxx.com/1637201758691.pdf"
      }
    }],
    "OO":[],
    "PO":[],
    "RP":[],
    "WD":[{<!-- -->
      "attachment": {<!-- -->
        "url":"http://xxx.com/1635848729053.pdf"
      }
    }],
    "WM":[{<!-- -->
      "attachment": {<!-- -->
        "url":"http://xxx.com/1628489193294.pdf"
      }
    }]
  }
}

Finally, a compressed package should be generated. After decompression, the content is:

myFileName/
├── B/
│ └── All files in materialsObj.EM
├── D/
│ └── All files in materialsObj.MM
├── E/
│ └── All files in materialsObj.NM
├── I/
│ └── All files in materialsObj.WD
└── J/
    └── All files in materialsObj.WM

Don’t ask me why this should be implemented by the front end, the back end said to relieve the pressure on the server

Technical implementation

Fortunately, I found a front-end library for generating zip compressed packages: JSzip, see the official website for details: https://stuk.github.io/jszip/

First introduce the relevant library in the component:

import JSZip from 'jszip'
import axios from 'axios'

Then define a method to be executed when the user clicks the “package” button:

packAll() {<!-- -->
  const fileObj = {<!-- -->} // The key name of each item in this object is a folder name in the subsequent packaging result, and the corresponding key value is the file in the folder
  Object.keys(this.materialsObj).forEach(key => {<!-- -->
    // get the file
    if (this.materialsObj[key].length > 0) {<!-- -->
      fileObj[this.materialsList.find(item => item.code == key).name] = this.materialsObj[key].map(item => {<!-- -->
        return {<!-- -->
          // Replace the content before the last "/" in the link
          // Because the project file is placed in the Tencent cloud server http://xxx.com, after deployment, the address of the front-end project is inconsistent with it, so there will be cross-domain problems. Now I debug locally
          // At this time, replace the prefix with /dev-file, and then configure the local proxy, and you can debug locally, but at this time, it will definitely not work online. The backend will tell me later
          // Provide an interface, I will pass http://xxx.com/1637292330025.pdf file address to him, and he will directly return to the corresponding Tencent cloud server for storage in the way of streaming
          // The file, in fact, is to use the server we deployed online as an intermediate proxy, which is the same as opening a proxy when I debug locally
          url: item.attachment.url.replace(item.attachment.url.match(/.*\//)[0].slice(0, -1), '/dev-file'),
          fileName: item.attachment.fileName // file name
        }
      })
    }
  })
  // console. log('fileObj', fileObj)
  if (Object. keys(fileObj). length <= 0) {<!-- -->
    this.$Message.error('There is no attachment in the current task')
  } else {<!-- -->
    this.loading = true // button loading style
    const zip = new JSZip()
    let promiseAllNum = 0 // Record the number of times Promise.all is currently executed, you can understand it later
    Object.keys(fileObj).forEach(key => {<!-- --> // traverse the keys of fileObj
      // add empty folder
      const folder = zip.folder(key); // Create a folder, at this time key is used as the name of the folder in the packaging result
      // console. log('folder:', folder);
      // Due to the data structure, I can only create a Promise.all for each folder, so in order to know when all folders are packaged, I use it here
      // Execute in the following way. The variable promiseAllNum is very important. It is used to judge the number of the currently executed Promise.all in then, and when the last one is executed
      // before the final packaging operation
      (async () => {<!-- -->
        const responses = await Promise.all(fileObj[key].map(item => axios.get(item.url, {<!-- --> responseType: 'blob' })))

        responses.forEach((res, index) => {<!-- -->
          const fileName = fileObj[key][index].fileName
          folder.file(fileName, res.data)
        })

        // Put the Promise object returned by Promise.all() to the outer layer
        return Promise. resolve()
      })().then(() => {<!-- -->
        promiseAllNum + = 1 // Every time Promise.all is executed, that is, every time a folder is created, + 1
        // After all requests are over (currently packaged is the last folder), follow-up operations, such as generating compressed packages, etc.
        if (promiseAllNum == Object. keys(fileObj). length) {<!-- -->
          // Generate and download the compressed package
          zip.generateAsync({<!-- --> type: 'blob' }).then((content) => {<!-- -->
            const downloadUrl = window.URL.createObjectURL(content)
            const link = document. createElement('a')
            link.href = downloadUrl
            link.setAttribute('download', `${<!-- -->this.$attrs['task-title']} related attachments.zip`)
            document.body.appendChild(link)
            link. click()
            // Release the URL object
            URL.revokeObjectURL(link.href)
            this.loading = false
          })
        }
      })
    })
  }
}

chatgpt explains my code:

This code uses JSZip library and axios library to realize asynchronous request for multiple files, and pack them into a ZIP compressed file and download it. Specifically, the fileObj object in the code represents a list of files with a directory structure, each of which contains a URL and a file name, the code first loops through each directory in fileObj, and in the ZIP file object zip Create a corresponding empty folder.

Then, by calling the Promise.all() method, all file download requests are merged into one Promise, and the operation continues until all requests are completed. In the callback function of Promise.all(), iterate through all the response results and add the files to the corresponding folders. When all requests are finished, generate a ZIP compressed file by calling the zip.generateAsync() method, then create a download link, and add the link to the DOM to trigger the download. Finally release the linked URL object.

It should be noted that in the above process, the async/await keyword and the Promise.resolve() method are used to return a new Promise object, which is subsequently processed through the .then() method. This method can ensure that all asynchronous requests are processed, and proceed to the next step after all requests are completed, making the code more elegant and easy to understand.

Proxy configuration defined during local debugging:

proxyTable: {<!-- -->
  '/api': {<!-- -->
    target: 'https://dev.cn',
    secure: false,
    changeOrigin: true,
    pathRewrite: {<!-- -->
      '^/api': '' // need rewrite,
    }
  },
  '/dev-file': {<!-- -->
    target: 'http://xxxx.com', // Tencent Cloud server address
    secure: false,
    changeOrigin: true,
    pathRewrite: {<!-- -->
      '^/dev-file': ''
    }
  }
}

Current defects

  1. Although a proxy is configured locally, it is definitely not possible to go online in this way. This will wait for the backend to provide an interface, and then the deployment server can be used as an intermediate proxy to request resources from the Tencent Cloud server.

  2. Now it introduces axios separately in the component, which is a bit unreasonable. In fact, this is to create a new request instance whose prefix is unified as /dev-file, and then After the client provides the interface, just use the axios instance created uniformly in the project. If you want to develop the agent locally, you can use the local proxy settings for the prefix /api.

Bug fixes

Code after fix:

 packAll() {<!-- -->
   const fileObj = {<!-- -->}
   Object.keys(this.materialsObj).forEach(key => {<!-- -->
     if (key !== 'upload' & amp; & amp; this.materialsObj[key].length > 0) {<!-- -->
       fileObj[this.materialsList.find(item => item.code == key).name] = this.materialsObj[key].map(item => {<!-- -->
         return {<!-- -->
           attachmentId: item.attachmentId, // The backend provides an interface to obtain a single file, and this file id is needed
           fileName: item.attachment.fileName
         }
       })
     }
   })
   console. log('fileObj', fileObj)
   if (Object. keys(fileObj). length <= 0) {<!-- -->
     this.$Message.error('There is no attachment in the current task')
   } else {<!-- -->
     this. $emit('update:loading', true)
     const zip = new JSZip()
     let promiseAllNum = 0
     Object.keys(fileObj).forEach(key => {<!-- -->
       // add empty folder
       const folder = zip. folder(key);
       // console. log('folder:', folder);
       (async () => {<!-- -->
       // urlConfig.baseUrl is the global api prefix
         const responses = await Promise.all(fileObj[key].map(item => axios.get(`${<!-- -->urlConfig.baseUrl}/base/attachment/file-cors?attachmentId=${< !-- -->item.attachmentId}`, {<!-- --> responseType: 'blob' })))

         responses.forEach((res, index) => {<!-- -->
           const fileName = fileObj[key][index].fileName
           folder.file(fileName, res.data)
         })

         // Put the Promise object returned by Promise.all() to the outer layer
         return Promise. resolve()
       })().then(() => {<!-- -->
         promiseAllNum += 1
         // After all requests are over, perform subsequent operations, such as generating compressed packages, etc.
         if (promiseAllNum == Object. keys(fileObj). length) {<!-- -->
           // Generate and download the compressed package
           zip.generateAsync({<!-- --> type: 'blob' }).then((content) => {<!-- -->
             const downloadUrl = window.URL.createObjectURL(content)
             const link = document. createElement('a')
             link.href = downloadUrl
             link.setAttribute('download', `${<!-- -->this.$attrs['task-title']} related attachments.zip`)
             document.body.appendChild(link)
             link. click()
             // Release the URL object
             URL.revokeObjectURL(link.href)
             this. $emit('update:loading', false)
           })
         }
       })
     })
   }
 }

This solves the first problem before, and the second problem feels unnecessary.