广州总部电话:020-85564311
广州总部电话:020-85564311

广州网站建设-小程序商城开发-广州小程序开发-企业微信开发公司-网站建设高端品牌-优网科技

20年
互联网应用服务商
请输入搜索关键词
知识库 知识库

优网知识库

探索行业前沿,共享知识宝库

一次性上传 1000 张图片, 总量 10GB 的方案设计
发布日期:2025-04-08 08:53:39 浏览次数: 833 来源:web前端开发之旅

作者:小识谭记

https://juejin.cn/post/7457474063633006643


背景

最近有一个上传文件方面的需求,上传图片,用户可以选择文件夹上传。

文件夹里的图片可能很多,而且由于特殊的项目背景,用户选择的图片可能会比较大,10MB左右。

这里就需要做一些方案上的设计了,确保整个过程的流畅和容错。

方案设计

这个过程中需要考虑到的细节主要是以下几点:

  • 重复图片的认定
  • 图片上传任务的控制
  • 上传失败和上传中断处理

图片去重

hash

图片去重这一块,我们可以在前端做hash,但是对于10MB左右的图片,走hash的话,每一张大概要花费30-60ms不等的时间,如果每张图片都先算hash,1000张这样大小的图片就要等待30s~60s,然后才能开始进行上传任务,我们觉得这个等待时间有点久了。

文件路径 + 文件名 + 文件大小

由于我们的项目支持文件夹选择上传,再递归遍历文件夹的过程中,我们是可以拿到图片的相对路径的,再加上图片的文件名,以及文件的size,我们决定就将这三者条件的联合作为图片是否上传过的对比凭证。

那么这样一来,在选择了文件之后,就可以立马得到这些信息,那么此时就可以把整个文件列表传给后端,后端返回哪些文件重复,哪些文件不重复。

这样就能快速拿到我们需要进行上传的图片列表了。

对文件做浏览器持久化缓存

为什么要在前端做持久化的缓存?

多文件的上传任务是一个耗时的任务,这个过程可能会出现不确定的情况,比如网页关了,或者浏览器关了,而前端无法记住文件在客户机器上的位置,如果没有持久化的存储,那么用户就必须手动再去选择一下文件或者文件夹,用户体验没那么友好。

那么我们就需要一个像localStorage那样的存储,浏览器关了也不会消失的存储。

既然这样说了,那localStorage肯定是不行的,因为它无法保存file类型的数据,也就是无法保存文件,就算能保存图片,大小也不够,5M左右的大小,都装不下一张图片。

我们用indexDB来做文件的存储。

上传过程的并发控制

前面两步设计好了,下面就该发上传的请求了,1000张图片,不可能走一个请求的,我们让每个图片走一个上传请求,也就是发1000个请求来上传这些图片。

如果用的是HTTP1.1,就要考虑并发控制的问题了,因为你不可能1000个请求一次性无脑发出去,那么这样,用户就必须等1000个上传任务完成,才能操作页面,因为接下来的网络请求都要排队,要等1000个图片上传完。

我们的设计是,上传过程中,用户不用干等,可以比较流畅的进行其他操作,留一个任务列表展示当前的任务状态。

如果用的是HTTP2.0, 虽然可以不用做并发控制,浏览器会把其他的请求提前,但是也要等当前的某个请求结束,因为并发数也是有限的,实测每个可能会等待个3s以内。

而且因为这个任务的主要瓶颈是1000个10MB图片的上传,是带宽。所以HTTP2.0的优势,多路复用,头部压缩,并不能很好的体现出来,也就是说总任务时长差不多。所以我们最终没有升级成HTTP2.0,还是用的HTTP1.1。

对HTTP1.1,chrome浏览器对同一个域名的并发数最大是6,所以并发数可以设置为5,留一点网络资源给前台任务。

失败和中断处理

失败和中断的任务主要还是依赖了本地持久化存储。

