Напишите плагин полноэкранной прокрутки с помощью ES6

внешний интерфейс JavaScript браузер jQuery

В этой статье рассказывается, как использовать собственный JS (в основном синтаксис ES6) для реализации плагина полноэкранной прокрутки, совместимого с IE 10+, сенсорным экраном мобильного телефона, оптимизацией сенсорной панели Mac, поддержкой пользовательской анимации страниц и сжатым файлом gzip. всего 2,15 КБ (включая файл CSS). Полный исходный код здесьpure-full-page, нажмите здесь, чтобы посмотретьdemo.

1) Предыдущие слова

Уже существует множество плагинов полноэкранной прокрутки, таких как знаменитыйfullPage, так зачем делать свои собственные колеса?

Существующие колеса имеют следующие проблемы:

  • Во-первых, самая большая проблема заключается в том, что самые популярные плагины полагаются на jQuery, а это означает, что их использование в проектах, использующих React или Vue, — заноза в заднице: мне нужна только функция полноэкранной прокрутки, и мне нужно, чтобы ощущение использования ножа мясника, чтобы зарезать цыплят;
  • Во-вторых, многие существующие плагины полноэкранной прокрутки часто очень богаты функциями, что было преимуществом в последние несколько лет, но сейчас (2018-5) можно расценивать как недостаток: фронтенд-разработка сильно изменилась , одно из самых важных изменений заключается в том, что ES6 изначально поддерживает модульную разработку.Самая большая особенность модульной разработки заключается в том, что модуль должен сосредоточиться только на том, чтобы хорошо делать одну вещь, а затем собирать полную систему.С этой точки зрения, большой и всеобъемлющий плагин противоречит принципам разработки модуля.

Напротив, построение колеса с помощью родного языка имеет следующие преимущества:

  • Плагины, написанные на родных языках, не будут затронуты сценариями использования зависимых плагинов (сейчас jQuery-зависимые плагины не подходят для разработки одностраничных приложений), поэтому они более гибки в использовании;
  • При модульной разработке плагины, разработанные на родных языках, могут фокусироваться только на одной функции, поэтому объем кода может быть очень небольшим;
  • Наконец, с развитием JS/CSS/HTML и постоянными итеративными обновлениями браузеров стоимость разработки плагинов на нативных языках становится все ниже и ниже, так почему бы и нет?

2) Принцип реализации и структура кода

2.1 Принцип реализации

Принцип реализации показан на следующем рисунке: контейнер и страница в контейнере берут высоту текущей визуальной области, а родительский элемент контейнераoverflowЗначение свойства установлено наhidden, заменив контейнерtopЗначение реализует эффект полноэкранной прокрутки.

全屏滚动实现原理

2.2 Архитектура кода

Идея написания кода состоит в том, чтобы определить класс полноэкранной прокрутки через класс и использовать его черезnew PureFullPage().init()использовать.

/**
 * 全屏滚动类
 */
class PureFullPage {
  // 构造函数
  constructor() {}

  // 原型方法
  methods() {}

  // 初始化函数
  init() {}
}

3) HTML-структура

Ввиду вышеизложенных принципов реализации существуют особые требования к структуре html, а именно: Контейнер страницы#pureFullPageContainer, все страницы являются их прямыми дочерними элементами, здесь для удобства прямо беремbodyкак его непосредственный родительский элемент.

<body>
  <div id="pureFullPageContainer">
    <div class="page"></div>
    <div class="page"></div>
    <div class="page"></div>
  </div>
</body>

4) настройки css

Во-первых, контейнер и страницы в контейнере принимают высоту текущей видимой области, чтобы подготовиться к отображению полной страницы для каждого переключателя;

Во-вторых, родительский элемент контейнера (вотbody)overflowЗначение свойства установлено наhidden, что гарантирует отображение только одной страницы за раз, а остальные страницы скрыты.

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

body {
  /* body 为容器直接的父元素 */
  overflow: hidden;
}

