Статьи

Ленивая загрузка изображений с JavaScript-фреймворком Igaro App

Некоторое время назад я писал о Igaro App JS Framework (отказ от ответственности: я являюсь автором фреймворка).

«Вздох! Не другая структура », я слышу, вы говорите (и, вероятно, это правильно). Что ж, позвольте мне рассказать вам, что отличает приложение Igaro.

Приложение Igaro НЕ является еще одной платформой, которая встраивается в ваш HTML. Это совершенно другой подход, который предлагает потенциально самую высокую производительность среди всех веб-приложений. Он основан на новейших стандартизированных технологиях, таких как обещания (и нулевые обратные вызовы), а также на архитектуре, управляемой событиями. Есть превосходное управление ошибками и восстановление, ленивая архитектура загрузки с использованием модулей в стиле CommonJS, множество виджетов для начала работы и ноль зависимостей (без jQuery).

В этой статье я покажу, как создать открытый виджет (ленивая загрузка изображений, когда они появляются) для приложения Igaro, и расскажу о многих концепциях, которые заставляют фреймворк сиять на этом пути. Если вы хотите сразу перейти к конечному результату, вы можете скачать полный код этой статьи .

Настройка среды

Первое, что нужно сделать, это взять копию фреймворка из репозитория GitHub .

mkdir igaro git clone https://github.com/igaro/app.git igaro/git cd igaro/git 

Затем установите пару зависимостей:

 npm install -g grunt-cli gem install compass npm install 

Интерфейс командной строки Grunt ( grunt-cli ) представляет собой пакет npm, что означает, что вам понадобятся Node.js и npm, установленные на вашем компьютере. Compass — это драгоценный камень Ruby, а это значит, что вам также необходимо установить Ruby. Процедура установки зависит от операционной системы. Лучше всего следовать инструкциям на домашней странице соответствующих проектов ( Node , Ruby ).

После этого вы можете начать с простого:

 grunt 

После клонирования и запуска у пользователя есть готовая среда разработки. Igaro компилируется в два режима — отладка и развертывание, а веб-сервер для каждого находится на портах 3006 и 3007 соответственно. Они будут автоматически перезагружаться по мере работы.

Снимок экрана стандартной установки приложения Igaro

Изложение спецификаций виджета

В процессе создания виджета я расскажу о благословении Игаро, средства предварительной настройки объектов, и объясню, как он позволяет объектам приводить себя в порядок. Для SPA это важно, чтобы предотвратить утечки памяти и проблемы с безопасностью, т. Е. Если аутентифицированная страница (приложение Igaro называет их маршрутами ) содержит несколько виджетов того типа, который мы собираемся создать, и учетные данные становятся недействительными (т. Е. Пользователь имеет вышли из системы), тогда необходимо удалить не только элементы DOM, но и события и зависимости.

Большинство фреймворков ожидают, что вы перезагрузите приложение, «обновив страницу», чтобы очистить исторические объекты (даже если стороны DOM удалены или скрыты) или обработать процесс очистки переменных вручную. Одной из особенностей «благословения» Игаро является двусторонняя связь между объектами, поэтому в этом случае, когда маршрут уничтожен, виджет идет вместе с ним. Точно так же, если мы уничтожим виджет, маршрут будет уведомлен, и он будет удален из пула массива одноуровневых элементов.

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

Без лишних слов, вот спецификация для нашего виджета:

  1. Контейнер должен быть пустым <div> .
  2. При прокрутке или изменении размера окна определите, находится ли вертикальная позиция в области просмотра, и, если это так, добавьте loading класс CSS.
  3. Получите любой ресурс и, если изображение, переключите <div> на <img> и запишите данные.
  4. Поддержка функции обратного вызова после вызова Ajax *. Это может добавить другие элементы DOM или обработать пользовательские данные.
  5. В случае ошибки добавьте класс CSS error , удалите класс loading .

* Для вызова Ajax могут потребоваться заголовки для аутентификации или поддержки CORS . Должен быть реализован механизм, позволяющий настроить запрос.

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

Создание необходимых файлов

Давайте рассмотрим четыре основных файла, необходимых для нашего виджета.

instance.unveil.js

Создайте файл с именем instance.unveil.js в compile/cdn/js/ и введите следующий код:

 module.requires = [ { name:'instance.unveil.css' } ]; module.exports = function(app) { "use strict"; var InstanceUnveil = function(o) {} return InstanceUnveil; }; 

Когда создается экземпляр виджета, передается литерал объекта o . Это используется для благословения объекта (подробнее об этом позже).

instance.unveil.scss

