Эта статья синхронизирована в личном блогеshymean.comвверх, добро пожаловать, чтобы следовать
Недавно я столкнулся с необходимостью загрузить большой файл размером 100 МБ и исследовал функцию многофрагментной загрузки Qiniu и Tencent Cloud, поэтому здесь организована реализация функций, связанных с загрузкой больших файлов.
В некоторых компаниях загрузка больших файлов является относительно важным сценарием взаимодействия, например загрузка данных таблицы Excel с относительно большой библиотекой, загрузка аудио- и видеофайлов и т. д. Если размер файла относительно большой или сетевые условия плохие, время загрузки будет относительно большим (необходимо передать больше пакетов, а также выше вероятность потери и повторной передачи пакетов), пользователь не может обновить страницу. , и остается только терпеливо ждать завершения запроса.
Следующее начинается с метода загрузки файлов, организует идею загрузки больших файлов и дает соответствующие примеры кодов.Поскольку PHP имеет встроенные удобные методы разделения и объединения файлов, код на стороне сервера написан на PHP в качестве примера. .
Соответствующий пример кода для этой статьи находится по адресуgithubвыше, основная ссылка
Несколько способов загрузки файлов
Во-первых, давайте рассмотрим несколько способов загрузки файлов.
загрузка в обычной форме
Использование PHP для отображения регулярных загрузок форм — хороший вариант. Сначала создайте форму для загрузки файла и укажите тип содержимого отправки формы какenctype="multipart/form-data"
, указывая на то, что форме необходимо загрузить двоичные данные.
<form action="/index.php" method="POST" enctype="multipart/form-data">
<input type="file" name="myfile">
<input type="submit">
</form>
затем написатьindex.php
Чтобы загрузить файл для получения кода, используйтеmove_uploaded_file
Метода достаточно (php Дафа хорош...)
$imgName = 'IMG'.time().'.'.str_replace('image/','',$_FILES["myfile"]['type']);
$fileName = 'upload/'.$imgName;
// 移动上传文件至指定upload文件夹下,并根据返回值判断操作是否成功
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $fileName)){
echo $fileName;
}else {
echo "nonn";
}
При загрузке больших файлов в форму легко столкнуться с проблемой таймаута сервера. Через xhr фронтенд тоже может асинхронно загружать файлы.В общем есть две идеи.
загрузка кодировки файла
Первая идея — закодировать файл, а затем раскодировать его на сервере, я уже писал статью.Сжатие изображений и загрузка на интерфейсеОсновной принцип реализации блога — преобразование изображения в base64 для передачи
var imgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
var data = canvas.toDataURL("image/jpeg", 0.5);
Что нужно сделать на стороне сервера тоже относительно просто, сначала декодировать base64, а потом сохранить образ
$imgData = $_REQUEST['imgData'];
$base64 = explode(',', $imgData)[1];
$img = base64_decode($base64);
$url = './test.jpg';
if (file_put_contents($url, $img)) {
exit(json_encode(array(
url => $url
)));
}
Недостатком кодировки base64 является то, что ее объем больше объема исходного изображения (поскольку Base64 преобразует три байта в четыре байта, поэтому закодированный текст будет примерно на треть больше, чем исходный текст), для больших объемов Для файлов, время загрузки и парсинга значительно возрастет.
Для получения дополнительных сведений о base64 вы можете обратиться кПримечания Base64.
В дополнение к кодировке base64 вы также можете загружать в двоичном формате после непосредственного чтения содержимого файла во внешнем интерфейсе.
// 读取二进制文件
function readBinary(text){
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
console.log(ui8a)
}
var reader = new FileReader();
reader.onload = function(){
readBinary(this.result) // 读取result或直接上传
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);
асинхронная загрузка formData
FormDataОбъект в основном используется для сборки набора пар ключ/значение для отправки запросов с помощью XMLHttpRequest, что позволяет более гибко отправлять запросы Ajax. Отправка формы может быть смоделирована с помощью FormData.
let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);
Метод обработки на стороне сервера в основном такой же, как и прямой запрос формы.
iframe не обновляет страницу
В браузерах с низкими версиями (таких как IE) xhr не поддерживает прямую загрузку данных формы, поэтому вы можете использовать форму только для загрузки файлов, а сама отправка формы будет выполнять переходы по страницам, потому что форма формыtargetобусловленное свойством, его значение равно
- _self, по умолчанию, открывает страницу ответа в том же окне
- _blank, открывается в новом окне
- _parent, открыть в родительском окне
- _top, открыть в самом верхнем окне
-
framename
, открыть в iframe с указанным именем
Если вам нужно, чтобы пользователь испытал ощущение асинхронной загрузки файлов, вы можете передатьframename
Укажите iframe для реализации. Установите целевой атрибут формы на невидимый iframe, тогда возвращаемые данные будут приняты iframe, поэтому будет обновляться только iframe.Что касается возвращаемого результата, его также можно получить путем разбора текста в iframe.
function upload(){
var now = +new Date()
var id = 'frame' + now
$("body").append(`<iframe style="display:none;" name="${id}" id="${id}" />`);
var $form = $("#myForm")
$form.attr({
"action": '/index.php',
"method": "post",
"enctype": "multipart/form-data",
"encoding": "multipart/form-data",
"target": id
}).submit()
$("#"+id).on("load", function(){
var content = $(this).contents().find("body").text()
try{
var data = JSON.parse(content)
}catch(e){
console.log(e)
}
})
}
Загрузка большого файла
Теперь давайте рассмотрим проблему тайм-аута, возникающую при реализации загрузки больших файлов в вышеупомянутых методах загрузки.
- Загрузка формы и загрузка iframe без обновления страницы фактически является загрузкой файлов через тег формы, таким образом, весь запрос полностью передается в обработку браузеру, при загрузке больших файлов может возникнуть ситуация тайм-аута запроса.
- Через fromData он фактически инкапсулирует набор параметров запроса в xhr для имитации запросов формы, что не может избежать проблемы тайм-аута загрузки больших файлов.
- Кодируя загрузку, мы можем более гибко управлять загружаемым контентом
Основная проблема с загрузкой больших файлов:В том же запросе для загрузки большого объема данных весь процесс будет долгим, и после неудачи нужно начинать загрузку сначала. Только представьте, если мы разобьем этот запрос на несколько запросов, время для каждого запроса будет сокращено, и если запрос не будет выполнен, нам нужно будет только повторно отправить этот запрос, не начиная с нуля, может ли он решить большие файлы. Что насчет проблем с загрузкой?
Исходя из вышеперечисленных проблем, кажется, что загрузка больших файлов должна соответствовать следующим требованиям.
- Поддержка разделенных запросов на загрузку (т. е. нарезка)
- Поддерживает возобновление с точки останова
- Поддержка отображения хода загрузки и приостановки загрузки
Далее реализуем эти функции по очереди.Кажется, самой важной функцией должна быть нарезка.
фрагмент файла
Ссылаться на:Обрезка и загрузка больших файлов
При загрузке метода кодирования на переднем конце нам нужно только получить двоичное содержимое файла, затем разделить содержимое и, наконец, загрузить каждый фрагмент на сервер.
В JavaScript файловый объект FILE является подклассом объекта Blob, который содержит важный методslice
, с помощью этого метода мы можем разделить двоичный файл.
Ниже приведен пример разделенного файла.
function slice(file, piece = 1024 * 1024 * 5) {
let totalSize = file.size; // 文件总大小
let start = 0; // 每次上传的开始字节
let end = start + piece; // 每次上传的结尾字节
let chunks = []
while (start < totalSize) {
// 根据长度截取每次需要上传的数据
// File对象继承自Blob对象,因此包含slice方法
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks
}
разделить файл наpiece
Размер блока, а то каждый запрос должен загружать только эту часть блока.
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH); // 首先拆分切片
chunks.forEach(chunk=>{
let fd = new FormData();
fd.append("file", chunk);
post('/mkblk.php', fd)
})
После того, как сервер получит эти слайсы, вы можете соединить их вместе.Ниже приведен пример кода сплайсинга PHP-слайсов.
$filename = './upload/' . $_POST['filename'];//确定上传的文件名
//第一次上传时没有文件,就创建文件,此后上传只需要把数据追加到此文件中
if(!file_exists($filename)){
move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
echo $filename;
}
Не забудьте изменить конфигурацию сервера nginx при тестировании, иначе большие файлы могут запрашивать413 Request Entity Too Large
ошибка.
server {
// ...
client_max_body_size 50m;
}
Есть некоторые проблемы с вышеуказанным методом
- Невозможно определить, к какому слайсу принадлежит слайс. При одновременном выполнении нескольких запросов содержимое присоединяемого файла будет неверным.
- Интерфейс загрузки слайсов асинхронный, и нет гарантии, что полученные сервером слайсы будут склеены в запрошенном порядке.
Итак, давайте посмотрим, как восстанавливать слайсы на стороне сервера.
восстановить фрагмент
В бэкенде несколько слайсов одного и того же файла необходимо восстановить в один файл.Приведенный выше метод обработки слайсов имеет следующие проблемы
- Как определить, что несколько фрагментов взяты из одного и того же файла, это может передавать один и тот же файл при каждом запросе фрагмента.
context
параметр - Как восстановить несколько фрагментов в один файл
- Подтвердите, что все фрагменты были загружены, это может быть вызвано клиентом после загрузки всех фрагментов.
mkfile
интерфейс для уведомления сервера о выполнении сплайсинга - Найдите все фрагменты в одном контексте, подтвердите порядок каждого фрагмента, это может пометить значение индекса позиции на каждом фрагменте.
- Сращивание слайсов по порядку и восстановление в файлы
- Подтвердите, что все фрагменты были загружены, это может быть вызвано клиентом после загрузки всех фрагментов.
Выше есть важный параметр, а именноcontext
, нам нужно получить его как уникальный идентификатор файла, который можно получить следующими двумя способами
- В соответствии с основной информацией, такой как имя файла и длина файла, выполняется объединение.Чтобы предотвратить загрузку одного и того же файла несколькими пользователями, для обеспечения уникальности может быть объединена дополнительная информация о пользователе, такая как uid.
- Вычислите хэш файла в соответствии с двоичным содержимым файла, поэтому, если содержимое файла отличается, идентификация также будет другой, недостатком является то, что объем вычислений относительно велик.
Измените код загрузки и добавьте соответствующие параметры.
// 获取context,同一个文件会返回相同的值
function createContext(file) {
return file.name + file.length
}
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其的context
let context = createContext(file);
let tasks = [];
chunks.forEach((chunk, index) => {
let fd = new FormData();
fd.append("file", chunk);
// 传递context
fd.append("context", context);
// 传递切片索引值
fd.append("chunk", index + 1);
tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
let fd = new FormData();
fd.append("context", context);
fd.append("chunks", chunks.length);
post("/mkfile.php", fd).then(res => {
console.log(res);
});
});
существуетmkblk.php
В интерфейсе переходимcontext
для сохранения фрагментов, связанных с одним и тем же файлом
// mkblk.php
$context = $_POST['context'];
$path = './upload/' . $context;
if(!is_dir($path)){
mkdir($path);
}
// 把同一个文件的切片放在相同的目录下
$filename = $path .'/'. $_POST['chunk'];
$res = move_uploaded_file($_FILES['file']['tmp_name'],$filename);
В дополнение к описанному выше методу простого различения слайсов по каталогам информация о слайсах также может храниться в базе данных для индексации. Далееmkfile.php
Реализация интерфейса, этот интерфейс будет вызываться после загрузки всех слайсов
// mkfile.php
$context = $_POST['context'];
$chunks = (int)$_POST['chunks'];
//合并后的文件名
$filename = './upload/' . $context . '/file.jpg';
for($i = 1; $i <= $chunks; ++$i){
$file = './upload/'.$context. '/' .$i; // 读取单个切块
$content = file_get_contents($file);
if(!file_exists($filename)){
$fd = fopen($filename, "w+");
}else{
$fd = fopen($filename, "a");
}
fwrite($fd, $content); // 将切块合并到一个文件上
}
echo $filename;
Это решает две вышеуказанные проблемы:
- Определите источник фрагмента
- Гарантированный порядок сращивания слайсов
http
Даже если большой файл разделен на фрагменты для загрузки, нам все равно нужно дождаться загрузки всех фрагментов.Во время ожидания может возникнуть ряд ситуаций, из-за которых некоторые фрагменты не могут быть загружены, например сбой сети, страница закрытие и др. Поскольку загружены не все фрагменты, сервер не может быть уведомлен о синтезе файла. В этом случае черезhttpдля обработки.
Возобновление загрузки с точки останова означает, что вы можете продолжить загрузку незавершенной части из уже загруженной части вместо того, чтобы начинать с самого начала, что экономит время загрузки.
Поскольку весь процесс загрузки выполняется по размеру среза, аmkfile
Интерфейс активно вызывается клиентом после завершения загрузки всех слайсов, поэтому реализация возобновления загрузки также очень проста:
- После успешной загрузки фрагмента сохраните информацию о загруженном фрагменте.
- Когда тот же файл будет передан в следующий раз, просмотрите список фрагментов и выберите для загрузки только незагруженные фрагменты.
- После того, как все слайсы загружены, вызовите
mkfile
Интерфейс уведомляет сервер о слиянии файлов
Следовательно, проблема заключается в том, как сохранить информацию о загруженных слайсах, обычно существует две стратегии сохранения.
- Его можно сохранить во фронтенд-браузере через locaStorage и другие методы.Этот метод не зависит от сервера и более удобен в реализации.Недостаток в том, что если пользователь очистит локальный файл, то запись загрузки будет потеряна.
- Сервер сам знает, какие фрагменты были загружены, поэтому сервер может дополнительно предоставить интерфейс для запроса загруженных фрагментов в соответствии с контекстом файла и вызова исторической записи загрузки файла перед загрузкой файла.
Давайте реализуем функцию загрузки точки останова, локально сохранив загруженную запись слайса.
// 获取已上传切片记录
function getUploadSliceRecord(context){
let record = localStorage.getItem(context)
if(!record){
return []
}else {
try{
return JSON.parse(record)
}catch(e){}
}
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
let list = getUploadSliceRecord(context)
list.push(sliceIndex)
localStorage.setItem(context, JSON.stringify(list))
}
Затем внесите небольшие изменения в логику загрузки, в основном, чтобы добавить логику, согласно которой обнаружение перед загрузкой уже загружено, а запись сохраняется после загрузки.
let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
// 已上传的切片则不再重新上传
if(record.includes(index)){
return
}
let fd = new FormData();
fd.append("file", chunk);
fd.append("context", context);
fd.append("chunk", index + 1);
let task = post("/mkblk.php", fd).then(res=>{
// 上传成功后保存已上传切片记录
saveUploadSliceRecord(context, index)
record.push(index)
})
tasks.push(task);
});
В это время при загрузке обновите страницу или закройте браузер.При повторной загрузке того же файла фрагменты, которые были успешно загружены, не будут загружены снова.
Логика сервера для реализации возобновления точки останова в основном похожа, еслиgetUploadSliceRecord
Достаточно внутренне вызвать интерфейс запроса сервера, чтобы получить записи загруженных слайсов, поэтому здесь он не будет раскрываться.
Кроме того, необходимо рассмотреть возможность возобновления передачи с точки останова.Срок действия фрагмента истекслучай: если называетсяmkfile
интерфейс, содержимое слайса на диске можно очистить, если клиент не вызываетmkfile
Очевидно, что постоянно хранить эти слайсы на диске ненадежно.Как правило, закачки слайсов имеют срок действия в течение определенного периода времени.По истечении срока действия они удаляются. По указанным выше причинам возобновление загрузки с точки останова также должно синхронизировать логику реализации срока действия слайса.
Загрузить прогресс и приостановить
пройти черезxhr.uploadсерединаprogress
Метод может отслеживать ход загрузки каждого фрагмента.
Реализация приостановки загрузки также относительно проста.xhr.abort
Вы можете отменить загрузку фрагментов незавершенной загрузки, чтобы добиться эффекта приостановки загрузки. Возобновление загрузки аналогично возобновлению загрузки с точки останова. Сначала получите список загруженных фрагментов, а затем повторно отправьте незагруженные фрагменты.
Из-за нехватки места здесь не реализованы функции прогресса загрузки и паузы.
резюме
В настоящее время в сообществе есть несколько зрелых решений для загрузки больших файлов, таких какЦиню SDK,Тенсент Облачный SDKПодождите, нам может и не понадобиться вручную реализовывать простую библиотеку загрузки больших файлов, но понять ее принцип все же необходимо.
В этой статье сначала организованы несколько способов фронтальной загрузки файлов, а затем рассмотрены несколько сценариев загрузки больших файлов, а также несколько функций, которые необходимо реализовать для загрузки больших файлов.
- через объект Blob
slice
способ разбить файл на части - Разобрал условия и параметры, необходимые для восстановления файлов на стороне сервера, и продемонстрировал, как PHP может восстанавливать слайсы в файлы.
- Возобновляемая загрузка путем сохранения записи загруженных фрагментов
Остались еще некоторые проблемы, такие как: предотвращение переполнения памяти при объединении файлов, стратегия аннулирования нарезки, приостановка загрузки и другие функции, которые не углубились или реализованы по одному, продолжайте учиться ~