Возобновляемая загрузка файлов
В этой статье мы рассмотрим концепцию возобновляемой загрузки файлов в JavaScript и предоставим подробное руководство с практическими примерами. Этот метод крайне важен для улучшения пользовательского опыта, особенно при работе с большими файлами или нестабильным сетевым соединением. Реализовав возобновляемую загрузку, мы сможем гарантировать, что пользователи смогут продолжить загрузку файлов с того места, где она была прервана, что минимизирует потерю данных и разочарование.
Введение в возобновляемую загрузку файлов
Возобновляемая загрузка файлов позволяет пользователям загружать файлы частями, гарантируя, что в случае прерывания загрузки из-за сетевых проблем или других причин, её можно будет продолжить с последней успешно загруженной части. Этот метод особенно полезен для больших файлов и может значительно повысить надёжность загрузки.
Преимущества возобновляемой загрузки файлов
- Улучшенный пользовательский опыт: Пользователи могут возобновить загрузку, не начиная её заново.
- Эффективность: Снижает объём передаваемых данных за счёт загрузки только отсутствующих частей.
- Обработка ошибок: Корректно обрабатывает прерывания сетевого соединения.
Реализация возобновляемой загрузки файлов в JavaScript
Настройка окружения
Прежде чем приступить к реализации, убедитесь, что у вас есть следующие инструменты и библиотеки:
- Современный веб-браузер с поддержкой JavaScript.
- Сервер, способный обрабатывать загрузку файлов.
- Библиотека
resumable.js(или аналогичная) для управления клиентской логикой.
Установите необходимые зависимости Node.js:
npm install express corsНастройка серверной части
Сначала настройте сервер для обработки фрагментов файлов и хранения метаданных загруженных файлов. Ниже приведён пример на Node.js и Express. Обратите внимание, что resumable.js по умолчанию отправляет метаданные фрагмента в строке запроса, поэтому мы читаем данные из req.query и используем отдельную временную директорию для каждого файла, чтобы безопасно обрабатывать фрагменты, приходящие не по порядку.
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(cors());
// Handle chunk verification for testChunks: true
app.head('/upload', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const identifier = req.query.resumableIdentifier;
const chunkPath = path.join('uploads', identifier, `chunk-${chunkNumber}.bin`);
fs.promises.access(chunkPath)
.then(() => res.status(200).end())
.catch(() => res.status(404).end());
});
app.post('/upload', async (req, res) => {
try {
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const totalChunks = parseInt(req.query.resumableTotalChunks);
const identifier = req.query.resumableIdentifier;
const fileName = req.query.resumableFilename;
const chunkDir = path.join('uploads', identifier);
await fs.promises.mkdir(chunkDir, { recursive: true });
// Read raw body (resumable.js sends chunks as application/octet-stream)
const buffer = await new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}.bin`);
await fs.promises.writeFile(chunkPath, buffer);
const receivedChunks = (await fs.promises.readdir(chunkDir)).length;
if (receivedChunks === totalChunks) {
const finalPath = path.join('uploads', fileName);
const writeStream = fs.createWriteStream(finalPath);
const finishPromise = new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
for (let i = 1; i <= totalChunks; i++) {
const chunkPath = path.join(chunkDir, `chunk-${i}.bin`);
fs.createReadStream(chunkPath).pipe(writeStream);
}
await finishPromise;
await fs.promises.rmdir(chunkDir);
res.status(200).send('File uploaded successfully');
} else {
// resumable.js expects a 200 OK for successful chunk uploads
res.status(200).send('Chunk uploaded successfully');
}
} catch (error) {
console.error('Upload error:', error);
res.status(500).send('Server error during upload');
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});Реализация на стороне клиента
Теперь реализуем логику на стороне клиента с помощью JavaScript и библиотеки resumable.js. Убедитесь, что библиотека resumable.js подключена к вашему проекту. Мы используем версию 2.1.0 для обеспечения современной совместимости. Для продакшн-окружений рекомендуется рассмотреть стандартизированный протокол tus или нативный File.slice в сочетании с fetch для лучшего контроля и кроссплатформенной поддержки.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Resumable File Upload</title>
</head>
<body>
<input type="file" id="fileInput" />
<button id="uploadButton">Upload</button>
<p id="progress">Ready</p>
<script src="https://unpkg.com/[email protected]/resumable.min.js"></script>
<script>
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressEl = document.getElementById('progress');
const r = new Resumable({
target: '/upload',
chunkSize: 1 * 1024 * 1024, // 1MB chunks
simultaneousUploads: 1,
testChunks: true,
throttleProgressCallbacks: 1,
});
r.assignBrowse(fileInput);
uploadButton.addEventListener('click', () => {
if (r.files.length > 0) {
r.upload();
} else {
alert('Please select a file to upload.');
}
});
r.on('progress', (file, loaded, total) => {
const percent = Math.round((loaded / total) * 100);
progressEl.textContent = `Uploading ${file.fileName}: ${percent}%`;
});
r.on('fileSuccess', (file, message) => {
console.log(`File ${file.fileName} uploaded successfully.`);
progressEl.textContent = 'Upload complete!';
});
r.on('fileError', (file, message) => {
console.error(`Error uploading file ${file.fileName}: ${message}`);
progressEl.textContent = 'Upload failed.';
});
</script>
</body>
</html>Нативная альтернатива: File.slice + fetch
Для проектов, где предпочтительно отсутствие зависимостей, возобновляемую загрузку можно реализовать нативно с помощью API File.slice и fetch. Этот подход даёт полный контроль над заголовками, повторными попытками и сборкой фрагментов.
async function uploadFileNative(file) {
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const identifier = `${file.name}-${Date.now()}`;
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const url = new URL('/upload', window.location.origin);
url.searchParams.set('resumableChunkNumber', i + 1);
url.searchParams.set('resumableTotalChunks', totalChunks);
url.searchParams.set('resumableIdentifier', identifier);
url.searchParams.set('resumableFilename', file.name);
await fetch(url, { method: 'POST', body: chunk });
}
console.log('Native upload complete');
}Управление метаданными
Крайне важно управлять метаданными загружаемого файла и его фрагментов. Эта информация помогает возобновить загрузку с правильного фрагмента в случае прерываний. Логика сервера для отслеживания и сборки фрагментов рассмотрена в предыдущем разделе. Для продакшн-окружений следует избегать хранения только в памяти или на файловой системе, так как они не обеспечивают сохранность данных и потокобезопасность. Вместо этого используйте базу данных или кэш (например, Redis) для отслеживания завершения фрагментов и надёжной сборки файлов.
Пример: Загрузка больших файлов
Конфигурация клиента остаётся идентичной предыдущему примеру. Для оптимизации под большие файлы можно увеличить chunkSize (например, до 5 МБ) и настроить simultaneousUploads в зависимости от мощности сервера и условий сети.
Профессиональные советы по возобновляемой загрузке файлов
- Оптимизируйте размер фрагмента: Настраивайте размер фрагмента в зависимости от средней скорости сети и размера файла, чтобы найти баланс между скоростью загрузки и надёжностью.
- Обработка ошибок: Реализуйте надёжные механизмы обработки ошибок для устранения проблем с сетевым соединением и сервером.
- Обратная связь для пользователя: Предоставляйте пользователям информацию в реальном времени о прогрессе загрузки и любых возникших проблемах.
- Безопасность: Убедитесь, что процесс загрузки файлов безопасен, проверяя типы файлов и внедряя правильную аутентификацию и авторизацию.
- Современные альтернативы: Для продакшн-окружений рассмотрите стандартизированные протоколы, такие как
tus, или нативныйFile.sliceсfetchдля лучшего контроля, возможности возобновления и кроссплатформенной совместимости.
Следуя этим рекомендациям и примерам, мы можем реализовать надёжную и эффективную систему возобновляемой загрузки файлов в JavaScript, улучшая пользовательский опыт и гарантируя стабильную загрузку файлов.
Practice
Какие из перечисленных являются преимуществами использования возобновляемой загрузки файлов?