Как я создавал плагин постраничной прокрутки One Page Scroll с открытым исходным кодом

Original author: Pete Rojwongsuriya
  • Translation
Эффекты прокрутки используются в вебе уже давно, и хотя есть уже много плагинов, из которых можно выбрать, лишь малая их часть обладает таким малым весом и простотой, какие требуются для многих дизайнеров и разработчиков. Большинство виденных мною плагинов пытаются сделать слишком много, в результате чего их трудно включать в свои проекты.

Не так давно Apple представила iPhone 5S, и сайт с презентацией, где страница была поделена на секции, и каждая секция описывала одну из особенностей продукта. Я подумал, что это – замечательный способ представления продукта, исключающий возможность пропустить ключевую информацию.

Я отправился на поиски подходящего плагина, и к удивлению, не обнаружил такового. Так и родился плагин постраничной прокрутки.

Плагин постраничной прокрутки.



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

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

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

К чему всё это?



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

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

1. Чертежи


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


Можно представлять всё в уме, а можно делать наброски.

Разобъём концепцию на мелкие задачи, решая каждую последовательно.

1. Подготовим раскладку секций
Отключим обычную прокрутку, применяя overflow: hidden к body. Расположим секции в нужной последовательности, подсчитаем и приспособим нужную информацию и классы.

2. Установим триггер ручной прокрутки
Триггер ловим через jQuery, определяем направление прокрутки, двигаем раскладку при помощи CSS.

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

4. Проверим в разных браузерах.
Проверим браузеры Chrome, Safari, Firefox, Internet Explorer 10 и самые популярные операционки Windows, Mac OS X, iOS and Android 4.0+.

5. Сделаем плагин доступным в репозитории
Создадим репозиторий, напишем инструкцию по использованию плагина

6. Расширим поддержку.
Изучим иные пути для увеличения поддержки плагина.

2. Строим основу


Спроектировав плагин, я занялся построением основы на этом шаблоне:

!function($) {

   var defaults = {
      sectionContainer: "section",
      …
   };

   $.fn.onepage_scroll = function(options) {
      var settings = $.extend({}, defaults, options);
      …
   }

}($)


Шаблон начинаем с модуля !function($) { … }($), который помещает глобальную переменную jQuery в локальную область – это поможет снизить нагрузку и предотвратить конфликты с другими библиотеками.

Переменная defaults содержит настройки по-умолчанию.

$.fn.onepage_scroll – основная функция, которая всё инициализирует. Если вы делаете свой плагин, не забудьте вместо onepage_scroll написать другое название.

Запретить стандартную прокрутку можно, назначив тегу body свойство overflow: hidden
через имя класса, специфичное для данного плагина. Важно использовать уникальные имена стилей, чтобы избежать конфликта с существующими. Я обычно использую аббревиатуру из названия плагина, а потом через тире – имя для стиля, к примеру: .onepage-wrapper.

Фундамент заложен, приступим к первой функции.

3. Подготовим раскладку и расположим секции


Сначала я пошёл неправильным путём. Я думал, что расположу все секции по порядку, проходя их в цикле. Что у меня получилось сначала:

var sections = $(settings.sectionContainer);
var topPos = 0;

$.each(sections, function(i) {
   $(this).css({
      position: "absolute",
      top: topPos + "%"
   }).addClass("ops-section").attr("data-index", i+1);
   topPos = topPos + 100;
});


Цикл перебирает все селекторы (sectionContainer определён в разделе переменных по-умолчанию), назначает position: absolute и присваивает каждому следующему разделу правильную top позицию, чтобы они не наезжали друг на друга.

Положение сверху (top) хранится в topPos. Начинаем с нуля и с каждым циклом прибавляем. Чтобы каждая секция занимала всю страницу, я устанавливаю их высоту в 100% и прибавляю к topPos 100.

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

4. Ручной триггер и преобразование страницы


Можно было бы подумать, что следующий шаг – передвигаем каждую секцию в новое положение, когда срабатывает триггер прокрутки… Но есть способ лучше. Вместо сдвига каждой из секций в цикле, я просто помещаю их все в один контейнер и использую функцию translate3d из CSS3 для его сдвига. Эта функция поддерживает проценты, мы можем передвигать секции так, чтобы они точно позиционировались в окне, не пересчитывая всё заново. Кроме того, это облегчает контроль над скоростью и другими параметрами анимации.


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

Теперь остаётся только определить направление прокрутки и сдвигать контейнер в нужную сторону.