我们在第一次拿到后端返回的需要上传的文件列表的时候,就应该把这个文件列表加上文件本身存进indexDB中,然后做上传,成功上传一个,就从indexDB中删除一条,那么最后留下来的,就是没有成功的。

当然也可以在上传任务中加入catch,这样可以对失败的任务做自动重传,这个不是必须的。

部分实现

对indexDB操作的封装


classFileStorage{
  private db: IDBDatabase | null = null
  private dbName = 'fileStore'
  private storeName = 'files'
  private openCallback: () =>void

constructor(open = () => {}) {
    this.openCallback = open
    this.openDatabase()
  }

// 打开或创建数据库
  private openDatabase(): void {
    const request = indexedDB.open(this.dbName, 1)

    request.onupgradeneeded = (event) => {
      this.db = (event.target as IDBOpenDBRequest).result
      if (!this.db.objectStoreNames.contains(this.storeName)) {
        this.db.createObjectStore(this.storeName, {
          keyPath'id',
          autoIncrementtrue
        })
      }
    }

    request.onsuccess = (event) => {
      this.db = (event.target as IDBOpenDBRequest).result
      this.openCallback()
    }

    request.onerror = (event) => {
      console.error('Error opening database:', event)
    }
  }

// 插入整个 fileList 数组
  public async insertFileList(fileList: fileInfoWithId[]): Promise<void> {
    const fileContents = awaitPromise.all(
      fileList.map((file) =>this.readFileAsArrayBuffer(file.raw))
    )

    awaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      for (let i = 0; i < fileList.length; i++) {
        const file = fileList[i]
        const content = fileContents[i]
        awaitthis.insertObjectStore(objectStore, { ...file, content })
      }
    })
  }

// 插入单个 fileInfo
  public async insertFile(fileInfo: fileInfoWithId): Promise<void> {
    this.insertFileList([fileInfo])
  }

// 读取文件为 ArrayBuffer
  private readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
    returnnewPromise((resolve, reject) => {
      const reader = new FileReader()
      reader.readAsArrayBuffer(file)
      reader.onload = () => resolve(reader.result asArrayBuffer)
      reader.onerror = (error) => reject(error)
    })
  }

// 插入对象到 objectStore
  private async insertObjectStore(
    objectStore: IDBObjectStore,
    file: fileInfoWithId & { contentArrayBuffer }
  ): Promise<void> {
    returnnewPromise((resolve, reject) => {
      const request = objectStore.add(file)
      request.onsuccess = () => resolve()
      request.onerror = (error) => reject(error)
    })
  }

// 根据 ID 列表删除数据
  public async deleteByIds(ids: number[]): Promise<void> {
    awaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      for (const id of ids) {
        const request = objectStore.delete(id)
        awaitthis.waitForRequest(request)
      }
    })
  }

// 根据单个 ID 删除数据
  public async deleteById(id: number): Promise<void> {
    awaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      const exists = awaitthis.checkIfExists(objectStore, id)
      if (!exists) {
        console.warn(`No file found with id ${id}`)
        return
      }
      console.log(`Deleting file with id ${id}`)
      const request = objectStore.delete(id)
      awaitthis.waitForRequest(request)
    })
  }

// 检查是否存在指定 ID 的数据
  private async checkIfExists(
    objectStore: IDBObjectStore,
    id: number
  ): Promise<boolean> {
    returnnewPromise((resolve) => {
      const getRequest = objectStore.get(id)

      getRequest.onsuccess = () => {
        resolve(getRequest.result !== undefined// 如果 result 是 undefined,说明没有找到该 ID
      }

      getRequest.onerror = (error) => {
        console.error('Check existence error:', error)
        resolve(false)
      }
    })
  }

// 查找所有数据
  public async findAll(): Promise<fileInfoWithId[]> {
    returnawaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      const request = objectStore.getAll()
      returnawaitthis.waitForRequest(request)
    })
  }

// 清空整个对象存储中的所有数据
  public async clearDatabase(): Promise<void> {
    awaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      const request = objectStore.clear()

      awaitthis.waitForRequest(request)
      console.log('Database cleared successfully')
    })
  }

