Руководство по реализации загрузки больших файлов и возобновления работы с точки останова — решение проблемы на собеседовании.

Как загружать большие файлы с помощью простого JavaScript?

Предисловие

Мне задали этот вопрос во время интервью, и это был вопрос онлайн-программирования. Хотя идея была правильной в то время, к сожалению, она не была полностью правильной.

В итоге пришлось долго разбираться с идеями, так как же реализовать загрузку большого файла, и как реализовать функцию возобновления закачки в выгрузке?

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

Фронтенд: Vue.js@2 + Element-ui

Бэкенд: Node.js@14 + многопартийный

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

Вся идея (фронтенд)

В большинстве статей о загрузке больших файлов через веб-интерфейс есть решения. Ядро заключается в использовании метода Blob.prototype.slice. Подобно методу среза массива, метод среза файла может возвращать фрагмент исходного файла.

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

Кроме того, из-за параллелизма может измениться порядок передачи на сервер, поэтому нам также необходимо записывать порядок для каждого чанка.

Вся идея (бэкенд)

Слияние всех чанков после их получения.

Вот еще два вопроса:

1. Когда объединять чанки, т.е. когда чанки переносятся?

2. Как объединить куски?

Первая проблема требует сотрудничества с интерфейсом. Внешний интерфейс несет информацию о максимальном количестве чанков в каждом чанке и автоматически объединяет чанки, когда сервер получает это количество чанков. Или вы можете отправить дополнительный запрос, чтобы активно уведомить сервер о слиянии фрагментов.

Второй вопрос, как объединить куски? Здесь вы можете использовать потоки чтения и записи Nodejs (readStream/writeStream) для передачи потока всех чанков в поток финального файла.

Говорить дешево, покажите мне код, тогда мы используем код для реализации вышеизложенных идей.

Реализация внешнего интерфейса

Фронтенд использует Vue.js в качестве фреймворка для разработки, который не предъявляет особых требований к интерфейсу. Можно родной. Учитывая красоту, Element-UI используется в качестве UI-фреймворка.

Управление загрузкой

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

Логика запроса

Учитывая универсальность, здесь нет сторонней библиотеки запросов, а есть простая инкапсуляция собственного XMLHttpRequest для отправки запросов:

Загрузить фрагмент

Затем, чтобы реализовать более важную функцию загрузки, загрузка должна делать две вещи:

• Разрезать файл

• Передать чанк на сервер

При нажатии кнопки загрузки вызывается createFileChunk для нарезки файла. Количество чанков зависит от размера файла. Здесь установлено значение 10 МБ, что означает, что файл размером 100 МБ будет разделен на 10 фрагментов по 10 МБ.

Используйте цикл while и метод slice в createFileChunk, чтобы поместить фрагмент в массив fileChunkList и вернуться.

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

Затем вызовите uploadChunks, чтобы загрузить все фрагменты файла, поместите фрагмент файла, хэш фрагмента и имя файла в formData, затем вызовите функцию запроса на предыдущем шаге, чтобы вернуть обещание, и, наконец, вызовите Promise.all для одновременной загрузки всех фрагментов. .

Отправить запрос на слияние

Используйте второй метод слияния чанков, упомянутый в общей идее, то есть фронтенд активно информирует сервер о слиянии.

Интерфейсная часть отправляет дополнительный запрос на слияние, а серверная часть объединяет фрагменты при получении запроса.

Бэкэнд-реализация

Используйте модуль HTTP для создания простого сервера.

Принять чанк

Используйте multiparty для обработки formData из внешнего интерфейса.

В обратном вызове multiparty.parse параметр files сохраняет файлы в formData, а параметр fields сохраняет нефайловые поля в formData.

Просмотр объекта чанка, обработанного многопартийным, путь — это путь для хранения временного файла, а размер — размер временного файла, в многостороннем документе упоминается, что можно использовать fs.rename (здесь заменено на fs.remove, потому что метод переименования fs-extra находится в Есть проблема с правами доступа на платформе Windows).

При приеме фрагментов файлов вам необходимо создать папку для временного хранения фрагментов с префиксом chunkDir и именем файла в качестве суффикса.

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

Объединить фрагменты

После получения запроса на слияние, отправленного внешним интерфейсом, сервер объединяет все фрагменты в папке:

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

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

Затем просмотрите всю папку фрагментов, создайте читаемый поток с помощью fs.createReadStream и объедините передачу с целевым файлом.