function init_scroll(event, delta) {
   var deltaOfInterest = delta,
   timeNow = new Date().getTime(),
   quietPeriod = 500;
   
   // Cancel scroll if currently animating or within quiet period
   if(timeNow - lastAnimation < quietPeriod + settings.animationTime) {
      event.preventDefault();
      return;
   }

   if (deltaOfInterest < 0) {
      el.moveDown()
   } else {
      el.moveUp()
   }
   lastAnimation = timeNow;
}

$(document).bind('mousewheel DOMMouseScroll', function(event) {
   event.preventDefault();
   var delta = event.originalEvent.wheelDelta || -event.originalEvent.detail;
   init_scroll(event, delta);
});


Сначала цепляем функцию на событие mousewheel (DOMMouseScroll в Firefox), тогда можно будет перехватить данные и определить направление. Встраиваем в обработку init_scroll, которая получает wheelData для этого.

В идеальном мире достаточно было бы посчитать изменение wheelData. Однако при анимации последовательностей необходимо встроить проверку, чтобы событие-триггер не дублировалось (иначе при анимации изображение будет перекрываться). Можно использовать setInterval для вызова каждой анимации по очереди, но это не обеспечит точности и надёжности, т.к. каждый браузер обрабатывает его по-своему. К примеру, у Chrome и Firefox setInterval тормозит в неактивных вкладках, в результате функции не отрабатывают вовремя. В результате я остановился на использовании функции, возвращающей текущее время.

var timeNow = new Date().getTime(),
quietPeriod = 500;
…
if(timeNow - lastAnimation < quietPeriod + settings.animationTime) {
   event.preventDefault();
   return;
}
…
lastAnimation = timeNow;


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

if (deltaOfInterest < 0) {
   el.moveDown()
} else {
   el.moveUp()
}


Функции moveUp и moveDown меняют атрибуты раскладки, чтобы они отражали текущее состояние сайта. Каждая из них в конце работы вызывает окончательный метод трансформации, чтобы передвинуть следующую секцию в окно просмотра.

$.fn.transformPage = function(settings, pos, index) {
   …
   $(this).css({
      "-webkit-transform": ( settings.direction == 'horizontal' ) ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)",
      "-webkit-transition": "all " + settings.animationTime + "ms " + settings.easing,
      "-moz-transform": ( settings.direction == 'horizontal' ) ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)",
      "-moz-transition": "all " + settings.animationTime + "ms " + settings.easing,
      "-ms-transform": ( settings.direction == 'horizontal' ) ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)",
      "-ms-transition": "all " + settings.animationTime + "ms " + settings.easing,
      "transform": ( settings.direction == 'horizontal' ) ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)",
      "transition": "all " + settings.animationTime + "ms " + settings.easing
   });
   …
}


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

5. Дополнительные возможности


Сначала я не хотел ничего добавлять, но получил столько отзывов от сообщества GitHub, что решил постепенно улучшать плагин. Я выпустил версию 1.2.1, в которой добавляется множество обратных вызовов и циклов, и что самое сложное – адаптивность.

Я не делал плагин изначально с расчётом на мобильные платформы (о чём сожалею). Вместо этого пришлось отслеживать и пересчитывать события тачскрина в подходящий для использования в init_scroll вид. Не во всех браузерах это работает хорошо, поэтому пришлось встроить возможность отката – когда браузеру возвращается нормальная прокрутка при достижении определённой ширины окна.

var defaults = {
   responsiveFallback: false
   …
};

function responsive() {
   if ($(window).width() < settings.responsiveFallback) {
      $("body").addClass("disabled-onepage-scroll");
      $(document).unbind('mousewheel DOMMouseScroll');
      el.swipeEvents().unbind("swipeDown swipeUp");
   } else {
      if($("body").hasClass("disabled-onepage-scroll")) {
         $("body").removeClass("disabled-onepage-scroll");
         $("html, body, .wrapper").animate({ scrollTop: 0 }, "fast");
      }

      el.swipeEvents().bind("swipeDown",  function(event) {
         if (!$("body").hasClass("disabled-onepage-scroll")) event.preventDefault();
         el.moveUp();
      }).bind("swipeUp", function(event){
         if (!$("body").hasClass("disabled-onepage-scroll")) event.preventDefault();
         el.moveDown();
      });

      $(document).bind('mousewheel DOMMouseScroll', function(event) {
         event.preventDefault();
         var delta = event.originalEvent.wheelDelta || -event.originalEvent.detail;
         init_scroll(event, delta);
      });
   }
}


Определим переменную по-умолчанию. Используем responsiveFallback, чтобы определить, когда плагин должен делать откат. Этот код определяет ширину браузера. Если ширина меньше значения из responsiveFallback, функция снимает все события, переносит страницу на начало и позволяет прокручивать её как обычно. Если ширина превосходит значение, плагин проверяет наличие класса disabled-onepage-scroll, чтобы узнать, инициализирован ли он. Если нет – инициализируется заново.

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

