Русский
Русский
English
Статистика
Реклама

Intersection observer api

Как писать хорошие библиотеки под Angular

27.07.2020 14:05:23 | Автор: admin

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


Angular превосходная платформа с одними из лучших инструментов во фронтэнд-среде. И, конечно, есть определенный способ делать по-ангуляровски. Что лично мне особенно нравится в этом фреймворке это то чувство удовлетворенности, которое испытываешь, когда все сделано как надо: аккуратный код, четкая архитектура. Давайте разберемся, что делает код правильно написанным под Angular.



The Angular way


Я уже давно пишу на Angular, перенимая опыт у отличных инженеров, с которыми работаю, и черпая знания из обширной базы, доступной в сети. Некоторое время назад я заметил, что, хотя браузеры предоставляют огромные возможности, мало что из этого включено в Angular из коробки. Так и задумано: ведь это просто платформа для создания своих продуктов и нужно заточить ее под свои нужды. Поэтому мы создали opensource-инициативу Web APIs for Angular. Ее цель создание легковесных, качественных и идиоматических оберток для использования нативных API в Angular. Я бы хотел обсудить принципы написания хорошего код на примере библиотеки @ng-web-apis/intersection-observer.


По моему мнению, эти три концепции играют основную роль:


  1. Angular декларативен по природе, в то время как нативный и традиционный JavaScript-код зачастую императивный.
  2. У Angular крутая система внедрения зависимостей, которую можно активно использовать себе во благо.
  3. Angular строит логику на Observable, тогда как многие API базируются на коллбэках.

Давайте пройдемся по этим пунктам подробнее.


Декларативный vs императивный


Вот типичный кусок кода, который у вас будет, если вы захотите использовать IntersectionObserver:


const callback = entries => { ... };const options = {   root: document.querySelector('#scrollArea'),   rootMargin: '10px',   threshold: 1};const observer = new IntersectionObserver(callback, options);observer.observe(document.querySelector('#target'));


Император одобряет нативный API


Здесь не так много кода, но мы успели нарушить все три принципа, названные выше. В Angular подобную логику выносят в директиву с декларативной настройкой:


<div   waIntersectionObserver   waIntersectionThreshold="1"   waIntersectionRootMargin="10px"   (waIntersectionObservee)="onIntersection($event)">   I'm being observed</div>

Вы можете узнать больше о декларативной природе директив Angular в этой статье на примере Payment Request API. Я очень советую прочитать ее, так как для подробного разбора этого аспекта тут просто слишком мало кода.

Нам понадобится 2 директивы: одна для создания наблюдателя, другая чтобы отметить наблюдаемый элемент. Так мы сможем отслеживать несколько элементов одним наблюдателем. Внутри второй директивы мы поручим всю работу сервису. Таким образом мы сможем следить и за хостом-компонентом, где директиву использовать не получится. Это также позволит абстрагироваться от императивных вызовов observe/unobserve.


Первая директива может наследоваться непосредственно от IntersectionObserver и хранить у себя Map для сопоставления элементов и обратных вызовов. Ведь если мы отслеживаем несколько элементов, нет смысла оповещать их все, если пересечение сработало только на одном:


@Directive({   selector: '[waIntersectionObserver]',})export class IntersectionObserverDirective extends IntersectionObserver   implements OnDestroy {   private readonly callbacks = new Map<Element, IntersectionObserverCallback>();   constructor(       @Optional() @Inject(INTERSECTION_ROOT) root: ElementRef<Element> | null,       @Attribute('waIntersectionRootMargin') rootMargin: string | null,       @Attribute('waIntersectionThreshold') threshold: string | null,   ) {       super(           entries => {               this.callbacks.forEach((callback, element) => {                   const filtered = entries.filter(({target}) => target === element);                   return filtered.length && callback(filtered, this);               });           },           {               root: root && root.nativeElement,               rootMargin: rootMargin || ROOT_MARGIN_DEFAULT,               threshold: threshold                 ? threshold.split(',').map(parseFloat)                 : THRESHOLD_DEFAULT,           },       );   }   observe(target: Element, callback: IntersectionObserverCallback = () => {}) {       super.observe(target);       this.callbacks.set(target, callback);   }   unobserve(target: Element) {       super.unobserve(target);       this.callbacks.delete(target);   }   ngOnDestroy() {       this.disconnect();   }}

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