Затем создайте файл с именем instance.unveil.scss в sass/scss и введите код ниже.

 .instance-unveil { display:inline-block } .instance-unveil-loading { background: inline-image("instance.unveil/loading.gif") no-repeat 50% 50%; background-size: 3em; } .instance-unveil-error { background: inline-image("instance.unveil/error.svg") no-repeat 50% 50%; background-size: 3em; } 

Найти подходящий загрузочный GIF и подходящее изображение ошибки в Интернете. Поместите их в папку с именем sass/images/instance.unveil и убедитесь, что имя и расширение совпадают с теми, которые вы только что создали.

route.main.unveiltest.scss

Тестовая страница (маршрут), содержащая несколько экземпляров нашего виджета, будет доступна через http: // localhost: 3006 / unveiltest .

Создайте файл с именем route.main.unveiltest.scss в route.main.unveiltest.scss sass/scss и введите код ниже.

 @import "../sass-global/mixins.scss"; body >.core-router >.main >.unveiltest >.wrapper { @include layoutStandard; } 

route.main.unveiltest.js

Создайте файл с именем route.main.unveiltest.js в compile/cdn/js и введите код ниже.

 //# sourceURL=route.main.unveiltest.js module.requires = [ { name: 'route.main.unveiltest.css' }, ]; module.exports = function(app) { "use strict"; return function(route) { var wrapper = route.wrapper, objectMgr = route.managers.object; return route.addSequence({ container:wrapper, promises:Array.apply(0,new Array(50)).map(function(a,i) { return objectMgr.create( 'unveil', { xhrConf : { res:'http://www.igaro.com/misc/sitepoint-unveildemo/'+i+'.jpeg' }, loadImg : true, width:'420px', height:'240px' } ); }) }); }; }; 

В приложении Igaro, когда запрашивается страница, маршрутизатор ( core.router ) запрашивает источник у поставщика, создает новый маршрут и передает его источнику для настройки. В файле маршрута, который вы только что создали, пятьдесят открытых виджетов создаются и передаются в секвенсор. Секвенсор гарантирует, что при разрешении возвращенных обещаний изображения будут размещены на странице в исходном порядке.

Метод create предоставляется менеджером. Он лениво загружает модуль и создает экземпляры (предварительно загрузите модуль, добавив его в список требований в верхней части файла). На этом этапе виджет также связан зависимостью с маршрутом, поэтому при уничтожении маршрута выполняются операции очистки.

Добавление функциональности виджета

Добавьте в свой файл instance.unveil.js следующий код:

 module.requires = [ { name:'instance.unveil.css' } ]; module.exports = function(app) { "use strict"; var bless = app['core.object'].bless; var InstanceUnveil = function(o) { var self = this; this.name='instance.unveil'; this.asRoot=true; this.container=function(domMgr) { return domMgr.mk('div',o,null,function() { if (o.className) this.className = o.className; this.style.width = o.width; this.style.height = o.height; }); }; bless.call(this,o); this.onUnveil = o.onUnveil; this.xhrConf = o.xhrConf; this.loadImg = o.loadImg; }; return InstanceUnveil; }; 

