[Из песочницы] Мой последний file uploader
Я — веб-разработчик с непрофильным образованием и ~10-летним стажем. Я делал для веба все, что только могло прийти в голову моим заказчикам и, иногда, начальству. Я люблю эту работу. Но все же есть несколько вещей, которые я делаю совсем не улыбаясь. Одна из них — это file uploader. From the very beginning — когда еще никому не приходило в голову делать его аяксовым — и до настоящего времени — когда он ресайзит картинки, загружает файлы в несколько потоков и многое-многое другое — он остается для меня одной из самых нелюбимых задач. Вроде как у меня получилось с этим справиться. Если интересно — добро пожаловать под кат.Итак, перед нами задача — всунуть на сайт file uploader. Конечно же AJAX, конечно же с прогрессом и конечно же в уже готовый дизайн. Для начала — почему это проблема? Кросс-браузерность. Старые добрые приемы (a-la сабмит формы в iframe) не даст нам прогресса, а новые (xhr.send) не сработают в старых браузерах. Появившаяся было надежда на стандартизацию браузеров скоропостижно скончалась, так что проблема кросс-браузерности не умрет. Дополнительная задачка — Drag’n'Drop. Еще есть беда с внешним видом input’a.Какие у нас есть варианты?
Путь №1, Google-driven-developmentШустренько нагуглить достаточно популярную jQuery библиотеку, реализующую все требуемые функции. Подход правильный при дефиците времени. Предельно быстро встраивается в проект, сама волнуется на тему совместимости с браузерами и даже выведет какой-никакой отформатированный DOM. Ведь нет же никаких проблем DOM переработать стилями (или даже с помощью jQuery, это ведь его основная специализация, правильно?), дизайнеров втихую убедить немножко подвинуться («Да не могу я вам ТАК сделать, подите к черту!»), даже договориться с заинтересованными лицами по поводу функционала («Ну да, это мы не сделали. Но зато смотрите какая фишечка сбоку нечаянно получилось. Ведь правда супер?»). И вроде бы все супер, и вроде бы все здорово. За день вполне можно справиться и отдать в тестировку. Но потом… Первая ласточка прилетит от дизайнеров — вылез какой-то див (кто-то прервал загрузку, или создал другую, более экзотическую ситуацию). А вы-то этот див не видели, и поэтому на нем стили, мягко говоря, не совсем те. Перевариваем заслуженные упреки дизайнеров, наблюдаем приход делегации тестировщиков. Оказывается, в каком-то замечательном браузере, в котором из-за корпоративных стандартов работают аж XX процентов наших пользователей (причем самых ценных!) не отображается прогресс! Ужас. Вы смело посылаете тестеров к черту («Ну как я могу это сделать, если браузер не поддерживает???») и с победным видом пропускаете хук в челюсть: «Так вот же, Вконтакте в этом браузере upload с прогрессом работает!». И именно в этот момент Вы совершаете одно из самых старшных преступлений против проекта — вы лезете в исходный код этого замечательного плагина. (Как варинат — находите позапрошлогоднее сообщение об этой ошибке в трекере плагина и, если вам повезет, какой-нибудь костыль затыкающий эту конкретную проблему). На самом деле это не так уж и страшно для вас, как для программиста. Вы научитесь читать чужой код — иногда хороший, иногда… разный. Дебаггером лишний раз попользуетесь. Я ни в ком случае не хочу сказать ничего плохого про разработчиков плагинов для jQuery. Просто в большинстве встреченных мной случаев их код не рассчитан на саппорт постороннего человека. Но сроки по проекту могут начинать подгорать. Помните, мы выбрали этот вариант именно из-за нехватки времени? И еще — если все таки всунете плагин — обязательно оставьте в репозитории проекта только минифицированную версию плагина. И не в коем случае нигде не оставляйте ссылок на сайт плагина.Резюме: незнакомые плагины юзаем только если на дизайн и функциональность можно подзабить. Ну или для мести / дрессировки дизайнеров и прочих тестеров. Или в качестве тренировки навыков реверс-инжиниринга.Путь №2, исторический Ха! Полгода назад мы уже писали что-то такое. Для голландцев (греков, австралийцев, персов …). Там еще зелененький овальный прогресс-бар был. Открываем старый проект (другой комп, сгоревший репозитарий, и еще 100500 причин почему это не так просто). Выпиливаем uploader, курим сорцы. А там… Во первых — поддержка только последних браузеров. Во-вторых flash-ресайз на стороне клиента. В третьих этот самый памятный овальный (дизайнеры — сволочи!) прогресс жестко запилен в сам код uploader-а. В четвертых, в-пятых и т.д. Плюч куча специфики именно того проекта. С точки зрения удовольствия — тоже не айс. Вместо полета мыслями к горним высям современных технологий — откат на полгода назад.Резюме: не интересно. Даже если вы полгода назад были гуру и выдавали только высококачественный код — сейчас-то вы уже по-любому круче.Путь №3, самонадееный Итак, давайте попробуем немного помечтать. Чего мы хотим? (помимо пива, соседку и котика). Как минимум — мы хотим сделать uploader. Как максимум — мы хотим сделать его один раз и надолго. Чтобы его было легко взять из текущего проекта и всунуть в следующий с минимальной модификацией. Желание, кстати, навеяно одной презабавненькой книжицей — Паттерны проектирования Эрика и Элизабет Фрименов. Нет, это не Та, Оригинальная Книга Банды Четырех, Которую Должен Прочитать Каждый. Эта другая, намного полегче. Не такая сильная, но и читается легче. Один из первых принципов, который ее авторы пытаются вбить читателю в голову — инкапсулируйте то, что изменяется. В нашем случае самым переменным аспектом архитектуры должен быть метод отправки данных на сервер (не только, но об этом чуть позже). Если помните, мы эту проблему обсуждали перед jQuery-плагином. Итак, если мы сделаем какой-нибудь пул механизмов отправки данных на сервер (если у вы предпочитаете более ООП-шные формулировки — пусть будет набор классов с общим интерфейсом) и определим интерфейс взаимодействия c uploader’ом — одна большая проблема разбивается на несколько мелких. А их решать уже приятнее. Как это выглядит? Очень просто: Uploader = senders: [xhrFile, formDataFile, formDataForm, iframe] send: (options)→ stream = false $.each @senders, (i, func)=> if stream = func (options) false stream В 2 словах — перебираем массив из функций (javascript же!), каждой подсовываем options (в большей степени потому что не знаем, что именно будем им на самом деле подсовывать) и, первая вернувшая непустой результат используется для отправки. (Как вариант — первая не сгенерировавшая исключение, так будет правильнее, но пока что лень.) При этом в массиве самые «ценные» функции стоят первыми, а последняя — безотказная (тот самый submit-form-to-iframe способ). Да, мы уже догадываемся, что функции нам будут возвращать deferred — потом в интерфейсной части uploader’а мы навесим все интересующии нас листенеры (а не будем передавать их в options). Немного сумбурно описал $.Deferred под спойлером после примеров функций.Теперь пару строк о качестве кода в этой статье: — код завязан на jQuery не потому что автор считает, что любой проект все равно рано или поздно его будет использовать, а из-за некоторого количества «удобств», в том числе и Deferred, который мы будем весьма плотно использовать— частично код взят из одного замечательного плагина. Если быть чесным, идея родилась с чтения этого плагина и всхлипов: «Ну почему, почему логику не отделили от реализации?»— на данный момент код достаточно слабо протестирован, это просто иллюстрация к архитектуре.
Теперь примеры функций:
iframe = (options)→ return false unless options.input && options.input.value id = 'frame' + Math.random () $form = $ '