Ajax-загрузка файлов с индикатором

241

В настоящее время довольно популярным решением для веб-сайтов является работа пользователя со страницей без ее перезагрузки. В основном это делается с помощью 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;
}

На данный момент форма выглядит следующим образом:

Тестовая форма для асинхронной загрузки файлов

Теперь нам нужно добавить скрипт, который должен обеспечивать следующий функционал:

Следующий скрипт (файл 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.

Ajax-загрузка картинок
Пройди тесты
Лучший чат для C# программистов