Асинхронная загрузка изображений в скрытом iframe: подводные камни

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

При обращению к гуглу выдаёт много статей по загрузке файлов через iframe. Алгоритм в общем сводится к:
1) Создаём скрытый фрейм (обычно просто обнуляется ширина и высота через HTML и CSS свойства)
2) Устанавливаем action формы в имя фрейма.
3) Отправляем файл. Радуемся.

Для удобства использования отдельная кнопка «Начать загрузку» не создавалась, а был повешен обработчик onChange для файлового input'a.


Далее тестирование: Chrome 9,10,11; Firefox 3.6 (четвёртый тогда ещё не вышел); Opera 9,10,11; IE 6 и IE 8, в том числе в режиме совместимости с IE 7. Chrome, Opera и FF тестировались и в Linux и в Windows. Полёт нормальный, тестирование командой пройдено. Отправляем на живую версию сайта.

Вот с этого момента и началось самое веселье. На сайт заходит несколько тысяч посетителей в день. И 6 из них написали в техподдержку, что новый метод загрузки фотографий не работает. После добавления в скрипт предупреждения об ошибках и логирования их на сервер выяснилось, что скрипт на самом деле не работает у нескольких сотен человек.

Первый найденный глюк был в IE 7. И касался он как раз обработчика события onChange. Как оказалось, это известный баг — при изменении DOM-дерева в IE7 вызывается onChange. В нём JS-код изменяет структуру HTML-документа. Это опять вызывает onChange. Т.е. получается race condition и IE7 просто игнорирует изменения и посылает пустое имя файла. Решение: обернуть вызов в функции в setTimeout. Этот метод корректно работает во всех тестируемых браузерах.

Осталось зарубить себе на носу: IE 7 и IE 8 в режиме совместимости — это совсем разные браузеры…

Следующий очень интересный глюк был в Opera 11, причём всего у 3 пользователей. Возможно, виновата не сама опера, а какой-нибудь вспомогательный софт. Дело в том, что JSON-ответ от сервера приходится передавать с mime-типом text/html, а не application/json. При попытке открыть в iframe данные с типом application/json некоторые браузеры предлагали пользователю скачать бинарный файл, в то время как он должен втихую обрабатываться в JavaScript.

Опера видит, что передаётся родной HTML, почему б не пихнуть туда баннер? И пихала. Естественно, парсер валился при попытке переварить такую строку. Решение: во-первых, желательно использовать text/plain, во-вторых было добавлено обрезание строки по первому вхождению открывающей и закрывающей фигурной скобки.

Третий глюк был самым страшным. Чаще всего проявлялся в Firefox, IE и Chrome. Данные отправлялись не в скрытый iframe, а в новую вкладку. Файл, конечно, передавался на сервер. Однако в другую вкладку JS обратиться не мог, поэтому данные не отображались и использовать их было нельзя.

Хуже всего, что повторить этот баг так и не удалось — даже IE на машинах команды отрабатывал всё хорошо. Была сделана тестовая страница, на которой были использованы разные способы отображения/скрытия фрейма. Как и ожидалось, видимые фреймы всё корректно отработали, скрытые создали вкладки. Решилось просто — нужно обрамить отображаемый frame в невидимый div (display: none).

Расскажу также немного о глюках, которые удалось выловить ещё на этапе написания скрипта.

Отдельно пришлось заниматься извращениями с кроппером (рамочка для выбора конкретной области изображения). Этот скрипт как раз и был сторонним. При статическом изображении всё работало на ура. А вот как только изображение создавалось динамически вылазили разные глюки — от неправильных размеров до полного затенения. Пересоздание объекта через delete/new не помогало. Методом проб и ошибок выяснилось, что инициализировать скрипт нужно обязательно после того, как изображение полностью загружено. А для перечитывания параметров есть метод reset. Только так, и никак иначе!