6. Тестирование в разных браузерах.


Тестирование – важная часть разработки, перед выпуском плагина надо убедиться, что он работает на большинстве машин. Я всегда разрабатываю в Chrome – во-первых, мне нравятся его инструменты разработчика, во-вторых я знаю, что если плагин работает в Chrome, скорее всего он будет работать в Safari и Opera.

Обычно я использую Macbook Air для разработки, а дома у меня есть PC для проверки. После того, как плагин работает в Chrome, я проверяю его вручную в Safari, Opera, и в конце – в Firefox на Mac OS X, а затем — Chrome, Firefox и Internet Explorer 10 на Windows.

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


Не забудьте тестировать свои плагины на мобильных устройствах.

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

7. Выкладываем плагин в опенсорс


Последний шаг – делимся плагином на GitHub. Для этого нужно создать там аккаунт, настроить Git и создать новый репозиторий. Затем клонировать его на локальную машину – это создаст директорию с названием плагина. Копируем туда плагин и настраиваем структуру.

Структура репозитория

Настраиваете, как вам удобно. Я делаю так:

— директория demo содержит работающие демки, со всеми необходимыми ресурсами
— обычная и сжатая версия плагина лежат в корне
— CSS и тестовые ресурсы, типа картинок (при необходимости) лежат в корне
— файл readme в корне

Структура readme

Важный этап – написание понятных инструкций для опенсорс-сообщества. Обычно я пишу их в readme, но для сложных случаев может понадобиться wiki-страница. Как я пишу readme:

1. Введение
Объясняю назначение плагина, даю изображение и ссылку на демку.
2. Требования и совместимость.
Лучше вынести эту секцию повыше, чтобы сразу было ясно, сможет ли человек воспользоваться плагином.
3. Основные принципы использования
Пошаговые инструкции, начиная от подключения jQuery, заканчивая HTML-разметкой и вызовом функции. Также описываются настройки.
4. Продвинутое использование.
Более сложные инструкции – публичные методы, обратные вызовы и другая полезная информация.
5. Другие ресурсы
Ссылки на обучалку, спасибки и т.п.

8 Расширяем поддержку


Вообще можно было бы обойтись без jQuery, но я торопился выложить его в опенсорс, поэтому решил сократить время на разработку и полагаться на готовые функции в jQuery.

Но для очистки совести я переработал плагин на чистом яваскрипте (также доступна версия с поддержкой Zepto). На чистом JS нет необходимости включать jQuery, всё работает «из коробки».

To make amends, and exclusively for Smashing Magazine’s readers, I have rebuilt One Page Scroll using pure JavaScript (a Zepto version is also available). With the pure JavaScript version, you no longer need to include jQuery. The plugin works right out of the box.

Чистый JS и версия для Zepto

Pure JavaScript Repository
Zepto repository

Перерабатываем плагин на чистом JavaScript

Такая переработка может показаться тяжким делом, но это только спервоначалу. Самое сложное – не ошибиться с математикой. Т.к. я это уже сделал, разработка вспомогательных функций для избавления от jQuery заняла у меня лишь несколько часов.

Плагин основывается на CSS3, поэтому нужно было просто заменить вызовы jQuery на аналогичные собственные. Заодно я реорганизовал структуру скрипта:

— значения переменных по-умолчанию
Всё то же, что и в предыдущей версии
— функция инициализации
Подготавливает и располагает раскладку и инициализацию того, что происходит, когда вызывается функция onePageScroll. Здесь сидят все процедуры, назначающие имена классов, атрибуты и стили позиционирования.
— приватные методы
Все внутренние методы плагина – события прокрутки, трансформация страницы, адаптивный откат и отслеживание прокрутки.
— публичные методы
Все методы для разработчиков: moveDown(), moveUp() и moveTo()
— вспомогательные методы
Всё, что переопределяет вызовы jQuery.

Встретилась пара неприятных моментов – отдельная функция только для того, чтобы добавить или убрать имя стиля, или использование document.querySelector вместо $. Но в конце мы получили лучше структурированный плагин.

Перестраиваем плагин для Zepto

Я решил поддержать Zepto, несмотря на то, что он рассчитан только на самые современные браузеры (IE10+), т.к. он работает быстрее и эффективнее чем jQuery 2.0+, при этом имеет более гибкое API. Zpeto в 4 раза меньше jQuery, что сказывается на скорости загрузки страницы. Из-за того, что люди чаще используют смартфоны, Zepto становится лучшей альтернативой.

