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

Angular

Дайджест свежих материалов из мира фронтенда за последнюю неделю 464 (19 25 апреля 2021)

26.04.2021 00:06:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры|Занимательное|


Медиа


podcast Callback Hell записи аудио-стримов о технологиях и не только от CSSSR
podcast Подкаст Фронтенд Юность #182: Не нужно платить разработчикам 200-300к
podcast Новости 512 от CSSSR: релиз Node.js 16 и Firefox 88, проектирование приложения с TypeScript и ООП, взгляд на Container Query, доклады с Я.Субботника.
podcast Новости 512 от CSSSR: Микрофронтенды в Delivery Club, JS-классы, состояние JS-фреймворков и стейт-менеджеров, минусы Dart
video IT-дебаты: JavaScript-программист vs фронтенд-разработчик
habr video Frontend Meetup 20/04
video Я.Субботник по разработке интерфейсов 2021


Веб-разработка


habr CORS для чайников: история возникновения, как устроен и оптимальные методы работы
habr Как создают и поддерживают веб-страницы tinkoff.ru
habr Трёхпроходный алгоритм рефакторинга Front End
en Что нового в DevTools (Chrome 91)
en Независимые компоненты: новые строительные блоки для веба
en Как я создал свой блог



CSS


habr Полное руководство по CSS Flex + опыт использования
CSS-нестинг больше, чем сахар
en Руководство по новым современным псевдо-селекторам CSS
en Создание (и потенциальные преимущества) CSS-шрифта
en Как добавить двойную границу к SVGShapes
en Начало работы с CSS Custom Properties
en TailwindCSS: добавляет сложности, ничего не делает.
en Работа на ошибками гибкой типографии, базирующихся на вьюпорте в Safari


JavaScript


habr Основы управления памятью в JavaScript: как это работает и какие проблемы могут возникнуть
Выпуск серверной JavaScript-платформы Node.js 16.0
en Чудесный мир Javascript бандлеров
en Улучшите управление состоянием в вашем фронтенде с помощью view models
en Шаблон для свойства отложенной загрузки в JavaScript
en Полное руководство по инкрементной статической регенерации (ISR) с Next.js
en Топ-5 самых популярных вопросов о JavaScript на Stack Overflow
en Руководство по MobX
en Понимание Array Reduce в JavaScript





Браузеры


В Firefox 88 молча удалён пункт контекстного меню Page Info
Apple, Microsoft, Opera и другие разработчики не горят желанием поддерживать технологию Google FLoC
Релиз Firefox 88
В Microsoft Edge тестируется новый режим производительности с иным принципом работы спящих вкладок


Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 465 (26 2 мая 2021)

02.05.2021 22:08:33 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст proConf #94: GraphQL Galaxy
podcast Новости 512 от CSSSR: Chrome 91 Beta, postcss-easy-z, tree-shakeable библиотеки, гайды по кастомным CSS-свойствам и CLS
podcast Подкаст Callback Hell: Падение последнего оплота Dart, визуальные ЯП на примере Enso, Lucy DSL для стейт-машин
podcast Подкаст Фронтенд Юность#184: Матрица для мешка с картошкой


Веб-разработка


История фронтенда: JavaScript как отражение новой эпохи
habr HTMHell адовая разметка
habr HTML-теги и атрибуты, о которых вы, возможно, не знали
habr Адаптивный дизайн как антипаттерн
en Скромный элемент img и Core Web Vitals
en Как реализовать выбор дейстий для выделенного текста с помощью SelectionAPI




CSS


habr Примеры применения переменных CSS на практике
habr Контейнерные запросы в CSS
habr Как обеспечить глассморфизм с помощью HTML и CSS
VDS (value definition syntax)
en fit-content и fit-content()
en Полное руководство по Custom Properties
en Первый взгляд на CQFill, полифилл для CSS Container Queries
en Изучение color-contrast() в первый раз
en GPT-3 и CSS-фреймворки
en Понимание easing-функций для анимации и переходов в CSS
en Новые возможности WebKit в Safari 14.1 (Flexbox Gap, Date & Time Inputs, CSS Individual Transform Properties)

JavaScript


habr Целительная сила JavaScript
habr Человеко-читаемый JavaScript: история о двух экспертах
Принцип мозаики, или Как мы сделали JavaScript по-настоящему модульным
en Fower утилитарная CSS in JS библиотека для быстрой разработки интерфейсов.
en Клиентские шаблоны API, о которых должен знать каждый разработчик фронтенда








Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 466 (3 9 мая 2021)

10.05.2021 00:07:48 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст 'Callback Hell': Убийцы вебпака, Microsoft + Bytecode Alliance, удалёнка
podcast Новости 512 от CSSSR: Bootstrap 5, V8 9.1, дженерики в TypeScript, RxJS в Angular, e2e-тесты с Cypress, баг в Safari 14.1
podcast Новости 512 от CSSSR: История фронтенда ч.2, Safari 14.1, CORS, Cookie Store API, Next.js 10.2, RxJS 7, Google I/O 2021
podcast Подкаст Веб-стандарты 280. Safari 14.1, гэпы во флексах, история JS, мозаичный JS, кому нужны алгоритмы
video Видеокаст Front-end. Вопросы на собеседовании #1
podcast video Подкаст Pro Conf #95: HollyJS Moscow 2020

Веб-разработка


habr Вышел Bootstrap 5: оцениваем 7 главных нововведений
habr Почему стоит использовать тег <picture> вместо <img>
habr Базовая структура HTML-документа с объяснением каждой строчки
habr HTML трюки
en Эволюция Jamstack
en Как мы используем веб-компоненты на GitHub
en Аудит дизайн-систем на предмет доступности
en Ускорение процесса разработки с помощью Bootstrap 5
en Как мы ускорили трассировку стека Chrome DevTools в 10 раз




CSS


en Состояние кроссбраузерной разработки CSS
en Container Queries: разъяснения и предложения
en Два варианта использования кастомных свойств
en Полное руководство по веб-шрифтам в шаблонах писем
en Является ли CSS языком программирования?
en CSS Hell Сборник распространенных ошибок в CSS и способы их исправления
en Текст размером 16 пикселей или больше предотвращает масштабирование формы в iOS
en Fluid typography Создавайте текст, масштабируемый в соответствии с размером окна, чтобы заголовки отлично смотрелись на любом экране.
en Вендорные префиксы мертвы?
en Компиляция CSS по запросу с помощью последней версии компилятора Tailwind


JavaScript


habr Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender
Кастомные типы данных в TypeScript: валидация на этапе компиляции
en Возможны ли 0kb JavaScript в вашем будущем?
en Vue Composition API против React Hooks основная разница
en Создайте трекер спутников с нуля 30-ю строками JavaScript кода







Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 467 (10 16 мая 2021)

17.05.2021 00:12:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты 281. SpiderMonkey 25 лет, Safari TP, Bootstrap 5, Гитхаб, префиксы, монорепы и свой git в Яндексе
podcast Подкаст Фронтенд Юность #186. Утюжить веб. В гостях создатель и главный редактор Smashing Magazine Виталий Фридман.
video Видеокаст Front-end. Вопросы на собеседовании #2
video Нужен ли джуну идеальный код: интервью с Вадимом Макеевым
podcast video Подкаст Да как так-то?. Выпуск 4: из филолога-япониста во фронтенд на фрилансе

Веб-разработка


Солидные фронтенды: мониторинг
en Регистрация обработчика протокола URL для PWA
en Различия между WebSockets и Socket.IO
habr Переход к Meta GSAP: поиски идеальной бесконечной прокрутки




CSS


habr Выявление устройств с сенсорными экранами на чистом CSS
habr Венец эволюции CSS-in-JS уже здесь: полностью типизированные стили без рантайма в vanilla-extract
habr Сравнение производительности CSS и CSS-in-JS в реальном мире
habr Инструменты для аудита CSS
Родительский селектор :has() в реальность!


en Дизайн для чтения: советы по оптимизации контента для режимов чтения и приложений-читалок
en Продвинутая CSS-анимация с использованием cubic-bezier()
en aspect-ratio и grid
en Создание Stylesheet Feature Flags с помощью Sass!default
en Плавная прокрутка Sticky ScrollSpy Navigation с фиксированным фоном на CSS
en Взгляд на CSS Tailwind

JavaScript


habr Отслеживание и визуализация положения МКС с помощью 30 строк JavaScript-кода
habr Шпаргалка по JS-методам для работы с DOM
habr Паттерны отложенной инициализации свойств объектов в JavaScript
habr Я выпустил Grafar JS-библиотеку для визуализации
en 7 шагов для безопасного JavaScript в 2021 году
en Современный Javascript: все, что вы пропустили за последние 10 лет (ECMAScript 2020)
en Создайте тетрис с помощью современного JavaScript








Браузеры


en Использование обработчиков пользовательских протоколов для кросс-браузерного отслеживания в Tor, Safari, Chrome и Firefox
Идентификация через анализ внешних обработчиков протоколов в браузере

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 468 (17 23 мая 2021)

24.05.2021 00:09:13 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты #282: Rome, CloudFront Functions, кроссбраузерность, has() и другой современный CSS, мониторинг, GDE
podcast Подкаст Фронтенд Юность #187: Bootstrap круче чем все сраные фреймворки
podcast Подкаст Callback Hell: Производительность CSS-in-JS, языки логического программирования, ООП в современном фронтенде
podcast Новости 512 от CSSSR: Angular 12, Deno 1.10, мониторинг, тестирование UI, :has(), курс по git, Rome + $, TypeScript 4.3 RC
podcast Подкаст Callback Hell Поддержка нескольких мажорных версий, венчурный капитал в Open Source и возвращение тонкого клиента
podcast Подкаст proConf #96: DeveloperWeek 2020
podcast video Подкаст Цинковый Прод #113: Сайт сына маминой подруги

Веб-разработка


W3C представил черновой вариант стандарта WebGPU
en Google AMP мертв! AMP-страницы больше не пользуются приоритетом в поиске Google
en Incremental Static Regeneration: создавайте статические сайты понемногу
en Тестирование фронтенд-приложений что, где, как?





CSS


habr Трюки CSS, которые сделают из вас ниндзя верстки
habr Взгляд на Tailwind CSS
en Новая отзывчивость: веб-дизайн в мире компонентов
en Нет, утилитарные классы это не то же самое, что инлайн стили
en Как создать неоновый текст с помощью CSS
en Как стилизовать любое поле ввода советы и методы
en 82% разработчиков неправильно проходят этот трехстрочный тест по CSS
en Learn CSS Постоянно обновляемый курс CSS и справочник для повышения вашего уровня знаний в области стилизации веба
en aspect-ratio

JavaScript


habr Швейцарский нож отладки JavaScript
habr Трасси что? Доклад Яндекса
en DOM Events изучение системы событий DOM с помощью визуального исследования
en ES12 сделает вашу жизнь проще
en Справочник по массивам JavaScript методы работы с JS-массивами с примерами
en Двухмерные оптические демки в Javascript
en JavaScript API для распознавания людей и ботов в Chrome







Браузеры


habr Microsoft прекратит поддержку приложения Internet Explorer 11 в Windows 10 с июня 2022 года
habr Кросс-браузерный трекинг на основе перебора обработчика внешних протоколов
В Chrome экспериментируют с поддержкой RSS, чисткой User-Agent и автосменой паролей
Компания Mozilla представила режим строгой изоляции сайтов для Firefox
Выпуск перенастраиваемого web-браузера Nyxt 2.0.0

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 469 (24 30 мая 2021)

31.05.2021 00:23:47 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Подкаст Веб-стандарты 83. Sublime Text 4, Sass, Svelte после React, Container Queries, Learn CSS, Google I/O, новые GDE
podcast Подкаст proConf #97: JavaScript for WordPress 2020
podcast Подкаст Callback Hell: Sublime Text 4 и другие редакторы, проблемы написания читаемого кода, завершение эпохи IE
podcast Новости 512 от CSSSR: Chrome 91, TypeScript 4.3, Server-Sent Events API, logux и logux/state, postTask, Parcel 2 beta 3
podcast Новости 512 от CSSSR: Sublime Text 4, PostCSS 8.3, ненадежность TypeScript, Angular DevTools, WebContainers, Google I/O 21
podcast Пилотный выпуск подкаста Goose & Duck: Babel, деньги, два гуся

Веб-разработка


habr Самая серьёзная проблема HTML? Разработчики, разработчики, разработчики
habr Использование веб-компонентов при работе над GitHub
habr Наиболее полное руководство по практическому использованию Web Speech API
en Эволюция и новое определение Jamstack
en 10 вариантов клиентских хранилищ и когда их использовать
en Нарушаете ли вы патент, публикуя PWA?
en Создайте эффект плавного наведения с помощью GSAP и SVG



CSS


habr HTML и CSS ошибки, ухудшающие UX
en Тщательный анализ CSS-in-JS
en CSS Container Queries для дизайнеров
en 25 лет CSS
en CSS Container Queries: примеры использования и стратегии миграции
en Новый способ уменьшить влияние загрузки шрифтов: дескрипторы шрифтов в CSS

JavaScript


habr Карманная книга по TypeScript. Часть 1. Основы, Часть 2. Типы на каждый день
habr 3 способа визуального извлечения данных с помощью JavaScript
en Sparkplug неоптимизирующий компилятор JavaScript
en Новые стандарты доступа к оборудованию устройств с использованием JavaScript
en 7 инструментов, трансформирующих JavaScript-разработку
en Введение в Clio lang: несложная реализация производительного critical js






Браузеры


habr Mozilla примет Manifest v3 для дополнений Firefox, но без мер против блокировщиков рекламы
Релиз Chrome 91
en Призрак Google Reader находит свой путь в новой сборке Chrome Canary

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 470 (1 6 июня 2021)

07.06.2021 00:10:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast video Подкаст Goose&Duck #1 Ржавеющий JavaScript
podcast CSSSR Callback Hell: Rescript, мысли пьяного Senior-разработчика, слежка за сотрудниками
podcast Новости 512 от CSSSR: Server-Sent Events: ограничения, поддержка Node.js-проектов, плагины для VSCode, 12 лет Node.js
podcast Подкаст Фронтенд Юность #189 Рон-дом-дом

Веб-разработка


habr С помощью перехода на микросервис мы ускорили бизнес-процесс в 60 раз
en Создание нескольких прогрессивных веб-приложений в одном домене
en Тестирование фронтенда для всех
en Разрушение мифов: Jamstack не может обрабатывать динамический контент
en История веба: часть 1
en Некоторые из лучших пасхальных яиц, спрятанных на сайтах в Интернете





CSS


habr 25 лет CSS
Нативная валидация ввода в CSS
en CSS in SVG in CSS: добавление конфетти в дизайн-систему Stack Overflow
en Новые функциональные селекторы псевдоклассов CSS: is() и: where()
en Тригонометрия в CSS и JavaScript: Введение в тригонометрию
en Тригонометрия в CSS и JavaScript: творческий подход с помощью тригонометрических функций
en The CSS Layout Generator визуальный инструмент для создания компонентов лейаута на CSS Grid
en Inherit, initial, unset, revert
en Шестиугольники и не только: гибкие, отзывчивые сеточные шаблоны, без медиа-запросов

JavaScript


habr Управление зависимостями в Node.js
habr Как мы потерпели неудачу, а затем преуспели в переходе на TypeScript
habr Создание нейронной сети Хопфилда на JavaScript
ES12 сделает вашу жизнь проще!
en Обеспечение быстрой работы JavaScript в WebAssembly
en Еще одна альтернатива Javascript: ReScript
en Взгляд на компиляцию в JavaScript-фреймворках






Браузеры


habr Firefox 89 обновил интерфейс браузера
Релиз Firefox 89 с переработанным интерфейсом
Mozilla, Google, Apple и Microsoft объединили усилия в стандартизации платформы для браузерных дополнений
en Что нового в DevTools (Chrome 92)

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 472 (7 13 июня 2021)

