Предварительный просмотр загружаемых файлов с помощью Axios и VueJS
Ранее я написал небольшую статью о загрузке файлов с помощью VueJS и Axios, которая должна помочь облегчить некоторые трудности при создание загрузки файлов. Так же была статья о отображение прогресса при загрузке файлов с помощью Axios. Теперь пришло время добавить несколько наворотов и немного улучшить UX. В этом уроке мы будем создавать превью загрузки картинок.
Предварительные условия
Как и в предыдущих статья, я собираюсь использовать Axios v0.16.2 и VueJS v2.1.0 для выполнения фронтальной части загрузки файлов.
Для предварительного просмотра файлов мы будем использовать FileReader API: FileReader — Web APIs | MDN. На этом сайте есть отличный обзор FileReader и примеры его работы. Я адаптировал большую часть примеров для работы с VueJS и Axios.
FileReader позволяет нам прочитать файл, выбранный пользователем в качестве большого двоичного объекта, и просмотреть его в браузере. Мы будем использовать это для отображения изображений, но я уверен, что это может быть адаптировано для PDF-файлов и других типов файлов, доступных для просмотра в браузере. В этом уроке мы будем работать только с изображениями.
Предварительный просмотр отдельно загружаемых изображений
Первый компонент, который мы создадим, позволит пользователям просматривать предварительное изображение выбранного ими файла изображения.
Создания компонента FilePreview.vue
Для этого урока я создал простой компонент Vue с именем FilePreview.vue со следующим содержимым шаблона:
<template> <div class="container"> <div class="large-12 medium-12 small-12 cell"> <label>File Preview <input type="file" id="file" ref="file" accept="image/*" v-on:change="handleFileUpload()"/> </label> <img v-bind:src="imagePreview" v-show="showPreview"/> <button v-on:click="submitFile()">Submit</button> </div> </div> </template>
Несколько замечаний по поводу шаблона. У нас есть input типа file, который имеет различные атрибуты. Первым, что мы отметим, это атрибут ref=»file». Через него мы получим доступ к выбранным файлам следующим образом:
this.file = this.$refs.file.files[0];
Другим атрибутом является атрибут accept=»image/*». Он ограничивает загрузку только файлами изображений. Для создаваемой нами функции предварительного просмотра мы хотим, чтобы пользователи загружали только изображения, чтобы мы могли показать их содержимое.
Наконец, у нас есть атрибут v-on:change=»handleFileUpload()». Когда пользователь загружает файл и содержимое input изменяется, мы вызываем наш метод handleFileUpload(). Это позволит нам прочитать файл с помощью FileReader() и отобразить изображение пользователю.
Шаблон также содержит элемент img. Через этот элемент мы отобразим выбранный файл. У него есть два атрибута. Во-первых, это привязка атрибута src с помощью v-bind:src=»imagePreview». Это, связывает src изображения с переменной imagePreview (обсудим позже в этом уроке). Вторым атрибутом является v-show=»showPreview». Он определяет, должны ли мы показать предварительный просмотр изображения или нет. Это полезно для улучшения UX. В случае если не будет ничего выбрано, в нашем предварительном просмотре ничего не будет отображаться. Мы установим эту переменную по умолчанию в значение false, чтобы оно не отображалось на следующем шаге. Когда пользователь выбирает файл, мы переключим его на true.
В шаблоне также есть элемент button. При нажатии он отправит файл для обработки сервером с помощью метода submitFile().
Добавим data в компонент
Мы уже обсудили несколько переменных, которые нам нужны, чтобы наш компонент функционировал, поэтому давайте добавим их в часть <script> нашего компонента:
data(){ return { file: '', showPreview: false, imagePreview: '' } },
Первая переменная — file. Она будет содержать файл и позволит нам отправить его в через API.
Переменная showPreview определяет, следует ли нам отображать предварительный просмотр файла. Она будет установлено в true, когда файл будет прочитан и готов к загрузке. По умолчанию она имеет значение false, что бы сразу же не показывать пустое изображение.
ImagePreview будет содержать blob данные, которые является результатом чтения файла. Если вы захотите изображение, которое будет играть роль заполнителя, вам нужно установить его для imagePreview URL-адрес изображения, а для showPreview — значение true.
Далее мы добавим некоторые функциональные возможности, чтобы все это работало!
Добавление метода загрузки файла handleFileUpload
Теперь, когда у нас есть data, давайте добавим метод загрузки файла. Этот метод будет вызываться как только пользователь выберит файл. Добавим метод handleFileUpload в объект methods:
methods: { handleFileUpload(){ this.file = this.$refs.file.files[0]; let reader = new FileReader(); reader.addEventListener("load", function () { this.showPreview = true; this.imagePreview = reader.result; }.bind(this), false); if( this.file ){ if ( /\.(jpe?g|png|gif)$/i.test( this.file.name ) ) { reader.readAsDataURL( this.file ); } } } }
В этом методе происходит вся магия!
Сначала мы устанавливаем локальную переменную file, выбранным файлом в input.
this.file = this.$refs.file.files[0];
Помните, когда мы устанавливаем атрибут ref=»file» для input? Вот где мы используем это. Мы можем получить доступ к тому, что было загружено в input, вызвав this.$refs.file, где file — это имя ссылки. Затем мы получаем первый файл из списка файлов.
Далее мы создаем FileReader() объект. Более подробно об этом описанно здесь: FileReader — Web APIs | MDN.
let reader = new FileReader();
FileReader() будет использоваться для чтения выбранного пользователем файла и создания предварительного просмотра изображения.
Затем мы добавляем event listener в наш FileReader() для события загрузки load:
reader.addEventListener("load", function () { this.showPreview = true; this.imagePreview = reader.result; }.bind(this), false);
Мы делаем это потому, что хотим узнать, когда файл завершил загрузку, а затем показать его пользователю. С нашим event listener мы используем .bind (this), который дает нам доступ к переменным нашего локального компонента Vue. Когда вызывается событие load, мы показываем изображение для предварительного просмотра и устанавливаем изображение в соответствии с тем, что было прочитано.
Наконец, мы читаем файл после нескольких проверок достоверности:
if( this.file ){ if ( /\.(jpe?g|png|gif)$/i.test( this.file.name ) ) { reader.readAsDataURL( this.file ); } }
Сначала мы проверяем, существует ли файл в file или имеет ли он значение NULL, то есть пользователь выбрал файл но он по каким то причинам не загрузился. Поскольку метод был вызван при загрузке файла, он должен быть там, но это простая проверка, чтобы убедиться, что мы не столкнемся с проблемами позже. Далее мы проверяем, что файл является изображением. Мы проверяем его имя на соответствующее расширение изображения. Если это изображение, то мы наконец читаем его:
reader.readAsDataURL( this.file );
Это запускает чтение загруженного пользователем файла, и когда оно будет завершено, запуститься событие load, которое мы слушаем, и в котором, мы покажем предварительное изображение.
Конечно, стоит добавить несколько стилей к тегу img, возможно, установить максимальную ширину или высоту, что бы, если пользователь загружает большой файл, оно не стало занимать весь экран, но в этом примере мы рассмотрим только функциональную часть. Далее выполним отправку файла на сервер.
Добавляем метод submitFile()
Это последний метод, который нам нужно будет добавить к объекту methods. Этот метод вызывается когда пользователь выберет файл и нажмет кнопку отправки. Мы будем использовать FormData() FormData — веб-API | MDN, как и прежде, чтобы отправить файл на сервер.
submitFile(){ let formData = new FormData(); formData.append('file', this.file); axios.post( '/file-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); }); },
В начале мы инициализируем наш объект FormData().
let formData = new FormData();
Далее мы добавляем локальную переменную file к объекту formData:
formData.append('file', this.file);
Здесь можно добавить любые другие данные, которые мы хотим отправить вместе с файлом выбранного изображения.
Далее мы делаем запрос к серверу, используя Axios:
axios.post( '/file-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); });
Первый параметр в методе axios.post() — это URL, куда мы будем отправлять наши данные. В моей тестовой среде я настроил конечную точку на API с именем /file-preview, который принимает с POST запрос. Второй параметр — это данные, которые мы отправляем на сервер. В данном случае это переменная formData. Третий параметр — это дополнительные настройки для запроса. Они чрезвычайно важны, так как нам нужно добавить заголовок ‘Content-Type’: ‘multipart/form-data’. Этот заголовок предупреждает сервер о том, что у нас есть данные формы и что они могут содержать файлы. Так же далее было указаны два обратных вызова. Один отработает в случае успеха и второй в случае неудачи передачи данных. Они ничего не делают, кроме вывода сообщения в консоль. Наш компонент на данный момент завершен, и вы можете проверить как работает предварительный просмотр загрузки файла для ваших пользователей!
Окончательная версия файла FilePreview.vue должна выглядеть следующим образом:
<style> div.container img{ max-width: 200px; max-height: 200px; } </style> <template> <div class="container"> <div class="large-12 medium-12 small-12 cell"> <label>File Preview <input type="file" id="file" ref="file" accept="image/*" v-on:change="handleFileUpload()"/> </label> <img v-bind:src="imagePreview" v-show="showPreview"/> <button v-on:click="submitFile()">Submit</button> </div> </div> </template> <script> export default { data(){ return { file: '', showPreview: false, imagePreview: '' } }, methods: { submitFile(){ let formData = new FormData(); formData.append('file', this.file); axios.post( '/file-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); }); }, handleFileUpload(){ this.file = this.$refs.file.files[0]; let reader = new FileReader(); reader.addEventListener("load", function () { this.showPreview = true; this.imagePreview = reader.result; }.bind(this), false); if( this.file ){ if ( /\.(jpe?g|png|gif)$/i.test( this.file.name ) ) { reader.readAsDataURL( this.file ); } } } } } </script>
Далее мы создадим компонент для загрузки и просмотра нескольких файлов! Концепции остаются прежняя, но есть несколько отличий.
Предварительное отображение загрузки нескольких файлов
Несколько файлов всегда представляют собой уникальную проблему. В этом случае нужно отображать все выбранные файлы. Конечно, вы всегда можете добавить способ удаления файлов перед загрузкой, как мы делали в предыдущем уроке: Загрузка файлов с помощью VueJS и Axios. В данном случае мы просто позволим пользователям загружать столько изображений, сколько они хотят, и отобразим предварительный просмотр каждого изображения.
Первое, что мы сделаем, это создадим файл FileMultiplePreview.vue, который будет обрабатывать все файлы предварительного просмотра.
Добавим шаблон и стили для загрузки нескольких файлов
Первое, что мы сделаем, это добавим шаблон для предварительного просмотра файлов:
<style> input[type="file"]{ position: absolute; top: -500px; } div.file-listing img{ max-width: 90%; } </style> <template> <div class="container"> <div class="large-12 medium-12 small-12 cell"> <label>Files <input type="file" id="files" ref="files" accept="image/*" multiple v-on:change="handleFilesUpload()"/> </label> </div> <div class="large-12 medium-12 small-12 cell"> <div class="grid-x"> <div v-for="(file, key) in files" class="large-4 medium-4 small-6 cell file-listing"> {{ file.name }} <img class="preview" v-bind:ref="'image'+parseInt( key )"/> </div> </div> </div> <br> <div class="large-12 medium-12 small-12 cell clear"> <button v-on:click="addFiles()">Add Files</button> </div> <br> <div class="large-12 medium-12 small-12 cell"> <button v-on:click="submitFiles()">Submit</button> </div> </div> </template>
Сначала мы добавили несколько стилей, необходимых для функциональности. Скрыли input со страницы, и при клики на кнопку Add Files, мы запускаем событие клика на элементе input, чтобы вызвать выбор файла. Мы сделали это, для организации более понятного интерфейса. Еще один стиль — просто устанавливает максимальную ширину изображения.
Если мы посмотрим на input в шаблоне, вы увидите несколько новых атрибутов:
<label>Files <input type="file" id="files" ref="files" accept="image/*" multiple v-on:change="handleFilesUpload()"/> </label>
Во-первых, и самое незначительное, мы изменили атрибут ref на files вместо file. Наиболее важным атрибутом является атрибут multiple, который позволяет пользователям выбирать несколько файлов. Мы также сохранили атрибут accept=»image/*», который позволяет пользователю загружать только изображения. Наконец, мы указали обработку загрузки с помощью метода v-on:change=»handleFilesUpload()».
Далее вы видите итератор VueJS, который перебирает загруженные файлы и отображает предварительный просмотр:
<div v-for="(file, key) in files" class="large-4 medium-4 small-6 cell file-listing"> {{ file.name }} <img class="preview" v-bind:ref="'image'+parseInt( key )"/></div>
Для этого отображается имя файла и настраивается предварительный просмотр изображения. В теге img у нас есть атрибут для ссылки, который является динамическим с префиксом v-bind:. Что мы делаем здесь, так это привязываем ссылку с именем image{key}. Мы используем индекс изображения в массиве files для генерации уникального идентификатора, на который мы можем ссылаться через глобальный объект $refs. Вы увидите, как мы это сделаем, когда добавим функциональность, но просто знайте, что мы будем заполнять каждый тег изображения соответствующим предварительным просмотром из ключевой ссылки.
Далее у нас есть кнопка добавления файлов:
<button v-on:click="addFiles()">Add Files</button>
Это позволяет пользователю добавлять исходные файлы и даже больше после их первоначального выбора. Каждый раз, когда пользователь добавляет файлы, они помещаются в массив локальных файлов, который запускает реактивный рендеринг через VueJS и отображает предварительный просмотр.
Наконец, у нас есть кнопка отправки, которая просто отправляет файлы на сервер:
<button v-on:click="submitFiles()">Submit</button>
Мы рассмотрим все эти методы и объясним, как они работают в следующих шагах.
Добавим data
Прежде чем мы добавим какую-либо функциональность, мы должны добавить переменные в data нашего компонента. В верхней части раздела <script> добавьте следующий код:
data(){ return { files: [] } },
Все что нам нужно для данных — это инициализировать пустой массив files.
Поскольку предварительный просмотр изображения отображается при наличии данных, нам не нужен метод show hide. Нам также не нужен заполнитель, мы будем заполнять атрибут src тега <img> через ссылки, которые мы увидим в следующих шагах.
Обработка нескольких загрузок файлов с предварительным просмотром
Когда пользователь загружает файлы, и input изменяется, поэтому мы слушаем это изменение и обрабатываем его с помощью метода handleFilesUpload() в объекте методов нашего компонента:
handleFilesUpload(){ let uploadedFiles = this.$refs.files.files; for( var i = 0; i < uploadedFiles.length; i++ ){ this.files.push( uploadedFiles[i] ); } this.getImagePreviews(); },
Сначала мы берем все загруженные файлы из input и устанавливаем его в локальную переменную uploadedFiles.
Затем мы перебираем все загруженные файлы и добавляем их в переменную files, чтобы мы могли перебирать ее в нашем шаблоне.
Наконец, мы вызываем метод getImagePreviews(). Он отобразит превью для каждого выбранного изображения.
Создание метода getImagePreviews()
Этот метод является основой отображения превью изображений для загруженных файлов. В методы нашего компонента Vue добавим следующий метод:
getImagePreviews(){ for( let i = 0; i < this.files.length; i++ ){ if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) { let reader = new FileReader(); reader.addEventListener("load", function(){ this.$refs['image'+parseInt( i )][0].src = reader.result; }.bind(this), false); reader.readAsDataURL( this.files[i] ); } } }
Хорошо, давайте пройдемся по функциональности.
Сначала мы перебираем все файлы, загруженные пользователем. Они копируются в локальный массив files:
for( let i = 0; i < this.files.length; i++ ){
Далее мы проверяем, является ли файл изображением. Я уверен, что есть другие способы предварительного просмотра PDF-файлов и других типов файлов, если они поддерживаются, но ради простоты в этой статье мы будем предварительно просматривать только изображения. Мы должны убедиться, что загруженный файл является изображением, проверив расширение:
if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {
Теперь мы создаем новый объект FileReader() и слушаем событие load. Мы связываем значения локального компонента через .bind (this), прикрепленный к функции-обработчику.
reader.addEventListener("load", function(){ this.$refs['image'+parseInt( i )][0].src = reader.result; }.bind(this), false);
Внутри этого метода мы ссылаемся на наш глобальный объект $refs с помощью ключевой ссылки, сгенерированной в нашем шаблоне. Когда мы перебираем изображения, индексы совпадают, и мы можем отобразить предварительный просмотр для каждого изображения.
Наконец, мы вызываем метод чтения файла:
reader.readAsDataURL( this.files[i] );
Он будет читать файлы по ключу в массиве. Когда файл будет загружен, запустится событие load и установится src тега изображения в массиве $refs с индексом, в котором он находится.
Вот и все, я очень быстро пробежусь по остальным методам, но они очень похожи на те, что обсуждались в предыдущем уроке. Теперь вы можете отображать изображения для превью нескольких загруженных файлов!
Метод addFiles()
Этот метод вызывается, когда пользователь нажимает кнопку Add Files. Метод должен выглядеть так:
addFiles(){ this.$refs.files.click(); },
Что это делает, запускает события клика на input, чтобы предложить пользователю загрузить файлы. Таким образом, мы можем скрыть input с экрана и создать приятный интерфейс.
Метод submitFiles()
Этот метод обрабатывает отправку файлов на сервер для обработки. Метод должен выглядеть так:
submitFiles(){ let formData = new FormData(); for( var i = 0; i < this.files.length; i++ ){ let file = this.files[i]; formData.append('files[' + i + ']', file); } axios.post( '/file-multiple-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); }); },
В первой части метода мы инициализируем FormData(), которая будет хранить все данные, которые мы отправляем на сервер.
let formData = new FormData();
Затем мы перебираем все файлы и добавляем их в только что созданную переменную formData.
for( var i = 0; i < this.files.length; i++ ){ let file = this.files[i]; formData.append('files[' + i + ']', file); }
Это создает массив файлов, которые мы отправляем на сервер.
Наконец, мы отправляем файлы на сервер:
axios.post( '/file-multiple-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); }); },
Для этого примера я только что создал конечную точку в своем API с URL /file-multiple-preview. Затем я передал formData методу axios.post() в качестве второго параметра. Если бы были какие-либо другие поля формы, вы могли бы добавить их в переменную formData. Третий и последний параметр является объектом конфигурации для запроса. Все, что я добавил здесь, это заголовок для предупреждения сервера о возможных файлах.
Я также добавил метод обработки в случае успеха, который выводит сообщение в консоль и метод перехвата любых неудачных запросов. Они должны быть настроены в зависимости от ваших потребностей и реализации.
Наш последний компонент FileMultiplePreview.vue должен выглядеть следующим образом:
<style> input[type="file"]{ position: absolute; top: -500px; } div.file-listing img{ max-width: 90%; } </style> <template> <div class="container"> <div class="large-12 medium-12 small-12 cell"> <label>Files <input type="file" id="files" ref="files" accept="image/*" multiple v-on:change="handleFilesUpload()"/> </label> </div> <div class="large-12 medium-12 small-12 cell"> <div class="grid-x"> <div v-for="(file, key) in files" class="large-4 medium-4 small-6 cell file-listing"> {{ file.name }} <img class="preview" v-bind:ref="'image'+parseInt( key )"/> </div> </div> </div> <br> <div class="large-12 medium-12 small-12 cell clear"> <button v-on:click="addFiles()">Add Files</button> </div> <br> <div class="large-12 medium-12 small-12 cell"> <button v-on:click="submitFiles()">Submit</button> </div> </div> </template> <script> export default { data(){ return { files: [] } }, methods: { addFiles(){ this.$refs.files.click(); }, submitFiles(){ let formData = new FormData(); for( var i = 0; i < this.files.length; i++ ){ let file = this.files[i]; formData.append('files[' + i + ']', file); } axios.post( '/file-multiple-preview', formData, { headers: { 'Content-Type': 'multipart/form-data' } } ).then(function(){ console.log('SUCCESS!!'); }) .catch(function(){ console.log('FAILURE!!'); }); }, handleFilesUpload(){ let uploadedFiles = this.$refs.files.files; for( var i = 0; i < uploadedFiles.length; i++ ){ this.files.push( uploadedFiles[i] ); } this.getImagePreviews(); }, getImagePreviews(){ for( let i = 0; i < this.files.length; i++ ){ if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) { let reader = new FileReader(); reader.addEventListener("load", function(){ this.$refs['image'+parseInt( i )][0].src = reader.result; }.bind(this), false); reader.readAsDataURL( this.files[i] ); } } } } } </script>
Если у вас есть какие-либо вопросы, обязательно дайте мне знать!
Заключение
Эта статья поможет добавить еще несколько наворотов при загрузке файла с AJAX с использованием VueJS и Axios. Предварительный просмотр изображений очень полезен при создании удобного пользовательского интерфейса.
Оригинальная статья: Dan Pastori — Preview File Uploads with Axios and VueJS