#pureFullPage {
  /* 只有当 position 的值不是 static 时,top 值才有效 */
  position: relative;
  /* 设置初始值 */
  top: 0;
}
.page {
  /* 此处不能为 100vh,后面详述 */
  /* 其父元素,也就是 #pureFullPage 的高度,通过 js 动态设置*/
  height: 100%;
}

Уведомление:

  • контейнерpositionЗначение свойства должно быть установлено вrelative,потому чтоtopтолько вpositionстоимость имущества неstaticдействует только тогда, когда

  • Высота страницы должна быть установлена ​​на текущую высоту области просмотра, но не может быть установлена ​​непосредственно на100vh, потому что мобильный браузер Safari учитывает адресную строку при расчете100vh, но область под адресной строкой не должна считаться «видимой областью», ведь на самом деле это «невидимая» область. Это приведет к100vhСоответствующее соотношение значений пикселейdocument.documentElement.clientHeightПолученное значение пикселя большое. Это переключениеtopКогда значение установлено, это не полноэкранный переключатель, на самом деле высота переключателя в этом случае меньше высоты страницы.

  • Решить проблему высоты визуальной области мобильного браузера Safari: так как она получена через jsdocument.documentElement.clientHeightvalue — ожидаемая высота области просмотра (исключая верхнюю адресную строку и нижнюю панель инструментов), затемУстановите это значение на высоту контейнера через js, и в то же время установите высоту страницы внутри контейнера на100%, чтобы можно было гарантировать высоту и переключение контейнера и страницыtopЗначение такое же, что обеспечивает полноэкранное переключение.

// 伪代码
'#pureFullPage'.style.height = document.documentElement.clientHeight + 'px';

5) Отслеживание событий прокрутки/перелистывания

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

5.1 Сторона ПК

Основная проблема, решаемая на стороне ПК, состоит в том, чтобы получить направление скольжения мыши или сенсорного панели. Сенсорная панель скользит вверх и вниз, а прокрутка мыши связана с тем же событием:

  • фаерфокс этоDOMMouseScrollсобытие, соответствующая информация о колесе (прокрутка вперед или назад) сохраняется вdetailВ атрибуте roll forward значение этого атрибута кратно 3, в противном случае оно кратно -3;
  • Другие браузеры, кроме firefox,mousewheelсобытие, соответствующая информация о колесе сохраняется вwheelDeltaВ атрибуте roll forward значение этого атрибута кратно -120, и наоборот, кратно 120.

macOS так, windows наоборот?

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

// 鼠标滚轮事件
getWheelDelta(event) {
  if (event.wheelDelta) {
    return event.wheelDelta;
  } else {
    // 兼容火狐
    return -event.detail;
  }
},

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

// 鼠标滚动逻辑(全屏滚动关键逻辑)
scrollMouse(event) {
  let delta = utils.getWheelDelta(event);
  // delta < 0,鼠标往前滚动,页面向下滚动
  if (delta < 0) {
    this.goDown();
  } else {
    this.goUp();
  }
}

goDown,goUpЭто логический код для прокрутки страницы, следует отметить, что он должен бытьОпределите границу прокрутки, чтобы содержимое страницы всегда отображалось в контейнере.:

  • Легко определить верхнюю границу, которая равна высоте 1 страницы (то есть области просмотра), то есть если текущая верхняя внешняя граница контейнера — это расстояние от верха всей страницы (здесь это значение равно точноoffsetTopабсолютное значение значения из-за его родительскогоoffsetTopзначения0) больше или равно высоте текущей визуальной области, прокрутка вверх разрешена, в противном случае доказывает, что на ней нет страницы, и прокрутка вверх не разрешена;
  • Нижняя границаn - 2(n представляет количество страниц, прокручиваемых в полноэкранном режиме) Высота видимой области, когда контейнерoffsetTopАбсолютное значение значения меньше или равноn - 2Когда высота видимой области равна 1, это означает, что вы можете прокручивать страницу вниз.

Конкретный код выглядит следующим образом:

goUp() {
  // 只有页面顶部还有页面时页面向上滚动
  if (-this.container.offsetTop >= this.viewHeight) {
    // 重新指定当前页面距视图顶部的距离 currentPosition,实现全屏滚动,
    // currentPosition 为负值,越大表示超出顶部部分越少
    this.currentPosition = this.currentPosition + this.viewHeight;

    this.turnPage(this.currentPosition);
  }
}
goDown() {
  // 只有页面底部还有页面时页面向下滚动
  if (-this.container.offsetTop <= this.viewHeight * (this.pagesNum - 2)) {
    // 重新指定当前页面距视图顶部的距离 currentPosition,实现全屏滚动,
    // currentPosition 为负值,越小表示超出顶部部分越多
    this.currentPosition = this.currentPosition - this.viewHeight;

    this.turnPage(this.currentPosition);
  }
}

Наконец, добавьте событие прокрутки:

// 鼠标滚轮监听,火狐鼠标滚动事件不同其他
if (navigator.userAgent.toLowerCase().indexOf('firefox') === -1) {
  document.addEventListener('mousewheel', scrollMouse);
} else {
  document.addEventListener('DOMMouseScroll', scrollMouse);
}

5.2 Мобильный терминал

Мобильный терминал должен определить, следует ли провести пальцем вверх или вниз, что можно комбинировать сtouchstart(уволен, когда палец начинает касаться экрана) иtouchend(Запуск, когда палец покидает экран) Судья по реализации двух событий: Когда вы получаете два запуска запуска событийpageYзначение, если в конце касанияpageYбольше, чем в начале касанияpageY, что означает, что палец скользит вниз, а соответствующая страница прокручивается вверх, и наоборот.

Здесь нам нужны события касания для отслеживания свойств касания:

  • touches: Массив сенсорных объектов текущей отслеживаемой сенсорной операции, используемый для начала сенсорного ввода.pageYстоимость;
  • changeTouches: Массив объектов Touch, которые изменились с момента последнего касания, используемые для получения касания в конце касания.pageYстоимость.

Соответствующий код выглядит следующим образом:

// 手指接触屏幕
document.addEventListener('touchstart', event => {
  this.startY = event.touches[0].pageY;
});
//手指离开屏幕
document.addEventListener('touchend', event => {
  let endY = event.changedTouches[0].pageY;
  if (endY - this.startY < 0) {
    // 手指向上滑动,对应页面向下滚动
    this.goDown();
  } else {
    // 手指向下滑动,对应页面向上滚动
    this.goUp();
  }
});

Чтобы избежать обновления по запросу, вы можете запретитьtouchmoveПоведение событий по умолчанию:

// 阻止 touchmove 下拉刷新
document.addEventListener('touchmove', event => {
  event.preventDefault();
});

6) Оптимизация производительности событий прокрутки на стороне ПК

6.1 Введение функции защиты от сотрясений и функции перехвата

Оптимизация в основном начинается с двух удобств:

  • При изменении размера страницы ограничить функцией устранения дребезгаresizeчастота срабатывания события;
  • Когда запускается событие прокрутки/прокрутки, используйте функцию дроссельной заслонки, чтобы ограничить частоту триггера события прокрутки/пролистывания.

Поскольку оба ограничивают частоту запуска (оба реализованы таймерами), в чем разница между ними?

Во-первых, при работе функции защиты от сотрясений, если событие срабатывает непрерывно в течение заданного времени задержки, функция обратного вызова, привязанная к этому событию, никогда не сработает, только в течение времени задержки событие не будет срабатывать повторно. Будет выполнена соответствующая функция обратного вызова. Функция устранения дребезга очень подходит для случая изменения размера окна, что также согласуется сЗапустите событие после перетаскивания его на место. Если оно продолжит перетаскивание, событие не будет запущено.Это все.

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

Учитывая два вышеуказанных свойства функции перехвата, она особенно подходит для оптимизации событий прокрутки/перелистывания:

  • частота может быть ограничена;
  • Функция обратного вызова, зарегистрированная для события, не будет выполняться, поскольку событие прокрутки/слайда слишком чувствительно (непрерывно запускается в течение времени задержки);
  • Функция обратного вызова может быть настроена на запуск в начале времени задержки, чтобы пользователь не почувствовал короткую задержку после операции.

