Компания
902,18
рейтинг
6 ноября 2013 в 11:03

Разработка → FileAPI 2.0: Загрузка файлов на сервер год спустя

FileAPI 2.0Привет Хабр! Примерно год назад я представил вашему вниманию первую версию open-source библиотеки FileAPI, предназначенную для работы с файлами на клиенте и последующей загрузки на сервер.

За это время был пройден долгий путь. Библиотека заработала 670+ звезд и 90+ форков. С помощью github-сообщества удалось исправить множество «детских» проблем и внести ряд улучшений. Было закрыто более 100 тасков, и благодаря Илье Лебедеву сделана загрузка файлов по частям. Сегодня я с гордостью хочу представить вам FileAPI 2.0.


Итак, первая версия обладала следующими возможностями:
  • множественный выбор файлов;
  • получение информации (название, размер и mime-тип);
  • генерация предпросмотра до загрузки;
  • масштабирование, кадрирование и поворот на клиенте;
  • загрузка всего, что получилось, на сервер + CORS;
  • поддержка всего вышеперечисленного в старых браузерах, включая IE6.


Для поддержки старых браузеров используется Flash. В отличие от других подобных решений, где нужно явным образом задать элемент, который будет кнопкой «Выбрать файлы», FileAPI не накладывает таких ограничений. Разработчику не нужно думать о том, какую технологию в данный момент использует библиотека. При этом написанный код максимально приближен к нативному, т.е. HTML5:

<span class="js-fileapi-wrapper">
    <input id="file" type="file" multiple />
</span>
<script>
    var input = document.getElementById("file");
    FileAPI.event.on(input, "change", function (){
        var list = FileAPI.getFiles(input); // Получаем список файлов
    
        // Загружаем на сервер
        FileAPI.upload({
             url: "./ctrl.php",
             files: { userFiles: list },
             complete: function (err, xhr){ /*...*/ }
        });
    });
</script>

Библиотека определят возможности браузера и, если чего-то не хватает, переключается на Flash.

Почти сразу после публикации я начал получать первые отзывы и пожелания, приведу самые интересные из них.

Ошибка при обфускации
У одного юзера код не работал, совсем. Проблема была в конструкции (api.expando + ++gid). Оказалось, его обфускатор не понимал её и просто убирал пробелы, что приводило к синтаксической ошибке, поэтому код пришлось изменить на (++gid, api.expando + gid).

Особенности интеграции с Amazon S3
При вызове метода FileAPI.upload к url, на который нужно сделать запрос, библиотека добавляет уникальный GET-параметр, чтобы избежать кеширования POST-пост запроса на мобильных устройствах. А при интеграции с Amazon S3 выяснилось, что он не допускает GET-параметры. Так как невозможно с достаточной точностью определить все мобильные устройства по user-agent, в upload добавлена опция cache, при помощи которой можно влиять на добавление уникального GET-параметра.

Работа с изображениями
Все изображения принудительно конвертировались в png, что приводило к увлечению размера файла на выходе, к тому же менялся исходный тип, что было критично для многих задач. Помимо этого, часто требуется добавить водяной знак в загружаемое изображение или сфотографировать «себя» при помощи WebCam.

«Загрузка» без файлов
Так как API создавалось для загрузки файлов, то метод FileAPI.upload выдавал ошибку при вызове его без самих файлов. Как оказалось, это достаточно частый случай. Например, когда у вас есть форма, в которой поле «файл» необязательно.

Кроме того, слабая документация и отсутствие кода в несжатом виде (исходники были, но сжимались при помощь своего «велосипеда») затрудняло отладку и внесение собственных изменений. Отсутствие юнит-тестов сильно сказывалось на скорости и качестве разработки. Как это не удивительно, но многим пользователям не нужно низкоуровневое API и каждый из них начинает писать какую-то свою обертку, в большинстве случаев jQuery plugin. Поэтому нужно было предложить готовое решение, которое бы охватывало все основные задачи.

Собрав и проанализировав отзывы, был составлен план действий:
  • Grunt — инструмент для сборки кода;
  • QUnit — тестирование основного функционала (Grunt + PhantomJS);
  • Фичи — улучшенная работа с изображениями и WebCam;
  • jQuery plugin — супер-пупер плагин для типовых задач;
  • Документация — подробное описание методов и примеры.



Grunt


Как я уже говорил, в первый версии js собирался при помощи примитивного скрипта, который просто сливал и обфуцировал 6 файлов в один. Для того, чтобы вносить изменения или дебажить код, нужно было подключить 6 исходников в определенном порядке. Это неудобно, поэтому требовался инструмент для сборки проекта. В качестве такого инструмента был выбран Grunt, который де-факто является стандартом при разработке и сборке проекта. С его помощью мы не только собираем FileAPI, но и прогоняем ее код через JSHint и QUnit-тесты, о которых я расскажу дальше. Для того, чтобы начать использовать Grunt, достаточно создать два файла: package.json c описанием пакета и Gruntfile.js с перечислением нужных тасков и их опций.

Давайте подробно рассмотрим Gruntfile.js. Он состоит из 5 основных тасков:

jshint

Это ответвление от JSLint, валидатора для проверки корректности JavaScript-кода. В отличие от него, JSHint можно настроить под ваш код, чтобы следить за единым стилем оформления, проверять на пропущенные точку с запятой, лишние запятые в конце массива или объекта, неиспользуемые переменные и части кода.