Стоит отметить, что каждый раз, когда читаемый поток будет передаваться в указанную позицию записываемого потока, которая управляется вторым параметром запуска createWriteStream, цель состоит в том, чтобы иметь возможность одновременно объединять несколько читаемых потоков в записываемый поток, так что даже если порядок потоков отличается, когда они параллельны, но также могут быть переданы в правильное место.

Следовательно, также необходимо, чтобы фронтенд предоставлял ранее установленный размер серверу при запросе, а сервер указывал начальную позицию читаемого потока в соответствии с размером.

На самом деле, вы также можете дождаться слияния предыдущего чанка перед слиянием следующего чанка, так что вам не нужно указывать местоположение, но скорость передачи будет снижена, поэтому используется метод параллельного слияния.

Затем просто не забудьте удалить фрагмент после каждого слияния и удалить папку фрагмента после объединения всех фрагментов.

На этом простая загрузка большого файла завершена, а дальше на этой основе мы расширим некоторые дополнительные функции.

Показать индикатор загрузки

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

Индикатор выполнения для одного фрагмента

XMLHttpRequest изначально поддерживает мониторинг хода загрузки, и ему нужно только отслеживать upload.onprogress. Мы передаем параметр onProgress на основе исходного запроса и регистрируем событие мониторинга для XMLHttpRequest.

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

Добавьте часть функции слушателя к исходной логике загрузки внешнего интерфейса.

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

Общий индикатор выполнения

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

Финальный дисплей выглядит следующим образом

Возобновить с точки останова

Принцип возобновления загрузки с точки останова заключается в том, что фронтенд/бэкенд должен запомнить загруженные фрагменты, чтобы ранее загруженная часть могла быть пропущена при следующей загрузке. Есть две схемы реализации функции памяти.

• Внешний интерфейс использует localStorage для записи хэша загруженного фрагмента.

• Сервер сохраняет хэш загруженного фрагмента, а передний конец получает загруженный фрагмент с сервера перед каждой загрузкой.

Первое — фронтенд-решение, второе — сервер, а у фронтенд-решения есть изъян. Если вы смените браузер, вы потеряете эффект памяти, поэтому здесь я выбираю последнее.

Сгенерировать хэш

Независимо от того, является ли это внешним интерфейсом или сервером, должен быть сгенерирован хэш файла и фрагмента. Раньше мы использовали имя файла + индекс чанка в качестве хэша чанка. Как только имя файла будет изменено, оно потеряет свою силу. На самом деле, пока содержимое файла остается неизменным, хэш не должен меняться, поэтому правильный способ — генерировать хэш на основе содержимого файла, поэтому давайте изменим правила генерации хэша.

webpack contenthash также основан на этой идее.

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

Кроме того, учитывая, что если загружается большой файл, чтение содержимого файла и вычисление хэша занимает очень много времени, а это вызовет блокировку пользовательского интерфейса и приведет к приостановленному состоянию страницы, поэтому мы используем web-worker для вычисления хеша в рабочем потоке, чтобы пользователь мог по-прежнему нормально взаимодействовать с основным интерфейсом.

При создании веб-воркера параметр представляет собой путь к js-файлу и не может быть междоменным, поэтому мы создаем отдельный файл hash.js и помещаем его в общедоступный каталог. Кроме того, в воркере не разрешен доступ к dom, но он предоставляет importScripts Функция используется для импорта внешних скриптов, через которые импортируется spark-md5.

В рабочем потоке примите фрагмент файла fileChunkList, используйте fileReader для чтения ArrayBuffer каждого фрагмента и непрерывно передайте его в spark-md5, после того, как каждый фрагмент будет рассчитан, событие выполнения отправляется в основной поток через postMessage и окончательный хэш завершается после отправки в основной поток.

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

Затем напишите логику для связи между основным потоком и рабочим потоком.

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

Плюс индикатор выполнения, который показывает хэш вычисления, выглядит так:

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

Сервер использует фиксированный префикс + хэш в качестве имени папки чанка, хеш + нижний индекс в качестве имени чанка и хэш + расширение в качестве имени файла.

Передача файлов за секунды

Прежде чем реализовать возобновление с точки останова, давайте кратко представим вторую передачу файла.

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

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

На самом деле это просто уловка для пользователей, чтобы увидеть, по сути, загрузки нет вообще.

:)

Логика сервера очень проста. Добавьте интерфейс проверки, чтобы проверить, существует ли файл.

Приостановить загрузку

Поговорив о генерации хэша и передаче файлов за считанные секунды, вернитесь к точке останова, чтобы возобновить передачу.