14.06.2021 00:15:08 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: Firefox 89, Safari 15 Beta, Jest 27, цикл статей о работе браузера, разработка базовых компонентов, обзорная статья о тестировании фронтенда и анонс WebExtensions Community Group.
podcast Подкаст Веб-стандарты #285: Бета Chrome92, Firefox89, якоря ирасширения, TeamCity, JSвнутри WASM, TypeScript4.3
podcast Подкаст Фронтенд Юность #190: Как подступиться к старому проекту и не сесть на кулак
podcast Новости 512 от CSSSR: React 18, Vue 3.1, анонс ESLint 8, курсы от CSSSR, :is(), where() и :has(), как прилёг Интернет
podcast Подкаст Callback Hell: Сервисы Google с плохими Web Vitals, шеринг логики между фронтом и бэком, документация на проектах


Веб-разработка


habr Будущее веба: станет ли рендеринг в <canvas> заменой DOM?
en Правильный тег для работы: почему следует использовать семантический HTML
en 5 проблем фронтенда, которые нельзя игнорировать





CSS


habr Выкладка нетрадиционной ориентации
en Полное руководство по CSS Grid с шпаргалкой
en Системные цвета CSS
en CSS определяет значения цвета, соответствующие системным настройкам.
en Media Queries во времена @container
en Давайте узнаем об Aspect Ratio в CSS
en CSS size-adjust для @font-face
en Равные столбцы с Flexbox: это сложнее, чем вы думаете
en Эксперимент с сортируемыми мультиколоночными таблицами
en Знакомьтесь с :has: нативный CSS селектор
en Рог изобилия ContainerQueries
en Создание правил для font-size CSS и создание Fluid Type Scale

JavaScript


habr Как я ускорил движок на 13%
habr Прогнозирование временных рядов на JS: анализ данных для самых маленьких фронтендеров
habr Sparkplug неоптимизирующий компилятор JavaScript в подробностях
en Как создать фулстек-приложение с помощью Supabase и Next.js
en Реализация приватных полей в JavaScript
en Forever Functional: Мемоизация промисов
en Как реализовать принципы SOLID в JavaScript
en Автоматизируйте форматирование и исправление JavaScript кода с помощью Prettier и ESLint
en Современный JavaScript
en Выходя за рамки ESLint: обзор статического анализа в JavaScript
en Доберенные типы API для безопасности JavaScript DOM
en Как создать NFT с помощью JavaScript
en Rust с точки зрения JavaScript





Браузеры


habr Vivaldi 4.0 Первое приближение
Google признал неудачным эксперимент с показом только домена в адресной строке Chrome
en Возможности WebKit в Safari, продемонстрированные на WWDC21


Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 473 (14 20 июня 2021)

21.06.2021 00:15:47 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст Веб-стандарты 286: Высокопроизводительное хранилище для вашего приложения: Storage Foundation API
podcast Подкаст Callback Hell: Микрофронтенды и Module Federation, почему компании боятся открывать свой код, игровая выставка E3
podcast Новости 512 от CSSSR: Canvas-рендеринг, Lighthouse 8, пропорции в CSS, PHP 8.1 alpha, Next.js 11, Линус и антипрививочник
podcast video Подкаст Ленивый фронтендер #2 Kaiwa Show | Как сохранить любовь к веб-разработке
podcast Подкаст Фронтенд Юность #191: HR'ы немножко осатанели


Веб-разработка


habr <img>. Доклад Яндекса
habr Темизация. История, причины, реализация
habr DIV должен уйти: улучшаем HTML
en Изучение Eleventy с нуля. Бесплатный курс, состоящий из 31 урока
en Как я использовал WAAPI для создания библиотеки анимации
en Десять лет веб-компонентам



CSS


video :has в CSS псевдокласс из будущего на примере карточки новости
en Использование свойства `outline` в качестве схлопывающейся границы
en Идеальные всплывающие подсказки с обрезкой и маскированием CSS
en Оптический размер, скрытая сверхспособность вариативных шрифтов
en Краткое руководство по логическим свойствам CSS
en Застенчивая кнопка стоимостью 8 миллионов долларов
en Создание таблиц с липким верхним и нижним колонтитулами стало немного проще

JavaScript


habr Скрываем номера курьеров и клиентов с помощью key-value хранилища
habr Юмористичный обзор Rust с перспективы JavaScript
en Управление состоянием: двусторонние биндинги и расширенные средства форматирования биндингов
en Что такое букмарклеты? Как использовать JavaScript для создания букмарклета в Chromium и Firefox
en Тестирование использования памяти в JavaScript
en Двойные кавычки против одинарных кавычек против обратных кавычек в JavaScript
en sorting-algos-visualizer Визуализация популярных алгоритмов сортировки: QuickSort, MergeSort, HeapSort, BubbleSort, InsertionSort







Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Только 39 функций в node_modules уникальны в дефолтном Angular проекте

30.04.2021 10:21:30 | Автор: admin

39% это количество уникальных функций в папке node_modules в дефолтном Angular проекте, созданном командой ng new my-app.


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



Как сравнить функции в Javascript?


Если вы захотите сравнить пару функци в javascript, то сделать это проще простого, после пребразования их в строковый вид:


const a = () => 'hi';a.toString(); // "() => 'hi'"

А если названия переменных разные?


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


Как же извлечь функции из Javascript файла?


Для этого я воспользовался библиотекой Esprima. То есть парсим файл и обходим AST дерево. Думаю что можно было бы обойтись только Uglifyjs, но что-то у меня сходу не завелось, а разбираться было лень.


В общем план действий я нарисовал следующий


  1. Перебираем все *.js файлы в директории
  2. Парсим каждый файл и извлекаем функции типов ArrowFunctionExpression, FunctionExpression и FunctionDeclaration
  3. Ужимаем каждую из функций при помощи UglifyJs и записываем её в файл, имя которого это хеш функции
  4. В отдельный файл info.csv записываем строку с id записи, названием файла, из которого была извлечена функция и хешем функции
  5. Загружаем файл info.csv в SQLite и делаем по нему всякие запросы, ведь эта база не игрушка!

Детали реализации


  • Всем стрелочным функциям и фунциональным выражениям я дал название z;
  • Обычные функции я переименовал в MORK, но сохранил отдельно названия функций для учета, потому что функция может быть той же самой, но называться по-другому;
  • Возможно с этими переименованиями я потерял часть статистики, связанной с рекурсивными функциями, ну и ладно!

Пример извлечения функций из файла


Код javascript файла, и которого будем извлекать функции:


(function () {    const arbuz = (test) => {        function apple(t) {            function test () {                return 'ttt';            }            return t + 3;        }        const aa = 1;        const b1 = () => 2;        // comment        return aa + b1() + apple(test);    }    return arbuz; })();

Обратите внимание, что некоторые выражения в извлеченных функциях вычеслены, что поможет сравнению функций. Список извлеченных функций:


"const z=function(){return n=>{return 3+(n+3)}};";"const z=n=>{return 3+(n+3)};";"function MORK(n){return n+3}";"function MORK(){return"ttt"}";"const z=()=>2;";

Полный скрипт можно найти тут


Первый объект исследования: node_modules дефолтного проекта Angular 11


Итак, создаём проект при помощи @angular/cli: ng new my-app, запускаем скрипт на парсинг node_modules и оставляем его на ночь.


Посмотреть package.json
{  "name": "my-app",  "version": "0.0.0",  "scripts": {    "ng": "ng",    "start": "ng serve",    "build": "ng build --prod",    "test": "ng test",    "lint": "ng lint",    "e2e": "ng e2e"  },  "private": true,  "dependencies": {    "@angular/animations": "~11.2.10",    "@angular/common": "~11.2.10",    "@angular/compiler": "~11.2.10",    "@angular/core": "~11.2.10",    "@angular/forms": "~11.2.10",    "@angular/platform-browser": "~11.2.10",    "@angular/platform-browser-dynamic": "~11.2.10",    "@angular/router": "~11.2.10",    "rxjs": "~6.6.0",    "tslib": "^2.0.0",    "zone.js": "~0.11.3"  },  "devDependencies": {    "@angular-devkit/build-angular": "~0.1102.9",    "@angular/cli": "~11.2.9",    "@angular/compiler-cli": "~11.2.10",    "@types/jasmine": "~3.6.0",    "@types/node": "^12.11.1",    "codelyzer": "^6.0.0",    "jasmine-core": "~3.6.0",    "jasmine-spec-reporter": "~5.0.0",    "karma": "~6.1.0",    "karma-chrome-launcher": "~3.1.0",    "karma-coverage": "~2.0.3",    "karma-jasmine": "~4.0.0",    "karma-jasmine-html-reporter": "^1.5.0",    "protractor": "~7.0.0",    "ts-node": "~8.3.0",    "tslint": "~6.1.0",    "typescript": "~4.1.5"  }}

Полный package-lock.json тут


Результаты


В папке node_modules 26982 *.js файлов:


$ find . -name '*.js' | wc -l26982

А в них найдено 338230 функций:


sqlite> select count(*) from info;338230

Из которых 130886 уникальных:


sqlite> Select count(*) from (SELECT hash, count(id) as c FROM info group By hash);130886

То есть 130886/338230 * 100% =39% функций действительно уникальны, а остальные это дубликаты уже существующих.


Скачать csv файл для самостоятельной проверки можно тут.


Топ 20 самых популярных функций в node_modules для проекта Angular


То есть функции с самым большим количеством дубликатов.


SELECT hash, count(id) as c FROM info group By hash order by c desc LIMIT 20;

# id количество дубликатов
1 285d00ca29fcc46aa113c7aefc63827d 2730
2 cf6a0564f1128496d1e4706f302787d6 1871
3 12f746f2689073d5c949998e0216f68a 1174
4 7d1e7aad635be0f7382696c4f846beae 772
5 c2da306af9b041ba213e3b189699d45c 699
6 c41eb44114860f3aa1e9fa79c779e02f 697
7 5911b29c89fa44f28ce030aa5e433327 691
8 05c2b9b254be7e4b8460274c1353b5ad 653
9 fcaede1b9e574664c893e75ee7dc1d8b 652
10 e743dd760a03449be792c00e65154a48 635
11 777c390d3cc4663f8ebe4933e5c33e9d 441
12 27628ad740cff22386b0ff029e844e85 385
13 f6822db5c8812f4b09ab142afe908cda 375
14 d98a03a472615305b012eceb3e9947d5 330
15 4728096fca2b3575800dafbdebf4276a 324
16 7b769d3e4ba438fc53b42ad8bece86ba 289
17 7d6f69751712ef9fa94238b38120adc6 282
18 b7081aad7510b0993fcb57bfb95c5c2c 255
19 d665499155e104f749bf3a67caed576a 250
20 99fa7dfce87269a564fc848a7f7515b9 250

  1. 285d00ca29fcc46aa113c7aefc63827d, 2730 идентичных


    const z=function(){};
    

  2. cf6a0564f1128496d1e4706f302787d6, 1871 идентичных, названия функций одинаковые: __export


    function MORK(r){for(var o in r)exports.hasOwnProperty(o)||(exports[o]=r[o])}
    

  3. 12f746f2689073d5c949998e0216f68a, 1174 идентичных, названия функций: _interopRequireDefault и __importDefault


    function MORK(e){return e&&e.__esModule?e:{default:e}}
    

  4. 7d1e7aad635be0f7382696c4f846beae, 772 идентичных, у всех у них было примерно 300 уникальных названий


    function MORK(){}
    

  5. c2da306af9b041ba213e3b189699d45c, 699 идентичных


    const z=function(o,_){o.__proto__=_};
    

  6. c41eb44114860f3aa1e9fa79c779e02f, 697 идентичных, имя __


    function MORK(){this.constructor=d}
    

  7. 5911b29c89fa44f28ce030aa5e433327, 691 идентичная


    const z=function(n,o){for(var r in o)o.hasOwnProperty(r)&&(n[r]=o[r])};
    

  8. 05c2b9b254be7e4b8460274c1353b5ad, 653 идентичных


    const z=function(t,n){return extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,n){t.__proto__=n}||function(t,n){for(var o in n)n.hasOwnProperty(o)&&(t[o]=n[o])},extendStatics(t,n)};
    

  9. fcaede1b9e574664c893e75ee7dc1d8b, 652 идентичных


    const z=function(t,o){function e(){this.constructor=t}extendStatics(t,o),t.prototype=null===o?Object.create(o):(e.prototype=o.prototype,new e)};
    

  10. e743dd760a03449be792c00e65154a48, 635 идентичных


    function(){var r=function(t,o){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,o){t.__proto__=o}||function(t,o){for(var n in o)o.hasOwnProperty(n)&&(t[n]=o[n])})(t,o)};return function(t,o){function n(){this.constructor=t}r(t,o),t.prototype=null===o?Object.create(o):(n.prototype=o.prototype,new n)}};
    

  11. 777c390d3cc4663f8ebe4933e5c33e9d, 441 идентичная, имена функций различные, чаще всего: Rule, AsapScheduler, ComplexOuterSubscriber и другие


    function MORK(){return null!==_super&&_super.apply(this,arguments)||this}
    

  12. 27628ad740cff22386b0ff029e844e85, 385 идентичных, имена функций чаще разные, чаще всего identity, forwardResolution и тд


    function MORK(n){return n}
    

  13. f6822db5c8812f4b09ab142afe908cda, 375 идентичных


    const z=function(n){};
    

  14. d98a03a472615305b012eceb3e9947d5, 330 идентичных


    const z=function(n,c){};
    

  15. 4728096fca2b3575800dafbdebf4276a, 324 идентичных


    const z=function(n){return n};
    

  16. 7b769d3e4ba438fc53b42ad8bece86ba, 289 идентичных, все имена plural


    function MORK(t){var r=Math.floor(Math.abs(t)),t=t.toString().replace(/^[^.]*\.?/,"").length;return 1===r&&0===t?1:5}
    

  17. 7d6f69751712ef9fa94238b38120adc6, 255 идентичных


    const z=function(){return this};
    

  18. b7081aad7510b0993fcb57bfb95c5c2c, 250 идентичных


    const z=function(){return!1};
    

  19. d665499155e104f749bf3a67caed576a, 250 идентичных


    const z=function(n){return null==n};
    

  20. 99fa7dfce87269a564fc848a7f7515b9, 255 идентичных


    const z=function(a,c){this._array.forEach(a,c)};
    


Файлы с самым большим количеством функций


SELECT count(id) as c, path FROM info group By path order by c desc LIMIT 20;

Количество Файл
13638 typescript/lib/tsserver.js
13617 typescript/lib/tsserverlibrary.js
12411 typescript/lib/typescriptServices.js
12411 typescript/lib/typescript.js
12411 @schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript.js
10346 sass/sass.dart.js
8703 typescript/lib/typingsInstaller.js
8528 typescript/lib/tsc.js
3933 @angular/compiler/bundles/compiler.umd.js
3803 @angular/compiler/bundles/compiler.umd.min.js
2602 selenium-webdriver/lib/test/data/js/tinymce.min.js
2264 @angular/core/bundles/core.umd.js
2028 @angular/core/bundles/core.umd.min.js
1457 terser/dist/bundle.min.js
1416 rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/schematics/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/core/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/build-webpack/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/build-angular/node_modules/rxjs/bundles/rxjs.umd.js
1416 @angular-devkit/architect/node_modules/rxjs/bundles/rxjs.umd.js

Вы спросите А как насчет собранного бандла?


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


Количество Функция
11 const z=function(){};
10 const z=()=>R;
8 const z=function(n){return new(n||t)};
6 const z=function(n){};
5 const z=()=>{};

А что там у нас с React'ом?


Я так же проверил и React. Сравнение я вынес в таблицу ниже:


В node_modules Angular React
всего файлов *.js 26982 23942
всего функций 338230 163385
уникальных функций 130886 92766
% уникальных функций 39% 57%

Скачать csv файл для самостоятельной проверки можно тут.


Топ 20 самых популярных функций в node_modules для проекта React


Я использовал create-react-app. Файлы package.json и yarn.lock можно найти тут.


# id количество дубликатов
1 12f746f2689073d5c949998e0216f68a 1377
2 285d00ca29fcc46aa113c7aefc63827d 1243
3 3f993321f73e83f277c20c178e5587b9 989
4 54782ec6cef850906484808b86946b33 299
5 7d1e7aad635be0f7382696c4f846beae 278
6 d11004e998280b565ad084b0ad5ca214 239
7 a02c66d8928b3353552e4804c6714326 237
8 79e9bd3cdf15cf0af97f73ccaed50fa0 231
9 7d6f69751712ef9fa94238b38120adc6 189
10 b8dd34af96b042c23a4be7f82c881fe4 176
11 863a48e36413feba8bb299623dbc9b20 174
12 2482d2afd404031c67adb9cbc012768b 174
13 4728096fca2b3575800dafbdebf4276a 170
14 bf8b05684375b26205e50fa27317057e 157
15 fd114ee6b71ee06738b5b547b00e8102 156
16 df1c43e5a72e92d11bdefcead13a5e14 156
17 094afc30995ff28993ec5326e8b3c4d4 156
18 042490db7093660e74a762447f64f950 156
19 5c5979ec3533f13b22153de05ffc64d5 154
20 50645492c50621c0847c4ebd1fdd65cd 154

  1. 12f746f2689073d5c949998e0216f68a, 1377 идентичных, названия функций обычно: _interopRequireDefault


    function MORK(e){return e&&e.__esModule?e:{default:e}}
    

  2. 285d00ca29fcc46aa113c7aefc63827d, 1243 идентичных


    const z=function(){};
    

  3. 3f993321f73e83f277c20c178e5587b9, 989 идентичных


    const z=function(){return data};
    

  4. 54782ec6cef850906484808b86946b33, 299 идентичных


    const z=()=>{};
    

  5. 7d1e7aad635be0f7382696c4f846beae, 278 идентичных, имена функций чаще всего emptyFunction, Generator


    function MORK(){}
    

  6. d11004e998280b565ad084b0ad5ca214, 239 идентичных


    const z=function(){return cache};
    

  7. a02c66d8928b3353552e4804c6714326, 237 идентичных, имя функции _getRequireWildcardCache


    function MORK(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return _getRequireWildcardCache=function(){return e},e}
    

  8. 79e9bd3cdf15cf0af97f73ccaed50fa0, 231 идентичных, имя функции _interopRequireWildcard


    function MORK(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache();if(t&&t.has(e))return t.get(e);var r,n,o={},c=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(r in e)Object.prototype.hasOwnProperty.call(e,r)&&((n=c?Object.getOwnPropertyDescriptor(e,r):null)&&(n.get||n.set)?Object.defineProperty(o,r,n):o[r]=e[r]);return o.default=e,t&&t.set(e,o),o}
    

  9. 7d6f69751712ef9fa94238b38120adc6, 189 идентичных


    const z=function(){return this};
    

  10. b8dd34af96b042c23a4be7f82c881fe4, 176 идентичных


    const z=function(n,o,c,i){n[i=void 0===i?c:i]=o[c]};
    

  11. 863a48e36413feba8bb299623dbc9b20, 174 идентичных


    const z=function(e,n,t,o){void 0===o&&(o=t),Object.defineProperty(e,o,{enumerable:!0,get:function(){return n[t]}})};
    

  12. 2482d2afd404031c67adb9cbc012768b, 174 идентичных


    const z=function(){return m[k]};
    

  13. 4728096fca2b3575800dafbdebf4276a, 170 идентичных


    const z=function(n){return n};
    

  14. bf8b05684375b26205e50fa27317057e, 157 идентичных


    const z=s=>exposed.has(s);
    

  15. fd114ee6b71ee06738b5b547b00e8102, 156 идентичных


    const z=(r,e,p)=>{var t=makeWrapper(r);return exports.setup(t,r,e,p)};
    

  16. df1c43e5a72e92d11bdefcead13a5e14, 156 идентичных


    const z=t=>utils.isObject(t)&&t instanceof Impl.implementation;
    

  17. 094afc30995ff28993ec5326e8b3c4d4, 156 идентичных


    const z=i=>utils.isObject(i)&&utils.hasOwn(i,implSymbol)&&i[implSymbol]instanceof Impl.implementation;
    

  18. 042490db7093660e74a762447f64f950, 156 идентичных


    const z=(r,e,t)=>{t=exports.create(r,e,t);return utils.implForWrapper(t)};
    

  19. 5c5979ec3533f13b22153de05ffc64d5, 154 идентичных


    const z=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&__createBinding(t,e,r);return __setModuleDefault(t,e),t};
    

  20. 50645492c50621c0847c4ebd1fdd65cd, 154 идентичных


    const z=function(e,n){Object.defineProperty(e,"default",{enumerable:!0,value:n})};
    


В каких файлах используется функция 8 (79e9bd3cdf15cf0af97f73ccaed50fa0)


Посмотреть список файлов

Список длинный


/jest-worker/build/base/BaseWorkerPool.js/@svgr/hast-util-to-babel-ast/lib/index.js/@svgr/hast-util-to-babel-ast/lib/handlers.js/@svgr/hast-util-to-babel-ast/lib/stringToObjectStyle.js/@svgr/hast-util-to-babel-ast/lib/getAttributes.js/babel-jest/node_modules/@babel/core/lib/transform-file.js/babel-jest/node_modules/@babel/core/lib/config/files/configuration.js/babel-jest/node_modules/@babel/core/lib/config/files/utils.js/babel-jest/node_modules/@babel/core/lib/config/full.js/babel-jest/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-jest/node_modules/@babel/core/lib/transformation/file/file.js/babel-jest/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-jest/node_modules/@babel/core/lib/index.js/jest-pnp-resolver/node_modules/jest-resolve/build/defaultResolver.js/jest-pnp-resolver/node_modules/jest-resolve/build/ModuleNotFoundError.js/jest-circus/build/utils.js/jest-haste-map/build/ModuleMap.js/jest-haste-map/build/lib/normalizePathSep.js/jest-haste-map/build/lib/fast_path.js/jest-haste-map/build/lib/WatchmanWatcher.js/jest-haste-map/build/worker.js/jest-haste-map/build/getMockName.js/jest-haste-map/build/HasteFS.js/jest-haste-map/build/crawlers/watchman.js/jest-jasmine2/build/index.js/eslint/node_modules/@babel/code-frame/lib/index.js/mini-css-extract-plugin/dist/index.js/react-scripts/node_modules/@babel/core/lib/transform-file.js/react-scripts/node_modules/@babel/core/lib/config/files/configuration.js/react-scripts/node_modules/@babel/core/lib/config/files/utils.js/react-scripts/node_modules/@babel/core/lib/config/full.js/react-scripts/node_modules/@babel/core/lib/transformation/normalize-file.js/react-scripts/node_modules/@babel/core/lib/transformation/file/file.js/react-scripts/node_modules/@babel/core/lib/tools/build-external-helpers.js/react-scripts/node_modules/@babel/core/lib/index.js/react-scripts/node_modules/jest-resolve/build/defaultResolver.js/react-scripts/node_modules/jest-resolve/build/ModuleNotFoundError.js/eslint-plugin-flowtype/dist/utilities/index.js/jest-util/build/index.js/jest-util/build/createDirectory.js/jest-util/build/installCommonGlobals.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx-development/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-optional-chaining/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-numeric-separator/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/targets-parser.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/index.js/babel-preset-react-app/node_modules/@babel/preset-env/lib/utils.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/preset-react/node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/core/lib/index.js/babel-preset-react-app/node_modules/@babel/core/lib/transform-file.js/babel-preset-react-app/node_modules/@babel/core/lib/config/files/configuration.js/babel-preset-react-app/node_modules/@babel/core/lib/config/files/utils.js/babel-preset-react-app/node_modules/@babel/core/lib/config/full.js/babel-preset-react-app/node_modules/@babel/core/lib/transformation/normalize-file.js/babel-preset-react-app/node_modules/@babel/core/lib/transformation/file/file.js/babel-preset-react-app/node_modules/@babel/core/lib/tools/build-external-helpers.js/babel-preset-react-app/node_modules/@babel/core/lib/index.js/@babel/code-frame/lib/index.js/@babel/traverse/lib/path/inference/inferer-reference.js/@babel/traverse/lib/path/inference/inferers.js/@babel/traverse/lib/path/inference/index.js/@babel/traverse/lib/path/comments.js/@babel/traverse/lib/path/replacement.js/@babel/traverse/lib/path/ancestry.js/@babel/traverse/lib/path/conversion.js/@babel/traverse/lib/path/index.js/@babel/traverse/lib/path/introspection.js/@babel/traverse/lib/path/removal.js/@babel/traverse/lib/path/lib/hoister.js/@babel/traverse/lib/path/lib/virtual-types.js/@babel/traverse/lib/path/modification.js/@babel/traverse/lib/path/family.js/@babel/traverse/lib/path/generated/asserts.js/@babel/traverse/lib/path/generated/validators.js/@babel/traverse/lib/path/generated/virtual-types.js/@babel/traverse/lib/index.js/@babel/traverse/lib/visitors.js/@babel/traverse/lib/context.js/@babel/traverse/lib/scope/index.js/@babel/traverse/lib/scope/lib/renamer.js/@babel/traverse/lib/types.js/@babel/helper-hoist-variables/lib/index.js/@babel/helper-wrap-function/lib/index.js/@babel/helper-builder-binary-assignment-operator-visitor/lib/index.js/@babel/helper-explode-assignable-expression/lib/index.js/@babel/helper-replace-supers/lib/index.js/@babel/helper-module-imports/lib/import-builder.js/@babel/helper-module-imports/lib/import-injector.js/@babel/helper-skip-transparent-expression-wrappers/lib/index.js/@babel/helper-compilation-targets/lib/index.js/@babel/types/lib/definitions/jsx.js/@babel/types/lib/definitions/misc.js/@babel/types/lib/definitions/typescript.js/@babel/types/lib/definitions/flow.js/@babel/types/lib/definitions/experimental.js/@babel/types/lib/definitions/core.js/@babel/types/lib/index.js/@babel/helpers/lib/index.js/@babel/helper-remap-async-to-generator/lib/index.js/@babel/helper-split-export-declaration/lib/index.js/@babel/helper-simple-access/lib/index.js/@babel/helper-module-transforms/lib/rewrite-this.js/@babel/helper-module-transforms/lib/rewrite-live-references.js/@babel/helper-module-transforms/lib/index.js/@babel/preset-env/lib/targets-parser.js/@babel/preset-env/lib/index.js/@babel/preset-env/lib/utils.js/@babel/highlight/lib/index.js/@babel/generator/lib/generators/jsx.js/@babel/generator/lib/generators/base.js/@babel/generator/lib/generators/template-literals.js/@babel/generator/lib/generators/typescript.js/@babel/generator/lib/generators/classes.js/@babel/generator/lib/generators/expressions.js/@babel/generator/lib/generators/statements.js/@babel/generator/lib/generators/flow.js/@babel/generator/lib/generators/modules.js/@babel/generator/lib/generators/types.js/@babel/generator/lib/generators/methods.js/@babel/generator/lib/node/parentheses.js/@babel/generator/lib/node/index.js/@babel/generator/lib/node/whitespace.js/@babel/generator/lib/printer.js/@babel/helper-get-function-arity/lib/index.js/@babel/helper-function-name/lib/index.js/@babel/helper-annotate-as-pure/lib/index.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transform-file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/files/configuration.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/files/utils.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/config/full.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transformation/normalize-file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/transformation/file/file.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/tools/build-external-helpers.js/@babel/helper-create-class-features-plugin/node_modules/@babel/core/lib/index.js/@babel/helper-create-class-features-plugin/lib/fields.js/@babel/plugin-transform-classes/lib/transformClass.js/@babel/template/lib/parse.js/@babel/template/lib/formatters.js/@babel/template/lib/populate.js/@babel/template/lib/index.js/@babel/core/lib/transform-file.js/@babel/core/lib/config/files/configuration.js/@babel/core/lib/config/files/utils.js/@babel/core/lib/config/full.js/@babel/core/lib/transformation/normalize-file.js/@babel/core/lib/transformation/file/file.js/@babel/core/lib/tools/build-external-helpers.js/@babel/core/lib/index.js/@babel/helper-optimise-call-expression/lib/index.js/jest-snapshot/build/SnapshotResolver.js/jest-snapshot/build/State.js/jest-snapshot/build/index.js/jest-serializer/build/index.js/react-dev-utils/node_modules/@babel/code-frame/lib/index.js/jest-resolve/build/defaultResolver.js/jest-resolve/build/ModuleNotFoundError.js/pretty-format/build/plugins/ReactElement.js/jest-each/build/table/array.js/@jest/transform/build/shouldInstrument.js/@jest/transform/build/index.js/@jest/reporters/build/NotifyReporter.js/@jest/reporters/build/CoverageWorker.js/@jest/reporters/build/utils.js/@jest/reporters/build/generateEmptyCoverage.js/@jest/core/build/collectHandles.js/@jest/core/build/watch.js/@testing-library/react/dist/@testing-library/react.umd.js/@testing-library/react/dist/@testing-library/react.pure.umd.js/@testing-library/dom/dist/@testing-library/dom.umd.js/jest-config/build/getCacheDirectory.js/jest-config/build/resolveConfigPath.js/jest-config/build/constants.js

А какие выводы?


Возможно это небольшое исследование кого-нибудь натолкнет на мысли какие-нибудь мысли о рефакторинге. Или изменении подхода к программированию.


Хорошо, что люди используют для программирования некоторые общие подходы и паттерны.


Плохо что очень много копипасты.


P.S.


Подробнее..

Упрощаем работу с Angular с помощью taiga-uicdk 5 наших лучших практик

11.05.2021 12:14:21 | Автор: admin

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

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

Дисклеймер о весе библиотеки

Перед написанием статьи хотелось бы ответить на вопрос: Для чего тащить в бандл целый мультитул, когда мне нужна пара функций?

По результатам bundlephobia мы получим следующую картинку:

23 КБ результат не самый страшный, но и не очень приятный. Но все сущности наших библиотек лежат в отдельных Secondary Entry Point, что делает их полностью tree shakable. Это значит, что такой объем в бандле мы получим только в случае импорта и использования всех сущностей библиотеки в нашем приложении. Если вы импортите пару сущностей только они и попадут к вам в бандл, добавив к нему в результате меньше 1 КБ.

tuiPure продвинутая мемоизация вычислений

Это декоратор, который можно вешать на геттеры и чистые методы классов. Давайте разберем оба сценария.

Как геттер

Можно повесить декоратор tuiPure на геттер. В таком случае он позволяет сделать отложенные вычисления.

Пример 1. Скрываем и показываем сороковое число Фибоначчи, когда пользователь нажимает на кнопку

// template<div *ngIf="show">fibonacci(40) = {{ fibonacci40 }}</div>// component@tuiPureget fibonacci40(): number {  return calculateFibonacci(40);}

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

Пример 2. У нас есть компонент pull to refresh, который эмулирует поведение под iOS и Android. Один из его стримов вызывается только для Андроида, а для iOS нет. Завернем его в getter с pure, и ненужный Observable не будет создан для iOS.

<tui-mobile-ios-loader   *ngIf="isIOS; else angroidLoader"></tui-mobile-ios-loader><ng-template #angroidLoader>   <tui-mobile-android-loader       [style.transform]="loaderTransform$ | async"   ></tui-mobile-android-loader></ng-template>
@tuiPureget loaderTransform$(): Observable<string> {    return this.pulling$.pipe(        map(distance => translateY(Math.min(distance, ANDROID_MAX_DISTANCE))),    );}