concat

Собирает файлы в один. В FileAPI эта секция состоит из двух частей `all` и `html5`, что соответствует двум сборкам: c поддержкой flash и без неё, например, для мобильных проектов.

uglify

Обфускация кода, в нашем случае он сжимает файлы, полученные после concat.

watch

Т.к. библиотека состоит из нескольких файлов, а подключается только один, то данный таск следит за изменениями js файлов и запускает таск concat.

qunit

При помощи PhantomJS выполняет QUnit тесты, что позволяет протестировать базовый функционал.

Использовать grunt очень просто, но перед началом работы нужно установить необходимые зависимости. Делается это единожды, при помощи команды npm install.

Теперь мы можем запускать таски:
$ /FileAPI/ > grunt — выполнить default таск (jshint + build)
$ /FileAPI/ > grunt build — сборка и обфускация (concat + uglify + qunit)
$ /FileAPI/ > grunt watch — следить за изменениями и, в случае обнаружения таковых, запускать concat


Тестирование


Как вы уже поняли, для тестирования используется QUnit, мне он всегда нравился своей лаконичностью и простотой. К тому же, для него есть готовый grunt-таск. Тесты запускаются через PhantomJS, а во время разработки можно просто обновлять страницу и ждать результатов теста.

FileAPI + Qunit

Но, как оказалось, в стандартном grunt-таске нельзя привязать файлы к нужным мне для теста инпутам. Поэтому пришлось его немного модифицировать:

qunit: {
    options: {
         // Файлы: ключ — название инпута, значение — список файлов
         files: {
             one: ["foo.jpeg"],
             multiple: ["bar.txt", "baz.png", "qux.zip"]
         }
     },
     all: ["tests/*.html"]
}

Первыми тестируются методы работы с файлами. Такие как получение мета информации (название, тип, размер, exif), чтение содержимого (DataURL, BinaryString и Text). Далее загрузка, в ходе которой проверяются события и ответ от сервера.

Но самое интересное, это тестирование работы с изображениями, тут всё хитрей. Так как FileAPI «из коробки» умеет трансформировать изображения согласно заданным правилам, то нужно проверять, чтобы изображение, полученное сервером, было именно тем, которое нужно. Для этого используется два набора изображений: исходные, которые загружает библиотека, и эталонные, с которыми сравнивается результат. Как же это происходит? FileAPI загружает изображение с тестируемыми параметрами трансформации и в ответ получает dataURL. Данные передаются в assert-функцию, преобразуются в canvas и попиксельно сравниваются с эталонным изображением. Если расхождение меньше < 1%, то тест пройден.

function imageEqual(actual/**Image*/, expected/**Image*/, message/**String*/){
	actual = toCanvas(actual);
	expected = toCanvas(expected);

	if( actual.width != expected.width ){
		ok(false, message + ' (width: ' + actual.width + ' != ' + expected.width + ')');
	}
	else if( actual.height != expected.height ){
		ok(false, message + ' (height: ' + actual.height + ' != ' + expected.height + ')');
	}
	else {
		var actualData = actual.getContext('2d').getImageData(0, 0, actual.width, actual.height);
		var expectedData = expected.getContext('2d').getImageData(0, 0, expected.width, expected.height);
		var pixels = 0, failPixels = 0;

		for( var y = 0; y < actualData.height; y++ ){
			for( var x = 0; x < actualData.width; x++ ){
				var idx = (x + y * actualData.width) * 4;
				pixels++;
				if( !pixelEqual(actualData.data, expectedData.data, idx) ){
					failPixels++;
				}
			}
		}

		ok(failPixels / pixels < .01, text + ' (fail pixels: ' + (failPixels / pixels) + ')');
	}
}

function pixelEqual(actual, expected, idx){
	var delta = 3; // допустимая погрешность
	return (Math.abs(actual[idx] - expected[idx]) < delta)
	     && (Math.abs(actual[idx+1] - expected[idx+1]) < delta)
             && (Math.abs(actual[idx+2] - expected[idx+2]) < delta);
}

Особенность этого метода заключается в том, что эталонное изображение нужно делать под каждый браузер (Phantom, Firefox, Chrome). Это связанно c тем, что цветопередача и алгоритмы сжатия в каждом браузере свои. Забавная ситуация произошла с Safari. Изначально я сохранял эталонные изображения средствами браузера, а не на серверной стороне. Так вот, в Safari изображение, построенное на основе dataURL и сохраненное диск, не совпадает с тем, что вы видите на экране, цвета искажены.

Увы, это только функциональное тестирование, которое очень сильно помогает, но не может заменить ручное там, где используется Flash. Помимо этого есть идея создать grunt-таск, который запустит QUnit тесты через Selenium, вот тогда заживем.


Фичи


Grunt, тестирование — это, конечно, хорошо, но никак не тянет на версию 2.0, хотелось чего-то эдакого.

Image overlay

Примерно через месяц после публикации, у меня спросили, как при помощи библиотеки наложить watermark на изображение и загрузить результат на сервер? Эту задачу можно было решить, предоставив прямой доступ к canvas, через который происходят трансформации (как это сделано в jQuery FileUpload). Но, увы, существуют IE ниже 10 версии, где все трансформации иду через Flash, поэтому решено было создать метод, который позволял бы делать любое количество наложений с гибкой системой позиционирования:

FileAPI.Image(file)
    .overlay([
         {
             x: 10, y: 5,    //  смещение
             rel: FileAPI.Image.RIGHT_BOTTOM, // центр
             opacity: 0.7, // степень прозрачности, от 0 до 1
             src: "watermark.png" // накладываемое изображение
         }
    ])
    .get(err/**Mixed*/, img/**HTMLElement*/){ /*__*/  })
;

Также, одноименное свойство поддерживается в imageTransform:

FileAPI.upload({
     imageTransform: {
           overlay: {
                x: 5, y: 5,
                rel: FileAPI.Image.CENTER_TOP,
                src: "watermark.png" // накладываемое изображение
           }
     }
});

Как видите, метод получился простой, но гибкий.


WebCam


Главным нововведением стала работа с веб-камерой. Для этого в HTML5 появился метод navigator.getUserMedia. Работать с ним очень просто:

http://jsfiddle.net/RubaXa/uZhRp/
function setWebCam(video/**HTMLVideo*/, doneFn/**Function*/) {
    var navigator = window.navigator;
    var getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

    getMedia.call(navigator, {
        video: true // запрашиваем доступ к веб-камере
    }, function (stream) {
        var URL = window.URL || window.webkitURL || window.mozURL;

        video.addEventListener('loadedmetadata', function () {
            doneFn();
        }, false);

        video.src = URL.createObjectURL(stream);
        video.play();
    }, function () { /* в доступе отказано */ });
}

setWebCam(videoEl, function () {
     // Есть сигнал
});

Вроде все работает, но если открыть этот пример в FireFox, то будет видно, что callback срабатывает раньше, чем появляется изображение. Поэтому пришлось сделать определение сигнала через canvas:

http://jsfiddle.net/RubaXa/uZhRp/
Листинг
function setWebCam(video/**HTMLVideo*/, doneFn/**Function*/) {
    function _detectVideoSignal() {
        var canvas = document.createElement('canvas'), ctx, res = false;
        try {
            ctx = canvas.getContext('2d');
            ctx.drawImage(video, 0, 0, 1, 1);
            res = ctx.getImageData(0, 0, 1, 1).data[4] != 255;
        } catch (e) {}
        return res;
    }

    var navigator = window.navigator;
    var getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

    getMedia.call(navigator, { video: true  }, function (stream) {
        var pid, URL = window.URL || window.webkitURL || window.mozURL;

        pid = setInterval(function () {
            if (_detectVideoSignal()) {
                clearTimeout(pid);
                doneFn();
            }
        }, 100);

        // ...
    }, function () { /* в доступе отказано */ });
}

setWebCam(videoEl, function () {
    // Есть сигнал
});

Увы, но на момент написания статьи метод navigator.getUserMedia поддерживают только FireFox и Сhrome, что, конечно, хорошо, но не достаточно. Поэтому мы сделали fallback во Flash, что позволило задействовать все остальные браузеры. В итоге получилось следующие API для работы с камерой:

var el = document.getElementById('container');

// Публикуем кумеру
FileAPI.Camera.publish(el, { width: 320, height: 240 }, function (err, cam/**FileAPI.Camera*/){
      var btn = document.getElementById('shot')

      // btn — кнопка, при помощи которой делаем скрин
      FileAPI.event.on(btn, 'click', function (evt){
           var shot = cam.shot(); // Экземпляр FileAPI.Image
           
           FileAPI.upload({
                url: './ctrl.php',
                files: { photo: shot }
           });
      });
});


FileAPI.Camera

  • publish(el, options, callback) — статический метод, для публикации камеры;
  • start(callback) — начать вещание;
  • shot() — создать скриншот, возвращает наследника FileAPI.Image;
  • stop() — остановить камеру.



Фильтры


Сделав две фичи, я подумал, что нужно что-то ещё, не хватает вау-эффекта. Но в голову ничего не приходило. Спустя некоторое время, я случайно наткнулся на замечательную библиотеку CamanJS, которая позволяет не только делать цветокоррекцию, но и использовать сложные режимы наложения изображений, а также мощные фильтры — очень советую. Это было то, что нужно: камера есть, работа с изображениями тоже, осталось добавить CamanJS — и свой «инстаграм» с FileAPI, WebCam и фильтрами готов.

Использовать все это очень просто, в составе CamanJS идут 10 предустановленных фильтров, посмотреть их в работе вы можете тут и тут.
FileAPI.Image(file)
    .filter('hazyDays') // CamanJS фильтр
    .get(function (err, img/**Image*/){
        /* ... */
    })
;

Помимо этого, можно передать функцию и в ней реализовать более сложные трансформации. На вход будут переданы ссылка на canvas и функция обратного вызова.

FileAPI.Image(file)
    .filter(function (canvas, callback){
          // модифицируем canvas
          callback();
    })
    .get(function (err, img/**Image*/){
        /* ... */
    })
;

Увы, всё это работает только при поддержке HTML5. Если честно, очень хотелось сделать поддержку вышеперечисленного функционала через Flash, и это даже возможно: всего-то нужно реализовать необходимые методы для работы с canvas во Flash. Как-нибудь в другой раз.


jQuery.FileAPI