Принцип реализации функции антиджиттера и функции перехвата здесь не представлен.Если вам интересно, то можете посмотретьThrottling and Debouncing in JavaScript, ниже приведен реализованный код:

// 防抖动函数,method 回调函数,context 上下文,event 传入的时间,delay 延迟函数
debounce(method, context, event, delay) {
  clearTimeout(method.tId);
  method.tId = setTimeout(() => {
    method.call(context, event);
  }, delay);
},

// 截流函数,method 回调函数,context 上下文,delay 延迟函数,
// 这里没有提供是在延迟时间开始还是结束的时候执行回调函数的选项,
// 直接在延迟时间开始的时候执行回调
throttle(method, context, delay) {
  let wait = false;
  return function() {
    if (!wait) {
      method.apply(context, arguments);
      wait = true;
      setTimeout(() => {
        wait = false;
      }, delay);
    }
  };
},

Функция дросселя, представленная в разделе 22.33.3 "Расширенное программирование на JavaScript - третье издание", отличается от определенной здесь. Функция дросселя, определенная в Elevation, соответствует здесь функции устранения дребезга, но большинство статей в Интернете отличаются от статей в Elevation , как определено в lodashdebounce.

6.2 Изменить событие прокрутки на стороне ПК

Из приведенного выше описания мы уже знаем, что функция перехвата может повысить производительность за счет ограничения частоты срабатывания события прокрутки, В то же время установите его вФункция обратного вызова события прокрутки вызывается сразу в начале времени задержки.Без ущерба для пользовательского опыта.

Функция перехвата была определена выше, и ее очень просто использовать:

// 设置截流函数
let handleMouseWheel = utils.throttle(this.scrollMouse, this, this.DELAY, true);

// 鼠标滚轮监听,火狐鼠标滚动事件不同其他
if (navigator.userAgent.toLowerCase().indexOf('firefox') === -1) {
  document.addEventListener('mousewheel', handleMouseWheel);
} else {
  document.addEventListener('DOMMouseScroll', handleMouseWheel);
}

Приведенный выше код написан в классеinitметод, поэтому контекст функции перехвата передается вthis, представляющий текущий экземпляр класса.

7) Другое

7.1 Кнопки навигации

Для упрощения структуры html кнопки навигации созданы с помощью js. Сложность здесь в том, чтоКак нажимать разные кнопки, чтобы перейти на соответствующую страницу и обновить стиль соответствующей кнопки.

Решение состоит в следующем:

  • Переход на страницу: количество страниц равно количеству кнопок навигации, поэтому нажатие i-й кнопки приведет к переходу на i-ю страницу и контейнер, соответствующий i-й странице.topзначение точно-(i * this.viewHeight)
  • Изменить стиль: чтобы изменить стиль, сначала удалите выбранный стиль всех кнопок, а затем добавьте выбранный стиль к нажатой в данный момент кнопке.
// 创建右侧点式导航
createNav() {
  const nav = document.createElement('div');
  nav.className = 'nav';
  this.container.appendChild(nav);
  // 有几页,显示几个点
  for (let i = 0; i < this.pagesNum; i++) {
    nav.innerHTML += '<p class="nav-dot"><span></span></p>';
  }
  const navDots = document.querySelectorAll('.nav-dot');
  this.navDots = Array.prototype.slice.call(navDots);
  // 添加初始样式
  this.navDots[0].classList.add('active');
  // 添加点式导航点击事件
  this.navDots.forEach((el, i) => {
    el.addEventListener('click', event => {
      // 页面跳转
      this.currentPosition = -(i * this.viewHeight);
      this.turnPage(this.currentPosition);
      // 更改样式
      this.navDots.forEach(el => {
        utils.deleteClassName(el, 'active');
      });
      event.target.classList.add('active');
    });
  });
}

7.2 Пользовательские параметры

Правильные пользовательские параметры могут повысить гибкость плагина.