Также можно обратиться к changes от ContentChild / ContentChildren: если мы вызываем такой геттер из шаблона, то уже можем быть уверены, что content готов. При соблюдении порядка также это можно провернуть и с ViewChild / ViewChildren.

Как метод

На метод тоже можно повесить декоратор @tuiPure. Тогда он будет работать следующим образом: при первом вызове метода посчитает значение и вернет его. Все последующие вызовы с теми же самыми аргументами будут возвращать уже посчитанный результат. Если аргумент изменится результат пересчитается.

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

get filteredItems(): readonly string[] {   return this.computeFilteredItems(this.items);}@tuiPureprivate computeFilteredItems(items: readonly string[]): readonly string[] {   return items.filter(someCondition);}

В этом случае можно вызвать геттер из шаблона, один раз отфильтровать items, и он будет возвращать тот же самый массив до тех пор, пока this.itemsне изменится. Это поможет избежать лишних операций пересоздания массива на каждый чих проверки изменений, а также проблем из-за постоянных изменений ссылки на массив при передаче дальше. При этом нам не придется, например, самим синхронизировать состояние в ngOnChanges, если this.items инпут компонента.

Документация по tuiPure

*tuiLet

Это простая структурная директива для объявления локальных переменных в шаблонах.

<ng-container *tuiLet="timer$ | async as time">   <p>Timer value: {{time}}</p>   <p>       It can be used many times:       <tui-badge [value]="time"></tui-badge>   </p>   <p>       It subsribed once and async pipe unsubsribes it after component destroy   </p></ng-container>

Вместо *tuiLetможно использовать *ngIf если вам не нужно показывать шаблон при falsy-значении (или если оно не предусмотрено). Но если вы работаете, например, с числами, то 0, скорее всего, является вполне адекватным значением. Тут и поможет *tuiLet

Документация по tuiLet

Метапайпы tuiMapper и tuiFilter

Мы создали пайп, чтобы не создавать другие пайпы, tuiMapper.

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

{{value | tuiMapper : mapper : arg1 : arg2 }}

Также удобно и преобразовывать данные для инпутов компонентов в шаблоне или использовать через *ngIf / *tuiLet:

<div    *ngIf="item | tuiMapper : toMarkers : itemIsToday(item) : !!getItemRange(item) as markers"    class="dots">    <div class="dot" [tuiBackground]="markers[0]"></div>    <div        *ngIf="markers.length > 1"        class="dot"        [tuiBackground]="markers[1]"    ></div></div>

Добавление цветных маркеров-точек в календарях @taiga-ui/core

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

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

Документация на mapper / документация на filter

destroy$

Это Observable-based сервис, который упрощает процесс отписки в компонентах и директивах.