Атрибуты, предоставляемые аргументом o могут использоваться напрямую, например, o.container и o.className (которые указывают, куда должен быть вставлен виджет, и предлагают имя пользовательского класса). Некоторые написаны напрямую, например, имя объекта, которое используется менеджером событий, предоставляемым функцией благословения Игаро. Bless может предоставить много вещей, например, если виджету требуется постоянное хранение данных, мы можем попросить его присоединить менеджер магазина ( см. Пример кода http: // localhost: 3006 / showcase / todomvc ).

Добавить обработчики событий окна

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

 module.requires = [ { name:'instance.unveil.css' } ]; module.exports = function(app) { "use strict"; var bless = app['core.object'].bless; var removeWindowListeners = function() { var wh = this.__windowHook; if (wh) { window.removeEventListener('scroll',wh); window.removeEventListener('resize',wh); } this.__windowHook = null; }; var InstanceUnveil = function(o) { var self = this; this.name='instance.unveil'; this.asRoot=true; this.container=function(domMgr) { return domMgr.mk('div',o,null,function() { if (o.className) this.className = o.className; this.style.width = o.width; this.style.height = o.height; }); }; bless.call(this,o); this.onUnveil = o.onUnveil; this.xhrConf = o.xhrConf; this.loadImg = o.loadImg; this.__windowHook = function() { return self.check(o); }; window.addEventListener('scroll', this.__windowHook); window.addEventListener('resize', this.__windowHook); this.managers.event.on('destroy', removeWindowListeners.bind(this)); }; InstanceUnveil.prototype.init = function(o) { return this.check(o); }; InstanceUnveil.prototype.check = function(o) { return Promise.resolve(); }; return InstanceUnveil; }; 

Экземпляр теперь присоединяет слушателей к scroll окна и resize событий, что вызовет функцию check (которая выполнит вычисление, чтобы увидеть, находится ли наш виджет в пространстве области просмотра). Критически, он также присоединяет другого слушателя к менеджеру событий в экземпляре, чтобы удалить слушателей, если экземпляр уничтожен. Также есть новая прототипная функция с именем init . Инстанцирование JavaScript с помощью new ключевого слова является синхронным, но вместо этого асинхронный код можно поместить в init и он будет называться для нас.

В приложении Igaro любой благословенный объект можно уничтожить, вызвав на нем команду destroy .

На данный момент код по-прежнему ничего не делает. Если вы /unveiltest к /unveiltest , вам будет предоставлена ​​пустая страница (но проверьте содержимое, и вы увидите пятьдесят пустых элементов <div> ). Тяжелый подъем еще не добавлен к функции check .

Функция проверки

Эта функция должна делать следующее:

  • Определить, находится ли контейнер экземпляра (элемент <div> ) в области просмотра
  • Добавить loading класс CSS
  • Создать экземпляр XHR
  • Получить ресурс
  • Если вы загружаете изображение, поменяйте <div> на <img>
  • При желании позвонить
  • Удалить loading класс CSS
  • Очистить обработчики событий

Для функции check достаточно много кода, но не торопитесь и выполняйте ее — она ​​хорошо читается. Добавьте его в свой файл, и не забудьте ссылку на модуль dom в верхней части.

 //# sourceURL=instance.unveil.js module.requires = [ { name:'instance.unveil.css' } ]; module.exports = function(app) { "use strict"; var bless = app['core.object'].bless, dom = app['core.dom']; var removeWindowListeners = function() { var wh = this.__windowHook; if (wh) { window.removeEventListener('scroll',wh); window.removeEventListener('resize',wh); } this.__windowHook = null; }; var InstanceUnveil = function(o) { var self = this; this.name='instance.unveil'; this.asRoot=true; this.container=function(domMgr) { return domMgr.mk('div',o,null,function() { if (o.className) this.className = o.className; this.style.width = o.width; this.style.height = o.height; }); }; bless.call(this,o); this.onUnveil = o.onUnveil; this.xhrConf = o.xhrConf; this.loadImg = o.loadImg; this.__windowHook = function() { return self.check(o); }; window.addEventListener('scroll', this.__windowHook); window.addEventListener('resize', this.__windowHook); this.managers.event.on('destroy', removeWindowListeners.bind(this)); }; InstanceUnveil.prototype.init = function(o) { return this.check(o); }; InstanceUnveil.prototype.check = function() { var container = this.container; // if not visible to the user, return if (! this.__windowHook || dom.isHidden(container) || dom.offset(container).y > (document.body.scrollTop || document.documentElement.scrollTop) + document.documentElement.clientHeight) return Promise.resolve(); var self = this, managers = this.managers, xhrConf = this.xhrConf; removeWindowListeners.call(this); container.classList.add('instance-unveil-loading'); return Promise.resolve().then(function() { if (xhrConf) { return managers.object.create('xhr', xhrConf).then(function(xhr) { return xhr.get(self.loadImg? { responseType: 'blob' } : {}).then(function(data) { if (self.loadImg) { self.container = managers.dom.mk('img',{ insertBefore:container }, null, function() { var img = this, windowURL = window.URL; // gc this.addEventListener('load',function() { windowURL.revokeObjectURL(img.src); }); this.src = windowURL.createObjectURL(data); this.className = container.className; this.style.height = container.style.height; this.style.width = container.style.width; }); dom.purge(container); container = self.container; } return data; }).then(function(data) { if (self.onUnveil) return self.onUnveil(self,data); }).then(function() { return xhr.destroy(); }); }); } if (self.onUnveil) return self.onUnveil(self); }).catch(function(e) { container.classList.add('instance-unveil-error'); container.classList.remove('instance-unveil-loading'); throw e; }).then(function() { container.classList.remove('instance-unveil-loading'); }); }; return InstanceUnveil; }; 

Зачем нам нужно было добавлять модуль core.dom когда у нашего благословенного объекта есть DOM-менеджер, который вы можете спросить?

Bless предоставляет только те функции, которые требуют настройки для благословляемого объекта, поэтому менеджер DOM не предоставляет метод purge необходимый для уничтожения исходного контейнера (и всех его зависимостей). По этой причине следующие два метода для создания элемента DOM не совпадают:

 app['core.dom'].mk(...) [blessed object].managers.dom.mk(...) 

Во втором примере уничтожается элемент DOM в случае уничтожения благословенного объекта, а также любые события, для которых элемент DOM зарегистрирован как зависимость. Он автоматизирует всю очистку и гарантирует отсутствие утечек памяти.

Обновите и на странице должно быть много красочных изображений.

Провал!

Как вы, надеюсь, узнали, у нас совсем немного изображений. Можете ли вы решить, что пошло не так?

Две вещи;

  1. Экземпляр не добавляет свой элемент DOM, что делается функцией addSequence но это происходит после нашего немедленного вызова для check .

  2. Маршрут не виден до тех пор, пока не будет выполнено его обещание, что потенциально позволяет маршрутизатору прервать загрузку испорченной страницы. Даже если мы исправим (1), изображения не будут в окне просмотра при вызове check .

Возникающая проблема вряд ли возникнет во многих случаях использования, но она является отличным примером того, что происходит, когда вы используете среду для создания SPA, и, в конечном счете, эта среда может легко разрешить неожиданное или просто помешает ?

На этом этапе отключение процесса через setTimeout (HACK!) Могло прийти вам в голову. Мы не будем этого делать.

Решение

core.router обрабатывает загрузку маршрутов и, будучи благословленным, запускает событие to-in-progress когда маршрут загружен и виден. Мы можем подключить наш экземпляр к этому вызову.

Исходя из кода, использованного ранее, должно подойти что-то вроде следующего.

 app['core.router'].managers.event.on('to-in-progress',function(r) { if (r === route) unveil.check(); // no return }, { deps:[theInstance] }); 

Обратите внимание, как экземпляр передается как зависимость события, а обещание check не возвращается. Это приведет к тому, что изображения будут загружаться одно за другим (события являются синхронными), а также, если при получении изображения произошла ошибка, это приведет к прерыванию загрузки страницы. Вместо этого экземпляр должен обрабатывать ошибку независимо (через класс error CSS).

Окончательный код для route.main.unveiltest.js выглядит следующим образом:

 //# sourceURL=route.main.unveiltest.js module.requires = [ { name: 'route.main.unveiltest.css' }, ]; module.exports = function(app) { "use strict"; var coreRouterMgrsEvent = app['core.router'].managers.event; return function(route) { var wrapper = route.wrapper, objectMgr = route.managers.object; return route.addSequence({ container:wrapper, promises:Array.apply(0,new Array(50)).map(function(a,i) { return objectMgr.create( 'unveil', { xhrConf : { res:'http://www.igaro.com/misc/sitepoint-unveildemo/'+i+'.jpeg' }, loadImg : true, width:'420px', height:'240px' } ).then(function(unveil) { coreRouterMgrsEvent.on('to-in-progress',function(r) { if (r === route) unveil.check(); // no return }, { deps:[unveil] }); return unveil; }); }) }); }; }; 

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

Gif демонстрирует ленивую загрузку

Обработка ошибок

Изменение количества изображений в файле маршрута на большее число вызовет сбой Ajax и отображение CSS-класса error .

Мысли об улучшении

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

Как выясняется, это возможно при прослушивании событий enter и leave на маршруте. Мы могли бы отслеживать их и вызывать методы регистрации / отмены регистрации в экземпляре.

Заключительные соображения

Одним из предостережений является наш друг Internet Explorer. Версия 9 не поддерживает XHRv2 и window.URL.createObjectURL , ни один из которых не может быть заполнен.

Чтобы указать пользователю, что его браузер не поддерживает требуемую функцию, мы можем добавить следующий код в начало страницы instance.unveil.js .

 if (! window.URL)) throw new Error({ incompatible:true, noobject:'window.URL' }); 

Для изображений, по крайней мере, я не считаю это приемлемым. Перед тем, как этот код будет готов к работе, он должен window.URL к немедленной записи изображения, если window.URL будет недоступен.

Вывод

Во время написания этой статьи я исследовал использование возвращаемого типа MIME для автоматической записи замены <img> и использование base-64 для поддержки IE9. К сожалению, XHRv1 требует переопределения MIME, которое затем переопределяет заголовок типа содержимого. Для его разрешения требуется два вызова XHR на один и тот же URL.

Я планирую интегрировать этот экземпляр модуля в предстоящую версию Igaro App, но вы можете победить меня, отправив запрос на извлечение (если вы это сделаете, не забывайте о поддержке не window.URL и документации через route.main.modules.instance.unveil.js ).

В противном случае, я надеюсь дать вам представление о том, что может сделать приложение Igaro. Я был бы рад ответить на любые ваши вопросы в комментариях ниже.