// 带分页的查找功能
  public async findWithPagination(
    page: number,
    pageSize: number
  ): Promise<fileInfoWithId[]> {
    returnawaitthis.transaction(async (transaction) => {
      const objectStore = transaction.objectStore(this.storeName)
      const start = (page - 1) * pageSize
      const result: fileInfoWithId[] = []
      let currentIndex = 0

      const cursorRequest = objectStore.openCursor()

      returnnewPromise<fileInfoWithId[]>((resolve, reject) => {
        cursorRequest.onerror = (error) => reject(error)

        cursorRequest.onsuccess = (event) => {
          const cursor = (event.target as IDBRequest).result
          if (cursor) {
            if (currentIndex >= start && currentIndex < start + pageSize) {
              result.push(cursor.value)
            }
            if (currentIndex < start + pageSize) {
              currentIndex++
              cursor.continue()
            } else {
              resolve(result)
            }
          } else {
            resolve(result)
          }
        }
      })
    })
  }

// 创建事务并执行回调
  private async transaction<T>(
    callback: (transaction: IDBTransaction) =>Promise<T>
  ): Promise<T> {
    returnnewPromise((resolve, reject) => {
      if (!this.db) {
        reject(newError('Database not initialized'))
        return
      }

      const transaction = this.db.transaction([this.storeName], 'readwrite')
      callback(transaction).then(resolve).catch(reject)
      transaction.oncomplete = () => {
        console.log('Transaction completed')
      }

      transaction.onerror = (error) => {
        console.error('Transaction error:', error)
        reject(error)
      }
    })
  }

// 等待请求完成
  private waitForRequest<T>(request: IDBRequest<T>): Promise<T> {
    returnnewPromise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result)

      request.onerror = (error) => {
        console.error('Request error:', error)
        reject(error)
      }
    })
  }
}

exportdefault FileStorage

并发任务控制


classconcurrencyControl{
  private maxConcurrency: number
  private queue: any[]
  private running: number
  private onAllTasksCompleted: Function

constructor(
    maxConcurrency: number,
    onAllTasksCompleted: Function = () => {}
  ) {
    this.maxConcurrency = maxConcurrency
    this.queue = []
    this.running = 0
    this.onAllTasksCompleted = onAllTasksCompleted
  }

  addQueue(queue: Function[]) {
    this.queue.push(...queue)
    this.run()
  }

  addTask(task: Function) {
    this.queue.push(task)
    this.run()
  }

  run() {
    while (this.running < this.maxConcurrency && this.queue.length) {
      this.running++
      const task = this.queue.shift()
      Promise.resolve(task()).finally(() => {
        this.running--
        this.run()
        if (this.running === 0 && this.queue.length === 0) {
          this.onAllTasksCompleted()
        }
      })
    }
  }
}

exportdefault concurrencyControl

优网科技,优秀企业首选的互联网供应服务商

优网科技秉承"专业团队、品质服务" 的经营理念,诚信务实的服务了近万家客户,成为众多世界500强、集团和上市公司的长期合作伙伴!

优网科技成立于2001年,擅长网站建设、网站与各类业务系统深度整合,致力于提供完善的企业互联网解决方案。优网科技提供PC端网站建设(品牌展示型、官方门户型、营销商务型、电子商务型、信息门户型、DIY体验、720全景展厅及3D虚拟仿真)、移动端应用(手机站APP开发)、微信定制开发(微信官网、微信商城、企业微信)、微信小程序定制开发等一系列互联网应用服务。


我要投稿

姓名

文章链接

提交即表示你已阅读并同意《个人信息保护声明》

专属顾问 专属顾问
扫码咨询您的优网专属顾问!
专属顾问
马上咨询
联系专属顾问
联系专属顾问
联系专属顾问
扫一扫马上咨询
扫一扫马上咨询

扫一扫马上咨询

和我们在线交谈!