@Component({   // ...   providers: [TuiDestroyService],})export class TuiDestroyExample {   constructor(     @Inject(TuiDestroyService)      private readonly destroy$: Observable<void>   ) {}   //    subscribeSomething() {       fromEvent(this.element, 'click')           .pipe(takeUntil(this.destroy$))           .subscribe(() => {               console.log('click');           });   }}

Все что нам нужно добавить его в providers компонента и заинжектить в конструкторе. Я предпочитаю писать типы сущностей из DI, которые минимально необходимы в компоненте. Здесь это Observable<void>. Но можно писать и покороче:

constructor(private destroy$: TuiDestroyService) {}

Кстати, сервис в такой ситуации привязывается не к лайфсайклу компонента, а к его DI Injectorу. Это может помочь в ситуации, когда нужно подписаться в сервисе или внутри DI фабрики. Такие кейсы довольно редки, но зато TuiDestroyService в них буквально спасает например, когда мы хотели дергать markForCheck из фабрики токена в статье о DI фокусах для проброса данных.

Ссылка на документацию

Плагины ng-event-plugins

Фактически это внешняя библиотека ng-event-plugins, которая поставляется вместе с cdk (прямая зависимость, которую не нужно устанавливать отдельно). Она добавляет свои обработчики к менеджеру плагинов Angular. В ней есть несколько очень полезных плагинов, которые добавляют ряд возможностей в шаблоны компонентов.

Например, .stopи .preventпозволяют декларативно делать stopPropagation и preventDefault на любой прилетающий ивент.

Было:

<some-input (mousedown)="handle($event)">    Choose date</some-input>
export class SomeComponent {   //    handle(event: MouseEvent) {      event.preventDefault();      event.stopPropagation();      this.onMouseDown(event);   }}

Стало:

<some-input (mousedown.prevent.stop)="onMouseDown()">    Choose date</some-input>

Или модификатор .silent который позволяет не запускать проверку изменений на событие:

<div (mousemove.silent)="onMouseMove()">    Callbacks to mousemove will not trigger change detection</div>

Можно отслеживать ивенты в capture-фазе с помощью .capture:

<div (click.capture.stop)="onClick()">    <div (click)="never()">Clicks will be stopped before reaching this DIV</div></div>

Все это работает и с @HostListenerами, и с кастомными событиями. Вы можете почитать подробнее в документации ng-event-plugins.

Итого

Мы посмотрели ряд сущностей пакета @taiga-ui/cdk. Надеюсь, какие-нибудь из них вам приглянулись и тоже будут помогать во всех дальнейших проектах!

Кстати, у меня еще есть статья про саму библиотеку Taiga UI, в которой описаны остальные пакеты и общая философия библиотеки.

Подробнее..

Перевод Простая архитектура приложений на фреймворке Angular

31.05.2021 20:20:52 | Автор: admin

Введение в атомарный дизайн

Можете ли вы с первого взгляда определить, что находится внутри общего модуля? И, не открывая другой файл, сказать, в каких компонентах есть зависимости или привязки к другим компонентам? Каким бы ни был ваш ответ, я предлагаю вам заварить чай или кофе и разобраться со мной в основах теории атомарного дизайна, придуманной Брэдом Фростом!

Определение атомарного дизайна в контексте Angular

Структура каталогов при атомарном дизайне в AngularСтруктура каталогов при атомарном дизайне в Angular

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

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

Атомы и молекулы глупые компоненты

Это простые по сути и удобные в тестировании фрагменты кода, которые могут использоваться многократно. Они получают данные на входе (@Input) и могут что-то выдавать на выходе (@Output). Следовательно, они идеально подойдут для описания библиотек пользовательского интерфейса.

  • Атомы самые крохотные модули, многократно применяемые во всем проекте. Обычно представляют собой одиночный HTML-элемент с базовым стилем.

  • Молекулы отдельная группа атомов.

Организмы и шаблоны простое представление умных компонентов

Шаблоны помогают сократить объемы типового HTML-кода, упрощая умные компоненты, а организмы являются их аналогом для TypeScript.

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

А все дело в том, что их необходимость осознаешь тогда, когда уже поздно. Проектируя сложный умный компонент, мы изначально рассматриваем его как единое целое. Мы создаем полную страницу, постепенно добавляя небольшие фрагменты кода. И тут внезапно этот умный компонент раздувается до пятисот строк TypeScript и тысячи строк HTML-CSS. В получившемся болоте каждый последующий шаг дается сложнее, чем предыдущий. Даже если логика простая, с файлом трудно обращаться из-за его размеров.

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

Шаблоны

Рассматривайте шаблоны как скаффолды и скиныкомпонентов. Обычный код HTML-CSS с элементом ng-content.

Шаблоны используются в высокоуровневых компонентах (таких как страницы или организмы), чтобы снизить вариативность стиля и уменьшить количество типового HTML-кода. У таких компонентов не должно быть никаких входных или выходных данных, логики и зависимостей от конструктора.

Давайте рассмотрим простой пример:

Компонент-шаблон, который группирует типовой код оберток наряду со стилем и не обладает логикой.Компонент-шаблон, который группирует типовой код оберток наряду со стилем и не обладает логикой.Страница или умный компонент на основе компонента-шаблона. В нем нет стилей, но он управляет логикой.Страница или умный компонент на основе компонента-шаблона. В нем нет стилей, но он управляет логикой.

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

Организмы

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

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

Это умные или глупые компоненты? Однозначного правила в этом отношении нет. Я предпочитаю по возможности собирать все зависимости от сервисов и логику в странице/контроллере. Но имея дело со сложными страницами, иногда разумнее структурировать их как макет и разнести логику на несколько дочерних компонентов-организмов, имеющих собственные зависимости.

Страницы умные компоненты

В общем случае считаются функциональными компонентами в рамках классической структуры проекта типа core/features/shared (базовые модули/ функциональные компоненты/ общие компоненты). Умные компоненты управляют входами/выходами, взаимодействующими с базовыми модулями приложения через сервисы.


Перевод подготовлен в рамках курса "JavaScript Developer. Professional".

Всех желающих приглашаем на открытый урок "Async Patterns в JavaScript". На этом занятии разберем асинхронное программирование в JavaScript, функцию обратного вызова в программировании, функции map, reduce и многое другое. Присоединяйтесь!

Подробнее..

Создание приложений на Angular с использованием продвинутых возможностей DI

01.06.2021 10:11:39 | Автор: admin

Меня зовут Андрей, и я занимаюсь разработкой фронтенда на Angular для внутренних продуктов компании. Фреймворк обладает обширными возможностями, одни и те же задачи можно решить огромным количеством способов. Чтобы облегчить свою работу и повысить продуктивность, я задался целью найти универсальный и не сложный подход, который бы упростил проектирование и позволил уменьшить объем кода при сохранении его читаемости. Перепробовав множество различных вариантов и учтя допущенные ошибки, я пришел к архитектуре, которой хочу поделиться в этой статье.

Слоеный пирог приложения

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

  1. Хранение данных и осуществление операций с данными (слой данных).

  2. Преобразование информации к виду, требуемому для отображения, обработка действий пользователя (слой управления или контроллер).

  3. Визуализация данных и делегация событий (слой представления).

В контексте фреймворка они будут обладать следующими характерными особенностями:

  • элементы слоя представления компоненты;

  • зависимости слоя управления находятся в элементных инжекторах, а слоя данных в модульных;

  • связь между слоями осуществляется средствами системы DI;

  • элементы каждого уровня могут иметь дополнительные зависимости, которые непосредственно к слою не относятся;

  • слои связаны в строгом порядке: сервисы одного уровня не могут зависеть друг от друга, компоненты слоя представления могут внедрять только контроллеры, а контроллеры только сервисы слоя данных.

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

Вообще говоря, под данными, передаваемыми между слоями, имеются в виду произвольные объекты. Однако, в большинстве случаев ими будут Observable, которые идеально подходят к описываемому подходу. Как правило, слой данных отдает Observable с частью состояния приложения. Затем в слое управления с помощью операторов rxjs данные преобразовываются к нужному формату, и в шаблоне компонента осуществляется подписка через async pipe. События на странице связываются с обработчиком в контроллере. Он может иметь сложную логику управления запросами к слою данных и подписывается на Observable, которые возвращают асинхронные команды. Подписка позволяет гибко реагировать на результат выполнения отдельных команд и обрабатывать ошибки, например, открывая всплывающие сообщения. Элементы слоя управления я буду дальше называть контроллерами, хотя они отличаются от таковых в MVC паттерне.

Слой данных

Сервисы слоя данных хранят состояние приложения (бизнес-данные, состояние интерфейса) в удобном для работы с ним виде. В качестве дополнительных зависимостей используются сервисы для работы с данными (например: http клиент и менеджеры состояния). Для непосредственного хранения данных удобно использовать BehaviourSubject в простых случаях, и такие библиотеки как akita, Rxjs или ngxs для более сложных. Однако, на мой взгляд, последние две избыточны при данном подходе. Лучше всего для предлагаемой архитектуры подходит akita. Ее преимуществами являются отсутствие бойлерплейта и возможность переиспользовать стейты обычным наследованием. При этом обновлять стейт можно непосредственно в операторах rxjs запросов, что гораздо удобнее, чем создание экшенов.

@Injectable({providedIn: 'root'})export class HeroState {  private hero = new BehaviorSubject(null);  constructor(private heroService: HeroService) {}  load(id: string) {    return this.heroService.load(id).pipe(tap(hero => this.hero.next(hero)));  }  save(hero: Hero) {    return this.heroService.save(hero).pipe(tap(hero => this.hero.next(hero)));  }  get hero$(): Observable<Hero> {    return this.hero.asObservable();  }}

Слой управления

Так как каждый сервис слоя относится к конкретному компоненту с его поддеревом, логично назвать сервис контроллером компонента. Благодаря тому, что контроллер компонента находится в элементном инжекторе, в нем можно использовать OnDestroy hook и внедрять те же зависимости, что и в компоненте, например ActivatedRoute. Безусловно, можно не создавать отдельный сервис для контроллера в тех случаях, где это равноценно вынесению кода из компонента.

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

@Injectable()export class HeroController implements OnDestroy {  private heroSubscription: Subscription;    heroForm = this.fb.group({    id: [],    name: ['', Validators.required],    power: ['', Validators.required]  });  constructor(private heroState: HeroState, private route: ActivatedRoute, private fb: FormBuilder) { }  save() {    this.heroState.save(this.heroForm.value).subscribe();  }  initialize() {    this.route.paramMap.pipe(      map(params => params.get('id')),      switchMap(id => this.heroState.load(id)),    ).subscribe();    this.heroSubscription = this.heroState.selectHero().subscribe(hero => this.heroForm.reset(hero));  }    ngOnDestroy() {    this.heroSubscription.unsubscribe();  }}

Слой представления

Функцией слоя представления является визуализация и связывание событий с их обработчиками. И то, и другое происходит в шаблоне компонента. При этом класс будет содержать только код, внедряющий зависимости уровня управления. Простые компоненты (в том числе из внешних библиотек), не использующие внедрение, будут относиться к дополнительным зависимостям. Они получают данные через Input поля и делегируют события через Output.

@Component({  selector: 'hero',  template: `    <hero-form [form]="heroController.heroForm"></hero-form>    <button (click)="heroController.save()">Save</button>  `,  providers: [HeroController]})export class HeroComponent {  constructor(public heroController: HeroController) {    this.heroController.initialize();  }}

Повторное использование кода

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

В демо-приложении я постарался уместить различные методы их использования. Возможно, здесь они немного избыточны, но будут полезны в реальных задачах.

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

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

export abstract class EntityState<T> {    abstract get entities$(): Observable<T[]>; // список сущностей    abstract get selectedId$(): Observable<string>; // id выбранного элемента    abstract get selected$(): Observable<T>; // выбранный элемент    abstract select(id: string); // выбрать элемент с указанным id    abstract load(): Observable<T[]> // загрузить список    abstract save(entity: T): Observable<T>; // сохранить сущность}

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

@Injectable()export class EntityCardController {    isSelected$ = this.entityState.selectedId$.pipe(map(id => id !== null));    constructor(private entityState: EntityState<any>, private snackBar: MatSnackBar) {    }    save(form: FormGroup) {        this.entityState.save(form.value).subscribe({            next: () => this.snackBar.open('Saved successfully', null, { duration: 2000 }),            error: () => this.snackBar.open('Error occurred while saving', null, { duration: 2000 })        })    }}

В самом компоненте используем еще один способ внедрения зависимости через директиву @ContentChild.

@Component({    selector: 'entity-card',    template: `        <mat-card>            <ng-container *ngIf="entityCardController.isSelected$ | async; else notSelected">                <mat-card-title>                    <ng-content select=".header"></ng-content>                </mat-card-title>                <mat-card-content>                    <ng-content></ng-content>                </mat-card-content>                <mat-card-actions>                    <button mat-button (click)="entityCardController.save(entityFormController.entityForm)">SAVE</button>                </mat-card-actions>            </ng-container>            <ng-template #notSelected>Select Item</ng-template>        </mat-card>    `,    providers: [EntityCardController]})export class EntityCardComponent {    @ContentChild(EntityFormController) entityFormController: EntityFormController<any>;    constructor(public entityCardController: EntityCardController) {        this.entityCardController.initialize();    }}

Для того чтобы это было возможно, необходимо в провайдерах компонента, который проецируется в entity-card, указать реализацию EntityFormController:

providers: [{ provide: EntityFormController, useClass: HeroFormController }]

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

<entity-card><hero-form></hero-form></entity-card>

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

export interface Entity {    value: string;    label: string;}@Injectable()export abstract class EntityListController<T> {    constructor(protected entityState: EntityState<T>) {}    select(value: string) {        this.entityState.select(value);    }    selected$ = this.entityState.selectedId$;    abstract get entityList$(): Observable<Entity[]>;}

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

@Injectable()export class FilmsListController extends EntityListController<Film> {    entityList$ = this.entityState.entities$.pipe(        map(films => films.map(f => ({ value: f.id, label: f.title })))    )}

Компонент списка использует этот сервис, однако его реализация будет предоставлена внешним компонентом.

@Component({    selector: 'entity-list',    template: `        <mat-selection-list [multiple]="false"                             (selectionChange)="entityListController.select($event.options[0].value)">            <mat-list-option *ngFor="let item of entityListController.entityList$ | async"                             [selected]="item.value === (entityListController.selected$ | async)"                             [value]="item.value">                {{ item.label }}            </mat-list-option>        </mat-selection-list>    `})export class EntityListComponent {    constructor(public entityListController: EntityListController<any>) {}}

Компонент, являющийся абстракцией всей вкладки, включает список сущностей и проецирует содержимое с формой.

@Component({    selector: 'entity-page',    template: `        <mat-sidenav-container>            <mat-sidenav opened mode="side">                <entity-list></entity-list>            </mat-sidenav>            <ng-content></ng-content>        </mat-sidenav-container>    `,})export class EntityPageComponent {}

Использование компонента entity-page:

@Component({    selector: 'film-page',    template: `        <entity-page>            <entity-card>                <span class="header">Film</span>                <film-form></film-form>            </entity-card>        </entity-page>    `,    providers: [        { provide: EntityState, useExisting: FilmsState },        { provide: EntityListController, useClass: FilmsListController }    ]})export class FilmPageComponent {}

Компонент entity-card передается через проекцию содержимого для возможности использования ContentChild.

Послесловие

Описанный подход позволил мне значительно упростить процесс проектирования и ускорить разработку без ущерба качеству и читаемости кода. Он отлично масштабируется к реальным задачам. В примерах были продемонстрированы лишь базовые техники переиспользования. Их комбинация с такими фичами как multi-провайдеры и модификаторы доступа (Optional, Self, SkipSelf, Host) позволяет гибко выделять абстракции в сложных случаях, используя меньше кода, чем обычное переиспользование компонентов.

Подробнее..

Перевод Кастомные операторы RxJS

10.06.2021 18:12:02 | Автор: admin

Библиотека RxJS благодаря широкому выбору операторов по праву считается крайне мощным инструментом в арсенале разработчика. В этом посте я хочу представить вам концепцию кастомных операторов RxJS с примерами реализации.

Библиотека RxJS благодаря широкому выбору операторов по праву считается крайне мощным инструментом в арсенале разработчика. Недавно я подготовил несколько самописных операторов, чтобы повысить удобство повторного использования некоторых комбинаций операторов. В этом посте я хочу представить вам концепцию кастомных операторов RxJS с примерами реализации.

Оператор идентификации

Оператор RxJS это всего лишь функция, которая берет некие наблюдаемые (observable) данные в качестве входных и возвращает результирующий поток. Следовательно, задача написания кастомного оператора RxJS сводится к написанию обычной функции JavaScript (TypeScript). Начнем с базового оператора идентификации (identity), который просто зеркалирует наблюдаемые исходные данные:

import { interval, Observable } from "rxjs";import { take } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function identity<T>(source$: Observable<T>): Observable<T> {  return source$;}const results$ = source$.pipe(identity);results$.subscribe(console.log);  // console output: 0, 1, 2

Далее напишем кастомный оператор с кое-какой элементарной логикой.

Оператор логирования

Следующий кастомный оператор выполняет побочное действие (логирует значения в консоли) для каждого значения исходного потока:

<>Copyimport { interval, Observable } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function log<T>(source$: Observable<T>): Observable<T> {  return source$.pipe(tap(v => console.log(`log: ${v}`)));}const results$ = source$.pipe(log);results$.subscribe(console.log);  // console output: log: 0, log: 1, log: 2

В основе результирующего потока лежат данные source$, которые видоизменяются посредством применения встроенных операторов в составе метода pipe.

Фабрика оператора

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

import { interval, Observable } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function logWithTag<T>(tag: string): (source$: Observable<T>) => Observable<T> {  return source$ =>    source$.pipe(tap(v => console.log(`logWithTag(${tag}): ${v}`)));}const results$ = source$.pipe(logWithTag("RxJS"));results$.subscribe(console.log);  // console output: logWithTag(RxJS): 0, logWithTag(RxJS): 1, logWithTag(RxJS): 2

Описание возвращаемого типа можно упростить, воспользовавшись функцией MonoTypeOperatorFunction библиотекиRxJS. Кроме того, с помощью статической функции pipe можно сократить определение оператора:

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function logWithTag<T>(tag: string): MonoTypeOperatorFunction<T> {  return pipe(tap(v => console.log(`logWithTag(${tag}): ${v}`)));}const results$ = source$.pipe(logWithTag("RxJS"));results$.subscribe(console.log);  // console output: logWithTag(RxJS): 0, logWithTag(RxJS): 1, logWithTag(RxJS): 2

Другие полезные советы по RxJS можно почитать здесь.

Уникальная для наблюдателя лексическая область видимости

Функция фабрики оператора вызывается лишь один раз в момент определения потока. В результате у всех наблюдателей будет общая лексическая область видимости:

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function tapOnce<T>(job: Function): MonoTypeOperatorFunction<T> {  let isFirst = true;  return pipe(    tap(v => {      if (!isFirst) {        return;      }      job(v);      isFirst = false;    })  );}const results$ = source$.pipe(tapOnce(() => console.log("First value emitted")));results$.subscribe(console.log);results$.subscribe(console.log);  // console output: First value emitted, 0, 0, 1, 1, 2, 2

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

import { defer, interval, MonoTypeOperatorFunction } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function tapOnceUnique<T>(job: Function): MonoTypeOperatorFunction<T> {  return source$ =>    defer(() => {      let isFirst = true;      return source$.pipe(        tap(v => {          if (!isFirst) {            return;          }          job(v);          isFirst = false;        })      );    });}const results$ = source$.pipe(tapOnceUnique(() => console.log("First value emitted")));results$.subscribe(console.log);results$.subscribe(console.log);  // console output: First value emitted, 0, First value emitted, 0, 1, 1, 2, 2

Другой способ решения задачи tapOnce рассматривается в одном из моих предыдущих постов.

Практические примеры

Оператор firstTruthy:

import { MonoTypeOperatorFunction, of, pipe } from "rxjs";import { first } from "rxjs/operators";const source1$ = of(0, "", "foo", 69);function firstTruthy<T>(): MonoTypeOperatorFunction<T> {  return pipe(first(v => Boolean(v)));}const result1$ = source1$.pipe(firstTruthy());result1$.subscribe(console.log);// console output: foo

Оператор evenMultiplied:

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { filter, map, take } from "rxjs/operators";const source2$ = interval(10).pipe(take(3));function evenMultiplied(multiplier: number): MonoTypeOperatorFunction<number> {  return pipe(    filter(v => v % 2 === 0),    map(v => v * multiplier)  );}const result2$ = source2$.pipe(evenMultiplied(3));result2$.subscribe(console.log);  // console output: 0, 6

Оператор liveSearch:

import { ObservableInput, of, OperatorFunction, pipe  } from "rxjs";import { debounceTime, delay, distinctUntilChanged, switchMap } from "rxjs/operators";const source3$ = of("politics", "sport");type DataProducer<T> = (q: string) => ObservableInput<T>;function liveSearch<R>(  time: number,  dataProducer: DataProducer<R>): OperatorFunction<string, R> {  return pipe(    debounceTime(time),    distinctUntilChanged(),    switchMap(dataProducer)  );}const newsProducer = (q: string) =>  of(`Data fetched for ${q}`).pipe(delay(2000));const result3$ = source3$.pipe(liveSearch(500, newsProducer));result3$.subscribe(console.log);  // console output: Data fetched for sport

Заключение

Типовые комбинации операторов RxJS можно вынести в кастомные операторы и многократно использовать их в будущем при реализации аналогичного функционала. Применение дженериков обеспечивает корректное приведение типов выходных значений, обрабатываемых в дальнейшей pipe-последовательности.

Живой пример: [смотрите в оригинале]

Надеюсь, вам понравился мой пост и вы узнали что-то новое.


Перевод материала подготовлен в рамках курса "JavaScript Developer. Professional". Если вам интересно узнать о курсе подробнее, приглашаем на день открытых дверей онлайн, где преподаватель расскажет о формате обучения и программе.

Подробнее..

Почему мы должны выбросить React и взяться за Angular

15.06.2021 14:10:21 | Автор: admin

Хочу представить перевод довольно интересной статьи Сэма Редмонда, Why We Should Throw Out React and Pick Up Angular. На мой взгляд, статья описывает основные возможности Angular. Она может показаться довольно вызывающей, но постарайтесь отнестись к ней немного с юмором :)

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

  1. Он популярен в основном от того, что вокруг него много шумихи.

  2. Он даёт слишком много свободы. Это приводит к фундаментальным ошибкам на ранней стадии разработки приложения, которые проявляются не сразу.

  3. Использует не оправдано много памяти и не поддаётся оптимизации(not tree-shakable).

  4. Сложность React приложения растёт экспоненциально с ростом размера приложения и это затрудняет его обслуживание.

  5. Нет ничего встроенного (например, обработка форм). По-этому вам нужно написать много кода, что бы это как-то компенсировать или использовать кучу сторонних библиотек.

  6. Обновление вашего приложения до последней версии React часто сопряжено с полным переписыванием этого самого приложения.

Это самые частые проблемы, с которыми я столкнулся, работая с различными проектами на React. В этой статье я хочу поделиться с вами, как Angular решает большую часть вышеперечисленных проблем и позволяет вам сосредоточиться на том, что вы создаёте, а не на том, как вы создаёте своё приложение.

All aboard the hype train

Angular также получает изрядное количество хайпа, так что я не могу сказать, что Angular решает эту проблему. Однако, я не думаю, что Angular получает такое же количество хайпа, как React. Мне кажется, что в основном это связано с ребрендингом, который сделал Google.. Сначала был AngularJs, который был как дымящаяся куча мусора. Но надо отдать должное Google, они решили полностью отремонтировать AngularJs и превратить его в Angular (или Angular 2), что является гигантским улучшением. Впрочем, это улучшение стоило ему популярности, как мне кажется.

Лично я думаю, что им следовало бы переименовать Angular во что-то другое, потому что AngularJs и Angular совсем не похожи. Единственное сходство - это названия фреймворков и компания, которая их разработала.

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

Вы получаете то, что вам нужно

Одна из больших разниц между Angular и React в простом факте, что Angular является полноценным фреймворком в противовес обычной библиотеке. Как я уже показал в предыдущей статье, React пытается совместить лучшее из библиотеки и фреймворка, но делает это плохо. В отличие от этого, Angular не пытается быть библиотекой. Он идёт ва-банк, чтобы быть фреймворком и делает это хорошо.

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

Кроме того вы получаете такую штуку, как Angular CLI, который один из самых мощных инструментов на поясе у Angular.

разработчики, использующие Angular CLIразработчики, использующие Angular CLI

Одна из самых больших проблем с React - это отсутствие стандартов. Если вы изучили одно React приложение, то вы изучили одно React приложение, потому что все они совершенно разные. С другой стороны, если вы изучили одно Angular приложение, то вы изучили все Angular приложения и Angular CLI является основным драйвером, стоящим за этим.

В отличие от React в Angular есть правильные и неправильные способы. Использование Angular CLI обычно всегда гарантирует, что всё будете делать правильно. Давайте возьмём самое начало. Мы хотим создать новое приложение. Как мы это сделаем?

ng new my-app

Да, вот, пожалуй и всё. Запустите эту команду и CLI настроит кучу вещей за вас. Он даже даст вам некоторый выбор, такой как использование линтинга и роутинга, перед тем, как будет создано приложение. Итак, вот что сделает CLI:

  1. Он создаст новую директорию my-app и проинициализирует в ней Angular приложение. А также установит зависимости.

  2. Настраивает инфраструктуру и интегрирует в неё приложение, снабдив вас всем, что нужно для запуска.

  3. Вместе с инфраструктурой вы также получаете встроенную базовую реализацию end-to-end тестов на Protractor, в которую вы можете добавлять тесты, по мере развития вашего приложения.

  4. Angular даёт вам простой в использовании конфигурационный файл (angular.json). В нём вы можете легко настроить, как Angular собирает приложение и даже то, как эта сборка делается средой окружения.

  5. Говоря о среде окружения, Angular имеет в своём составе простую и хорошо типизированную систему управления этой самой средой.

Ещё много есть того, чего я возможн коснусь, но и это уже очень круто, да ещё и прямо из коробки. В React вы такого не получите. Что ещё даёт вам CLI?

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

ng generate component my-component OR ng g c my-component

Существует два способа как создать компонент и они довольно долгие, по сравнению с короткой командой, что довольно приятно. Эта команда делает следующее:

  1. Создаёт директорию с именем my-component и помещает в него пустой компонент.

  2. Автоматически генерирует unit tests для данного компонента.

  3. Автоматически встраивает ваш компонент в инфраструктуру приложения.

Путь Angular

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

https://stackblitz.com/edit/angular-examples-modules

ng g m buttonng g c button

В этом примере у нас есть папка button. В этой папке есть модуль button, компонент button, тестовый файл, файл стилей и HTML файл.

Это было автоматически сгенерировано Angular CLI. Если вы заметили, команды имеют определённый порядок и на это есть причина. Во-первых нам нужен модуль, чтобы включить в него наш компонент button. Затем мы создаём компонент button и CLI автоматически импортирует его в модуль. Затем мы экспортируем наш компонент для того, чтобы мы могли использовать его в других модулях нашего приложения.

Чтобы всё было просто, давайте импортируем это в app.module.ts. Всё, что мы сделаем, это импортируем наш компонент ButtonModule в app.module.ts и потом включим его в раздел imports декоратора @NgModule приложения AppModule.

Вот и всё. Теперь мы можем использовать тэг <app-button></app-button> в app.component.html файле.

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

Доставьте меня из пункта А в пункт Б

Я хотел бы коснуться ещё одной фичи, которую предоставляет вам Angular из коробки. Это роутинг и ленивая загрузка модулей. Я не буду здесь сильно углубляться и приведу небольшой пример. Если вы хотите подробнее разобрать эти фичи, попробуйте это сделать своими руками в Angular.

Как хороший CLI, Angular имеет опции. Одна из них задействует роутинг во время создания инфраструктуры приложения.

ng new my-app --routing

На сегодняшний день вы можете просто выбрать опцию во время установки, без указания флага. Но флаг тоже можно указывать, если вы так больше предпочитаете. Когда вы выбираете routing, Angular создаёт файл роутинга за вас. Он будет служить вам в качестве основного конфигурационного файл. Если вы захотите создать модули для ваших страниц, вы также можете указать флаг routing. CLI тогда создаст конфиг роутинга для этого модуля.

В созданном конфиге роутинга вы сможете легко настроить ленивую загрузку модуля:

const routes: Routes = [   {   path: 'main',   loadChildren: () =>    import('src/app/routes/main/main.module').then((mod) => mod.MainModule)   },];

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

Переиспользование делает вашу жизнь проще

Итак, мы рассмотрели, как Angular использует Modules для организации страниц и компонент и хочу остановиться на этом немного подробнее. Вероятно, что вы уже знаете, что Angular использует TypeScript вместо обычного javascript. Это явное преимущество Angular использует в полной мере.

Полагаю, что вы уже говорите - React тоже использует TypeScript, и будете правы. Но React использует Typescript не в полной мере. Типизация - это только часть того, что делает TypeScript таким продвинутым. Другая часть состоит в том, что вы можете в полной мере использовать классы и декораторы. React дал ясно понять, что движется в сторону функциональных компонент, а не в сторону компонент основанных на классах, что отнимает половину всей мощи TypeScript. Теперь стало намного сложнее переиспользовать функциональность в моём приложении и оно стало более сложным в обслуживании.

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

@my-decorator()export class MyClass {}

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

Прежде чем я начну, мне хотелось бы поблагодарить Виктора Савкина за его превосходную статью, из которой я многое почерпнул об Angular Ivy.

Understanding Angular Ivy: Incremental DOM and Virtual DOM

В предыдущей статье я немного рассказывал о том, что виртуальный DOM это своего рода пожиратель памяти и что невозможно оптимизировать расход этой памяти (tree shaking), поскольку виртуальное дерево создаётся всякий раз заново при перерисовке. Всё изменилось с приходом Angular Ivy. Вы сможете прочитать как работает Ivy в статье Виктора. Я лишь приведу некоторые моменты из неё.

Angular Ivy использует так называемый инкрементный DOM. Идея заключается в том, что каждый компонент ссылается на набор инструкций, которые известны на стадии компиляции. И если некоторые инструкции не используются, то они исключаются из сборки.

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

Помните, как я ранее сказал: Я уже знаю, что вы думаете и мы к этому вернёмся? Давайте разберём это. В том месте статьи я уверен, что вы подумали про себя: это всё прелестно, но что если мне не понадобиться ВСЁ, что Angular предоставляет? Хорошо, ну вы сами вдумайтесь, если вы не используете какую-то часть Angular, она просто не попадёт в сборку! Их технология оптимизации сборки постоянно улучшается и вы получите более стройные билды, в особенности если вы не используете абсолютно всё, что есть в Angular.

Апгрейд - проще простого

Если вы читаете это, то вероятно вы, вы уже проработали каким-то разработчиком ПО. Полагаю, что не нужно объяснять какую боль может причинить переход на новую версию библиотеки или фреймворка. Апгрейды - это неотъемлемая часть жизни и вместе с Angular, они больше не будут болью.

Я долго работал с Angular и видел все апдейты с первого релиза (я даже работал с AngularJs, но предпочитаю не говорить об этом). Безусловно Angular прошёл долгий путь, как и CLI. Где в 2018 году в Angular CLI появилась ещё одна команда - ng update. Вы можете использовать её так:

ng update @angular/core

Дальше происходит магия. Все зависимости ядра Angular обновятся до последней версии. Если ваш код нуждается в обновлении, CLI сделает это за вас и, если нельзя, то скажет, где вам нужно самим вручную обновить свой код. Обновление до последней версии Angular займёт от нескольких секунд до нескольких минут, в то время, как с React это может занять от нескольких часов до нескольких дней (или даже недель).

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

Давайте свяжем всё вместе

Хорошо, в этой статье мы о многом поговорили, и мне кажется, что я бы хотел ещё многое сказать. Тем не менее, мне кажется я коснулся всех болевых точек, которые я упоминал в начале статьи.

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

Хорошая совместимость - уменьшение сложности обслуживания. Когда ваш проект растёт, стоимость обслуживания не будет возрастать экспоненциально. Наверное это самая большая проблема в React приложениях, которую превосходно решает Angular. Следуя тому же ходу мысли, мы также получаем всё, что нам нужно прямо из ядра Angular. Обработка форм? Пожалуйста. Роутинг? Пожалуйста. Ленивая загрузка? Пожалуйста. Я мог бы продолжить, но остановлюсь на этом. Даже если вы что-то не используете, то это не войдет в ваш билд, потому что всё, что в ядре Angualr является оптимизируемым деревом (tree shakable), включая рендеринг.

Чтобы завершить статью, скажу последнее. Используйте Angular. Смотрите видео на YouTube. Читайте документацию или учитесь, как у вас получается лучше всего. А затем смело бросайте React в мусор, потому что он вам больше не понадобится.

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

Подробнее..
Категории: Typescript , React , Reactjs , Angular , Angular2 , Upgrade

От одного приложения к сотне. Путь микрофронтенда в Тинькофф Бизнес

16.06.2021 12:16:05 | Автор: admin

Привет, меня зовут Ваня, недавно я выступил на CodeFest 11, где рассказал про путь Тинькофф Бизнеса на фронтенде от одного приложения к сотне. Но так как в ИT очень быстро все меняется, а ждать запись еще долго, сейчас я тезисно расскажу о нашем шестилетнем путешествии в дивный мир микрофронтенда!

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

Этапы развития

  1. Одно приложение на AngularJS в 20142015 годах.

  2. Миграция на Angular2.

  3. Утяжеление десяти приложений новой функциональностью.

  4. Переход к микросервисам и разбиение на 100 приложений.

На дворе начало 2015 года. К нам приходит бизнес и говорит: Мы хотим сделать зарплатный проект! Посмотрите, что есть сейчас на рынке по технологиям, и сделайте. Выбираем AngularJS, быстро создаем приложение. Спустя некоторое время аппетиты вырастают, мы создаем еще два сервиса. На этот момент фронтенд-приложения никак не взаимодействуют друг с другом.

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

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

В одном из проектов у нас было пять фронтенд-команд, в каждой по 35 человек, то есть в самый лучший момент в одном проекте работали 25 фронтендеров! Иногда было действительно больно: ты вот-вот замержишь свою задачку, но нет! Перед тобой кто-то успевает и все твои пайплайны начинают проходить заново! До сих пор мне не по себе от этих воспоминаний.

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

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

Сайдбар

Первые три приложения мы подружили между собой с помощью сайдбара.

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

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

Подсвеченная область отдельное приложение СайдбарПодсвеченная область отдельное приложение Сайдбар

Frame Manager

Именно рваные переходы мы убрали с появлением Frame Manager'а (далее буду называть его ФМ).

Подсвеченная область отдельное приложение Frame ManagerПодсвеченная область отдельное приложение Frame Manager

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

Слева концепция сайдбара (было), справа Frame Manager'а (стало)Слева концепция сайдбара (было), справа Frame Manager'а (стало)

Для клиента ФМ так же не заметен, как и сайдбар, но для нас он выполняет уже больше функций. Кроме рейтинга, списка продуктов и авторизации он может передавать состояние между приложениями, например через глобальный window. Пользователь больше не замечает, что ходит между несколькими отдельными приложениями, так как верхняя часть у него никуда не пропадает и визуально весь процесс выглядит как подгрузка контента на одну страницу.

В плане интеграции приложения тоже все поменялось:

  • Раньше приложению-клиенту достаточно было подключить необходимый скрипт к себе в index.html.

  • Теперь все приложения ФМа хранятся в отдельной конфигурации и используются как единый источник правды.

Минус этого подхода мы все равно остались с iframe, который нам не особо нравился из-за особенностей работы с ним.

Однажды через поддержку к нам обратились пользователи с ситуацией: Раньше у меня работал плагин для Google Chrome, а с недавнего времени именно на вашем сайте перестал. Почините, пожалуйста! Обычно на такие просьбы не реагируют: пользователь что-то себе установил пусть сам и разбирается. Но только не в нашей компании. Команда долго изучала вопрос, смотрела, какое окружение у клиента, версия браузера и все-все, но ответа так и не было. В итоге мы полностью повторили окружение, загрузили себе плагины и путем дебагинга установили, что данный плагин не работает, если у iframe динамически менять атрибут src или пересоздавать фрейм. К сожалению, мы так и не смогли исправить такое поведение, поскольку на этой концепции построено все взаимодействие ФМ и дочерних приложений.

Бесфрейм-менеджер

Однажды мы собрались и подумали: Несколько лет страдаем от iframe. Как перестать страдать? Давайте просто уберем его! Сказано сделано. Так и появился бесфрейм-менеджер с фантазией у нас, конечно, не фонтан ;-)

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

В решении три составляющие:

  1. Webpack-плагин основа нашего решения, подробнее о которой можно прочитать в статье Игоря.

  2. Angular builder обвязка для настройки и запуска плагина.

  3. Angular schematics скрипт для упрощения работы с файловой структурой с помощью AST.

В 2021 году плагин становится менее актуальным, потому что вышел Webpack 5 с Module Federation, но напомню, что мы вели разработку в 2018 году, а Angular стал поддерживать последнюю версию вебпака лишь с двенадцатой версии, которая вышла 12 мая 2021 года. Мы пока не уверены, сможет ли MF заменить наше решение, и изучаем комбинацию подходов.

Что же касается других решений, на которые можно было перейти для отказа от iframe, то это Single SPA. Он всем хорош и очень популярен, но в плане Angular есть небольшой дисклеймер.

http://personeltest.ru/aways/single-spa.js.org/docs/ecosystem-angular/https://single-spa.js.org/docs/ecosystem-angular/

Мы понимали, что глобально менять концепцию фреймворка нам никто не даст, потому решили доделать имеющийся.

Что же касается Angular builder и schematics, то они нужны, чтобы разработчики, которые будут интегрировать наше решение к себе, не выполняли километровую инструкцию, а просто написали в консоли:

ng update @scripts/deframing

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

Тестирование

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

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

  2. Сам ФМ определяет несколько жизненно важных процессов, работоспособность которых гарантирует при любых условиях: это авторизация, роутинг, работа с данными приложений. Для этого создаются приложения-стабы (stub), суть которых подключиться к ФМу и выполнить одну из вышеперечисленных функций. То есть на каждое изменение кодовой базы ФМа будет гарантированно работать эта функция.

Ну и как же обойтись без описания багов, которые мы встретили на своем пути. Их тоже можно поделить глобально на две группы: накопление стилей и сторонние библиотеки.

Накопление стилей пользователь гуляет между приложениями, и у него накапливается состояние. Допустим, один из разработчиков написал:

.my-pretty-header {    display: none;}

Если у кого-то из следующих приложений есть такое же название класса, этот стиль применится так же!

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

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

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

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

Microzord

Вот мы и прошли шесть лет технического развития нашего решения. И что может быть лучше, чем поделиться этим опытом с сообществом? Все наработки будут публиковаться под npm scope @microzord с открытым кодом на Гитхабе.

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

Подробнее..

Перевод Okta безопасный доступ к приложениям на Angular Spring Boot

30.04.2021 20:15:00 | Автор: admin

Перевод статьи подготовлен в рамках набора учащихся на курс Разработчик на Spring Framework.

Приглашаем также всех желающих на открытый демо-урок Конфигурация Spring-приложений. На занятии рассмотрим, как можно конфигурировать Spring Boot приложения с помощью свойств:
- properties vs. YAML
- @Value + SpEL
- @ConfigurationProperties
- Externalized конфигурация.
Присоединяйтесь!


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

Okta это облачный сервис, который позволяет управлять удостоверениями и доступом. Согласно документации Okta, это сервис корпоративного уровня, который создан для управления удостоверениями в облачной среде, но при этом совместим и со многими локальными приложениями. С его помощью ИТ-специалисты могут управлять доступом сотрудников к любому приложению или устройству. Сервис развернут на надежной и защищенной облачной платформе, которая обладает широкими возможностями аудита и тесно интегрируется с локальными приложениями, службами каталогов и системами управления удостоверениями. Подробную информацию о том, как Okta интегрируется с приложениями, можно получить по этой ссылке.

Для авторизации пользователей в Okta используется протокол OAuth2. Подробнее о протоколе OAuth2 и стандарте Open ID Connect (OIDC) можно почитать в блоге Okta.

В этой статье мы рассмотрим демонстрационное приложение Контакты и настройку разрешений на просмотр, создание, редактирование и удаление контактов. Это вторая часть цикла статей о приложении Контакты, посвященная его защите с помощью Okta. О базовом приложении подробно написано в первой статье цикла: Разработка веб-приложения на Spring Boot + Angular.

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

В итоге наше приложение будет иметь следующую архитектуру:

Компонентная архитектура приложения Контакты, использующего протокол OAuthКомпонентная архитектура приложения Контакты, использующего протокол OAuth

О том, как контейнеризировать это приложение и развернуть его в кластере Kubernetes, можно узнать по ссылкам:

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure

Конфигурация на портале разработчика Okta

Для начала нам нужно настроить учетную запись Okta и добавить приложение в Okta. Здесь можно создать учетную запись разработчика бесплатно.

После создания учетной записи мы можем добавить наше приложение в консоль администрирования Okta. Для этого выберем вкладку Applications (Приложения) и щелкнем Add Application (Добавить приложение).

Выберем в качестве платформы Single Page App (Одностраничное приложение) и настроим его так, как показано на скриншоте ниже. Здесь мы отметим только опцию Authorization code (Код авторизации), поскольку нам не нужно, чтобы токен возвращался непосредственно в URL-адресе (о связанных с этим рисках безопасности написано в этой статье в блоге Okta).

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

OAuth предлагает различные потоки авторизации (authorization code flows) выбор конкретного потока зависит от сценария использования приложения. О том, какой поток OAuth выбрать, можно прочитать в этой статье на Medium или в документации Okta. Пользовательский интерфейс нашего демонстрационного приложения представляет собой одностраничное приложение на Angular, поэтому выберем неявный поток (implicit flow) с дополнительной проверкой для обмена кодом (Proof Key Code Exchange, PKCE).

Авторизация запросов через сервер авторизации

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

Чтобы увидеть сервер авторизации на портале Okta, нужно выбрать пункт меню, показанный ниже на скриншоте.

Добавление заявлений о группах (groups claims) в приложение Контакты

Для демонстрации создадим группу Admin. Администратор будет обладать правами на создание/редактирование/удаление.

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

На скриншотах ниже показано, как мы создали группу, связали ее с нашим приложением и включили заявление о группах в токен доступа.

Управление группамиУправление группамиНазначение групп/пользователей для приложенияНазначение групп/пользователей для приложенияДобавление заявления о группах в токен доступа, выдаваемый сервером авторизацииДобавление заявления о группах в токен доступа, выдаваемый сервером авторизации

Настроив проект через консоль администрирования Okta описанным выше способом, можно приступать к его интеграции с приложением Контакты для защиты доступа к приложению.

Настройка Okta в клиентской части приложения

Для защиты клиентской части приложения Контакты воспользуемся SDK-библиотекой Okta для Angular. Мы будем авторизовывать пользователя, перенаправляя его на конечную точку авторизации, настроенную для нашей организации в Okta. Библиотеку можно установить с помощью npm:

npm install @okta/okta-angular --save

Чтобы реализовать этот промежуточный слой, после установки нужно будет указать значения из клиента OIDC, полученные на предыдущем этапе. Также понадобится URL-адрес организации в Okta его можно узнать на домашней странице консоли разработчика Okta.

В файле app.module.ts создадим объект конфигурации и в нем установим true для параметра PKCE.

const oktaConfig = {issuer: 'https://{yourOktaDomain}/oauth2/default',redirectUri: window.location.origin + '/implicit/callback',clientId: 'your clientId',pkce: true};

Этот объект нужно добавить в раздел providers. Также импортируем OktaAuthModule:

import { OktaAuthModule, OktaCallbackComponent } from '@okta/okta-angular';import {OKTA_CONFIG} from '@okta/okta-angular';....imports:[...OktaAuthModule...]providers: [...{ provide: OKTA_CONFIG, useValue: oktaConfig }...]

В модуле обработки маршрутов приложения зададим защиту маршрута, воспользовавшись Route Guard из Angular. Вот фрагмент кода из app-routing-module.ts:

import { OktaCallbackComponent } from '@okta/okta-angular';...const routes: Routes = [{ path: 'contact-list',canActivate: [ OktaAuthGuard ],component: ContactListComponent },{ path: 'contact',canActivate: [ OktaAuthGuard, AdminGuard ],component: EditContactComponent },{path: 'implicit/callback',component: OktaCallbackComponent},{ path: '',redirectTo: 'contact-list',pathMatch: 'full'},];

Здесь мы защищаем просмотр контактов с помощью AuthGuard, а создание, обновление, удаление контактов с помощью AdminGuard и OktaAuthGuard.

Метод canActivate в OktaAuthGuard используется для проверки того, прошел ли пользователь процедуру аутентификации. Если нет, он перенаправляется на страницу входа Okta; если да, мы позволяем ему просматривать страницу, на которую ведет маршрут.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.authenticated = await this.okta.isAuthenticated();if (this.authenticated) { return true; }// Redirect to login flow.this.oktaAuth.loginRedirect();return false;}

Аналогичным образом метод canActivate в AdminGuard проверяет, принадлежат ли вошедшие в систему пользователи к группе Admin; если нет, запрос на доступ к маршруту отклоняется и выводится предупреждение.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.user = await this.okta.getUser();const result = this.user.groups.find((group) => group === 'Admin');if (result) {return true;} else {alert('User is not authorized to perform this operation');return false; }}

Ниже показана последовательность операций, которые выполняются, когда пользователь пытается обратиться к маршруту /contact-list для просмотра списка контактов.

Последовательность операций при выводе контактов на экранПоследовательность операций при выводе контактов на экран

Ниже представлена последовательность операций, демонстрирующая принцип работы PKCE. В случае одностраничного приложения, если токен возвращается непосредственно как часть URL-адреса перенаправления, любое расширение браузера может получить доступ к токену еще до того, как его увидит приложение. Это представляет потенциальную угрозу безопасности. Для снижения этого риска в PKCE вычисляется хэш SHA256 от случайной строки верификатора кода (code verifier), и этот хэш включается в запрос кода доступа как контрольное значение (code challenge). Когда поступает запрос на обмен токена доступа на код доступа, сервер авторизации может снова вычислить хэш SHA256 от верификатора кода и сопоставить результат с сохраненным контрольным значением.

Библиотека для Angular производит все эти операции за кадром: вычисляет хэш от верификатора кода с помощью алгоритма SHA256 и обменивает токен на код доступа, сопоставляя верификатор кода с контрольным значением.

Неявное разрешение на доступ (implicit grant) с использованием PKCE в приложении КонтактыНеявное разрешение на доступ (implicit grant) с использованием PKCE в приложении Контакты

Настройка Okta в серверной части приложения

Для того чтобы приложение Spring Boot поддерживало Okta, нужно установить следующие зависимости:

<dependency><groupId>com.okta.spring</groupId><artifactId>okta-spring-boot-starter</artifactId><version>1.2.1</version><exclusions><exclusion><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.4.0.RELEASE</version></dependency>

Spring Security OAuth2 и Okta Spring Boot Starter следует добавить в classpath, тогда Spring настроит поддержку Okta во время запуска.

Укажем следующие значения конфигурации для настройки и подключения Spring к приложению Okta:

okta.oauth2.client-id=<clientId>okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/defaultokta.oauth2.groups-claim=groups

В классе SpringSecurity нужно указать, что приложение Spring Boot является сервером ресурсов:

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)@Profile("dev")@Slf4jpublic class SecurityDevConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception{http.cors().and().csrf().disable();http.cors().configurationSource(request -> new CorsConfiguration(corsConfiguratione()));// http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()// .and().http.antMatcher("/**").authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll().antMatchers("/").permitAll().anyRequest().authenticated();http.oauth2ResourceServer();}

При такой конфигурации конечные точки нашего приложения защищены. Если попробовать обратиться к конечной точке /contacts по адресу http://localhost:8080/api/contacts, будет возвращена ошибка 401, поскольку запрос не пройдет через фильтр аутентификации.

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

@EnableGlobalMethodSecurity(prePostEnabled = true)

На конечных точках, которые мы хотим защитить, можно выполнять проверку ролей с помощью PreAuthorize следующим образом:

@PreAuthorize("hasAuthority('Admin')")public Contact saveContact(@RequestBody Contact contact,@AuthenticationPrincipal Principal userInfo) {

Для получения сведений о вошедшем в систему пользователе можно использовать объект Principal (@AuthenticationPrincipal).

Защита доступа к API из клиентской части приложения с помощью токена доступа

Теперь Okta защищает и клиентскую, и серверную часть нашего приложения. Когда клиентская часть направляет запрос к API, она должна передавать токен доступа в заголовке авторизации. В Angular для этого можно использовать перехватчик HttpInterceptor. В классе перехватчика авторизации можно задать токен доступа для любого HTTP-запроса следующим образом:

private async handleAccess(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {const accessToken = await this.oktaAuth.getAccessToken();if ( accessToken) {request = request.clone({setHeaders: {Authorization: 'Bearer ' + accessToken}});}return next.handle(request).toPromise();}}

Прямой доступ к API приложения через внешний сервер API

Если все настроено правильно, пользовательский интерфейс нашего приложения сможет получать доступ к API серверной части. В корпоративных приложениях часто присутствует несколько микросервисов, к которым обращается пользовательский интерфейс приложения, а также ряд микросервисов, доступ к которым открыт непосредственно для других приложений или пользователей. Так как наше приложение настроено как одностраничное, в настоящее время оно поддерживает доступ только из JavaScript-кода. Чтобы открыть доступ к микросервисам как к API для других потребителей API, нужно создать в Okta серверное приложение с типом веб-приложение, а затем создать микросервисы как одностраничные приложения и разрешить единый вход для доступа к ним (технически мы имеем дело с двумя разными приложениями в Okta).

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

Последовательность операций при прямом доступе к серверному APIПоследовательность операций при прямом доступе к серверному API

Я создал сервер API, тоесть микросервис-обертку, предоставляющий список API, к которым мы хотим открыть доступ как к серверным API.

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

Она выступает в роли прокси-сервера для основного серверного API нашего приложения и открывает доступ к необходимым сервисам. В ней используется библиотека Spring Cloud OpenFeign, предназначенная для написания декларативных REST-клиентов. Такая обертка представляет собой очень простой способ создания клиентов для веб-сервисов. Она позволяет сократить количество кода при написании потребителей веб-сервисов на базе REST, а также поддерживает перехватчики, позволяющие реализовать любые промежуточные операции, которые должны выполняться до направления запроса к API. В нашем случае мы будем использовать перехватчик для задания токена доступа и распространения прав доступа пользователей. Дополнительные сведения о Feign доступны по этой ссылке.

При правильной настройке все конечные точки нашего приложения, а также компоненты клиентской части приложения будут надежно защищены. Доступ к нему будет открыт только для авторизованных пользователей. О том, как контейнеризировать это приложение и развернуть его в кластере Kubernetes, можно прочитать по ссылкам:

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure


Узнать подробнее о курсе Разработчик на Spring Framework

Смотреть вебинар Конфигурация Spring-приложений

Подробнее..

Angular Moscow Meetup 19 наш митап и там, и тут передают

19.05.2021 12:19:31 | Автор: admin

После длительного перерыва Angular Moscow Meetup наконец вернулся, да еще и в новом гибридном формате! 28 апреля мы собрали в офисе почти 100 человек и еще столько же людей следили за происходящим в прямой трансляции. Мероприятие длилось больше двух часов, мы послушали два доклада и круглый стол с экспертами, среди которых аж три GDE по Angular.

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

Инвертируем зависимости правильно Владимир Агеев, Тинькофф

Владимир показал реальную и довольно хитрую бизнес-задачу, которую он решал со своей командой. Рассказал он и о том, как делать не стоит. В итоге задачу красиво решили с помощью Open-Closed и Dependency Inversion принципов.

Облачный Nx Иван Ишмаметьев, Тинькофф

Иван рассказал об эволюции фронтенда в Тинькофф Бизнес от одной огромной монорепы к многим маленьким монорепам, описав плюсы и минусы каждого подхода.

После он достаточно подробно остановился на Nx, разобрал его преимущества и то, какие проблемы он помог решить. Маствотч для тех, у кого огромные репозитории с долгими билдами на Pull Requestах.

Круглый стол Как не дать Angular-сообществу умереть

Мероприятие завершил круглый стол (хоть и не совсем круглый, и не совсем стол).

На пуфиках расположились лидеры русскоязычного сообщества Angular: Алексей Охрименко, Андрей (Angular Fox) Греков, Александр Инкин, Игорь Кацуба и я между ними. Еще позвали гостем Алексея Корнеева активного драйвера PHP-сообщества.

Тема была немного провокационной, но разговор быстро вышел в позитивное русло. Похоже, Angular-сообщество по-прежнему медленно, но верно процветает. Мы даже пришли к выводу, что и в PHP-сообществе все живенько и хорошо. Посмотрите сами:

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

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

Приходите к нам

У нас на постоянной основе открыт Call For Papers. Можно предложить полноценный доклад, лайтнинг доклад на 510 минут или даже просто тему, о которой было бы интересно послушать:

https://github.com/AngularMoscow/Talks/issues/new/choose

Что еще есть

Вот здесь у нас все фотографии с офлайн-ивента: ссылка на VK.

Большое спасибо телеграм-каналам angular_ru и angular_fox за информационную поддержку!

А еще можно подписаться на канал itstinkoff, там регулярно выходят анонсы чего-нибудь нового по Ангуляру.

До новых встреч!

Подробнее..

Сам себе DevOps строим cloud-only CI для веб приложения

22.05.2021 18:12:52 | Автор: admin

Привет, Хабр! Сегодня мы немного поговорим о DevOps и самоорганизации на примере одного из наших проектов.

Начнем с фразы, с которой не соглашается добрая половина разработчиков в индустрии: "каждый разработчик должен быть сам себе DevOps". Кто-то считает, что этим должен заниматься отдельно выделенный человек, чтобы у разработчика оставалась забота только о качестве кода. А кому-то свойственно думать о конвейере доставки кода в той же степени, как и о самом коде. Я же считаю, что в современных реалиях рынка и избытке инструментов/знаний разработчик должен уметь настроить и обслуживать конвейер быстрой и предсказуемой доставки артефакта в нужную ему среду. В отличие от мобильных разработчиков, для которых вопросы инфраструктуры и доставки приложения в большей степени решены самим вендором (Google и Apple), backend и web разработчики должны если не владеть, то хотя бы интересоваться практиками доставки кода.

И речь не идет о настройке каких-то больших и громоздких билд-систем, для которых обычно приносится в жертвую целая штатная единица. Нет. DevOps - не человек, а система ежедневных маленьких привычек, основанных на самоорганизации. Понятие, взрастающее снизу вверх, а не сверху или в бок. И если вы, как разработчик, смогли ускорить поток артефактов (любимое американцами понятие "Value Stream") на небольшой процент, то поздравляем - это уже DevOps way. Рекомендуем прочесть книгу "DevOps Handbook" by Gene Kim - лучшая книга для понимания этого концепта (ссылка в конце статьи).

В этой статье мы представим вам маленькую историю зарождения DevOps в нашей команде, позволившую нам ускорить разработку проекта. Эта история применима как к разработчику-одиночке, так и к большой команде.

Кто

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

  • 3 фронтенд разработчика с кучей пулл реквестов в день

  • 2 тестировщика, бастующие за улучшение QX (QA experience)

Что

Клиентское и администраторское web-приложения на Angular 9.0, собираемые из одного репозитория.

Где

Моя команда известна как ярый адепт продуктов Atlassian, поэтому вся экосистема нашего проекта живет в "австралийских облаках":

  • задачи и релизы в Jira

  • код в Bitbucket

  • CI в Bitbucket Pipelines

  • подробная документация в Confluence.

Наша команда использует стандартный план Bitbucket за $4/чел, включающий в себя 1500 минут сборки в Bitbucket Pipelines. О нем в сегодняшней статье и пойдет речь. Принцип работы и синтаксис настройки на 90 процентов похожи на Gitlab CI, поэтому любому пользователю Gitlab вся схема работы будет максимально понятной.

Сама система интернет банкинга разбита на микросервисы и работает в контейнерах на серверах Банка. Но в этой статье речь будет идти не о контейнерах, хотя настройка CI с помощью Docker-образов звучит очевидным.

Немного контекста

Первые наши шаги в DevOps и конкретно в улучшении QX (QA experience) мы начали задолго до этого в проектах мобильных приложений. Мы интегрировали между собой Jira, Bitbucket и сервис Bitrise.io во всех наших пулл-реквестах, что позволило иметь на выходе конкретный билд на каждый коммит по конкретной задаче. Для наглядности: тестировщик понимал, что пулл реквест 30 выдает билд приложения 170, в которой нужно тестировать Jira-задачу 500. Если вкратце описать процесс пулл-реквестов, то обязательными требованиями к слиянию пулл-реквеста являются

  • Зеленый билд на последнем коммите

  • Добро от разработчика-ревьюера

  • Добро от тестировщика

Если один из этих шагов давал красный свет, то пулл-реквест проходит все шаги заново.
Такой процесс позволяет нам обеспечить высокое качество кода и продукта в стабильной ветке репозитория. Мы с высокой долей уверенности можем релизить приложение, собранное с master (мы начали работать по trunk-based development и поэтому master наша стабильная ветка).

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

Подобного механизма пулл-реквестов никогда не существовало в web приложениях. Мы всегда делали приемку задач после слияния пулл-реквестов в стабильную ветку, из-за чего каждый третий коммит в ней был дефектным. Настроить такой же процесс приема пулл реквестов, как в мобилке, было для нас очевидным шагом. Сделать CI окружение для web приложения на инфраструктуре Банка было для нас слишком долгой историей, потому что хотелось настроить и поехать очень быстро. А все, кто работал с большими банками, почувствовал "скорость" продвижения задач по железу. Все процессы, что мы опишем в этой статье, мы планируем воссоздать в инфраструктуре банка с помощью оркестратора (Kubernetes или OpenShift, на усмотрение заказчика), но это уже другая история. В тот момент нам нужно было как можно быстрее начать работать правильно.

Первый очевидный вопрос: куда доставлять? Мы начали присматриваться к разным вариантам: Heroku, AWS, Netlify, Surge итд. В итоге остановились на использовании AWS S3. Для тех, кто думал, что S3 это всего лишь файловое хранилище - S3 может работать как сайт и его можно привязать к доменному имени. Подробнее об этом можно прочитать на страничке AWS.

Так почему же AWS?

  • Доступная цена. При всей репутации AWS как дорогой экосистемы, ежемесячные счета за S3 выходят в среднем 2 доллара при следующих метриках:

    • Новых ПР в день ~ 2

    • Пайплайнов в день ~ 12

    • Кол-во единовременно существующих бакетов ~ 5

    • Средний размер бакета = 13 Mb

  • У AWS отличный API и CLI. у "Surge" и других легковесных сервисов хостинга не настолько качественный и полноценный тулинг, как у Amazon AWS. Надо отметить, что CLI и документация Heroku не уступает Амазону, но высокий на наш взгляд порог вхождения и специфика работы Heroku Dynos заставили нас отойти от его выбора.

  • У команды уже был опыт работы с продуктами AWS.

Можно было бы настроить весь этот процесс в контейнерах в самом Amazon, но это повлечет за собой запуск EC2 машин. Даже с использованием Docker Hub вместо Elastic Container Registry, прогноз затрат вываливался у нас за $100 в месяц. В конечном итоге у нас получилась именно та схема работы с пулл-реквестами, которую мы представляли себе в самом начале. Но давайте проанализируем каждую ступень нашей эволюции и посмотрим на принятые решения.

Уровень 1: создание S3 бакета

Мы начали с того, что создали по одному выделенному S3 bucket для хостинга клиентского и админского приложений. Настроили конфигурацию сборки нашего проекта (bitbucket-pipelines.yml), чтобы он собирал приложения (html/css/js/img) и заливал их в соответствующий S3 bucket. В начале был использован AWS CLI, но, как оказалось, Bitbucket предоставляет набор готовых официальных Pipes (аналог Github actions), среди которых оказался Pipe для выгрузки файлов в S3 bucket. В итоге: тестировщик имеет сайт, на котором он может проверить реализацию задачи пулл-реквеста с постоянной условной ссылкой web.s3-website.ap-northeast-2.amazonaws.com.

Обязательным предварительным шагом при создании бакета через консоль AWS является включение опции "Enable static hosting" в настройках бакета. Без этой опции bucket является просто файловым хранилищем.

- step:      name: Build and deploy webadmin PR version into AWS for QA      caches:        - node      script:        # начальная конфигурация        - apk update && apk add git        - npm install        # сборка        - npm run build:admin        - cd dist/admin        # загрузка в S3        - pipe: atlassian/aws-s3-deploy:0.2.4          variables:            AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID            AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY            AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION            S3_BUCKET: $S3_WEBADMIN_BUCKET_NAME            DELETE_FLAG: 'true'            LOCAL_PATH: $(pwd)            ACL: 'public-read'

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

Оценка:

  • за старания - четверка

  • за QX - двойка

Уровень 2: выделение S3 bucket под каждого автора

В ответ на обратную связь от тестировщиков командой было решено выделить по одному S3 bucket на каждого фронтенд разработчика. В нашем проекте были разработчики Манар, Миша - следовательно были созданы условные S3 бакеты jsn-web-manar и jsn-web-michael. В bitbucket-pipelines.yml в step для пулл-реквестов была добавлена логика определения конечного S3 бакета в зависимости от PR автора.

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

  1. Гонка пулл-реквестов одного автора. Если один и тот же разработчик создаст 3 параллельных пулл-реквеста, то все они вызовут запуск пайплайна сборки. Мы не можем точно знать, какой из пайплайнов закончится быстрее. Команде, в частности тестировщику, без использования консоли Chrome сложно понять, какой из пулл-реквестов сейчас развернут на S3 бакете разработчика Михаила.

  2. Появление нового автора. В наших репозиториях создавать пулл-реквест может любой член команды, поэтому эта схема сломалась ровно в тот момент, когда ПР создал кто-то, кроме фронтенд-разработчиков. По нашей тривиальной логике определения бакета его запущенный пайплайн "угонит" S3 бакет одного из разработчиков. В итоге другой тестировщик может потерять version-under-test сайт прям в момент тестирования.

  3. Смена никнейма. Наши разработчики забавы ради любят менять свои git author name время от времени. Для нас это никогда не являлось проблемой до того, как мы применили логику с бакетами на каждого автора. К сожалению, Bitbucket Pipelines из коробки не предоставляют возможности определения автора по его Jira account, поэтому в логике присвоения бакета пришлось оперировать стандартным commit git author. Как вы и сами догадались, при смене имени с "Manar Kurmanov" на "Dark Lord" повторилась ситуация из пункта 2 - был угнан бакет другого разработчика.

С этой шаткой схемой команда прожила еще несколько месяцев.

Оценка:

  • за старания - четверка

  • за QX - тройка

Уровень 3: добавление штампа авторства в web приложение

Команда решила проблему гонки пулл-реквестов добавлением пояснительного текста в footer сайта:

Каждый пайплайн добавлял в футер сайта название ветки, автора и timestamp. Таким образом решалась проблема параллельных пулл-реквестов от одного автора - тестировщик четко понимает, какая Jira-задача разработчика Георгия представлена в бакете.

Фрагмент из bitbucket-pipelines.yml

- step:    name: Build PR version    caches:      - node    script:      # initial configuration      - apk update && apk add git      - npm install      # preparing site footer text      - TIMESTAMP_FILE="./src/app/some/folder/copyright.timestamp.html"      - GIT_AUTHOR=$(git log -n 1 --format=format:'%an')      - PR_URL="$BITBUCKET_GIT_HTTP_ORIGIN/pull-requests/$BITBUCKET_PR_ID"      - BRANCH_TEXT="PR branch <a href=\\"$PR_URL\\">$BITBUCKET_BRANCH</a><br>"      - echo $BRANCH_TEXT >> $TIMESTAMP_FILE      - echo "Author $GIT_AUTHOR<br>" >> $TIMESTAMP_FILE      - echo "Built at $(TZ=UTC-6 date '+%d-%m-%Y %H:%M') <br>" >> $TIMESTAMP_FILE      - echo "</small>" >> $TIMESTAMP_FILE      - cat $TIMESTAMP_FILE > src/app/target/folder/copyright.component.html      # building artefacts      - npm run build    artifacts:      paths:        # кеширование артефактов для следующего Build Step         - dist/web/**

Казалось бы, +100 к QX, куда еще прозрачнее. Но поставьте себя на место тестировщика в ежедневной работе и вы поймете еще одно скрытое неудобство. Допустим, что разработчик создал 3 параллельных пулл-реквеста и тестировщик проверил сайт на S3 бакете. Что он должен делать дальше? Тестировщику не очевидно, что он находится в ситуации очереди ПР-ок на один и тот же S3 бакет. После он должен зайти в странице Pipelines, найти нужную ветку и сделать ручной Rerun.

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

Оценка:

  • за старания - четверка

  • за QX - тройка с плюсом

Уровень 4: динамичные бакеты под каждый пулл реквест

Мы решил копнуть глубже в возможности AWS API и воссоздать поведение динамических сред для тестировщиков и разработчиков. Какие были требования:

  • Каждый пулл реквест должен породить свой отдельный S3 бакет и задеплоить сайт туда.

  • Нужно, чтобы в комментарий к пулл-реквесту писалась ссылка на этот бакет при каждом новом билде.

  • Автоматика должна уметь подчищать за собой неиспользуемые бакеты

Для реализации этих требований не хватало стандартных Bitbucket Pipes, поэтому нужно было писать кастомные скрипты для взаимодействия с AWS S3. К счастью Bitbucket Pipelines, как и многие CI системы, является cloud-first и предоставляет возможность запускать свои пайплайны на базе любого публичного Docker образа. Мы использовали официальный образ aws-cli, включающий в себя AWS CLI и все базовые утилиты командной строки (curl, sed, xargs).

Ниже фрагмент из bitbucket-pipelines.yml по загрузке статики сайта в динамический бакет. NOTE: в скрипте используются ключи и секреты из учетной записи AWS S3, их можно сгенерировать по официальной инструкции.

- step:    name: Deploy PR version into AWS bucket for QA    image:      name: amazon/aws-cli    script:      # 1. Настройка сессии в aws cli с помощью ключей      - aws configure set aws_access_key_id=$AWS_ACCESS_KEY_ID aws_secret_access_key=$AWS_SECRET_ACCESS_KEY      # 2. определяем название для динамического бакета      - export BUCKET_NAME=web-pullrequest-$BITBUCKET_PR_ID      # 3. если в AWS нету бакета с таким названием, создаем его с нужными флагами      - if [ -z $(aws s3 ls | grep $BUCKET_NAME) ]; then aws s3api create-bucket --bucket $BUCKET_NAME --acl public-read --region ap-northeast-2 --create-bucket-configuration LocationConstraint=ap-northeast-2; fi      # 4. задаем это бакету настройку статичного хостинга      - aws s3api put-bucket-website --website-configuration "{\\"ErrorDocument\\":{\\"Key\\":\\"error.html\\"},\\"IndexDocument\\":{\\"Suffix\\":\\"index.html\\"}}" --bucket $BUCKET_NAME      # 5. очищаем содержимое бакета      - aws s3 rm s3://$BUCKET_NAME --recursive       # 5. заливаем в него собранные html/css/js      - aws s3 cp dist/web s3://$BUCKET_NAME --acl public-read --recursive      # 6. Пишем коммент со ссылкой от имени сервисной учетки в нужный пулл реквест      - export PR_API_URL=https://api.bitbucket.org/2.0/repositories/$BITBUCKET_REPO_FULL_NAME/pullrequests/$BITBUCKET_PR_ID/comments      - export BUCKET_PUBLIC_URL=http://$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com      - curl $PR_API_URL -u $CI_BB_USERNAME:$CI_BB_APP_PASSWORD --request POST --header 'Content-Type:application/json' --data "{\\"content\\":{\\"raw\\":\\"[http://$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com](http://personeltest.ru/away/$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com)\\"}}"

В качестве автора комментарий в пулл реквест мы использовали нашу сервисную учетную запись для CI с использованием App-specific password. В этой статье от Atlassian можно узнать, как создать такой пароль.

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

"Единственный ручной процесс в этой схеме - чистка неиспользуемых S3 бакетов раз в неделю. Зачем это автоматизировать?" - подумали мы. Но по закону жанра команда благополучно забывала подчищать бакеты и вспомнила об этом только после того, как бухгалтер показал счет на 25 долларов от AWS из-за скопившихся бакетов.

В итоге мы решили добавить логику чистки неиспользуемых бакетов в пайплайн при слиянии пулл-реквеста.

- step:    name: Remove dangling s3 buckets left after PR merges    image:        name: amazon/aws-cli    script:      # 1. Запросить список 10 последних MERGED пулл реквестов      - export API_URL="<https://api.bitbucket.org/2.0/repositories/$BITBUCKET_REPO_FULL_NAME/pullrequests?state=MERGED>"      - curl "$API_URL" -u $CI_BB_USERNAME:$CI_BB_APP_PASSWORD > pr_list.json      # 2. выделить бакеты, соответствующие спец-формату       - aws s3 ls | grep -o '[a-zA-Z\\-]\\+pullrequest\\-[0-9]\\+' > buckets.txt- set +e      # очистить все бакеты с номер ПР-ок, которые уже MERGED      # (AWS API требует очистки бакета перед его полным удалением)      - echo "$(cat pr_list.json | grep -o '"id":\\s[0-9]\\+')" | sed 's/[^0-9]//g' | xargs -I{} grep {} buckets.txt | xargs -I{} aws s3 rm s3://{} --recursive      # удалить все бакеты с номер ПР-ок, которые уже MERGED      - echo "$(cat pr_list.json | grep -o '"id":\\s[0-9]\\+')" | sed 's/[^0-9]//g' | xargs -I{} grep {} buckets.txt | xargs -I{} aws s3api delete-bucket --bucket {}

Оценка:

  • За старания пятерочка

  • за QX - четверка с плюсом. Почему не пять? Потому что на своей шкуре мы поняли, что улучшение любого X (QX, DevX, HX) - это бесконечный процесс

Технические ремарки

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

#1: По поводу CORS

Так как API запросы совершаются с одного хоста (.amazonaws.com) на другой хост (*.somebank.com), по умолчанию они будут блокироваться браузером из-за настроек CORS (cross origin resource sharing) сервера. Если вкратце, то браузер позволяет отправлять запросы только из того же хоста, откуда сайт был запрошен. Для примера, API на api.server.com будет принимать запросы только с сайта server.com. При попытке сделать GET запрос с сайта another.com браузер сначала совершит "pre-flight" запрос на сервер и поймет, что сервер строго выдерживает правило "same-origin-policy".

Для того, чтобы запросы со статичного сайта S3 бакета проходили в ваш API, вы должны добавить хост бакета в серверные настройки Headers.

Access-Control-Allow-Origin: <http://bucket.s3-website.amazonaws.com># илиAccess-Control-Allow-Origin: *

Во всех популярных фреймворках есть поддержка управления Cross Origin.

#2: По поводу расходов

В уровне 4 в скрипте присутствует строка очистки содержимого бакета:

aws s3 rm s3://$BUCKET_NAME --recursive 

Это микро оптимизация расходов компании на AWS. Мы на каждом запуске ПР пайплайна очищаем предыдущее содержимое бакета, чтобы в нем не скопились файлы от 4 предыдущих сборок одного и того же пулл реквеста.

Если этого не делать, то размер бакета будет увеличиваться пропорционально кол-ву пайплайнов на 1 ПР. В масштабах 3 разработчиков это экономит нам пару центов, но в масштабе десяток разработчиков и долгих ПР - это десятки долларов. Мы считаем, что это полезное упражнение как минимум с точки зрения практики владения AWS API.

ВАЖНО! Если в вашем проекте будет использоваться долгоживущий S3 bucket и вы будете использовать официальный aws-s3-deploy pipe, то убедитесь, что вы используете DELETE_FLAG. Этот флаг очищает bucket перед очередной выгрузкой файлов. Во время уровня #1 наша команда об этом флаге не знала в течение 2 месяцев и узнала только после обнаружения нескольких тысяч файлов в одном бакете. Поэтому парочку десяток американских долларов было сожжено во имя наших познаний.

# вызов пайпа загрузки файлов в S3 с флагом DELETE_FLAG- pipe: atlassian/aws-s3-deploy:0.2.4    variables:      AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID      AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY      AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION      S3_BUCKET: $S3_WEBADMIN_BUCKET_NAME      DELETE_FLAG: 'true' # не забыть этот флаг      LOCAL_PATH: $(pwd)      ACL: 'public-read'

Вывод

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

Финальную версию bitbucket-pipelines.yml можно посмотреть в github репозитории.

Материалы к прочтению

Подробнее..
Категории: S3 , Angular , Devops , Frontend , Amazon web services , Aws , Cicd , Pipelines , Bitbucket

Заметка о вариантах организации SassSCSS в Angular приложении

13.06.2021 14:11:44 | Автор: admin

Как структурировать sass/scss файлы в проекте Angular? Однажды задавшись этим вопросом, нашел несколько вариантов.

1 способ - держать все файлы стилей в папке /styles

В это случае мы создаем все файлы в папке /styles, соблюдая примерную структуру:

styles  base  components xxxxx

Одним из популярных способов, при таком структурировании файлов, может являться паттерн 7-1, предлагаемый в документации к sass. Подробнее можно ознакомиться тут по ссылке.

Плюсы такого подхода:

  1. Все файлы стилей в одном месте.

  2. Ускоряется билдинг стилей.

Минусы:

  1. Разработчик должен представлять где должен находится нужный файл.

  2. Файловая структура может стать грязной при большом проекте.

2 способ - держать каждый файл стиля в компоненте, а глобальные в папке /styles

В этом варианте, оставляем файлы стилей компонента к каждом компоненте а глобальные стили, выносим в /styles. Туда же выносим файлы с переменными и миксинами, которые потом импортируем в файлы стилей компонентов. Папка /styles выглядеть так же, как в первом способе, но без стилей компонентов.

Плюсы такого подхода:

  1. Компоненты имеют свои файлы стилей.

  2. Легче добиться чистой структуры файлов стилей.

Минусы:

  1. Билдинг файлов стилей может замедлиться при большом проекте.

3 способ (странный) - выносим все стили компонентов в единый файл, а глобальные в папку /styles

Похоже на первый вариант за исключением того что стили компонентов содержаться в одном файле а сам файл лежит в папке с компонентами.

Плюсы:

  1. Компоненты имеют свой единый файл стилей что обеспечивает быстрый доступ.

  2. Билдинг стилей происходит быстрее.

Минусы:

  1. Необходимо каждый раз добавлять импорт общего файла стилей компонентов, в файл стилей нового компонента.

Из трех найденных вариантов, второй выглядит более рациональным для Angular приложения. Первый скорее подойдет для проектов с версткой без фреймворков. Третий способ выглядит довольно странным, хотя возможно и реально может принести удобство.

А как вы организуете файлы стилей в Angular проектах?

Подробнее..
Категории: Css , Angular , Scss

Категории

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

  • Имя: Макс
    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-2023, personeltest.ru