Ajax-загрузка файлов с индикатором
241IT блог --- Ajax-загрузка файлов с индикатором
В настоящее время довольно популярным решением для веб-сайтов является работа пользователя со страницей без ее перезагрузки. В основном это делается с помощью Ajax – технологии асинхронного взаимодействия с сервером, основанной на объекте XMLHttpRequest. Мы описывали работу с Ajax с помощью библиотеки jQuery в статьях jQuery и Ajax и Использование Ajax.
В этой статье мы рассмотрим решение одной из самых распространенных задач – асинхронная загрузка файла на сервер и отображение процесса выполнения. Наш сценарий будет загружать картинки на сервер, показывать индикатор выполнения и затем отображать загруженную картинку. В некоторых случаях вам может потребоваться вернуть идентификатор загруженного вами файла или сделать что-то еще с файлом на бэкенде, например сохранить в базу данных и т.п. В этой статье основной упор сделаем на фронтенд – т.е. реализацию асинхронной загрузки на клиенте.
В качестве тестового проекта создайте пустое приложение ASP.NET MVC в Visual Studio. Мы будем использовать C# на бэкенде (как я уже говорил, основное внимание мы уделим написанию JavaScript, так что для серверной части вы можете использовать любой другой язык). Добавьте библиотеку jQuery с помощью диспетчера пакетов NuGet (View --> Other Windows --> Package manager Console):
Install-Package jQuery
Добавьте в папку Controllers класс контроллера HomeController.cs со следующим содержимым (напомню, контроллер Home используется по умолчанию в настройках маршрутизации проекта – файл /App_Start/RouteConfig.cs):
using System.Web.Mvc;
namespace UploadFiles.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
Щелкните правой кнопкой мыши по методу Index и выберите в контекстном меню команду Add View. Visual Studio создаст файл представления /Views/Home/index.cshtml, а также компоновку по умолчанию /Views/Shared/_Layout.cshtml. Давайте подключим библиотеку jQuery в файле компоновки:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Загрузка файлов Ajax</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" rel="stylesheet" type="text/css" />
<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
@RenderBody()
<script src="~/scripts/jquery-3.1.1.min.js"></script>
<script src="~/scripts/script.js"></script>
</body>
</html>
Здесь мы также добавили сброс стилей CSS для браузера и подключили таблицу стилей /Content/Site.css. Добавьте также файл script.js в папку scripts. Давайте теперь добавим форму для загрузки файлов в проект. Для этого откройте представление Index.cshtml и используйте следующую разметку:
@{
ViewBag.Title = "Index";
}
<section>
<figure></figure>
<p>Загрузка файлов</p>
<p><small>Перетащите ваши файлы в эту область!</small></p>
<input type="file" multiple="multiple" accept="image/x-png,image/jpeg">
</section>
<div class="progress">
<div class="progress-bar"></div>
<div class="progress-value">0 %</div>
</div>
<div class="error"></div>
<div class="images"></div>
В элементе section находится форма со вставкой загружаемых файлов. Файлы можно поместить в этот контейнер путем перетаскивания (drag-and-drop), либо через диалоговое окно после щелчка по элементу section. Элемент progress будет содержать индикатор загрузки файлов, в элементе error будут отображаться ошибки, а контейнер images нужен для отображения сохраненных на сервере картинок.
Теперь нам нужно добавить CSS-стили для страницы. Для этого отредактируйте файл /Content/Site.css следующим образом:
body {
font-family:Arial,Helvetica,sans-serif;
}
section {
position: relative;
width: 380px;
height: 160px;
margin: 40px auto;
color: #40444f;
border: .2rem dashed #616778;
border-radius: 1.5rem;
cursor: pointer;
-webkit-transition: color 0.2s ease-out, border-color 0.2s ease-out;
-moz-transition: color 0.2s ease-out, border-color 0.2s ease-out;
transition: color 0.2s ease-out, border-color 0.2s ease-out;
overflow: hidden;
padding-top: 90px;
box-sizing: border-box;
}
section:hover, section.dd {
border-color: #4d90ff;
color: #4d90ff;
background-color: #e7f0fe;
}
figure {
position: absolute;
width: 100%;
height: 160px;
left: 0;
top: 0;
display: block;
}
figure:after {
position: absolute;
display: block;
content: '';
height: 80px;
width: 80px;
top: 5px;
left: 50%;
margin-left: -40px;
background-repeat: no-repeat;
background-size: 80px 80px;
background-image: url(https://professorweb.ru/my/it/blog/net/images/upload_icon.png);
-webkit-transition: opacity 0.2s ease-out, border-color 0.2s ease-out;
-moz-transition: opacity 0.2s ease-out, border-color 0.2s ease-out;
transition: opacity 0.2s ease-out, border-color 0.2s ease-out;
}
section:hover figure:after, section.dd figure:after {
opacity: .65;
}
p {
text-align: center;
font-weight: bold;
font-size: 16px;
line-height: 24px;
}
p small {
font-weight: normal;
font-size: 12px;
opacity: .7;
}
[type="file"] {
position: absolute;
top: -16rem;
opacity: 0;
}
.error {
width: 380px;
margin: 0 auto 20px;
line-height: 20px;
font-size: 14px;
color: red;
font-style: italic;
display: none;
text-align: center;
}
/* Прогресс-бар */
.progress {
height: 20px;
width: 380px;
margin: 0 auto 20px;
overflow: hidden;
background-color: #999;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
position: relative;
display: none;
}
.progress-bar {
height: 100%;
font-size: 12px;
float: left;
width: 0;
background-color: #428bca;
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
-webkit-transition: width .6s ease;
transition: width .6s ease;
}
.progress-value {
position: absolute;
left: 0;
top: 0;
line-height: 20px;
height: 100%;
width: 100%;
color: #fff;
text-align: center;
}
/* Контейнер с загруженными картинками */
.images {
width: 380px;
overflow: hidden;
margin: 0 auto;
}
.images a {
width: 116px;
height: 116px;
margin: 0 10px 10px 0;
float: left;
display: block;
box-sizing: border-box;
padding: 4px;
border: 1px solid #d2d2d2;
border-radius: 6px;
position: relative;
}
.images a:hover {
border-color: #428bcb;
}
.images span {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
display: block;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
На данный момент форма выглядит следующим образом:
Теперь нам нужно добавить скрипт, который должен обеспечивать следующий функционал:
При клике по элементу section должно открываться модальное окно с выбором файла.
При перетаскивании файлов из проводника в окно браузера, элемент section должен подсвечиваться как при наведении курсора мыши (для этого мы добавили CSS-класс «dd&raqio;). При отпускании файлов (как и при выборе файла из диалогового окна) должна происходить загрузка картинок на сервер.
При загрузке файлов необходимо добавить индикатор и отобразить процент выполнения загрузки.
В случае ошибки на сервере, необходимо отобразить ошибки и скрыть индикатор.
В случае успешной загрузки картинок на сервер, необходимо отобразить их в контейнере images.
Следующий скрипт (файл script.js) решает все вышеуказанные вопросы:
$(function () {
// Программное открытие окна выбора файла по щелчку
$('figure').on('click', function () {
$(':file').trigger('click');
})
// При перетаскивании файлов в форму, подсветить
$('section').on('dragover', function (e) {
$(this).addClass('dd');
e.preventDefault();
e.stopPropagation();
});
// Предотвратить действие по умолчанию для события dragenter
$('section').on('dragenter', function (e) {
e.preventDefault();
e.stopPropagation();
});
$('section').on('dragleave', function (e) {
$(this).removeClass('dd');
});
$('section').on('drop', function (e) {
if (e.originalEvent.dataTransfer) {
if (e.originalEvent.dataTransfer.files.length) {
e.preventDefault();
e.stopPropagation();
// Вызвать функцию загрузки. Перетаскиваемые файлы содержатся
// в свойстве e.originalEvent.dataTransfer.files
upload(e.originalEvent.dataTransfer.files);
}
}
});
// Загрузка файлов классическим образом - через модальное окно
$(':file').on('change', function () {
upload($(this).prop('files'));
});
});
// Функция загрузки файлов
function upload(files) {
// Создаем объект FormData
var formData = new FormData();
// Пройти в цикле по всем файлам
for (var i = 0; i < files.length; i++) {
// С помощью метода append() добавляем файлы в объект FormData
formData.append('file_' + i, files[i]);
}
// Ajax-запрос на сервер
$.ajax({
type: 'POST',
url: '/Home/Upload', // URL на метод действия Upload контроллера HomeController
data: formData,
processData: false,
contentType: false,
beforeSend: function () {
$('section').removeClass('dd');
// Перед загрузкой файла удалить старые ошибки и показать индикатор
$('.error').text('').hide();
$('.progress').show();
// Установить прогресс-бар на 0
$('.progress-bar').css('width', '0');
$('.progress-value').text('0 %');
},
success: function (data) {
if (data.Error) {
$('.error').text(data.Error).show();
$('.progress').hide();
}
else {
$('.progress-bar').css('width', '100%');
$('.progress-value').text('100 %');
// Отобразить загруженные картинки
if (data.Files) {
// Обертка для картинки со ссылкой
var img = '<a href="0" target="_blank"><span style="background-image: url(0)"></span></a>';
for (var i = 0; i < data.Files.length; i++) {
// Сгенерировать вставляемый элемент с картинкой
// (символ 0 заменяем ссылкой с помощью регулярного выражения)
var element = $(img.replace(/0/g, data.Files[i]));
// Добавить в контейнер
$('.images').append(element);
}
}
}
},
xhrFields: { // Отслеживаем процесс загрузки файлов
onprogress: function (e) {
if (e.lengthComputable) {
// Отображение процентов и длины прогресс бара
var perc = e.loaded / 100 * e.total;
$('.progress-bar').css('width', perc + '%');
$('.progress-value').text(perc + ' %');
}
}
},
});
}
Для сохранения списка файлов и передачи его на сервер через Ajax используется объект FormData.
Обратите внимание, что за отслеживание процесса загрузки отвечает свойство xhrFields объекта, передаваемого методу $.ajax. В этом свойстве хранится объект с функцией обработки события onprogress. Этому событию передается объект со свойствами loaded – объем уже загруженных данных, и total – общий размер данных. Благодаря этим двум параметрам мы можем отображать процесс выполнения загрузки на индикаторе.
В методе $.ajax() мы ссылаемся на метод действия Upload контроллера HomeController, который еще не был добавлен. Давайте исправим это и отредактируем файл HomeController.cs:
using System;
using System.Web;
using System.Linq;
using System.Web.Mvc;
using System.Collections.Generic;
using System.IO;
namespace UploadFiles.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public JsonResult Upload()
{
string __filepath = Server.MapPath("~/uploads");
int __maxSize = 2 * 1024 * 1024; // максимальный размер файла 2 Мб
// допустимые MIME-типы для файлов
List<string> mimes = new List<string>
{
"image/jpeg", "image/jpg", "image/png"
};
var result = new Result
{
Files = new List<string>()
};
if (Request.Files.Count > 0)
{
foreach (string f in Request.Files)
{
HttpPostedFileBase file = Request.Files[f];
// Выполнить проверки на допустимый размер файла и формат
if (file.ContentLength > __maxSize)
{
result.Error = "Размер файла не должен превышать 2 Мб";
break;
}
else if (mimes.FirstOrDefault(m => m == file.ContentType) == null)
{
result.Error = "Недопустимый формат файла";
break;
}
// Сохранить файл и вернуть URL
if (Directory.Exists(__filepath))
{
Guid guid = Guid.NewGuid();
file.SaveAs($@"{__filepath}\{guid}.{file.FileName}");
result.Files.Add($"/uploads/{guid}.{file.FileName}");
}
}
}
return Json(result);
}
}
public class Result
{
public string Error { get; set; }
public List<string> Files { get; set; }
}
}
Здесь мы получаем файлы из индексатора Files объекта HttpRequestBase, который доступен в контроллере ASP.NET через свойство Request. Далее мы выполняем две простые проверки – размер файла не должен превышать 2 Мб и тип файлов должен быть либо JPG либо PNG. В случае несоответствия файла проверкам в скрипт возвращается объект с ошибкой, которая отображается пользователю. Иначе файл сохраняется в папке uploads проекта, ему присваивается сгенерированное с помощью GUID случайное имя.
Перед тестированием данного примера не забудьте добавить в корень проекта папку uploads.