Dependency Injection


Мы часто используем DI для передачи встроенных в Angular сущностей или сервисов, которые создаем сами. Но с его помощью можно делать куда больше. Я говорю про провайдеры, фабрики, токены и тому подобное. Например, нашей директиве необходимо получить корневой элемент, с которым мы будем отслеживать пересечения. Предоставим его с помощью токена и простой директивы:


@Directive({   selector: '[waIntersectionRoot]',   providers: [       {           provide: INTERSECTION_ROOT,           useExisting: ElementRef,       },   ],})export class IntersectionRootDirective {}

Тогда наш шаблон станет выглядеть так:


<div waIntersectionRoot>   ...   <div       waIntersectionObserver       waIntersectionThreshold="1"       waIntersectionRootMargin="10px"       (waIntersectionObservee)="onIntersection($event)"   >       I'm being observed   </div>   ...</div>

Подробнее прочитать про DI и про то, как обуздать его мощь, можно в статье о нашей декларативной Web Audio API библиотеке под Angular.

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


Сервис дочерней директивы получает родительскую через DI и превращает работу IntersectionObserver в RxJS Observable, что мы обсудим далее.


Observables


В то время как нативные API полагаются на коллбэки, мы в Angular используем RxJs и его реактивную парадигму. Одна особенность Observable, про которую часто забывают, это просто класс и от него можно наследоваться. Давайте сделаем сервис-абстракцию над IntersectionObserver, который превратит его в Observable. У нас уже есть подготовленная директива, осталось в ней зарегистрироваться:


@Injectable()export class IntersectionObserveeService extends Observable<IntersectionObserverEntry[]> {   constructor(       @Inject(ElementRef) {nativeElement}: ElementRef<Element>,       @Inject(IntersectionObserverDirective)       observer: IntersectionObserverDirective,   ) {       super(subscriber => {           observer.observe(nativeElement, entries => {               subscriber.next(entries);           });           return () => {               observer.unobserve(nativeElement);           };       });   }}


Теперь у нас есть Observable, инкапсулирующий логику IntersectionObserver. Мы даже можем использовать эти классы вне Angular, передавая параметры в new-вызовы.


Мы применили похожий подход для создания Observable-сервиса в Geolocation API и Resize Observer API, где подробно разобрали его.

Директива просто передаст этот сервис в качестве Output. Ведь класс EventEmitter, который мы привыкли использовать тоже наследуется от Observable и, соответственно, совместим с нашим сервисом:


@Directive({   selector: '[waIntersectionObservee]',   outputs: ['waIntersectionObservee'],   providers: [IntersectionObserveeService],})export class IntersectionObserveeDirective {   constructor(       @Inject(IntersectionObserveeService)       readonly waIntersectionObservee: Observable<IntersectionObserverEntry[]>,   ) {}}

Теперь мы можем либо использовать директиву в шаблоне, либо запрашивать сервис и добавлять его в связки RxJs-операторов, таких как map, filter, switchMap, чтобы получить желаемую логику.


Заключение


Мы следовали всем трем озвученным принципам, чтобы создать декларативную библиотеку для использования IntersectionObserver в виде Observable. С ней можно работать всеми удобными способами благодаря DI и токенам. Она весит 1 КБ в .gzip и доступна на Github и npm.


Активное применение наследования, конечно, решение на любителя. Но мне кажется, тут смотрится вполне аккуратно. Работу полифиллов оно не нарушает, в чем можно убедиться, открыв демо в Internet Explorer.


Надеюсь, эта статья была для вас полезна и поможет создавать качественные и красивые приложения. Мне эти принципы дают еще и удовольствие от работы, и мы продолжим переносить нативные API в Angular. Если вам хочется попробовать в своих приложениях что-то более экзотическое, например создать двухмерную игру на Canvas или виртуальный инструмент для игры на MIDI-клавиатуре, посмотрите все наши релизы.