Одной из самых неприятных проблем было то, что не всегда корректно срабатывает отправление на сервер пересекающийся форм (например, форма в форме). А файловый input — обязательно элемент формы. Первое приходящее на ум решение — копировать данные из input'a и создавать аналогичный в другом месте DOM-дерева. Но это поддерживают не все браузеры. Переносить input по DOM-дереву также не получилось. Зато если обрамить его в DIV — то его можно перемещать спокойно, вместе с элементом формы.

Кстати, о динамическом создании элементов. В конечной версии скрипта формы создаются динамически и помещаются в не отображаемый слой вначале страницы. Это позволило избежать пересечений форм и дало возможность удобнее создавать большое количество форм-загрузчиков. А вот iframe динамически создать так и не получилось, зато я наконец-то увидел своими глазами глюк с открытием новой вкладки.

В заключение скажу, что о решении разрабатывать скрипт своими силами мы не пожалели. Лично я получил много опыта и весёлых моментов при ловле жуков, иногда весёлых до истерики и кровавых слёз. Зато в результате получилось именно то, что нужно.
Share post

Comments 32

    +3
    Вы бы хоть демо, код или исходники показали. А то одна вода.
      +2
      Ну это же не инструкция или HowTo для начинающих, а вспомогательный материал указывающий, где именно могут быть подводные камни. Я даже не претендую на то, что мои решения были самыми правильными. Однако они работают

      Простейший код скрыт за словесными конструкциями. Например:
      «нужно обрамить отображаемый frame в невидимый div (display: none)» следует понимать как "<div style="display: none"><frame>...</frame></div>"
        +6
        Ну это же не инструкция или HowTo для начинающих

        Учитывая количество багов полученное после «окончания тестирования» и начала продакшн эксплуатации, я могу предположить что реализацией занимались как раз таки «начинающие».
        И вообще зачем эти костыли с установкой action формы в имя фрейма? У формы уже тысячу лет есть аттрибут target в который надо записать id iframe-а. А в iframe надо не json возвращять, а

        <html>
          <head>
            <script type="text/javascript">
              window.parent.showUploadedImages(["file1.jpg","..",...]);
            </script>
          </head>
          <body>
          </body>
        </head>
        </html>


        И ничего парсить не прейдеться. Плюс соответственно надо реализовать в основном окне функцию showUploadedImages

        Но это все тоже для «начинающих», в обще лучше использовать plupload который позволяет реализовать:
        — Chunking
        — Drag/Drop
        — PNG Resize
        — JPEG Resize
        — Type filtering
        — Stream upload
        — Multipart upload
        — File size restriction
        — Upload progress

        и объединяет в себе следующие способы загрузки:
        — Flash
        — Gears
        — HTML 5
        — Silverlight
        — BrowserPlus
        — HTML 4

        При этом plupload позволяет делать resize изображения еще на стороне клиента, тем самым разгружая сервер.
      +2
      > Одной из самых неприятных проблем было то, что не всегда корректно срабатывает
      > отправление на сервер пересекающийся форм (например, форма в форме).
      А вы в курсе, что по стандартам это недопустимо?
        0
        В курсе. И пришлось решить данную проблему в соответствии со стандартом
        0
        iframe можно еще скрыть с position:absolute; left:-1000px;top:-1000px;
          0
          Как насчёт кроссбраузерности? Точно не помню, но по-моему в каком-то браузере косячило
            0
            А что тут не так с кроссбраузерностью?
            +5
            1к мало, нужно 10.
            +1
            Если сайт использует jquery, то поможет code.google.com/p/ocupload/
            Мы от собственного решения перешли на него в свое время.
              0
              Ещё один подводный камень связан с обработкой ошибок. Если скрипт, который принимает аплоад, упадёт с ошибкой, то скрипт формы об этом не узнает. Решается перехватом всех-всех-всех исключений в обработчике аплоада на сервере, и в случае проблем возвращает 200 OK с сообщением об ошибке.
                +1
                Абсолютно все исключения, к сожалению, не перехватишь :( Например, если сервер физически вырубили шваброй — то ближайшие пару минут он уже точно ничего не вернет. А JS часть должна уметь обрабатывать и такие проблемы
                  0
                  Это факт. Вы научились их обрабатывать?
                    0
                    Таймер… Если полоска прогресса не двигается больше определённого времени, то на сервер высылается лог об ошибке, а JS-часть пытается переотправить файл. Физически его будет принимать уже другой сервер
                      0
                      Я сейчас с этим мучаюсь. Решил бомбежкой скрытого фрейма через каждые 100мс с попыткой получения доступа к документу фрейма. Если доступ к документу (или body внекоторых браузерах) падает с ошибкой доступа — сервер прислал ошибку или вообще лег.
                  0
                  А зачем этот геморой? Существует милион решений. uploadify Например. Гиперудобная вещь. И т.д. и т.п.
                    –2
                    1) JQuery. У нас не используется
                    2) Готовые модули намного тяжелее тесно интегрировать в свои скрипты. Так же чтобы оптимизировать нагрузку в них надо хорошо разбираться
                      0
                      uploadify, если помнится, загружает файлы через флешь, что само по себе в данном конкретном случае сильное усложнение.
                      0
                      Странненько, буквально вот недавно писал точно такую же лабуду, которая просто вешается на определённый элемент, с возможностью передачи дополнительных параметров. Всё создаётся динамически и iframe и форма, при этом после работы это же всё вычищается… хотя можно и подумать над тем чтобы оставлять, для дальнейшего использования (надо записать где-то :)).

                      И судя по тому что отзывов по поводу неработоспособности нет, то пока всё тьфу-тьфу-тьфу.
                        0
                        А что если на странице потребуется, ну допустим, 30 кнопочек «загрузить изображение»(это может быть в комментах, в футере, хедере, в левом столбце и т.д. и т.п.)… Реально такое может понадобиться…

                        Работать будет?
                          0
                          Если фрейм один — то нет.
                            0
                            Если фрейм один — то одновременная загрузка не будет работать (по очереди — будет). У нас фрейм вынесен прямо в шаблон с html-кодом загрузчика. Каждому загрузчику свой фрейм
                              0
                              А вы знаете, что более 6-10 потоков браузер не обрабатывает? Вопрос с 30-ю кнопочками не просто так задавался…
                                0
                                Нет, не пробовал. Тем более у нас стоит лимит на количество изображений (6 штук) + на их размер. Так что реально одновременно будут загружаться не больше 6 изображений
                                  0
                                  ну хорошо, реальные тесты были с 6ю фреймами?

                                  но больше меня интересует другое… все и вся пытаются всё сделать динамичным… здесь вы предлагаете аяксом картинки грузить, кто-то comet серверы изобретают, ну и т.д… Тестировалось ли это совместно? Какие результаты? Какие проблемы и пути их решения?
                                    0
                                    Comet сервер не пробовал. А нас стоит nginx. На аякс загрузку после исправления глюков никто не жаловался
                                  0
                                  кайкой браузер обрабатывает 6-10 потоков? откуда такие цифры?
                                    0
                                    Цифры?

                                    developer.mozilla.org/en/Mozilla_Networking_Preferences
                                    Смотрим параметр network.http.max-persistent-connections-per-server = 8

                                    У остальных браузеров примерно одинаковые цифры. А вообще часто сами операционки ограничивают количество одновременных http-соединений (в XP такое было точно)
                                      0
                                      ааа вон о чём речь… ну тут я проблемы не вижу… это ж с какой надо скоростью мышкой кликать чтоб больше 8 картинок параллельно отправить на сервер… 30 штук на одной странице это всего лишь юзерфрендли, а не распараллеливание…

                                      больше интересует как обойти политики безопасности в разных браузерах… например IE6-7 при 2х iframe'ах после 2 — 3го клика на сайте врубают блокировку и например при обращении к document.location.hash или document.location.protocol получаем ACCESS DENIED… В других браузерах тоже начинаются свои косяки… только в Firefox начиная с 4й версии боле-менее всё стало лояльно…

                                      как автор статьи решал данные проблемы?
                                        0
                                        Не нужно их обходить, не давайте пользователю работать напрямую с iframe, переносите содержимое в тело основной страницы. За 4 года работы по такой схеме ни один клиент не жаловался.
                            +4
                            Впечатление от статьи резко отрицательное. Такое ощущение, что вы вместо того, чтобы почитать хорошую статью/стандарт/quirksmode, делаете все наугад, а потом ругаетесь, что не работает: «не всегда корректно срабатывает отправление на сервер пересекающийся форм (например, форма в форме)» — нельзя делать форму в форме вообще-то!

                            Ну и эти цитаты тоже чего стоят:

                            > Устанавливаем action формы в имя фрейма.

                            > Как оказалось, это известный баг — при изменении DOM-дерева в IE7 вызывается onChange. В нём JS-код изменяет структуру HTML-документа. Это опять вызывает onChange.

                            Я подозреваю, вы куда-то пытались перенести в дереве File Input, файл в нем сбрасывался и срабатывал onchange — это нельзя считать багом МС, а багом разработчика.

                            > Т.е. получается race condition и IE7 просто игнорирует изменения и посылает пустое имя файла.

                            Вы хоть в википедии почитайте, что такое race condition…

                            > Дело в том, что JSON-ответ от сервера приходится передавать с mime-типом text/html, а не application/json. При попытке открыть в iframe данные с типом application/json некоторые браузеры предлагали пользователю скачать бинарный файл, в то время как он должен втихую обрабатываться в JavaScript.

                            Не должны они обрабатываться, Опера все делает правильно.

                            — Добавлю еще по своему опыту загрузки через iframe: поскольку File Input сабмитится в ифрейм отдельно от остальной формы (которая сабмитится как обычная форма), то необходимо размещать его в дереве DOM в отдельной форме. Это может быть сложно, если выше и ниже инпута поля «обычной» формы. Решения тут 2: (более сложное но более правильное) — разместить форму с инпутом до/после основной и спозиционировать инпут средствами CSS (position: relative например) и (корявое, некрасивое) — вставить в основную форму ифрейм, в него File Input и скрытый фрейм. Получается нелогично и несеманично, но это работает, если например в форму динамиечски добавляются поля и инпут с файлом должен сдвигаться вниз.

                            Печально, что HTML ничего не предлагает (HTML 5 вроде начал предлагать File API и еще что то там) для
                            асинхронной загрузки файлов с докачкой.

                            Второй момент — трудно обнаружить ошибки загрузки файла в ифрейм, например из-за разрыва соединения, так как нет события ошибки. Можно ловить событие onload ифрейма, но оно не сработает, если загрузка по каким-то причинам замедлилась до очень низкой скорости или остановилась. Особенно трудно, когда файл надо загружать на сторонний сайт (например, ютуб). На своем сайте можно периодически аяксом спрашивать состояние загрузки, а на чужом — никак.
                              0
                              >Я подозреваю, вы куда-то пытались перенести в дереве File Input

                              Не правильно подозреваете. Возможно, я не корректно высказался. Race condition там происходит из-за рекурсивного вызова обработчика onChange. File input изменился — вызвался onChange. В onChange произошло считывание значение input'a. Ie7 думает, что input опять изменился. Опять вызывает onChange

                              >Вы хоть в википедии почитайте, что такое race condition…
                              Угу, и про железнодорожные семафоры там же почитать? Работал, знаю

                              >Не должны они обрабатываться, Опера все делает правильно.
                              Вот именно, что не должны. Тем не менее в логе была изменённая строка. А в ней base64 закодирование изображение «Подождите, загрузка»…

                              >то необходимо размещать его в дереве DOM в отдельной форме
                              Ага. Этим и занимаюсь позже, динамически таская его по DOM-дереву

                              >трудно обнаружить ошибки загрузки файла в ифрейм
                              Это точно. Кроме таймер так ничего и не придумал. Хорошо хоть всё на своём сайте и в запросах не ограничен…

                            Only users with full accounts can post comments. Log in, please.