Параметры передаются через конструктор и передаются черезObject.assign()Чтобы объединить параметры:

constructor(options) {
  // 默认配置
  const defaultOptions = {
    isShowNav: true,
    delay: 150,
    definePages: () => {},
  };
  // 合并自定义配置
  this.options = Object.assign(defaultOptions, options);
}

7.3 Обновление данных при изменении размера окна

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

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

// window resize 时重新获取位置
getNewPosition() {
  this.viewHeight = document.documentElement.clientHeight;
  this.container.style.height = this.viewHeight + 'px';
  let activeNavIndex;
  this.navDots.forEach((e, i) => {
    if (e.classList.contains('active')) {
      activeNavIndex = i;
    }
  });
  this.currentPosition = -(activeNavIndex * this.viewHeight);
  this.turnPage(this.currentPosition);
}

handleWindowResize(event) {
  // 设置防抖动函数
  utils.debounce(this.getNewPosition, this, event, this.DELAY);
}

// 窗口尺寸变化时重置位置
window.addEventListener('resize', this.handleWindowResize.bind(this));

7.4 Совместимость

Совместимость здесь в основном относится к двум аспектам: во-первых, разные браузеры определяют разные API для одного и того же поведения, например, упомянутый выше API Firefox для получения информации о прокрутке мыши отличается от других браузеров; во-вторых, это новый синтаксис ES6, совместимая обработка. нового API.

Для преобразования новых синтаксисов, таких как классовые и стрелочные функции, это может быть выполнено через Babel.Ввиду небольшого объема кода в этом плагине он находится в контролируемом состоянии, а решение полифилла, предоставляемое Babel, не введено, потому что новый API имеет толькоObject.assign()Совместимость нужно делать, только полифилл написать отдельно, вот так:

// polyfill Object.assign
polyfill() {
  if (typeof Object.assign != 'function') {
    Object.defineProperty(Object, 'assign', {
      value: function assign(target, varArgs) {
        if (target == null) {
          throw new TypeError('Cannot convert undefined or null to object');
        }
        let to = Object(target);
        for (let index = 1; index < arguments.length; index++) {
          let nextSource = arguments[index];
          if (nextSource != null) {
            for (let nextKey in nextSource) {
              if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                to[nextKey] = nextSource[nextKey];
              }
            }
          }
        }
        return to;
      },
      writable: true,
      configurable: true,
    });
  }
},

Цитата из:MDN-Object.assign()

Поскольку этот плагин совместим только с IE10, мы не планируем делать совместимую обработку событий, в конце концов.IE9 поддерживается addEventListener.

7.5 Дальнейшая оптимизация производительности с отложенной загрузкой

Написано в 5.1getWheelDeltaКаждый раз, когда функция выполняется, она должна проверять, поддерживается ли онаevent.wheelDelta, на самом деле, браузер должен обнаружить его только при первой загрузке.Если он поддерживает его, он будет поддерживать его и в следующий раз.Нет необходимости делать обнаружение снова.

И это обнаружение будет выполняться много раз в жизненном цикле страницы, в этом случае вы можете пройтиленивая загрузкаМетоды оптимизированы следующим образом:

getWheelDelta(event) {
  if (event.wheelDelta) {
    // 第一次调用之后惰性载入,无需再做检测
    this.getWheelDelta = event => event.wheelDelta;
    // 第一次调用使用
    return event.wheelDelta;
  } else {
    // 兼容火狐
    this.getWheelDelta = event => -event.detail;
    return -event.detail;
  }
},

Полный исходный код здесьpure-full-page, нажмите здесь, чтобы посмотретьdemo.

использованная литература

Чистая полноэкранная прокрутка JS / полноэкранное перелистывание страниц
Throttling and Debouncing in JavaScript
Debouncing and Throttling Explained Through Examples
JavaScript Debounce Function
Simple throttle in js
Simple throttle in js - jsfiddle
Viewport height is taller than the visible part of the document in some mobile browsers
MDN-Object.assign()
Бабель скомпилирован или ES 6? Можно ли использовать только полифиллы? - ответ Генри