Месяц назад я выступал на GDG DevParty Russia, рассказывая про использование нативных браузерных API в Angular. Если вам понравилась эта статья и хотелось бы увидеть больше примеров, приглашаю посмотреть запись:


Подробнее..

Примеры использования наблюдателей в JavaScript

29.10.2020 08:07:06 | Автор: admin


Наблюдатель (observer) это объект, который следит за состоянием определенного элемента и регистрирует происходящие в нем изменения. Элемент, который находится под наблюдением (чуть не написал за которым организована слежка), называется целевым. Наблюдатель может следить за состоянием как одного, так и нескольких элементов, а в некоторых случаях также и за потомками целевого элемента.

В JavaScript существует три основных вида наблюдателей:

  1. ResizeObserver
  2. IntersectionObserver
  3. MutationObserver

В данной статье я предлагаю сосредоточиться на практической реализации каждого наблюдателя.

Resize Observer


Назначение

Наблюдение за изменением размеров целевого элемента.

Теория

MDN
Моя статья на Хабре

Поддержка



Пример

В следующем примере мы наблюдаем за шириной контейнера с идентификатором box. При ширине контейнера, меньшей 768px, мы меняем цвет фона контейнера и цвет текста (на противоположные с помощью filter: invert(100%)), уменьшаем размер шрифта заголовка и основного текста, а также скрываем дополнительную информацию.

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

<div id="box" class="box">  <h1 id="title" class="title">Some Awesome Title</h1>  <p id="text" class="text">Some Main Text</p>  <span id="info" class="info">Some Secondary Info</span></div>

Стили:

* {  margin: 0;  padding: 0;  box-sizing: border-box;}.box,.title,.text,.info {  transition: 0.3s;}.box {  background: #ddd;  color: #222;  min-height: 100vh;  display: flex;  flex-direction: column;  justify-content: space-between;  align-items: center;}.title,.info {  margin: 1rem;}.title {  font-size: 2rem;}.text {  font-size: 1.25rem;}

Скрипт:

// вспомогательная функция для изменения стилейconst changeStyles = (elements, properties, values) =>  elements.forEach((element, index) => {    element.style[properties[index]] = values[index];  });// создаем экземпляр ResizeObserverconst observer = new ResizeObserver((entries) => {  // при каждом изменении размеров (ширины и высоты)  for (const entry of entries) {    // получаем ширину целевого элемента    const width = entry.contentRect.width;    // осуществляем проверку    // если ширина целевого элемента меньше 768px    // меняем соответствующие стили    if (width < 768) {      changeStyles(        [box, title, text, info],        ["filter", "fontSize", "fontSize", "opacity"],        ["invert(100%)", "1.5rem", "1rem", "0"]      );    } else {      // если ширина целевого элемента больше 768px      // восстанавливаем стили      changeStyles(        [box, title, text, info],        ["filter", "fontSize", "fontSize", "opacity"],        ["invert(0%)", "2rem", "1.25rem", "1"]      );    }  }});// устанавливаем наблюдение за контейнером с идентификатором "box"observer.observe(box);

Песочница:


IntersectionObserver


Назначение

Наблюдение за пересечением целевого элемента с вышестоящим элементом или областью просмотра страницы (viewport).

Теория

MDN
Моя статья на Хабре

Поддержка



Пример

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

Разметка:
<main id="main">  <section id="1" class="section">    <h3 class="title">First Section Title</h3>    <p class="text">      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam nostrum ex delectus distinctio reprehenderit facere vitae beatae ab dolores, aliquam maiores officia mollitia unde et! Quaerat odit in minus dolor corrupti nemo nam beatae. Ex consequatur rem laborum necessitatibus omnis, soluta fuga maiores repellendus eveniet? Blanditiis quae officiis maiores vitae nobis in voluptate, dicta voluptas rerum. Et laudantium consequuntur vitae tenetur doloremque accusantium tempora quos magni repudiandae voluptatem perferendis velit reprehenderit laborum libero soluta quis id, quidem assumenda nihil obcaecati expedita, aliquam suscipit nesciunt facere. Voluptate rem perferendis ab iste? Maxime, earum quos! Modi, aut quis nihil quidem accusamus vero sunt debitis architecto soluta repellendus fugit suscipit aspernatur labore a est sit dolores in necessitatibus ea tenetur corporis. Exercitationem mollitia impedit qui nemo voluptate numquam perspiciatis repellendus repellat a odio fugit dolor ducimus labore ex veritatis pariatur aliquam enim distinctio libero doloremque saepe quaerat consectetur, ut sapiente. Laboriosam dignissimos iure praesentium modi ab perferendis at molestias maiores suscipit, expedita aut aperiam nam voluptates similique optio minus quam! Voluptas ullam sunt, a officiis accusamus adipisci sed saepe voluptatem minima maxime est assumenda cum quibusdam voluptates provident in quasi vitae. Corrupti voluptatibus laborum ipsum quia, cupiditate adipisci assumenda dolores sunt distinctio, recusandae nesciunt aliquid, explicabo ullam eligendi perspiciatis rerum architecto? Cumque numquam blanditiis, magnam delectus velit laudantium aliquid quibusdam excepturi vero nihil necessitatibus, sed officiis, molestias hic autem modi consequuntur iusto sapiente dolore. Voluptates tenetur provident eius distinctio iure rerum minima eum eaque. Ea autem, deleniti atque magnam eius modi dicta assumenda tempore ducimus molestias. Aperiam enim tenetur, hic blanditiis velit quod odio deserunt sequi quisquam dignissimos animi amet magnam excepturi dicta quidem error quis officia natus. Temporibus nobis dolores veritatis eius illo quas perspiciatis reiciendis dolorum optio, commodi, animi quos at! Amet praesentium totam ab error esse optio quo, quis iusto!    </p>  </section>  <section id="2" class="section">    <h3 class="title">Second Section Title</h3>    <p class="text">      ...    </p>  </section>  <section id="3" class="section">    <h3 class="title">Third Section Title</h3>    <p class="text">      ...    </p>  </section></main>


Стили:

* {  margin: 0;  padding: 0;  box-sizing: border-box;}body {  background: #eee;  color: #222;  text-align: center;}main {  max-width: 768px;  margin: auto;}.section {  padding: 1rem;}.title {  font-size: 1.5rem;  margin: 1rem;}.text {  font-size: 1.25rem;}

Скрипт:

// функция нахождения последней секции, которую просматривал пользовательconst findLastSection = () => {  // получаем номер секции из локального хранилища  // номер секции - это ее идентификатор  const number = localStorage.getItem("numberOfSection") || 1;  // находим нужную секцию  const section = document.getElementById(number);  // определяем отступ от верхнего края всей страницы (не области просмотра)  const position = Math.round(section.offsetTop);  // прокручиваем область просмотра до нужной позиции  scrollTo({    top: position,    // плавно    behavior: "smooth",  });};findLastSection();// функция создания наблюдателя и установки наблюденияconst createObserver = () => {  // создаем экземпляр IntersectionObserver  const observer = new IntersectionObserver(    (entries) => {      entries.forEach((entry) => {        // если целевой элемент находится в зоне видимости        if (entry.isIntersecting) {          // записываем его идентификатор в локальное хранилище          localStorage.setItem("numberOfSection", entry.target.id);        }      });    },    {      // процент пересечения целевого элемента с областью просмотра      // 10%      threshold: 0.1,    }  );  // находим все секции  const sections = main.querySelectorAll("section");  // начинаем за ними наблюдать  sections.forEach((section) => observer.observe(section));};createObserver();

Песочница:


MutationObserver


Назначение

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

Теория

MDN
Современный учебник по JavaScript

Поддержка



Пример

В следующем примере мы реализуем простую тудушку, в которой наблюдатель следит за количеством задач в списке. По своему функционалу наш наблюдатель будет поход на useEffect в React.js или watch в Vue.js.

Разметка:

<div id="box" class="box"></div>

Стили:
@import url("https://fonts.googleapis.com/css2?family=Stylish&display=swap");* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: stylish;  font-size: 1rem;  color: #222;}.box {  max-width: 512px;  margin: auto;  text-align: center;}.counter {  font-size: 2.25rem;  margin: 0.75rem;}.form {  display: flex;  margin-bottom: 0.25rem;}.input {  flex-grow: 1;  border: none;  border-radius: 4px;  box-shadow: 0 0 1px inset #222;  text-align: center;  font-size: 1.15rem;  margin: 0.5rem 0.25rem;}.input:focus {  outline-color: #5bc0de;}.btn {  border: none;  outline: none;  background: #337ab7;  padding: 0.5rem 1rem;  border-radius: 4px;  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);  color: #eee;  margin: 0.5rem 0.25rem;  cursor: pointer;  user-select: none;  width: 92px;  text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);}.btn:active {  box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;}.btn.danger {  background: #d9534f;}.list {  list-style: none;}.item {  display: flex;  flex-wrap: wrap;  justify-content: space-between;  align-items: center;}.item + .item {  border-top: 1px dashed rgba(0, 0, 0, 0.5);}.text {  flex: 1;  font-size: 1.15rem;  margin: 0.5rem;  padding: 0.5rem;  background: #eee;  border-radius: 4px;}


Скрипт:

// задачиconst todos = [  {    id: "1",    text: "Learn HTML",  },  {    id: "2",    text: "Learn CSS",  },  {    id: "3",    text: "Learn JavaScript",  },  {    id: "4",    text: "Stay alive",  },];// шаблон элемента спискаconst Item = (todo) => `<li  class="item"  id="${todo.id}">  <span class="text"}">    ${todo.text}  </span>  <button    class="btn danger"    data-type="delete"  >    Delete  </button></li>`;// общий шаблонconst Template = `<form id="form" class="form">    <input      type="text"      class="input"      id="input"    >    <button      class="btn"      data-type="add"    >      Add    </button></form><ul id="list" class="list">    ${todos.reduce(      (html, todo) =>        (html += `          ${Item(todo)}        `),      ""    )}</ul>`;// оборачиваем код в IIFE(() => {  // вставляем шаблон в контейнер с идентификатором "box"  box.innerHTML = `  <h1 id="counter" class="counter">    ${todos.length} todo(s) left  </h1>  ${Template}`;  // фокусируемся на поле для ввода текста  input.focus();  // создаем экземпляр MutationObserver  const observer = new MutationObserver(() => {    // получаем количество задач в списке    const count = todos.length;    // сообщаем либо о том, сколько задач осталось, либо о том, что задач не осталось    counter.textContent = `    ${count > 0 ? `${count} todo(s) left` : "There are no todos"}  `;  });  // начинаем наблюдать за списком и его дочерними элементами  observer.observe(list, {    childList: true,  });  // функция добавления новой задачи в список  const addTodo = () => {    if (!input.value.trim()) return;    const todo = {      id: Date.now().toString().slice(-4),      text: input.value,    };    list.insertAdjacentHTML("beforeend", Item(todo));    todos.push(todo);    input.value = "";    input.focus();  };  // функция удаления задачи из списка  const deleteTodo = (item) => {    const index = todos.findIndex((todo) => todo.id === item.id);    item.remove();    todos.splice(index, 1);  };  // отключаем обработку отправки формы браузером  form.onsubmit = (e) => e.preventDefault();  // обрабатываем нажатие кнопок  box.addEventListener("click", (e) => {    if (e.target.tagName !== "BUTTON") return;    // получаем тип кнопки и элемент списка    const { type } = e.target.dataset;    const item = e.target.parentElement;    // в зависимости от типа кнопки вызываем ту или иную функцию    switch (type) {      case "add":        addTodo();        break;      default:        deleteTodo(item);        break;    }  });})();

Песочница:


Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru