[Angular, JavaScript, Open source, TypeScript] Как писать хорошие библиотеки под Angular
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Веб — наполненная фичами среда. Мы можем перемещаться по виртуальной реальности с помощью геймпада, играть на синтезаторе с MIDI-клавиатуры, покупать товары одним касанием пальца. Все эти впечатляющие возможности предоставляют нативные API, которые, как и их функциональность, крайне многообразны.
Angular — превосходная платформа с одними из лучших инструментов во фронтэнд-среде. И, конечно, есть определенный способ делать «по-ангуляровски». Что лично мне особенно нравится в этом фреймворке — это то чувство удовлетворенности, которое испытываешь, когда все сделано как надо: аккуратный код, четкая архитектура. Давайте разберемся, что делает код правильно написанным под Angular.
The Angular way
Я уже давно пишу на Angular, перенимая опыт у отличных инженеров, с которыми работаю, и черпая знания из обширной базы, доступной в сети. Некоторое время назад я заметил, что, хотя браузеры предоставляют огромные возможности, мало что из этого включено в Angular «из коробки». Так и задумано: ведь это просто платформа для создания своих продуктов и нужно заточить ее под свои нужды. Поэтому мы создали opensource-инициативу Web APIs for Angular. Ее цель — создание легковесных, качественных и идиоматических оберток для использования нативных API в Angular. Я бы хотел обсудить принципы написания хорошего код на примере библиотеки @ng-web-apis/intersection-observer.
По моему мнению, эти три концепции играют основную роль:
- Angular декларативен по природе, в то время как нативный и традиционный JavaScript-код зачастую императивный.
- У Angular крутая система внедрения зависимостей, которую можно активно использовать себе во благо.
- 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. Если вам понравилась эта статья и хотелось бы увидеть больше примеров, приглашаю посмотреть запись:
https://www.youtube.com/embed/G4qftFVsJj8
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Big Data, Хранение данных, Data Engineering] Apache Airflow: делаем ETL проще
- [Open source, OpenStreetMap, Визуализация данных, Научно-популярное, Программирование] Делаем маршрутизацию (роутинг) на OpenStreetMap. Добавляем поддержку односторонних дорог
- [*nix, Open source] FOSS News №26 – обзор новостей свободного и открытого ПО за 20–26 июля 2020 года
- [JavaScript] Формульный движок с обратной польской нотацией на JavaScript
- [DevOps, Go, Open source, Системное администрирование, Тестирование веб-сервисов] Squzy — бесплатная open-source self-host система мониторинга с инцидентами и уведомлениями
- [JavaScript, Node.JS, Программирование, Разработка веб-сайтов] Руководство по Deno: примеры работы со средой выполнения TypeScript (перевод)
- [DIY или Сделай сам, Open source] Openwrt сниффер витой пары
- [Open source, Работа с 3D-графикой, Софт] Новости Blender3d
- [JavaScript, Дизайн, Дизайн мобильных приложений, Платежные системы, Разработка под e-commerce] BinKing готов
- [CSS, HTML, JavaScript, Программирование, Разработка веб-сайтов] Как стать Front-End разработчиком
Теги для поиска: #_angular, #_javascript, #_open_source, #_typescript, #_angular, #_intersection_observer_api, #_intersectionobserver, #_blog_kompanii_tinkoff (
Блог компании Tinkoff
), #_angular, #_javascript, #_open_source, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:13
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Веб — наполненная фичами среда. Мы можем перемещаться по виртуальной реальности с помощью геймпада, играть на синтезаторе с MIDI-клавиатуры, покупать товары одним касанием пальца. Все эти впечатляющие возможности предоставляют нативные API, которые, как и их функциональность, крайне многообразны. Angular — превосходная платформа с одними из лучших инструментов во фронтэнд-среде. И, конечно, есть определенный способ делать «по-ангуляровски». Что лично мне особенно нравится в этом фреймворке — это то чувство удовлетворенности, которое испытываешь, когда все сделано как надо: аккуратный код, четкая архитектура. Давайте разберемся, что делает код правильно написанным под Angular. The Angular way Я уже давно пишу на Angular, перенимая опыт у отличных инженеров, с которыми работаю, и черпая знания из обширной базы, доступной в сети. Некоторое время назад я заметил, что, хотя браузеры предоставляют огромные возможности, мало что из этого включено в Angular «из коробки». Так и задумано: ведь это просто платформа для создания своих продуктов и нужно заточить ее под свои нужды. Поэтому мы создали opensource-инициативу Web APIs for Angular. Ее цель — создание легковесных, качественных и идиоматических оберток для использования нативных API в Angular. Я бы хотел обсудить принципы написания хорошего код на примере библиотеки @ng-web-apis/intersection-observer. По моему мнению, эти три концепции играют основную роль:
Давайте пройдемся по этим пунктам подробнее. Декларативный 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. Я очень советую прочитать ее, так как для подробного разбора этого аспекта тут просто слишком мало кода.
Первая директива может наследоваться непосредственно от 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, где подробно разобрали его.
@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. Если вам понравилась эта статья и хотелось бы увидеть больше примеров, приглашаю посмотреть запись: https://www.youtube.com/embed/G4qftFVsJj8 =========== Источник: habr.com =========== Похожие новости:
Блог компании Tinkoff ), #_angular, #_javascript, #_open_source, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:13
Часовой пояс: UTC + 5