Финальным нововведением, стал полноценный плагин для jQuery. В нем я постарался учесть самые распространенные особенности загрузки файлов, такие как:

  • «Одной кнопкой» — выбрать и автоматически загрузить файл;
  • «Ограничения» — минимальный/максимальный размер как файла, так и изображения, по ширине и высоте;
  • «Работа с очередью» — сортировка и фильтрация очереди загрузки файлов;
  • «Изображения» — предпросмотр, поворот и кадрирование;
  • «Интерфейс» — гибкая и прозрачная настройка интерфейса;
  • А также Drag’n’Drop и WebCam.


Оценить возможности вы можете на демо странице, github или заглянув под спойлер.

«Одной кнопкой»
image
Загрузка аватары
image
Множественная загрузка
image

Здесь разберу только один пример, загрузку аватара, а именно: выбор фотографии, кадрирование и последующую загрузку на сервер. Как это ни удивительно, но ни одно из популярных решений не дает такой возможности. По этой причине данный кейс был реализован в первую очередь:

Верстка
<div id="userpic" class="userpic">
   <div class="js-preview userpic__preview"></div>
   <div class="btn btn-success js-fileapi-wrapper">
      <div class="js-browse">
         <span class="btn-txt">Choose</span>
         <input type="file" name="filedata">
      </div>
      <div class="js-upload" style="display: none;">
         <div class="progress progress-success"><div class="js-progress bar"></div></div>
         <span class="btn-txt">Uploading</span>
      </div>
   </div>
</div>

$("#userpic").fileapi({
	url: "./ctrl.php",
	accept: "image/*",
	imageSize: { minWidth: 200, minHeight: 200 },
	elements: {
		// Если аплоудер активен (идет загрузка), то
		active: {
			show: ".js-upload", // показать блок
			hide: ".js-browse" // скрыть блок
		},
		// Куда выводить предпросмотр выбранного излбражения
		preview: {
			el: ".js-preview",
			width: 200, // его размеры
			height: 200
		},
		// Элемент отвечающий за отображение хода загрузки
		progress: ".js-progress"
	},
	onSelect: function (evt, ui){
		var image = ui.files[0];
		if( image ){
			createModal(function (overlay){
				$(overlay).on("click", ".js-upload", function (){
					closeModal();
					$("#userpic").fileapi("upload"); // Загружаем
				});

				$(".js-img", overlay).cropper({
					// Файл изображения
					file: image,
					// Цвет затемнения
					bgColor: "#fff",
					// Максимальные размеры, в которые нужно вписать изображение
					maxSize: [$(window).width() - 100, $(window).height() - 100],
					// Минимальные размера кроп-рамки
					minSize: [130, 130],
					// Размер кроп-рамки: Math.min(width*.9, height*.9)
					selection: 0.9, // или "90%"
					// Соотношение сторон кроп-рамки
					aspectRatio: 1,
					// Выбрана новая кроп-область
					onSelect: function (coords/**Object*/){
						$("#userpic").fileapi("crop", image, coords);
					}
				});
			});
		}
	}
});


Кроме того, плагин обладают очень гибкими настройками внешнего вида, что позволяет подстроиться под большинство задач. Ну, а если чего-то не хватает или вы нашли баг, то вы всегда можете оставить тикет на github, либо написать мне — буду рад помочь.

Помимо самой библиотеки и плагина, очень сильно переработана документация. Теперь это два полноценных сайта:



Также вы можете следить за нашими проектами через:
github.com/mailru — FileAPI, Tarantool, Fest и многое другое
github.com/rubaxa — мой github
@ibnRubaXa
Автор: @RubaXa