Возобновление точки останова, как следует из названия, — это точка останова + возобновление, поэтому наш первый шаг — реализовать «точку останова», то есть приостановить загрузку.

Прервать метод XMLHttpRequest, чтобы отменить отправку запроса XHR. По этой причине нам нужно сохранить объект XHR, который загружает каждый фрагмент. Давайте переделаем метод запроса.

Таким образом, при загрузке чанка передайте массив requestList в качестве параметра, и метод запроса сохранит все XHR в массиве.

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

Затем создайте новую кнопку паузы, при нажатии на кнопку вызовите метод прерывания, сохраненный в XHR в requestList, то есть отмените и очистите все куски загрузки.

Нажмите кнопку паузы, чтобы увидеть, что XHR был отменен.

Возобновить загрузку

Когда я представил возобновленную загрузку из точки останова, я упомянул об использовании второго метода хранения на стороне сервера для реализации возобновленной загрузки.

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

И этот интерфейс можно комбинировать с предыдущим интерфейсом проверки второй загрузки, интерфейс отправляет запрос на проверку перед каждой загрузкой и возвращает два результата:

• Файл уже существует на сервере и не требует повторной загрузки.

• Если файл не существует на сервере или некоторые фрагменты файла были загружены, уведомите внешний интерфейс о загрузке и верните загруженный фрагмент файла на внешний интерфейс.

Итак, давайте преобразуем интерфейс проверки предыдущей передачи файла на стороне сервера в считанные секунды.

Затем вернитесь к внешнему интерфейсу, во внешнем интерфейсе есть два места, в которых нужно вызвать интерфейс проверки:

• При нажатии на загрузку проверьте, требуется ли загрузка, а также фрагменты, которые уже были загружены.

• Нажмите «Возобновить загрузку после паузы», чтобы вернуться к загруженному фрагменту.

Добавлена ​​кнопка восстановления и изменена исходная логика загрузки чанков.

Здесь к исходной функции загрузки чанков добавляется параметр uploaded list, то есть список имен чанков, возвращаемый сервером на рисунке выше. Загруженные фрагменты отфильтровываются фильтром, и, поскольку загруженная часть добавляется, условие срабатывания предыдущего интерфейса слияния претерпевает некоторые изменения.

На этом функция возобновления точки останова в основном завершена.

Улучшения индикатора выполнения

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

Индикатор выполнения одного фрагмента

Поскольку при нажатии кнопки загрузки/возобновления загрузки будет вызван интерфейс проверки для возврата загруженного фрагмента, поэтому прогресс загруженного фрагмента необходимо изменить на 100%.

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

Общий индикатор выполнения

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

Нажатие паузы отменит и очистит запрос XHR чанка. Если часть была загружена в это время, вы обнаружите, что индикатор выполнения файла регрессировал.

При нажатии «Возобновить» индикатор общего прогресса будет двигаться назад из-за повторного создания XHR, что приводит к очистке прогресса фрагмента.

Решение состоит в том, чтобы создать «поддельный» индикатор выполнения, который основан на индикаторе выполнения файла, но только останавливается и увеличивается, а затем показывает пользователю этот поддельный индикатор выполнения.

Здесь мы используем свойство listener Vue.js.

Когда uploadPercentage увеличивает реальный индикатор выполнения файла, fakeUploadPercentage также увеличивается, как только индикатор выполнения файла возвращается, поддельный индикатор выполнения просто должен остановиться.

На этом решение для загрузки большого файла + возобновления точки останова завершено.

Подведем итог

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

• Когда внешний интерфейс загружает большой файл, используйте Blob.prototype.slice для нарезки файла, одновременной загрузки нескольких фрагментов и, наконец, отправки запроса на слияние, чтобы уведомить сервер о необходимости объединения фрагментов.

• Сервер получает фрагмент и сохраняет его, а затем использует поток для слияния фрагмента с окончательным файлом после получения запроса на слияние.

• Функция upload.onprogress собственного XMLHttpRequest отслеживает ход загрузки фрагментов.

• Используйте вычисляемые свойства Vue для расчета хода загрузки всего файла на основе хода выполнения каждого фрагмента.

Возобновление с точки останова:

• Используйте spark-md5 для вычисления хэша файла на основе содержимого файла.

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

• Приостановить загрузку фрагментов с помощью метода прерывания XMLHttpRequest.

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

Исходный код

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

загрузка файла

Спасибо за прочтение:)

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Посетите наш Community Discord и присоединитесь к нашему Коллективу талантов.