Переделывать плагин с jQuery на Zepto проще, потому что у них сходные API. Почти всё одинаково, кроме части с анимацией. Поскольку у функции Zepto $.fn.animate() есть поддержка CSS3-анимации и поддержка обратного вызова animationEnd, следующую часть:

$(this).css({
   "-webkit-transform": "translate3d(0, " + pos + "%, 0)",
   "-webkit-transition": "-webkit-transform " + settings.animationTime + "ms " + settings.easing,
   "-moz-transform": "translate3d(0, " + pos + "%, 0)",
   "-moz-transition": "-moz-transform " + settings.animationTime + "ms " + settings.easing,
   "-ms-transform": "translate3d(0, " + pos + "%, 0)",
   "-ms-transition": "-ms-transform " + settings.animationTime + "ms " + settings.easing,
   "transform": "translate3d(0, " + pos + "%, 0)",
   "transition": "transform " + settings.animationTime + "ms " + settings.easing
});
$(this).one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e) {
   if (typeof settings.afterMove == 'function') settings.afterMove(index, next_el);
});


Можно заменить таким кодом:

$(this).animate({
      translate3d: "0, " + pos + "%, 0"
   }, settings.animationTime, settings.easing, function() {
      if (typeof settings.afterMove == 'function') settings.afterMove(index, next_el);
   });
}


Zepto позволяет делать анимацию без определения всех стилей или самостоятельного назначения обратных вызовов.

И зачем этим заморачиваться?



Поскольку всё больше людей используют jQuery, он становится всё более сложным и иногда тормозит. Если сделать поддержку других фреймворков, ваш плагин будет более популярным.
Переделка с самого начала также поможет вам в будущем делать плагины лучше. jQuery и другие библиотеки прощают мелкие ошибки, типа пропущенных запятых – в результате вы не очень заботитесь о качестве своей работы. Без этих поблажек на чистом JavaScript я лучше чувствовал работу своего плагина – что как работает, что влияет на производительность, и что можно улучшить.

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

Заключение.



Ну вот вам и весь процесс создания плагина «One Page Scroll». Были ошибки, но я учился на них по ходу разработки. Если б я разрабатывал его сегодня, я бы сконцентрировался на мобильных устройствах, и добавил бы больше комментариев в код.

Без поддержки таких сообществ, как GitHub, StackOverflow и Smashing Magazine я бы не смог сделать плагин так быстро. Эти сообщества сильно помогали мне в работе, поэтому я и делаю свои плагины доступными бесплатно для всех желающих. Этой мой способ расплатиться за замечательную поддержку.

Ресурсы

Демка

скачать плагин
Support the author
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +3
    Немного не понятен текст:
    jQuery и другие библиотеки прощают мелкие ошибки, типа пропущенных запятых

    Это где там такие «прощения» с запятыми?
      +1
      Не знаю, так в оригинале было.
      0
      Нам нужна ссылка на онлайн-демонстрацию.
      +2
      Вот еще один очень похожий плаг, только там функционал намного шире, мне он больше понравился.

      fullPage.js
        0
        А якорные ссылки для доступа к определенной секции поддерживает?
          0
          FullPage.js поддерживает из коробки, а в OnePageScroll нужно разбираться что к чему.
          0
          У этого плагина, как и других подобных, есть 1 серьезная проблема: они плохо работают с иннерционными девайсами, такими, как трекпады или magicmouse — за 1 прикосновение к девайсу прокручивают 2 и более слайда. Причем чем быстрее сделать анимацию между слайдами, тем больше все усугубится. Эту проблему я относительно поборол на kit.yandex.ru, если кому интересно, могу пост запилить.
            0
            Интересно!
              0
              А я бы сказал, что есть еще одна проблема, не менее существенная (по крайней мере для меня). Что если высота окна меньше, чем высота содержимого слайда? И в fullPage.js, и в OnePageScroll слайд обрезается по высоте окна браудера (хотя в первом есть функция «подгонки» по высоте, но это не всегда приемлемо), игнорируя высоту слайда. Это как-то… убого. В первую очередь, человек приходит на сайт за информацией. Я считаю, что если блок содержимое какого-то слайда не вмещается по высоте в окно браузера, нужно делать обычную прокрутку.
                0
                Ну если перечислять недостатки, то их много. Например, нет скроллбара в сабже поста. Описанная вами проблема имеет место быть. Я сейчас как раз кодю так, чтоб при превышении N-ой высоты экрана отключался режим скроллера. Как все доделаю, запилю пост.
                0
                Посмотрел. Интересно. Хотелось бы узнать как Вы так сделали.
                0
                Хорошая работа, но плохо искали. Fullpage.js. Использовал много раз — отличная вещь.
                  0
                  Упс, уже упомянули. Извиняюсь.

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