Оптимизация загрузки файлов большого формата

Node.js задняя часть
Оптимизация загрузки файлов большого формата

В процессе разработки я получил такие отзывы, что загрузка файлов более 100 МБ на сайт часто не удалась, а повторная попытка занимает много времени, что затрудняет загрузку больших файлов для пользователей. Так что же нужно сделать, чтобы закачать быстро, даже если снова не получится отправить, может продолжить загрузку с того места, где она была прервана в прошлый раз? Вот вам ответ~

Напоминание: сотрудничатьИсходный код демоЛучше читать вместе

вся идея

Первым шагом является исследование более оптимизированного решения на основе истории проекта. Ошибка при загрузке файла является распространенной проблемой. Распространенным решением является нарезка большого файла на несколько небольших файлов и запрос на параллельную загрузку интерфейса. После того, как на все запросы будут даны ответы, все нарезанные файлы объединяются на стороне сервера. Когда частичная загрузка не удалась, об этом можно судить при повторной загрузке, и загружается только та часть, которая не удалась в прошлый раз, что сокращает время ожидания пользователя и снижает нагрузку на сервер. Это составной загружаемый файл.

Загрузка большого файла

Итак, как добиться загрузки больших файлов по частям?

Блок-схема выглядит следующим образом:

图片

Разделить на следующие шаги для достижения:

1. Шифрование файлов MD5

MD5 — это уникальный идентификатор файла, вы можете использовать MD5 файла для запроса статуса загрузки файла.

В зависимости от времени модификации файла, имени файла, времени последней модификации и другой информации, черезspark-md5MD5 сгенерированного файла. Следует отметить, что файлы большого размера необходимо читать сегментами, а содержимое прочитанного файла добавляется вspark-md5При вычислении хэша, пока файл не будет прочитан, и, наконец, верните окончательный хэш-код в функцию обратного вызова обратного вызова. Здесь вы можете добавить индикатор выполнения для чтения файла по мере необходимости.

图片

Метод реализации следующий:

// 修改时间+文件名称+最后修改时间-->MD5
md5File (file) {
  return new Promise((resolve, reject) => {
    let blobSlice =
      File.prototype.slice ||
      File.prototype.mozSlice ||
      File.prototype.webkitSlice
    let chunkSize = file.size / 100
    let chunks = 100
    let currentChunk = 0
    let spark = new SparkMD5.ArrayBuffer()
    let fileReader = new FileReader()
    fileReader.onload = function (e) {
      console.log('read chunk nr', currentChunk + 1, 'of', chunks)
      spark.append(e.target.result) // Append array buffer
      currentChunk++
      if (currentChunk < chunks) {
        loadNext()
      } else {
        let cur = +new Date()
        console.log('finished loading')
        // alert(spark.end() + '---' + (cur - pre)); // Compute hash
        let result = spark.end()
        resolve(result)
      }
    }
    fileReader.onerror = function (err) {
      console.warn('oops, something went wrong.')
      reject(err)
    }
    function loadNext () {
      let start = currentChunk * chunkSize
      let end =
        start + chunkSize >= file.size ? file.size : start + chunkSize
      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
    }
    loadNext()
  })
}

2. Запросить статус файла

После того, как внешний интерфейс получает MD5 файла, он запрашивает из фона, существует ли файл с именемMD5Папка, если присутствует, перечислены в папке и все файлы, получите список разделов были загружены, и если нет, список уже загруженных ломтиков пуст.

已上传分片列表

// 校验文件的MD5
checkFileMD5 (file, fileName, fileMd5Value, onError) {
  const fileSize = file.size
  const { chunkSize, uploadProgress } = this
  this.chunks = Math.ceil(fileSize / chunkSize)
  return new Promise(async (resolve, reject) => {
    const params = {
      fileName: fileName,
      fileMd5Value: fileMd5Value,
    }
    const { ok, data } = await services.checkFile(params)
    if (ok) {
      this.hasUploaded = data.chunkList.length
      uploadProgress(file)
      resolve(data)
    } else {
      reject(ok)
      onError()
    }
  })
}

3. Фрагмент документа

Основой оптимизации загрузки файлов является нарезка файла. Метод slice в объекте Blob может разрезать файл. Объект File наследует объект Blob, поэтому объект File также имеет метод slice.

Определите переменную размера каждого нарезанного файла как chunkSize, получите количество фрагментов фрагментов через размер файла FileSize и размер фрагмента chunkSize, используйте цикл for и метод file.slice() для нарезки файла, серийный номер 0 - n и Сравните загруженные списки фрагментов, получите все незагруженные фрагменты и поместите их в список запросов requestList.

图片

async checkAndUploadChunk (file, fileMd5Value, chunkList) {
  let { chunks, upload } = this
  const requestList = []
  for (let i = 0; i < chunks; i++) {
    let exit = chunkList.indexOf(i + '') > -1
    // 如果已经存在, 则不用再上传当前块
    if (!exit) {
      requestList.push(upload(i, fileMd5Value, file))
    }
  }
  console.log({ requestList })
  const result =
    requestList.length > 0
      ? await Promise.all(requestList)
        .then(result => {
          console.log({ result })
          return result.every(i => i.ok)
        })
        .catch(err => {
          return err
        })
      : true
  console.log({ result })
  return result === true
}

4. Загрузить осколки

Вызовите Promise.all для одновременной загрузки всех фрагментов и передачи серийного номера фрагмента, файла фрагмента и файла MD5 в фоновом режиме.

После получения запроса на загрузку в фоновом режиме сначала проверьте имя как文件 MD5Существует ли папка, создайте папку, если она не существует, а затем передайтеfs-extraМетод переименования, который перемещает фрагменты с временного пути в папку фрагментов, приводит к следующему:

上传分片

Когда все части успешно загружены, сервер будет уведомлен о необходимости их объединения. Если одна часть не может быть загружена, он предложит "Ошибка загрузки". При повторной загрузке статус загрузки файла получается через файл MD5.Когда на сервере уже есть слайс, соответствующий MD5, это означает, что слайс был загружен и его не нужно загружать повторно. не может найти слайс, соответствующий MD5, это означает, что слайс слайса необходимо загрузить.Пользователям нужно загрузить только эту часть слайса, а весь файл можно загрузить полностью.Это возобновление файла.

图片

// 上传chunk
upload (i, fileMd5Value, file) {
  const { uploadProgress, chunks } = this
  return new Promise((resolve, reject) => {
    let { chunkSize } = this
    // 构造一个表单,FormData是HTML5新增的
    let end =
      (i + 1) * chunkSize >= file.size ? file.size : (i + 1) * chunkSize
    let form = new FormData()
    form.append('data', file.slice(i * chunkSize, end)) // file对象的slice方法用于切出文件的一部分
    form.append('total', chunks) // 总片数
    form.append('index', i) // 当前是第几片
    form.append('fileMd5Value', fileMd5Value)
    services
      .uploadLarge(form)
      .then(data => {
        if (data.ok) {
          this.hasUploaded++
          uploadProgress(file)
        }
        console.log({ data })
        resolve(data)
      })
      .catch(err => {
        reject(err)
      })
  })
}

5. Загрузить прогресс

Хотя пакетная загрузка, состоящая из нескольких частей, намного быстрее, чем одиночная загрузка больших файлов, все же существует период времени загрузки.В это время следует добавить подсказку о ходе загрузки, чтобы отображать ход загрузки файла в режиме реального времени.

XMLHttpRequest нативного Javascript предоставляет событие прогресса, которое возвращает загруженный размер и общий размер файла. Использование проектаaxiosИнкапсулировать ajax, который можно добавить в конфигonUploadProgressметод для отслеживания процесса загрузки файла.

上传进度

const config = {
  onUploadProgress: progressEvent => {
    var complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
  }
}
services.uploadChunk(form, config)

6. Объединить осколки

После загрузки всех файловых сегментов интерфейс активно информирует сервер о слиянии, а когда сервер получает запрос, он активно объединяет сегменты и находит папку с таким же именем в пути загрузки файлов сервера через файл MD5. . Как видно из вышеизложенного, фрагменты файлов именуются в соответствии с порядковым номером фрагмента, а интерфейс загрузки фрагментов является асинхронным, и нет гарантии, что фрагменты, полученные сервером, будут склеены в запрошенном порядке. Таким образом, вы должны отсортировать по имени файла перед объединением раздробленных файлов в папке, а затем передатьconcat-filesОбъедините разделенные файлы, чтобы получить файлы, загруженные пользователем. На этом загрузка большого файла завершена.

merge

图片

Код стороны узла:

// 合并文件
exports.merge = {
  validate: {
    query: {
      fileName: Joi.string()
        .trim()
        .required()
        .description('文件名称'),
      md5: Joi.string()
        .trim()
        .required()
        .description('文件md5'),
      size: Joi.string()
        .trim()
        .required()
        .description('文件大小'),
    },
  },
  permission: {
    roles: ['user'],
  },
  async handler (ctx) {
    const { fileName, md5, size } = ctx.request.query
    let { name, base: filename, ext } = path.parse(fileName)
    const newFileName = randomFilename(name, ext)
    await mergeFiles(path.join(uploadDir, md5), uploadDir, newFileName, size)
      .then(async () => {
        const file = {
          key: newFileName,
          name: filename,
          mime_type: mime.getType(`${uploadDir}/${newFileName}`),
          ext,
          path: `${uploadDir}/${newFileName}`,
          provider: 'oss',
          size,
          owner: ctx.state.user.id,
        }
        const key = encodeURIComponent(file.key)
          .replace(/%/g, '')
          .slice(-100)
        file.url = await uploadLocalFileToOss(file.path, key)
        file.url = getFileUrl(file)
        const f = await File.create(omit(file, 'path'))
        const files = []
        files.push(f)
        ctx.body = invokeMap(files, 'toJSON')
      })
      .catch(() => {
        throw Boom.badData('大文件分片合并失败,请稍候重试~')
      })
  },
}

Суммировать

В этой статье описываются некоторые методы загрузки крупномасштабных файлов, которые сводятся к следующим 4 пунктам:

  1. BLOB.SLICE SEESERS файл и отправляет множество прорези и сообщает серверу, чтобы объединить все загрузку нарезки, реализовать большую загрузку фрагмента файлов;
  2. OnProgress нативных XMLHTTTPREQUEST контролирует прогресс загрузки нарезки и получает прогресс загрузки файла в режиме реального времени;
  3. spark-md5 вычисляет MD5 файла в соответствии с содержимым файла и получает уникальный идентификатор файла, привязанный к статусу загрузки файла;
  4. Запросите список загруженных фрагментов через файл MD5 перед загрузкой фрагмента и загрузите только фрагменты, которые не были загружены до загрузки, чтобы возобновить загрузку с точки останова.

Ссылаться наИсходный код демоВы можете быстро начать работу с вышеуказанными функциями, я надеюсь, что эта статья поможет вам, спасибо за чтение ❤️


Добро пожаловать в блог Bump Labs:aotu.io

Или обратите внимание на официальный аккаунт AOTULabs и время от времени публикуйте статьи:

image