Комментарии (83)

  • 0
    rubaxa.github.io/jquery.fileapi/
    пример номер 2 у меня почему-то не работает: выбираю картинку и ничего не происходит
    Chrome 30.0.1599.101 m
    • 0
      Что за картинка, какой тип, размеры? Можете прислать на k.lebedev@corp.mail.ru?
      P.S. Написал вам, чтобы тут не «раздувать».
    • +1
      Получил изображение, оно у вас меньше 200х200 (imageSize: { minWidth: 200, minHeight: 200 }), просто не делал нотификацию.
  • 0
    Возможно я пропустил, но в глаза бросилось только
    >«Но, увы, существуют IE ниже 10»
    Можно где-нибудь добавить список поддерживаемых браузеров?
    • +3
      Поддержка браузерами (IE6+, Chrome 10+, FF 3.8+, Opera 10+, Safari 5+):
      • Загрузка по олному файлу — все браузеры;
      • Множественная загрузка — все браузеры с поддержкой Flash или HTML5;
      • Работа с изображениями — все браузеры с поддержкой Flash или HTML5;
      • WebCam — все браузеры с поддержкой Flash или HTML5;
      • Drag'n'Drop — только HTML5.
      • 0
        Добавьте детектирование возможностей. Если Drag'n'Drop не поддерживает браузер, то выдайте сообщение об этом (поведение по умолчанию с возможностью отключения).
        • 0
          Кстати вы заявили что «Для поддержки старых браузеров используется Flash» я расчитывал что если браузер не поддерживает Drag'n'Drop то будет использована соответствующая Flash заглушка. А это не так, не вводите людей в заблуждение.
          • 0
            Никто и не вводит, написано же «только HTML5» (FileAPI.support.dnd). Если вы говорите про пример на странице плагина, то это просто недоработка самого примера, обязательно поправлю.
  • 0
    Хороший компонент, буду тестить ;-)
  • 0
    Android 2.3.6. 1-ый раз выскочило — «отсутствуют файлы изображений». Возможно это флеш (Adobe Flash Player 11.1) тупит. Или Android как всегда
    • 0
      Андроид это отдельная песня, если нашли проблему, напишите таск, будет разбираться. Увы, очень многое зависит от версии и устройства.
  • +19
    И ведь не придерешься:
    var shot = cam.shot();
    • +9
      Я знал что вам понравится :]
  • +1
    собирать бы это дело по зависимостям и разделить на amd или commonJs модули! будет время сделаю пулл реквест!
  • +5
    image

    что ж вы так неаккуратно списки файлов подбираете :)

    а за библиотеку огромное спасибо, обязательно попробую использовать в своих проектах, как раз не хватало подобного функционала.
    • +12
      Все файлы были загружены в ознакомительных целях и для тестирования библиотеки.
      • 0
        Кстати, как вам? С чем порекомендуете тоже ознакомиться?
        • +11
          Fallout II — вполне неплохая игра, мне понравилась;
          Police Squad! — для любителей Лесли Нильсена;
          Adventure Time — норм;
          «Стальные пещеры» — очень советую, вся трилогия великолепна, но лучше книгу, озвучка слабовата.
  • 0
    Не хватает выхода из режима кропа, если передумал.
    • +2
      Он просто выключен: closeOnEsc: false, closeOnOverlayClick: false.
  • 0
    Библиотека поддерживает загрузку больших файлов по частям? Дробление на клиенте на мелки фрагменты и их сборку на сервере в готовый файл?
    • 0
      Да, единственное чего нет, это примеров сервереной части. Но думаю в ближайшее время появится инструкция на эту тему.
  • 0
    Как это не удивительно, но многим пользователям не нужно низкоуровневое API

    Ключевая мысль. Когда она придет в голову еще раз, глядишь, 3-й версией плагина можно будет пользоваться) Не так уж много вариантов использования загрузчика в реальных проектах (остальные либо от незнания, либо баловство). Самые распространенные представлены на демо-страничке, что обнадеживает. Очевидно, что описываются они слишком многословно, можно было бы сократить код раз в пять, или пресеты сделать. Хотелось бы видеть плагины для популярный фреймворков (ангуляр, бекбон) и, конечно, кастомизатор, чтобы пользователь, которому не нужна поддержка старых браузеров или веб-камеры мог бы сделать сборку без них. И еще, хороший тон — насильно объединять кнопку загрузки и область перетаскивания, чтобы файлы загружались при перетаскивании на кнопку.

    А, вообще, спасибо за плагин!
    • 0
      Я не зря написал «для большинства», например мне и моим коллегам плагин не нужен (сейчас он используется только в одном месте), для большинства задач нужно именно API. Это отдельная часть библиотеки, я очень надеюсь, что найдутся люди, готовые участвовать в его развитии.

      Очевидно, что описываются они слишком многословно, можно было бы сократить код раз в пять, или пресеты сделать
      Мне вот неочевидно, я очень старался сделать его локаничным, если есть способ ещё сократить код, буду рад услышать как.

      Хотелось бы видеть плагины для популярный фреймворков (ангуляр, бекбон) и, конечно, кастомизатор, чтобы пользователь, которому не нужна поддержка старых браузеров или веб-камеры мог бы сделать сборку без них.
      Под ангуляр если и делать, то jquery.fileapi не нужен, там хватит чистого FileAPI. Сборки есть, работа с камерой — 244 строчки вместе с комментариями, так что не вижу смысла делать её опциональной.

      • 0
        Что нужно так это кросбраузерность, даже если какой то из браузеров не поддерживает мультизагрузки или Drag'n'Drop, то это бы детектилось подгружался нужный плагин. Чтобы всместе с файлом на сервер уходили дополнительно данные: CSRF-токен, данные авторизации и т.п. Всё это корректно работало бы и через https (у флеша там есть особенность передачи кукисов). Вот это было бы круто. А низкоуровневое апи — если идти на низкий уровень, то зачем какой-то плагин, всё сам пишешь.
        • 0
          Не очень понял о чем, работу с Flash и проблемы связанные с этим, я разбирал в первой статье .

          А низкоуровневое апи — если идти на низкий уровень, то зачем какой-то плагин, всё сам пишешь.
          Это заблуждение, практически всё, что сейчас есть в HTML5 невозможно использовать без оберток, начиная от localStorage, кончая IndexedDB.
  • 0
    Потихоньку пилю загрузчик для Ангуляра. Тут описал какие-то свои мысли: habrahabr.ru/post/191464/
    Думаю, нужно подходить с другой стороны. Сначала собрать статистику наиболее распространенных вариантов загрузки, потом хорошенько продумать эти варианты (со стороны правильности разработки и со стороны удобства для пользователей) и заточить АПИ под них. Например, как могла бы выглядеть настройка превьюшки:

    preview: {
      cropper: '200px', //область всегда квадратная, меняется только масштаб
      copyright: 'watermark.jpg' //правый нижний угол
    }
    

    Остальные 100500 настроек можно закопать в АПИ, чтобы не мозолили глаза
    • 0
      Не очень понял, что это за часть кода. Попробуй взять любой пример и написать его как бы вы хотели его видеть.
      • 0
        Мне тоже кажется, что такой многословный код со стороны пользователя библиотеки не нужен. Если подумать, все действия можно было описать в декларативном стиле с параметрами. При необходимости некоторой кастомизации передавать в параметрах колбеки.

        Вот мне, например, требуется такой алгоритм (пока не понял, возможен ли он с вашей либой):
        1) 2 кнопки: выбрать файл | сделать фото;
        2) если делаем фото, открывается снималка;
        3) если выбираем файл или после сделанного фото, открывается кропалка;
        4) файл заливается на сервер.

        Не понял пока, можно ли совместить камеру и кроп. Хотя даже если нельзя, это не жизненно критично.

        Так вот как бы я хотел это описать на JS:

        var a = FileApi({
            url: 'url.php',
            max_...: ...,
            ...
        });
        $('#select-photo').on('click', a.delegate(['select', 'crop', 'upload']);
        $('#cam').on('click', a.delegate(['cam', 'crop', 'upload']);
        


        Я понимаю всю наивность такого подхода, но считаю, что если все круто продумать, то новых проблем это не создаст. Вот вы пользуетесь МакОсью. Наверное, знаете про ее 100500 ограничений в стиле Эппл решил что для вас лучше так, и вариантов больше нет. Но все равно все довольны (кроме мня). Так же и тут. Некоторые возможности можно закрыть, но вместо это дать на порядок более простой API, который устроит 98 % пользователей.

        Конечно, в моем примере можно было вместо строк в массиве порядка действий дать хэши с параметрами (если нужно), с колбеками, которые будут делать кастомизацию. Т.е. тот АПИ, что есть сейчас, он никуда не денется, просто над ним будет офигенная обертка. Именно об этом и пишут люди в комментах. В этой ветке, и веткой выше.
      • 0
        Simple button
        $('#simple-btn').fileapi({
           url: 'http://rubaxa.org/FileAPI/server/ctrl.php',  
           maxSize: 20 * FileAPI.MB,
           elements: {...}
        });
        

        maxQuantity: false // 1, 2, 3… максимальное количество загружаемых файлов. По-умолчанию без ограничений
        onSelect: false //по умолчанию автозагрузка

        Userpic + crop
        $('#userpic').fileapi({
           url: 'http://rubaxa.org/FileAPI/server/ctrl.php',
           accept: ['jpg', 'png', 'gif'], //так намного понятнее, да и серверные функции чаще с конкретным типом работают
           elements: {...},
           onSelect: function (evt, file){
              if( file ){
                 $('#popup').modal({
                    onOpen: function (overlay){
                       $(overlay).on('click', '.js-upload', function (){
                          $.modal().close();
                          $('#userpic').fileapi('upload');
                       });
                       $('.js-img', overlay).cropper({
                          file: file,
                          size: '200px', //по умолчанию квадрат. Остальные настройки, вообще, не нужны
                          onSelect: function (coords){
                             $('#userpic').fileapi('crop', file, coords);
                          }
                       });
                    }
                 }).open();
              }
           }
        });
        

        imageSize: { minWidth: 200, minHeight: 200 } Изображение должно автоматом растягиваться до нужного размера

        File upload
        $('#file-upload').fileapi({
           onSelect: function (evt, file){
              $( '.js-send').click(function () {
                 $('#file-upload').fileapi('upload'); //Загрузка по клику редкая операция, не стоит для нее создавать отдельные абстракции типа ctrl
               })
              $('.js-reset' ).click(function () {
                 $('#file-upload').fileapi('abort');
              })
           }
           elements: {...}
        });
        


        Как-то так хотя бы
        • 0
          В статье и на сайте, я специально развернул примеры, чтобы показать возможности для кастомизации (тут их меньше половины). Даже сейчас работа с плагином может выглядеть просто `$('..').fileapi({ })` (http://jsfiddle.net/RubaXa/VEBvN/ — Simple button, File upload). Так же, ваша запись про «File upload» будет работать, только вместо «abort» -> «reset».

          imageSize: { minWidth: 200, minHeight: 200 } Изображение должно автоматом растягиваться до нужного размера
          На своей практике не знаю ни одного такого примера.

          feedbee, чем сильней сокращаешь запись, вкладывая в параметры «глубокий смысл», тем меньше понимания у человека, который будет этим пользоваться, в конечном итоге это превратится в магию и лишении вопросы.

          P.S. Это не значит, что я с вами не согласен, отнюдь, очень хочется сделать галерею примеров, где были бы уже готовые варианты использования (html + js), будет время обязательно займусь.
          • 0
            imageSize: { minWidth: 200, minHeight: 200 } Тут скорее хотел сказать, что не стоит отсеивать картинки по размеру (по весу — да). Тем более запрещать загрузку маленьких изображений. Если, вообще, правильно понял эту настройку.
            тем меньше понимания у человека, который будет этим пользоваться
            На то и расчет, что программист должен меньше напрягаться + нужно усложнить ему создание плохих дизайнерских решений + не давать настраивать то, что обычно настраивается более привычным способом (в ваших примерах это ctr вместо стандартного способа задания обработчика; bgColor, который всегда настраивается CSS и т.п.)
            • 0
              imageSize — это необязательный параметр, который позволяет на этапе выбора предупредить человека что выбранная им фотография (например для аватары) меньше минимальный размеров.

              + не давать настраивать то, что обычно настраивается более привычным способом (в ваших примерах это ctr вместо стандартного способа задания обработчика;
              Не понял, «обычным» это каким?
              • 0
                Для Жиквери обычный способ это:
                $('selector').click(function () {...}) или .on('type', function () {...})

                Это и понятнее и гибче. Может быть разработчик по свайпу вправо захочет грузить, а по свайпу влево отменять. Вы же вводите новое понятие:
                ctrl: { upload: '.js-send', reset: '.js-reset' }

                Тем более для операций, которые, вообще, не стоит давать пользователям.
                • 0
                  Мне кажется вы не понимате. Это возможность задать selector элемента управления, чтобы как раз не писать дополнительный код, который вы привели, плагин сам повесит нужные события. Это общепринятая практика, посмотрите любой iquery plugin, а лучше jQuery UI (например draggable: $( "#draggable2" ).draggable({ cancel: "p.ui-widget-header" });)
                  • 0
                    Это как раз понятно) Но встречается такое очень редко, а если и встречается, то для пары-тройки методов, так же как и у вас. Нужно ли такое упрощение? Тут, видимо, вопрос предпочтений.

                    В любом случае, если вынести их из объекта ctrl, хуже не будет
          • 0
            только вместо «abort» -> «reset».
            Почему, кстати reset? В xhr называется abort, а reset будет лишний раз сбивать с толку
            • 0
              // Controls
              ctrl: {
              	upload: '[data-fileapi="ctrl.upload"]',
              	reset: '[data-fileapi="ctrl.reset"]',
              	abort: '[data-fileapi="ctrl.abort"]'
              }
              мы же говорили про пример «File upload», а там небыло abort, вот я и поправил. abort как видите тоже есть.
  • 0
    Интересует возможность генерации превьюшек с кропом на клиенте.

    Например пользователь выбирает файл, ему предлагается кроп для превью, и на сервер отправляется 2 файла — оригинал для полноценного просмотра и кропнутая версия для превью картинки. Бывает нужно, например, когда загружаются фотографии в фотогалерею и просматривать можно фото полного размера, а превью у всех фоток фиксированного, и часто при автоматическом кропе в таких ситуациях возникает проблема «отрезанных голов» и тд
  • 0
    Не нашел примера с сортировкой загруженных картинок, есть ли возможность их сортировать перетаскиванием?
    • 0
      Есть опция sortFn для сортировки выбранных файлов перед загрузкой. То что вы хотите, не относится к тематике моего плагина, для это можно взять например jQuery UI / Sortable.
      • 0
        как раз хочется отсортировать перед загрузкой на сервер… эта функция отрабатывает один раз или можно сделать несколько вызовов?
        • 0
          Вы уж определитесь, вам до или после нужно сортировать. sortFn — сортирует выбранные файлы.
          • 0
            да простите, неверно выразился, все таки есть ли какое-то событие которое запускает sortFn? возможно onDrop?
            • 0
              Фунция выполняется в момент выбора файлов пользователем, пример при помощи диалога или drag'n'drop.
              • 0
                понял спасибо ) может быть добавите событие для сортировки без выбора нового файла, мне кажется это будет удобно
  • 0
  • 0
    >FileAPI.event.on(input, «change», function (){… });
    Мне кажется, здесь лучше поменять местами первые два параметра, по аналогии как метод .on() сделан у jQuery.on(). Слово «on» как бы говорит — «на событие инпута реакция такая-то» — .on('change', input, fn)
    • 0
      Библитека Bean с вами не согласна + в jQuery такая запись означает делегирование, могла возникнуть путаница. Да и менять уже поздно, библиотеке год.
  • 0
    Пробую прикрутить jquery плагин к своему проекту и возник вопрос:
    — А каким образом получить сообщение об ошибке, когда пользователь пытается выбрать файл большего размера чем разрешено?
    • +1
      $('...').fileapi({
          onSelect: function (evt, ui){
              ui.all; // Все выбранные файлы
              ui.files; // Прошли 
              ui.other; // Не прошли условия фильтрации
      
              if( ui.other.length ){
                   alert("Ошибка: ...");
              }
          }
      });
      
      • 0
        Спасибо, а как отделить ошибку по максимальному размеру файла, от ошибки по ширине изображения?
        Размер файла я могу взять из ui.other[0].size, а ширину, высоту изображения не нашел.
        • +1
          Высоту изображения можно получить только через FileAPI.getInfo(file, callback), давайте лучше сделаю дополнительный объект ui.errors: [{ error: "maxSize", file: {...} }, { error: "maxSize", file: {...} }]. Так буде удобнее работать.
  • 0
    Подскажите, пожалуйста.
    Я могу каким-то образом после успешной загрузки изображений на сервер обрабатывать ответ, который получаю по url? Прикручиваю через jquery и логично что это должно быть в onComplete но что-то не могу сообразить…

    В данном случае, хочется сделать дополнительную проверку на сервере, обработку и возврат html кода в нужном виде, а также переинициализацию формы для дальнейшей работы.
  • 0
    Да, я как раз начал тестировать на этом методе, считая что он идентичен onComplete и было не ясно. Очень приятная библиотека, спасибо вам.
    • 0
      Всегда пожалуйста. Если что пишите в личку или на github.
  • 0
    Пробую внедрить вместе с caman.js, ничего не выходит(

    Я загрузил превью. Применил фильтр. Теперь в канвасе у меня готовое изображение, для загрузки на сервер.

    Получаю его данные:
    <code>
     FileAPI.event.on(upload, 'click', function (){
                    var element = document.getElementById('result').children[0];
                    if (element) {
                        var base = element.toDataURL();
                        FileAPI.upload({
                            url: 'http://localhost/FileAPI/server/ctrl.php',
                            files: { file: file },
                            upload: function (){
    
                            },
                            progress: function (evt){
    
                            },
                            complete: function (err, xhr){
    
                            }
                        });
    
                    }
    
                });
    </code>
    


    Но АПИ не позволяет послать данные, только файл. А файл исходный, без фильтра. Надо теперь снова здорово применять фильтр? Не совсем понял как, так как файлапи.имадж — возвращает канвас, и о5 снова здорово. Что посоветуете?
    • +1
      Можно так github.com/mailru/FileAPI/blob/master/tests/tests.js#L443
      или
      FileAPI.upload({
          imageTransform: {
                maxWidth: 320,
                maxHeight: 240,
                filter: "vintage",
                rotate: "auto"
          }
      });
      
      • 0
        Спасибо за ответ! Там же, в тестах, нашел более подходящий для меня параметр — блоб, это именно то что мне было нужно. Странно что в доке его не заметил
        • 0
          Потому что это не документированая возможность. Для работы с изображениями есть FileAPI.Image, его и нужно загружать как файл.
          • 0
            Т е вы эффект 2 раза применять советуете? 1 раз показал — потом в файл аплоад 2 раз его применил при загрузке? Я к загруженному в канвас изображению применяю всеразличные эффекты при помощи сторонних js библиотек (пока это каман, поддержка которой у вас есть, но я потом другие захочу либы, поддержки котрых у вас нет. В этом случае как я применю эффекты сторонних либ через вашу либу?). Сейчас можно так — выдрал из канваса результат и тупо сохранил его. Это же здорово. Не убирайте ее.

            Еще пожелание — примеров с серверсайд побольше. Сейчас в примере с каманжс — например — сервер сайд нет. Не очень понятно как и откуда взять файл после примененного эффекта. Вернее мне совсем непонятно. Сейчас для меня выглядит как магия то как вы извлекаете из инпута файл кручу верчу загрузить хочу. Нифига непонятно. Простите за сумбур.
  • 0
    А как удалить (очистить) все выбранные для загрузки файлы используя jquery.fileapi?
    • 0
      $('.b-thumb__del').click();
      пока сделал так, но это костыль
    • 0
      $('#...').fileapi('clear');?
      • 0
        Похоже нет, нет реакции.
        • 0
          Версия плагина? (на данный момент 0.3.1)
  • 0
    Великолепная библиотека. Мне нужно было сделать resize изображения перед отсылкой на сервер и FileAPI отлично с этим справляется, без лишних телодвижений. Подскажите по поводу лицензии на библиотеку, могу ли я её использовать свободно для своих проектов? Не очень разбираюсь в лицензировании, знаю что MIT можно.
    • 0
      BSD, иначе говоря «Да», можно использовать, даже в коммерческих целях.
  • 0
    На iphone/ipad файлы автоматом поворачивается на 90 градусов. Как пофиксить?
    • 0
      тут скорее наоборот: автоматом не поворачиваются
  • 0
    При загрузке файл возрастает в несколько раз. Загружаю JPG 1.3MB, а получается на сервере 3.4MB. Можете рассказать почему так происходит?

    И второй вопрос можно ли передавать исходник чтобы 1 в 1 файл получался на сервере?
    • 0
      Видно у вас стоят какие-то параметры обработки изображения на клиенте. Чтобы ответить тончее, нужен пример параметров загрузки и версия браузера.
      • 0
        Именно в jquery версии наблюдается данный момент — rubaxa.github.io/jquery.fileapi/
        В НЕ jquery версии все работает отлично, файл 1 в 1 отправляется.
        Первый пример «Simple button» (rubaxa.github.io/jquery.fileapi/), только в «url» пропишите не полный путь а относительный, например, "/FileAPI/server/ctrl.php", то есть все скрипты на одном сервере.
        Аналогично и в Firefox 35.0.1 и в Chrome 40.0.2214.111 m (64-bit) на Windows 8 (64-bit)
        Проверил и на Windows 7 (32-bit) — такое же поведение.
        • 0
          В плагине по умолчанию включен автоповорот, возмодно вы грузите именно такое изображние. Пришлите пример на trash@rubaxa.org

          P.S. Ну и для подобных вопросов лучше использовать github/issue
          • 0
            • +1
              Собственно всё верно, если включен автоповорот, то в независимости от реального поворота, изображение отправляется в канвас и уже он загружается на сервер. Размер увеличивается именно по этой причине. Тут нужно дорабатывать FileAPI, чтобы если изображение не изменилось, но не преобразовывать его в canvas.

              Подписывайтесь на задачу: github.com/mailru/FileAPI/issues/303
              • 0
                Спасибо что уделили время!
                С гитом ещё знакомлюсь, в следующий раз туда буду писать.
  • 0
    Крутая штука

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка