ContentChild, ViewChild, template reference variables / Хабр | Веб-студия Nat.od.ua
ContentChild, ViewChild, template reference variables / Хабр
В Angular принято писать декларативный код. Это значит, что нам не стоит руками запрашивать нужные нам сущности. Во фреймворке есть инструменты для работы с элементами шаблона, которые помогут нам. О них сегодня и поговорим.
Кто есть кто
Для начала давайте разберемся, что такое вью и что такое контент.
Вью — это шаблон нашего компонента, директивы его не имеют.
Контент — это то, что оборачивает наш компонент или директива.
Для компонентов потребуется добавить тег ng-content в шаблон, иначе все содержимое заменится на шаблон компонента при рендере.
Совет по производительности: даже если ng-content спрятан за *ngIf и не прикреплен к документу, он все равно рендерится и проходит все циклы проверки изменений. Если нужна ленивая инициализация — используем ng-template.
ViewChild
Когда мы создаем компонент, может потребоваться доступ к частям его шаблона. Его можно получить через декоратор @ViewChild. Для начала нужно предоставить селектор — можно пометить элемент строкой:
А затем запросить его через декоратор: @ViewChild(‘ref’).
#ref называется template reference variable, и далее мы обсудим их детально.
Можно также использовать класс, если вам нужно получить компонент или директиву: @ViewChild(MyComponent). Это может быть DI-токен: @ViewChild(MY_TOKEN).
Запросы через декораторы обрабатываются в рамках лайфсайкл-хуков, ngOnInit, ngAfterViewInit и других. Подробнее о них — в следующей статье.
Особенно полезен второй аргумент декоратора — объект параметров. Первый ключ простой — static: boolean. Он говорит Angular, что некий элемент существует всегда, а не по условию:
static: true означает, что этот элемент будет доступен уже в ngOnInit;
static: false означает, что элемент появится только в ngAfterViewInit, когда весь шаблон будет проверен.
Значение по умолчанию — false, значит, результат будет получен только после прохождения первой проверки изменений.
Второй ключ — read. Первый аргумент декоратора говорит Angular: «найди мне инжектор, в котором есть данная сущность», а read говорит, что из этого инжектора взять. Если его не писать, то мы получим сам токен из первого аргумента.
Можно запросить любую сущность из целевого инжектора: сервис, токен, компонент и так далее. Чаще всего это используется, чтобы получить DOM-элемент целевого компонента:
@ViewChild(MyComponent, { read: ElementRef })
readonly elementRef?: ElementRef
Часто в @ViewChild вовсе нет нужды — во многих случаях отлично подойдет template reference variable и передача ее в обработчик события:
“// Обратите внимание, что тут HTMLElement, а не ElementRef
onClick(element: HTMLElement) {
element.focus();
}
Можно рассматривать template reference variable как некое замыкание в шаблоне — ссылка есть, но доступна только там, где нужна. Так код компонента остается чистым.
Template reference variable обращается к инстансу компонента, если поместить ее на него или к DOM-элементу, если компонента там нет. А еще можно получить сущность директивы, для этого используется exportAs в декораторе @Directive:
@Directive({
selector: ‘’,
exportAs: ‘someName’,
})
export class MyDirective {}
ViewChildren
Иногда нужно получить множество элементов одного типа. В такой ситуации можно использовать @ViewChildren — отметить множество элементов в шаблоне одной и той же строчкой и получить всю коллекцию.
Все вышесказанное применимо и тут, только static недоступен для списков и типом поля будет QueryList
Контент
Удобным способом кастомизации компонентов может стать контент. В Angular он напоминает слоты из нативных веб-компонентов и позволяет проецировать содержимое в разные участки шаблона с помощью тега ng-content.
ng-content позволяет гибко раскидать части содержимого в разные места с помощью атрибута select. Его синтаксис похож на selector в директивах и компонентах — можно завязываться на имена тегов, классы, атрибуты и комбинировать это всевозможным образом, вплоть до отрицания через :not(). Всегда можно оставить ng-content без селектора, чтобы все остальное содержимое угодило в него.
Важно помнить, что хоть контент и находится по DOM внутри вьюхи компонента, на самом деле он является частью родительского вью и следует его циклам проверки изменений. Это значит, что если элемент в контенте будет помечен для проверки, к примеру, через наступление события из @HostListener, то вью оборачивающего контент компонента проверен не будет. Поэтому если компонент зависит от контента, убедитесь, что вы не пропустите изменения в нем.
changes: Observable
ContentChild и ContentChildren
Обращаться к контенту можно так же, как к шаблону компонента. В Angular есть аналогичные декораторы @ContentChild и @ContentChildren с тем же синтаксисом.
Вот пример компонента меню, в котором элементы передаются в виде контента. Это позволяет разработчику завязаться на события, такие как клики или нажатия клавиш, не забивая этим логику меню. Сам же компонент отвечает за навигацию с клавиатуры.
Кое-что в синтаксисе для декораторов контента отличается от вью. В них появляется дополнительный параметр опций descendants: boolean. Он позволяет получать детей из контента, находящихся в контенте другого вложенного компонента, но не в их вью:
С такими возможностями в Angular можно добиться очень многого. Практика в работе с вью и контентом позволит заметить, где эти знания помогут создавать надежные, легко поддерживаемые компоненты!
Самые необычные правила паролей / Хабр | Веб-студия Nat.od.ua
Самые необычные правила паролей / Хабр
В связи с ростом вычислительной мощности CPU и GPU правила генерации паролей периодически пересматриваются. Специалисты обычно ориентируются на показатель информационной энтропии (в битах). Но в отношении паролей это не универсальное мерило, потому что пространство возможных вариантов не равномерно, то есть различные элементы этого пространства встречаются с разными вероятностями. Поэтому существуют более эффективные техники вскрытия парольных хэшей, в том числе атаки по словарю, по радужным таблицам, а в последнее время — с применением генетических алгоритмов и нейросетей.
Поскольку единых парольных правил нет, многие компании публикуют собственные рекомендации, которые сильно отличаются друг от друга. Иногда эти правила весьма экстравагантые.
В коллекции Dumb Password Rules собрано несколько сотен сайтов с нестандартными правилами для паролей.
Например, некоторые сайты жёстко ограничивают не только минимальную, но и максимальную длину пароля. Например, в 14, 16 или 32 символа:
Другие запрещают использовать последовательность из трёх и более одинаковых символов:
Некоторые требуют вводить пароль только мышкой, выбирая шесть из десяти цифр:
У одного из банков пароль тоже обязательно вводить мышкой, выбирая символы на очень необычном экранном кейпаде (на КДПВ).
Примечание. Если ваша компания случайно оказалась в этом списке, можно подать пулл-реквест на удаление.
Для контраста, ниже приведены базовые рекомендации по генерации надёжных паролей от нескольких авторитетных организаций и компаний.
Рекомендации по генерации надёжных паролей
В 2017 году Национальный институт стандартов и технологий США (NIST) обновил парольные спецификации, которые рекомендуются в качестве стандартов NIST. Новые требования изложены в Специальной публикации NIST 800-63B, раздел 5.1.1.2 «Запоминаемые секретные верификаторы» (NIST, 2017).
Основные требования NIST 800-63B для верификаторов (компаний и организаций), которые устанавливают собственные парольные политики:
- Верификаторы не должны устанавливать правила составления паролей, например, требовать сочетания различных типов символов или запрещать последовательное повторение символов.
- Верификаторы не должны требовать произвольной или регулярной смены паролей, как, например, предыдущее правило 90 дней. С другой стороны, смена пароля должна быть обязательной в случае его компрометации.
- Пароли должны быть длиной не менее 8 символов.
- Системы паролей должны позволять абонентам выбирать пароли длиной не менее 64 символов.
- В паролях должны быть допустимы все печатные символы ASCII, символ пробела и символы Unicode.
- При установлении или изменении паролей верификатор должен сообщить абоненту, что ему необходимо выбрать другой пароль, если он выбрал слабый или скомпрометированный пароль.
- Верификаторы должны предлагать рекомендации, например, измеритель стойкости пароля, чтобы помочь пользователю выбрать надёжный пароль.
- Верификаторы должны хранить пароли в форме, устойчивой к офлайновым атакам. К паролям следует добавлять соль и хэшировать с помощью подходящей односторонней функции деривации ключа. Функции выработки ключа принимают на вход пароль, соль и требуемые вычислительные ресурсы (cost factor), а затем генерируют хэш пароля. Их цель — сделать каждую попытку угадывания пароля злоумышленником, получившим хэш-файл пароля, дорогостоящей и, следовательно, стоимость атаки на угадывание высокой или запретительной.
Дополнительно, в вопросах B05 и B06 в разделе FAQ документации NIST к обновлённым специальным публикациям (NIST, 2020) организация официально пояснила, что обязательная смена пароля «снижает общую безопасность системы паролей» и не должна использоваться. Согласно объяснению NIST, периодическая смена паролей снижает их энтропию. Хотя это довольно спорный вопрос, по которому нет единого мнения.
Кроме того, нет единого мнения и по всем остальным правилам гненерации паролей, включая минимальную длину, минимальную энтропию, наличие/отсутствие спецсимволов.
В качестве примера можно привести рекомендации по созданию надёжных паролей от ведущих компьютерных корпораций: Microsoft, Google и Apple.
Microsoft
Советы по созданию защищённых паролей:
- Минимум 12 символов, лучше от 14-ти
- Буквы в верхнем и нижнем регистрах, цифры и символы
- Запрещено использовать слова из словаря, а также имена людей, символов, продуктов и компаний
- Пароль должен значительно отличаться от предыдущих паролей пользователя
- Простая в запоминании, но трудная для подбора фраза. В качестве примера Microsoft приводит 6MonkeysRLooking
Google
Рекомендации для паролей:
- Минимум 12 символов
- Буквы в верхнем и нижнем регистрах, цифры и символы ASCII
Google рекомендует максимально увеличивать энтропию пароля, создавая очень длинные пароли. Чтобы их было легче запомнить, можно брать знакомые отрывки текста:
- Лирика из песни или стихотворения
- Осмысленная цитата из фильма или речи
- Отрывок из книги
- Серия слов, которые имеют для вас значение
- Аббревиатура: можно составить пароль из первой буквы каждого слова в предложении
Apple
Требования к паролю Apple ID:
- Минимум 8 символов
- Буквы в верхнем и нижнем регистрах
- Минимум одна цифра
Ну и полезная рекомендация для любых паролей — использовать хороший парольный менеджер, правила шифрования и резервного копирования приватных данных, а также двухфакторную авторизацию.
Если вам повстречался сайт или иная компьютерная система с экстравагантными парольными правилами, то можно добавить его в коллекцию Dumb Password Rules и упомянуть в комментариях к этой статье.
Хорошая замена Celery / Хабр | Веб-студия Nat.od.ua
Хорошая замена Celery / Хабр
В своей прошлой статье “Как подружить Celery и SQLAlchemy 2.0 с асинхронным Python” я разбирал возможность запускать асинхронные задачи “из-под Celery” и в комментариях мне сообщили о существовании ещё одной библиотеки под названием aio_pika. И признаться, о ней я раньше никогда не слышал. Оно и не удивительно, библиотека имеет всего в районе 1К звёзд на GitHub (по сравнению с 20К+ у Celery). Я рассмотрел абсолютно все популярные (500+ звёзд) решения и остановился именно на этом из-за активной (на текущий момент) разработке и относительной популярности.
Стек, который вы увидите в статье: FastAPI, RabbitMQ, aio_pika и docker. Статья будет полезна тем кто использует Celery в своих проектах, а так же тем, кто только слышал о том, что такое очереди и RabbitMQ.
Навигация:
Конфигурация RabbitMQ
Task router для consumer’a
Написание consumer’a
Интеграция в основное приложение
Предисловие
Библиотека позиционирует себя “обёрткой aiormq для asyncio для людей”. Моей целью стало заменить Celery, используемый в проекте на неё. Решил я это сделать из-за того, что его интерфейс не предполагает разбиение приложения и worker’ов в отдельные сервисы, чего очень хотелось бы. Второстепенными причинами стали: отсутствие асинхронности, запах legacy (я про атрибут self, который необходимо писать первым аргументом функций) и отсутствие type-хинтов. Celery в проекте использовался для IO-Bound и Delay задач, поэтому интеграция асинхронности была очень кстати.
Конфигурация RabbitMQ
Я обновил свой RabbitMQ добавив плагин “RabbitMQ Delayed Message Plugin”. Он нужен был для того, чтобы делать “отложенные” задачи (выполняются по истечении определённого времени). Celery с этим справлялся, т.к. у него была нативная поддержка данной фичи, но aio-pika такого не имеет. Этот плагин позволяет добавить этот функционал в сам RabbitMQ. Мой docker-compose конфиг стал выглядеть следующим образом:
docker-compose.yaml
image: rabbitmq:3-management
hostname: rabbit
env_file:
– .env
volumes:
– ./services/rabbit/delayed_message.ez:/opt/rabbitmq/plugins/delayed_message.ez
– ./services/rabbit/enabled:/etc/rabbitmq/enabled_plugins
ports:
– “15672:15672”
Через volumes я подключил скачанный плагин, а так же добавил его в список активированных по умолчанию. Мой enabled_plugins файл выглядел следующим образом:
.*Точка в конце обязательна
Task router для consumer’a
Следующим этапом я написал Router для моего worker’а, который был бы для меня удобен. На этом моменте я немного заморочился:
router.py
_routes: dict] = {}
def __init__(self):
modules = list(filter(
lambda x: x != ‘__init__’,
map(lambda y: y.split(‘.’), os.listdir(‘tasks’))
))
for module in modules:
imported = import_module(f’tasks.{module}’)
if not hasattr(imported, ‘__all__’):
continue
self._routes = imported.__all__
del imported
def get_method(self, action: str) -> Optional:
module = action.split(‘:’) # Название файла
method = action.split(‘:’) # Название функции
if self._exists(module, method):
return getattr(import_module(f’tasks.{module}’), method)
Переменная _router заполняется задачами, которые расположены в папке tasks, в которой лежат сами функции (задачи). Так же они указаны в переменной all для экспорта. Для наглядности задачи выглядела примерно так:
async def test(is_test: bool):
print(f’Hello world! Value is: {is_test}’)
__all__ =
Следующей задачей предстояло решить проблему с тем, что эти функции имеют произвольное количество аргументов. Я написал ещё один метод для роутера, который мог бы учесть и это:
router.py
hints = get_type_hints(func)
for arg, arg_type in hints.items():
if arg not in data:
return False
if not isinstance(data, arg_type):
return False
return True
Мы передаем в данный метод функцию, которую импортировали из файла, а так же данные, которые пытаемся ей подсунуть. Мы так же проверяем типы указанные в аргументах функции. Если всё ок – то возвращаем True
Таким образом я регулировал количество доступных задач созданием удалением файлов из папки tasks. Это оказалось очень удобным и гибким решением.
Написание consumer’aconsumer.py
async with message.process():
message = MessageSchema.parse_obj(json.loads(message.body.decode()))
method = router.get_method(message.action) # Импортируем функцию и записываем в переменную
if method:
if not router.check_args(method, message.body): # Проверяем атрибуты, которые собираемся передавать
print(‘Invalid args’)
return
if inspect.iscoroutinefunction(method): # Проверяем является ли функция async или нет
await method(**message.body)
else:
method(**message.body)
async def main() -> None:
queue_key = rabbit_config.RABBITMQ_QUEUE
connection = await aio_pika.connect_robust(rabbit_config.url)
# Для корректной работы с RabbitMQ указываем publisher_confirms=False
channel = await connection.channel(publisher_confirms=False)
# Кол-во задач, которые consumer может выполнять в момент времени. В моём случае 100
await channel.set_qos(prefetch_count=100)
queue = await channel.declare_queue(queue_key)
exchange = await channel.declare_exchange(
# Объявляем exchange с именем main и типом, который поддерживает отложенные задачи
# Важно чтобы это имя (main) совпадало с именем на стороне publisher
‘main’, ExchangeType.X_DELAYED_MESSAGE,
arguments={
‘x-delayed-type’: ‘direct’
}
)
await queue.bind(exchange, queue_key)
await queue.consume(process_message)
try:
await asyncio.Future()
finally:
await connection.close()
if __name__ == “__main__”:
asyncio.run(main())
В целом на этом сторона consumer’a закончена и можно приступить к интеграции всего этого добра в основное приложение (publisher).
Интеграция в основное приложение
На помощь снова приходит ООП и я написал класс для работы с aio-pika, который полностью закрыл мои нужды. Его инициализация происходила в новеньком lifespan (который скоро полностью вытолкнет старые способы):
@asynccontextmanager
async def lifespan(_: FastAPI):
await rabbit_connection.connect()
yield
await rabbit_connection.disconnect()
app = FastAPI(lifespan=lifespan)
Далее идет реализация этого класса:
rabbit_connection.py
_connection: AbstractRobustConnection | None = None
_channel: AbstractRobustChannel | None = None
_exchange: AbstractRobustExchange | None = None
async def disconnect(self) -> None:
if self._channel and not self._channel.is_closed:
await self._channel.close()
if self._connection and not self._connection.is_closed:
await self._connection.close()
self._connection = None
self._channel = None
async def connect(self) -> None:
try:
self._connection = await connect_robust(rabbit_config.url)
self._channel = await self._connection.channel(publisher_confirms=False)
self._exchange = await self._channel.declare_exchange(
# Повторяем из consumer’a. Важно указать одинакое
# имя exchange’ов. В моём случае `main`
‘main’, ExchangeType.X_DELAYED_MESSAGE,
arguments={
‘x-delayed-type’: ‘direct’
}
)
except Exception as e:
await self.disconnect()
async def send_messages(
self,
messages: list,
*,
routing_key: str = rabbit_config.RABBITMQ_QUEUE,
delay: int = None # Задержка, через которое нужно выполнить задачу (в секундах)
) -> None:
async with self._channel.transaction():
headers = None
if delay:
headers = {
‘x-delay’: f'{delay * 1000}’ # Это тоже из документации плагина для RabbitMQ
}
for message in messages:
message = Message(
body=json.dumps(message.dict()).encode(),
headers=headers
)
await self._exchange.publish(
message,
routing_key=routing_key,
mandatory=False if delay else True # Чтобы в логах был порядок 😉
)
rabbit_connection = RabbitConnection()
В итоге для того, чтобы отправить работки worker’у достаточно было сделать следующее:
main.py
async def test():
message = MessageSchema(
action=’images:delete’,
body={‘path’: ‘assets/temp/temp.png’}
)
await rabbit_connection.send_messages(
,
delay=20
)
return {‘status’: ‘published’}
Подводя итоги хочется сказать что worker теперь чувствует себя намного увереннее и может выполнять намного больше и быстрее. Надеюсь статья оказалась полезной. Всем спасибо, всем пока.
Детокс для i18n / Хабр | Веб-студия Nat.od.ua
Детокс для i18n / Хабр
NPM библиотека для интернационализации и локализации i18n очень популярна, однако за последние годы она сильно разрослась. В ней много возможностей для локализации дат, чисел, плурализации, двунаправленных алфавитов, загрузки локалей с сервера и кучи еще чего. На сайте i18next она называется уже даже “интернационализационным фреймворком”.
в тему
Новый авиалайнер. Входит стюардесса в пассажирский салон:
— Вы находитесь на нашем новом авиалайнере. В носовой части самолета у нас находится кинозал, в хвостовой — зал игровых автоматов, на нижней палубе — бассейн, на верхней — сауна. А теперь, уважаемые господа, пристегните ремни, и мы со всей этой хренотенью попробуем взлететь.
В то же время часто для локализации сайта в большинстве случаев требуются очень простые вещи, занимающие всего пару процентов от всего функционала тяжеловеса i18n.
В частности лично мне обычно нужны:
Нахождение перевода по составному ключу -t(“finance.transactions.deposit”)
Перевод с параметром – t(“hello-message”, “Вася”)
Массивы для списков или параграфов текста
На примере Vue 3 я покажу как можно избавиться от i18next без потери функционала в данном случае, не только облегчая js бандл, но и сокращая код, при этом с сохранением реактивности (смена языка сайта налету)
Простоту и элегантность нижеописанного рефакторинга обеспечит Vue 3 Composition API, но в целом данная методика должна подойти для любого реактивного фреймворка
Свой i18n
Вот чистая реализация вышеуказанного функционала в 30 строчек супротив полутора мегабайт без каких-либо зависимостей – https://stackblitz.com/edit/i18n-detox?file=src%2FApp.vue
Проект Vue 3 с i18next
Стандартно в проекте с Composition API подключение и использование i18next происходят примерно следующим образом
До
import { i18n, useI18n } from “@/app/composables/i18n”;
const { initI18n } = useI18n();
initI18n();
app.use(i18n);
// useI18n.js
import { ref } from “vue”;
import { api } from “@/services”;
import { createI18n } from “vue-i18n”;
const locales = ;
const locale = ref();
export const i18n = createI18n({
I18nScope: “global”,
globalInjection: true,
legacy: false,
allowComposition: true,
fallbackLocale: import.meta.env.VITE_I18N_FALLBACK_LOCALE || “en”,
formatFallbackMessages: true,
});
export function useI18n() {
function initI18n() {
const lang =
localStorage.getItem(import.meta.env.VITE_APP_NAME + “_lang”) ??
(import.meta.env.VITE_DEFAULT_LOCALE || “en”);
loadLanguage(lang);
}
async function loadLanguage(lang) {
if (i18n.global.locale !== lang) {
locale.value = locales.find((l) => l.code === lang);
const data = await api.utils.downloadLanguage(lang);
i18n.global.setLocaleMessage(lang, data);
i18n.global.locale.value = lang;
localStorage.setItem(import.meta.env.VITE_APP_NAME + “_lang”, lang);
}
}
return {
i18n,
locale,
locales,
initI18n,
loadLanguage,
};
};
// Использование в компонентах Composition API
import { useI18n } from “vue-i18n”;
const { t } = useI18n();
t(“finance.transactions.deposit”),
// Использование в js файлах
import { i18n } from “@/app/composables/i18n”;
i18n.global.t(“finance.transactions.deposit”)
Всё, что нужно для избавления от i18next, это задать явно объект messages для хранения всех локалей, и добавить в useI18n() реактивную функцию t(), которая как раз и будет обрабатывать составной ключ, параметр и массив.
После этого можно закомментировать всё использование библиотеки vue-i18n
После
import { useI18n } from “@/app/composables/i18n”;
const { initI18n } = useI18n();
initI18n();
// app.use(i18n);
// useI18n.js
import { ref } from “vue”;
import { api } from “@/services”;
// import { createI18n } from “vue-i18n”;
const locales = ;
// export const i18n = createI18n({
// I18nScope: “global”,
// globalInjection: true,
// legacy: false,
// allowComposition: true,
// fallbackLocale: import.meta.env.VITE_I18N_FALLBACK_LOCALE || “en”,
// formatFallbackMessages: true,
// // messages: { en: messages }
// });
const locale = ref();
let messages;
// Делаем доступ для использования в js модулях
export const t = useI18n().t;
export function useI18n() {
function initI18n() {
messages = [];
const lang =
localStorage.getItem(import.meta.env.VITE_APP_NAME + “_lang”) ??
(import.meta.env.VITE_APP_DEFAULT_LOCALE || “en”);
loadLanguage(lang);
}
async function loadLanguage(lang) {
if (locale.value !== lang) {
const localeMessages = await api.utils.downloadLanguage(lang);
messages = localeMessages;
locale.value = locales.find((l) => l.code === lang);
// i18n.global.setLocaleMessage(lang, localeMessages);
// i18n.global.locale.value = lang;
localStorage.setItem(import.meta.env.VITE_APP_NAME + “_lang”, lang);
}
}
function t(msg, param = null) {
let val = msg.split(“.”).reduce((val, part) => val, messages);
if (param) {
val = val.replace(“{0}”, param);
}
return val;
}
return {
t,
// i18n,
locale,
locales,
initI18n,
loadLanguage,
};
}
// Использование в компонентах Composition API
import { useI18n } from “@/app/composables/i18n”;
const { t } = useI18n();
t(“finance.transactions.deposit”),
// ИЛИ
import { t } from “@/app/composables/i18n”;
t(“finance.transactions.deposit”)
// Использование в js файлах
import { t } from “@/app/composables/i18n”;
t(“finance.transactions.deposit”)
В данном примере перевод для конкретной локали грузится с сервера по запросу, но объект messages можно иметь на клиенте сразу
Alias export const t = useI18n().t; позволяет использовать один синтаксис и в компонентах, и в js модулях.
I18next расширения
У I18next есть расширение для `Vue DevTools` (довольно бесполезное), и есть расширение I18next Ally для MS VS Code (весьма полезное). Так вот I18next Ally работает с новой реализацией если в package.json будет прописан пакет vue-i18n в dependencies (в коде подключать его не надо). Рекомендую. Оба расширения, впрочем, неплохо едят ресурсы, так что пользоваться ими лучше по надобности.
Итого
Мы закомментировали больше строк, чем добавили, и JavaScript бандл после билда уменьшился на 50 Кб. Функционал остался. Реактивный.
До (vue 3, vue-router, toaster, vue-i18n)
После (vue 3, vue-router, toaster)
Спасибо, I18next, и до свидания.
Другая моя статья по теме – “Работа с i18n — автоматизация Google Translate и другие полезные советы”
Чистая архитектура на практике / Хабр | Веб-студия Nat.od.ua
Чистая архитектура на практике / Хабр
Для начала хотелось бы затронуть так называемую «микросервисную архитектуру», которая стала довольно популярной. Однако называть её архитектурой не совсем корректно, как заметил Роберт Мартин в своей книге «Чистая архитектура». Микросервис — это один из способов представления компонента общего приложения. Но архитектура, взаимодействие между компонентами, при этом может остаться такой же. Под компонентом понимается наименьшая единица развёртывания — пакет, библиотека или отдельное приложение, сервис, микросервис. В книге критикуется данная «микросервисная архитектура», а точнее, неправильное и неуместное её применение, которое есть во многих случаях и влечёт за собой негативные или крайне негативные последствия для проекта, и происходит «благодаря» низкой компетентности разработчиков или управляющих, которые хотят найти несуществующую здесь «серебряную пулю».
В книге «Чистая архитектура» выделяется два основных слоя: инфраструктура и бизнес-логика. В различной литературе выделяют и другие слои, но какой-то относительно чёткой информации об этом я не встречал. Под инфраструктурой будем понимать логику более характерную для многих приложений, не только для данного, какие-то конкретные интеграции со сторонними компонентами, а также подготовку данных для бизнес-логики. Под бизнес-логикой — логику более характерную для данного приложения она и представляет наибольшую ценность, её нужно стараться выделять, делать более независимой от инфраструктуры. Во-первых, чтобы было проще её читать, тестировать, модифицировать, а во-вторых, чтобы была возможность с наименьшими затратами заменить инфраструктуру или её части, если потребуется.
Примеры будут для PHP и Symfony, но и для других языков и систем программирования многое может быть похожим. Примеры абстрактные, и в разных приложения может всё отличаться. В ранних версиях Symfony была рекомендация делить приложение на бандлы-плагины. Но начиная с версии 3.4 рекомендовано делить приложение через пространства имён. Это практичнее. Структура проекта будет стандартной. В AppService, будет находиться основная логика приложения, бизнес-логика.
Пусть, нужно сделать возможность оформления заказа. Для этого можно создать AppServiceOrderCheckoutOrderCheckoutService. Здесь будет общая логика оформления заказа. Входные данные можно принимать через параметры функции, но часто удобнее передавать структуру с данными, DTO. Могут понадобиться AppServiceOrderCheckoutInputDto, возможно, и InputDtoInterface. Он может содержать массив, коллекцию, из OrderInputDto, если заказов сразу несколько, например. Похожим образом обстоит дело и с выходными данными.
Если в OrderCheckoutService много разнообразной логики, можно выносить её в другие классы с разными названиями. Чтобы следовать принципу единой ответственности можно разделить его на несколько классов, например, OrderCheckoutAcmeFeature1Service, OrderCheckoutAcmeFeature2Service и так далее.
Ещё понадобится хранить данные заказов в БД, поэтому создадим сущность по зеркальному к сервису пути AppEntityOrderOrder. Соответственно будет создан и репозиторий AppRepositoryOrderOrderRepository. Имеет смыл делать логику независимой от Entity и Repository. Проще всего это сделать через интерфейсы в AppServiceOrderCheckout: OrderInterface (или CheckoutOrderInterface) и OrderRepositoryInterface (или CheckoutOrderRepositoryInterface). OrderInterface должен содержать только геттеры и сеттеры данных, необходимых для оформления заказа. OrderRepositoryInterface должен содержать только нужные запросы к БД и логику связанную с построением запросов. Сущность и репозиторий реализуют эти интерфейсы соответственно. Можно вместо интерфейсов использовать и промежуточную структуру, DTO, но такой способ будет более объёмным в реализации и иметь некоторые другие минусы.
Таким образом, принцип единой ответственности из SOLID можно легко реализовать не привязываясь к структуре БД. Также, реализуется принцип разделения интерфейсов: класс (сервис) не должен ничего знать о данных и функциях, которые ему не нужны. Логика не привязана к конкретной сущности, её можно применить к любым другим данным — принцип инверсии зависимости. Класс с реализацией можно легко подменить через сервис-контейнер — принцип открытости-закрытости. Такой код проще в поддержке, каждый разработчик может выполнять свою задачу независимо от других разработчиков проекта.
Ещё понадобится фабрика или creator для создания экземпляра конкретной сущности. Для этого создадим интерфейс AppServiceOrderOrderFactoryInterface (или OrderCreatorInterface, как кому нравится). Обязанность создания можно возложить, например, на класс AppFactoryOrderOrderFactory, реализующий этот интерфейс и другие подобные интерфейсы, если появятся. Фабрика может содержать единственный метод create. Фабрику можно положить и в AppServiceOrderOrderFactory, но нужно помнить, что она относится к инфраструктуре, в отличие от интерфейса.
Надо находить наиболее важную логику приложения, так называемую бизнес-логику, и выделять её в отдельные классы, делать более независимой от других частей приложения, библиотек. Для этого понадобится делать обёртки, адаптеры, посредники, применять инверсию зависимости.
Если интерфейс реализован только в одном месте, сервис-контейнер Symfony, сам подставит соответствующую реализацию в конструктор сервиса. Если реализаций несколько, то нужно уточнить конкретную в файле конфигурации контейнера. У контейнера есть много возможностей, которые полезно знать.
И так, у нас есть OrderCheckoutService, содержащий бизнес-логику оформления заказа. Он может вызывать напрямую другие сервисы, выше уровнем. Или через инверсию зависимости другие, более конкретные реализации, менее важные детали. Например, может вызвать AppServiceOrderDeliveryOrderDeliveryService, содержащий логику связанную с доставкой заказа. И эта логика может относиться не только к оформлению. В таком случае мы подразумеваем, что OrderDeliveryService — это более важная логика, более высокого уровня, чем OrderCheckoutService, бизнес-логика оформления заказа зависит от бизнес-логики доставки заказа. OrderDeliveryService, в свою очередь, может вызвать уже конкретный расчёт через сторонний API, например, с помощью AppServiceDeliveryDeliveryCalculatorInterface с реализацией AppServiceDeliveryDeliveryCalculator. Здесь калькулятор находится в AppServiceDelivery потому, что он содержит логику доставки, которая может быть использована отдельно от Order. Интерфейс использован, чтобы не было зависимости от инфраструктурного кода. Если есть какая-то общая бизнес-логика, которая относится к доставке Order и к доставке чего-то ещё не являющегося Order, что сложно представить, то можно поместить её, например, в AppServiceDeliveryDeliveryService.
Здесь DeliveryCalculator зависит от бизнес-логики, он реализует её интерфейс. А если понадобится сделать калькулятор независимым от данного приложения и, возможно, вынести его в отдельный пакет и использовать в других приложениях, можно выделить его логику и поместить, например, в AppUtilDeliveryDeliveryCalculator. А в калькуляторе приложения уже просто вызывать его. Тогда класс AppServiceDeliveryDeliveryCalculator становится конкретной реализацией адаптера. К нему может добавиться ещё некоторая логика, тогда его уже можно назвать декоратором. А если он будет обращаться к нескольким классам, то это уже посредник. Но смысл один и тот же. Не помешает так же создать обёртки над другими сторонними компонентами, например, над EntityManager. Если есть необходимость в ещё более чётком отделении бизнес-логики, можно весь подобный инфраструктурный код, соединяющий инфраструктуру с бизнес-логикой, перенести из AppService, например, в AppServiceBridge c зеркальной к AppService структурой. И в AppService тогда останется чистая бизнес-логика.
Ещё может быть логика оформления заказа пользователем на сайте и администратором в панели управления, она может в чём-то совпадать, в чём-то отличаться. Тогда можно создать AppServiceOrderCheckoutUserOrderCheckoutService и AppServiceOrderCheckoutAdminOrderCheckoutService со своей логикой. Они могут обращаться к общему OrderCheckoutService, который будет более важен, выше уровнем в таком случае. А могут быть и самостоятельными, тогда их следует перенести на уровень выше, например, в AppServiceOrderUserCheckout. Логику, связанную с взаимодействием с пользователем, со страницей оформления заказа можно вынести в AppServiceOrderUserCheckoutUserOrderCheckoutInteractor, который будет обращаться к UserOrderCheckoutService. И уже в контроллере вызывать сервис/интерактор. Так же и с AdminOrderCheckoutService.
Надо следить за зависимостями, не следует допускать двухсторонних, циклических зависимостей между модулями. Иначе их не получится рассматривать каждый как отдельный компонент системы, они все вместе будут представлять собой единый компонент.
Что касается сокрытия классов, в PHP нет такой возможности. Есть какие-то сторонние решения. Но в общем случае делать этого не нужно, так как должно быть понятным из документации или из примеров, как использовать какой-либо компонент, библиотеку. Если этого нет, то это уже проблема другого уровня, профессионального, организационного, и методом сокрытия классов её не решить. Зато сокрытие классов может создать проблемы в использовании пакета. Можно помечать некоторые классы и функции, которые нежелательно переопределять, аннотацией internal, этого должно быть достаточно. Также, при создании пакета не следует злоупотреблять final, и private, а использовать их только там, где это действительно надо, если есть другое, более правильное решение. Если private используется в коде самого приложения, поправить его будет легко, а вот в стороннем пакете это сделать уже сложнее.
В директории config можно создать поддиректорию и подключить всё её содержимое в основном файле конфигурации фреймворка services.yaml. Тогда проще будет разделять конфигурацию на части по файлам, чтобы соотносить с частями приложения. Подобным образом следует делить и другие директории, если они есть: templates, translations, docs, tests, AppController, AppEventListener и другие.
С возможностями современных IDE выделить какой-то определённый компонент из такой системы будет не сложно, если это, конечно, потребуется.
Ural Digital Weekend 2023 — конференция про разработку и управление в Digital / Хабр | Веб-студия Nat.od.ua
Ural Digital Weekend 2023 — конференция про разработку и управление в Digital / Хабр
Привет! На связи Spectr.
4-5 августа в Перми пройдет большая конференция про разработку и управление бизнесом в Digital — Ural Digital Weekend. В статье рассказываем, что вас ждет и вспоминаем UDW прошлого года.
Формат конференции
Конференция пройдет в офлайн-формате с качественной онлайн-трансляцией. Мы очень рекомендуем посетить событие вживую — в Перми. В этом году городу исполняется 300 лет, и мы ожидаем летом большое количество культурных активностей, которые сделают нахождение тут еще более интересным.
Пермь — комфортный и логистически удобный город. К нам летают прямые рейсы из крупнейших городов России (Москва, Новосибирск, Казань, Сочи, Минеральные Воды, Нижний Новгород).
Кроме официальной части вас ждет: препати (3 августа, четверг), афтепати (4 августа, пятница, после докладов) и этнографическая экскурсия (5 августа, суббота).
Секция «Разработка»
В рамках секции вас ждут доклады о лучших инженерных практиках, актуальных технологиях, архитектуре, информационной безопасности, инфраструктуре и высоких нагрузках. Все доклады будут состоять из практического и жестко-технического контента. Большая часть докладов секции рассчитана на middle+ инженеров.
На данный момент программа секции формируется и будет опубликована в июне. Формированием программы занимается Виктор Корейша, руководитель отдела MessageBus в Ozon и ведущий замечательного подкаста о жизни в IT «Кода кода». Благодаря ему на ивенте будет представлена только самая интересная, актуальная и практически применимая информация, все доклады проходят тщательный отбор.
Секция «Управление бизнесом и продуктовая разработка»
Ключевой темой секции «Управление бизнесом» станет разработка собственных продуктов и формирование продуктовой культуры внутри агентства.
Вы можете успеть подать свой доклад, если у вас есть крутой / уникальный материал про разработку собственного продукта в агентстве, либо про внедрение продуктовой культуры в сервисную разработку.
Как это было в 2022
Пока идет активная подготовка к конференции — вспомним прошлогодний Ural Digital Weekend.
Ural Digital Weekend 2022 — это более 300 офлайн-участников, 3 потока и 32 спикера из крупнейших российских компаний. Трансляции и опубликованные записи докладов конференции на данный момент набрали суммарно более 50 тыс. просмотров.
В прошлом году конференция была разделена на 3 независимых потока, которые проходили параллельно: «Разработка», «Воркшопы и мастер-классы» и «Управление бизнесом». Записи всех потоков конференции доступны на нашем YouTube-канале.
Я в полном восторге, что региональные сообщества просыпаются ото сна, снова начинаются оффлайн-конференции, при этом в новых регионах. Ural Digital Weekend’у удалось создать прекрасную атмосферу одновременно праздника для местных разработчиков и шикарного технологического хаба для приезжих экспертов. Оборудование, ведущие, афтепати – всё было на высочайшем уровне и не осталось незамеченным у искушенной до подобных мероприятий аудитории.
Руководитель управления клиентской разработки в X5 Tech, бессменный ведущий подкаста Frontend Weekend
Организаторы конференции — огромные молодцы. Собрали крутых экспертов и сделали высокого качества трансляции, живые выступления и в целом атмосферу профессионализма, после чего хочется еще круче развиваться в прогрессировать в IT. Отдельная благодарность за проведенный воркшоп: для многих такие практические мероприятия дают классные возможности увидеть работу эксперта в живую и «пощупать» технологии в интенсивном формате.
Кроме насыщенной контентной части мы хотели сделали максимально комфортным и интересным пространство, где проходила конференция. И организовали множество активностей, которые не позволили участникам заскучать в перерывах между докладами: музыка во входной группе, тайский массаж, уроки игры на барабанах, кофе-пойнт с ароматными напитками и фудтраки с вкусной едой, а также квиз для тех, кому было интересно проверить свои харды по JS.
Но и на этом событие не закончилось, а продолжилось в одном из пермских баров, где прошла афтепати. Там участников ожидало еще больше специальных активностей: лекция и дегустация крафтового пива Пермского края, мастер-класс по приготовлению посикунчиков, чилл-зона с кальянами, игровая зона: кикер, Beer Pong. На самой конференции разработчики из Перми как будто скромничали и в кулуарах не очень активно задавали вопросы спикерам. А вот на афтепати все было иначе, и ребята буквально проходу не давали спикерам и гостям из других городов: вопросы, обмен опытом, байки, холивары…
Заключительным этапом Ural Digital Weekend стала поездка в Пермскую ледяную пещеру. На следующий день после конференции мы сделали туда экскурсию для гостей нашего региона.
Присоединяйтесь к Ural Digital Weekend 2023
Регистрация на конференцию уже открыта. Цена участия будет стремительно повышаться вплоть до старта конференции. А до 1 июня у вас есть возможность купить билет по самой низкой стоимости!
К тому же для читателей Хабра мы приготовили специальный промокод, который даст приятную скидку: HABRAHABR (промокод действует вплоть до начала конференции).
Все актуальные новости о событии мы публикуем в официальном канале конференции. Подписывайтесь!
Ждем вас на Ural Digital Weekend!
Организаторы конференции: Spectr, Тэглайн
Redux-saga: обзорная экскурсия | Веб-студия Nat.od.ua
Redux-saga: обзорная экскурсия
Сегодня я бы хотел рассказать о библиотеке redux-saga. Она уже достаточно давно используется во frontend-программировании, но не является интуитивно понятной, что может помешать начинающим разработчикам освоить её быстро и начать применять в своих проектах. В данной статье я максимально просто постараюсь объяснить максимально основные принципы этой технологии и некоторые полезные возможности. Намеренно отказываюсь от сравнительного анализа в пользу одних либо других технологий, т.к. выбор — это личное дело каждого, но чтобы его сделать, необходимо обладать определёнными знаниями.
В статье используются специализированные термины, поэтому предполагается, что вы имеете общее представление о React, Redux, генераторах и итераторах из ES6.
Из официальной документации следует, что redux-saga — это библиотека, которая ориентирована на упрощение и улучшение работы с сайд-эффектами (side-effects, любыми взаимодействиями с внешней средой, например, запрос на сервер) и облегчение их тестирования. В redux сага — это middleware (слой, работающий с момента диспатча (dispatch) экшена (action) и до обработки его редьюсером (reducer)), который может запускаться, останавливаться и отменяться из основного приложения с помощью обычных действий redux. Библиотека использует такое понятие ES6 как генераторы (Generators), и благодаря этому наши асинхронные потоки выглядят как обычный синхронный код.
Читать далее
Делаем кастомный RadioGroup в 99 строк для React / Хабр | Веб-студия Nat.od.ua
Делаем кастомный RadioGroup в 99 строк для React / Хабр
Пишем минималистичный кастомный RadioGroup компонент для React приложения и парочку unit тестов на Jest.
План действий
Общий план действий состоит из 6 этапов:
Понять, что хотим получить
Реализовать компонент Option
Написать компонент RadioGroup
Собрать всё в контейнере и запустить
Сделать поддержку ввода с клавиатуры
Покрыть тестами
Поехали!
Целевой результат
Нам нужна кастомная радио группа для выбора одного из множества вариантов. Для удобства
предположим, что у нас есть некая форма и в ней нужна “выбиралка” периода, для выгрузки какой-либо статистики/контента за определённый период времени.
Сделаем компонент в виде горизонтальной плашки, с набором вариантов в виде кнопок. В целом нет никаких ограничений в том, чтобы изменить ui компонента так, как вам это будет требоваться. Feel free to edit, как говорится.
По итогу получим вот такой минималистичный компонент. Демо: codesandbox.custom-radio
PS: в данной статье не будет описания работы с формами и валидацией. Решений подобных задач очень много, стоит только погуглить). Например один из вариантов я описываю в статье Валидация форм без зависимостей.
Поехали!
Пишем компонент OptionИнтерфейсы
Начнём с того, что определим структуру нашего варианта выбора. Он будет минималистичен и включать 2 параметра:
type OptionType = { value: string; title: string; };
Сам же компонент Options должен уметь делать несколько вещей:
отображать один вариант выбора
промечать выбранный элемент отличным от других
вызывать onChange при выборе клике на элемент
При переводе на typescript интерфейс компонента Option выглядит следующим образом:
type OptionProps = {
value: OptionType;
title: OptionType;
selected: OptionType;
groupName: string;
onChange?: (value: string) => void;
};
Верстка
Для стилизации будем использовать css modules для стилизации (поскольку в основе приложения лежит react-create-app с шаблоном ts, то поддержка css modules у нас уже реализована из коробки).
Нам достаточно только импортировать стили и применять к элементам:
import Styles from ‘./index.module.css’;
…
Сам же компонент выглядит очень просто:
const Option = (props: OptionProps) => {
const {
value,
title,
selected,
groupName,
onChange
} = props;
const handleChange = () => onChange?.(value);
const inputId = `${groupName}_radio_item_with_value__${value}`;
return (
);
};
Простановка data-checked в true закрывает требование “промечать выбранный элемент отличным от других”. Затем просто рендерим title и вешаем handleChange на onChange нашего инпута.
Пишем компонент RadioGroupИнтерфейсы
Компонент RadioGroup должен принимать список options, коллбэк onChange и значение выбранного элемента. Ну и поскольку мы делаем именно Radio group, а не что-то другое, нам нужно проставлять имя этой группы.
В итоге получаем интерфейс, состоящий из 4х пропсов:
type RadioGroupProps = {
name: string;
options: OptionType[];
selected: OptionType;
onChange?: (value: string) => void;
};
Вёрстка
В компоненте нам надо отрендерить список option и объявить handleChange для обработки выбранного элемента. Плюс для оптимизации обернём компонент в React.memo.
const RadioGroup = (props: RadioGroupProps) => {
const { name, options, selected, onChange } = props;
const handleChange = (value: string) => onChange?.(value);
return (
))}
);
};
export default React.memo(RadioGroup);
Собираем всё в контейнере и запускаемimport { useState } from “react”;
import options from “./components/radio/options.json”;
import Radio from “./components/radio”;
import “./styles.css”;
export default function App() {
const = useState(“”);
const handlePeriodChange = (val: string) => {
setPeriod(val);
};
return (
Custom RadioGroup component example
Выбрать период
);
}
Поддержка ввода с клавиатуры
Для реализации возможности взаимодействия с RadioGroup с клавиатуры, нам потребуется немного доработать наш Option компонент. А именно:
в Option нам нужно слушать событие нажатия, но при этом проверять находится ли наш option в фокусе или нет. Если option в фокусе, то вызываем обработчик onClick
немного поколдовать с tabindex.
В итоге получаем следующие доработки:
import { useEffect, useRef } from ‘react’;
const Option = (props: OptionProps) => {
const optionRef = useRef
…
useEffect(() => {
const option = optionRef.current;
if (!option) return;
const handleEnterKeyDown = (event: KeyboardEvent) => {
if ((document.activeElement === option) && event.key === ‘Enter’) {
onChange?.(value);
}
}
option.addEventListener(‘keydown’, handleEnterKeyDown);
return () => {
option.removeEventListener(‘keydown’, handleEnterKeyDown);
};
}, );
return (
…
);
}
Мы исключаем input из обхода элементов при использовании клавиши tab, проставляя tabindex в отрицательное значение. И включаем в этот обходи div обёртку всего нашего кастомного option.
Таким образом дефолтное поведение браузера при фокусе на элемент будет работать для всего нашего компонента. Потом можем через css добавить псевдоклассов focus-visible.
activeElement содержит в себе ссылку на элемент документа, который находится в фокусе. Подробнее можно прочитать на MDN: document.activeElement.
Есть тонкости в разнице focus и focus-visible, про которые можно почитать в статье Doka:focus-visible
Пишем пару unit тестов
Перед началом проставляем атрибут data-testid для каждого Option, для того, чтобы было проще искать элементы в тестах.
const Option = (props: OptionProps) => {
…
const inputId = `${groupName}_radio_item_with_value__${value}`;
return (
);
};
Про структуру теста и используемые методы можно прочитать в другой моей статье про пагинацию в React приложении в разделе Структура теста.
Всё первоначальные настройки для запуска тестов у нас уже есть из коробки create-react-app.
Для нашего мини компонента напишем парочку мини тестов. Проверим, что атрибут data-checked проставляется при выборе элемента и корректно вызывается onChange:
import ‘@testing-library/jest-dom’;
import { render, screen, fireEvent } from ‘@testing-library/react’;
import RadioGroup from ‘./index’;
import options from ‘./options.json’;
describe(‘React component: RadioGroup’, () => {
it(‘Должен проставляться атрибут на option, если было выбрано его значение’, async () => {
render(
);
const radioItem = screen.getByTestId(`radio_item_with_value__${options.value}`);
expect(radioItem).toHaveAttribute(‘data-checked’, ‘true’);
});
it(‘Должен вызываться обработчик “onChange” при клике на option’, async () => {
const handleChange = jest.fn();
render(
);
const label = screen.getByLabelText(options.title);
fireEvent.click(label);
expect(handleChange).toHaveBeenCalledTimes(1);
});
});
PS:
Про фронтовые тесты есть отличная статья из блога Samokat.tech Как тестировать современный фронтенд.
Итого
Спасибо за чтение и удачи в написании ваших кастомных компонентов)
PS: Ссылки из статьи:
как убедить бизнес инвестировать в технику / Хабр | Веб-студия Nat.od.ua
как убедить бизнес инвестировать в технику / Хабр
При разработке приложений, в частности – продуктовой, каждый программист, архитектор или тестировщик понимает важность технических инвестиций и наличия стратегии гашения технического долга. Бизнес, особенно тот, который хочет считать себя передовым, модным и молодежным, тоже знаком с этими понятиями и даже иногда использует.
В нашей компании ресурсы на разработку распределяются в пропорции 80% на продуктовые задачи и 20% – на технические (на самом деле чуть сложнее, т.к. зависит от “зрелости” продукта, но опустим детали)
К сожалению, реальность же намного суровее, и очевидно такое распределение практически всегда нарушается в сами знаете какую сторону 🙂 Не успели к дедлайну, недооценили объем работ, неожиданные “срочные” задачи, которых не было на планировании – и вот технина, грустно взмахнув ручкой архитектору, отъезжает на следующий спринт, а потом на следующий, а потом и в следующий квартал.
Продуктовый офис можно понять – они ответственны за бизнес метрики, а технические, вроде нагрузки, покрытия тестами, чистоты кода и т.д. – совершенно для них непонятны и/или неинтересны. Как донести их значимость?
Неприятная правда в том, что бизнес понимает только бизнесовые и продуктовые метрики. Это задача технической команды (техлидов или архитекторов, зависит от компании) конвертировать непонятные технические метрики в понятные деньги. Расскажу о нашем опыте на примере двух ситуаций: как мы столкнулись с проблемами отказоустойчивости нашего приложения и как мы осознали, что инфраструктура не готова удовлетворить ожидания бизнеса.
Для контекста: наша компания предоставляет SaaS электронной коммерции для других компаний.
Слабое звено или хрупкая инфраструктура
По мере развития приложения в ходе компромиссов и из-за ограниченного контекста мы пришли к ситуации, когда Redis из кэша превратился в единую точку отказа (не спрашивайте как). Хоть у него и был аптайм 6 лет, по закону Мёрфи в какой-то момент виртуалка ушла в себя и приложение было недоступно 20 минут.
Что говорит бизнесу даунтайм в 20 минут? Ну полежали чуть чуть, пара пользователей не смогли совершить транзакцию, а через 20 минут смогли. Че бухтеть то? Лучше продолжать пилить фичи.
В таком случае нужно понять, сколько денег стоит подобный инцидент и вернуться с цифрами. В нашем случае 2 составляющих:
Количество активных пользователей, которые получили ошибку. Зная воронку конверсии можно оценить, сколько денег недополучили
Посмотреть SLA, которые указаны в договорах. В нашем случае у одного из крупных наших партнерах в договоре указаны следующие SLA
Один из примеров SLA, которые указываются в договоре с партнером
20 минут даунтайма в месяц – это уже 0.046% недоступности, т.е. бюджет на ошибки будет практически полностью исчерпан одним таким инцидентом, после чего комиссия, получаемая за период с партнера будет уменьшена на 0.3%. Зная исторический оборот этого партнера, нетрудно посчитать сколько денег будет упущено
Можно сюда же добавить репутационные потери, но, их очень тяжело посчитать
Остается проблема в том, как оценить риск повторного инцидента. Повторюсь, за 6 лет это был первый отказ Redis. Честно говоря, для нас самих эта задача оказалась нетривиальной, но описание катастрофичности последствий уже было достаточно, чтобы удалось запланировать развертывание нового кластера на следующий квартал.
В ожидании прекрасного будущего
В какой-то момент мы поняли, что текущая нагрузка на нашем приложении подозрительно близка к максимальной. По метрикам вышло, что за первые 2 месяца года нагрузка на API выросла в 2 раза.
Далее возникает вопрос: “И что с этим делать? Пора паниковать? Или это нормально?”. На этот случай у нас была тактика, и мы ее придерживались
Сбор текущих показателей
Мы предоставляем бекенд для интеграции партнерами, поэтому в простейшем случае можно посмотреть метрики по использованию различных методов API разными партнерами + дополнительно сгруппировать по типу интеграции (например, партнеры которые продают физический мерч генерят намного меньше трафика) и размеру партнера (мидтир, энтерпрайз и т.д.).
Поэтому важна хорошая система мониторинга, которая позволяет собирать не только метрики реального времени, но и хранить их для исторического анализа.
В нашем приложении мы используем New Relic для метрик приложений и Prometheus + Grafana для инфраструктурных метрик.
Примеры дашбордов с трафиком от партнеров. Есть возможность строить дашборды по разным сегментам и продуктам
После анализа нам стало понятно сколько примерно каждый партнер генерирует нагрузки на нашу API, а по инфраструктурным метрикам можно было сказать, станет ли какая-то часть системы (базы, брокеры сообщений и т.п.) узким горлышком при горизонтальном масштабировании приложения
Следующий шаг – актуализировать текущие значения максимальной пропускной способности. Проще говоря, провести нагрузочное тестирование, которое можно сделать, например, с помощью k6.
Одна из виртуалок стала узким местом во время очередной распродажи
Самое главное – фиксировать все инциденты, связанные с этой проблемой. В нашем случае разбирать и писать постмортемы для случаев, когда была запущена какая-то маркетинговая активность/распродажа и т.п., которые привели к росту трафика, который мы зарезали рейт лимитами (или у приложения начали деградировать SLO, такие как время ответа или время на процессинг транзакции). Если приложение не выдержало, то можно посчитать потери в течение этого даунтайма, в текущей ситуации – посчитать по трафику, который был отброшен. Хорошая идея добавлять в отчеты цифры в деньгах или хотя бы в пользователях вместо сухих цифр отброшенных запросов к API.
Эти инциденты будут дополнительным аргументом, когда будете убеждать бизнес, что проблема реальная.
Планируемые бизнес показатели
После этого мы выяснили планы бизнеса на развитие на ближайший год. Пришлось долго трясти людей, потому что как обычно ничего не формализовано, у разных людей разное понимание. Но суть в том, что когда кто-либо, даже если разработчики, приходят с такими вопросами – приходится что-то делать, собирать и актуализировать. В результате смогли собрать примерные планы по подключению новых партнеров на год, а также планы соседних департаментов, которые используют наши продукты и тоже могут генерировать нагрузку.
Полученную информацию мы сконвертировали в нагрузку, используя исторические данные каждого партнера с разными типами интеграциями.
Построили календарь, где указали конкретный месяц, когда мы перестанем справляться с новыми интеграциями (без учета пиковых нагрузок во время активностей). Учитывая прогнозируемые ревенью по каждой интеграции, нетрудно будет посчитать упущенную прибыль.
Наглядное представление, когда настанет судный день для приложения
Решения
На последнем этапе мы занялись генерацией и анализом возможных решений. Нет смысла идти с тем, что есть сейчас, потому что бизнесу непонятно сколько ресурсов нужно выделить и какой профит в итоге мы получим.
Поэтому стоит расписать все варианты, дать оценку, подсветить риски и профит.
На этом этапе появляется искушение схитрить и не показывать вариант, который не выгоден для самих разработчиков (например, дешевый, но который приведет к обилию костылей и кода с запашком). Это рабочая тактика в некоторых случаях, но, как мне кажется, негативно влияет на доверие между продуктовым офисом и отделом разработки.
Доверительные отношения ценны, т.к. приводит к осознанию важности технических задач второй стороной, поэтому капасити будет выделяться с большей готовностью.
Табличка с кратким обзором возможных решений, которую мы презентовали
Что в итоге?
Мы представили продуктовому менеджменту актуальные проблемы и предложили возможные решения, прошлись по цепочке вверх вплоть до бизнес хедов. После некоторых дискуссий, нам удалось добиться того, чтобы на целый квартал нам были выделены два опытных разработчика, которые будут заниматься только этой проблемой и никак не участвовать в разработке фичей или поддержке текущих партнеров
Резюме
Вот краткий пересказ основных моментов, которые я вынес из этих ситуаций:
Хорошая система мониторинга, сбора и хранения технических метрик (не только продуктовых) жизненно необходима. Без метрик что-то доказать будет сложно. Например, будет намного понятнее, если сопоставить время на разработку задачи в конкретном компоненте/время на онбординг в конкретный продукт с метриками качества кода.
Нужно конвертировать профит технических задач в понятные бизнесовые метрики и наглядно их презентовать. Например, “Обновить версии фреймворка/языка” – для бизнеса непонятно, но если сказать, что мы не пройдем сертификацию в следующем году из-за устаревшего ПО или что в этой версии обнаружены следующие эксплойты, которые могут привести к утечке данных – это уже прозрачнее
Фиксировать все инциденты с причинами, последствиями и работами, которые могли бы предотвратить их, но не были сделаны. Это будет дополнительным аргументом.
Нужно быть готовым, что придется вытягивать самостоятельно информацию из бизнеса: примеры SLA в договорах, которые подписываются с партнерами, планы по привлечению трафика к разным продуктам и т.п.
Иногда, следуя этим практикам, мы осознаем, что задача, которую мы хотели сделать, на самом деле не так важна, и даже непонятно, зачем мы пытались ее впихнуть в техническую дорожку 🙂
Издавна разработчики и менеджеры противопоставляются друг другу, выступают героями постоянных мемов. Я поделился своим опытом общения с продуктовым офисом. А как обстоят дела у вас? Как вы приходите к взаимному пониманию?
Как мы отказались от поддержки Internet Explorer в интернет-клиент-банке и никто не пострадал | Веб-студия Nat.od.ua
Как мы отказались от поддержки Internet Explorer в интернет-клиент-банке и никто не пострадал
Добрый день, на связи Дмитрий Захаров, фронтенд-разработчик Росбанка. В этом посте я поделюсь тем, как мы отказались от поддержки Internet Explorer в интернет-клиент-банке для крупного бизнеса. Расскажу, как мы к этому пришли, как организовали процесс и что получилось в результате.
Не секрет, что большинство библиотек (например, Tailwind CSS, который используем мы) и фреймворков последних версий не поддерживают IE вообще. Кто они, как не герои, двигающие прогресс вперед? Но это создает и трудности. В какой-то момент нам пришлось застрять на 11-м Angular, потому что были проблемы с перформансом при обновлении на 12-й, а в 13-м поддержку IE уже убрали. Также во многих библиотеках с поддержкой IE содержались уязвимости. Нужно было что-то с этим делать.
Согласно аналитике, пользователи IE в январе 2022 года составляла 23% от всей аудитории интернет-клиент-банка:
Начинаем есть слона по частям. Для начала мы стали уведомлять пользователей о том, что IE — это, грубо говоря, зло и стоит переходить на более популярные браузеры. После анонса Microsoft о скором прекращении поддержки IE мы усилили давление на пользователей и подготовили инструкции, как менее болезненно сменить браузер. Повесили баннер на логин пользователя. Клиентским менеджерам подготовили скрипт с инструкциями.
Стоит отметить, что сама Microsoft оказала нам существенную поддержку и подготовила «удаление» IE из некоторых своих операционных систем через патч 15 июня 2022. В действительности сначала удаление не происходило, а срабатывал редирект запуска на Edge. Он обеспечивает некоторую совместимость, подобие эмуляции IE, что позволяет в некотором роде заблокировать сам IE.
Примерно к дате патча количество клиентов в IE уменьшилось до 2,2-3% ежедневных пользователей, что, конечно, не могло не радовать. Чтобы переход был максимально мягким и люди не впадали в ступор при виде белого экрана в IE, мы сделали заглушку, которая предлагала возможность скачать новый браузер и предоставляла инструкции по переносу данных.
Но если Microsoft сам блокирует и удаляет IE, зачем нам вообще думать об этом? Дело в том, что не у всех клиентов могут обновляться системы; поэтому они этот патч не получат и могут продолжить пользоваться IE.
Использование Tailwind CSS версии 1.9.x накладывало дополнительные расходы. Библиотека генерировала 12 МБ классов стилей и «вытряхивала» лишние только на этапе сборки. 12 МБ классов в рантайме браузера — достаточно громоздко. Из-за этого растягивалась разработка и сборка: холодный старт приложения, hot reloading и сама сборка занимали существенно больше времени, чем мы могли позволить.
Что нам мешало? Со второй версии в Tailwind CSS полностью отказались от поддержки IE, и поэтому, не убрав поддержку IE в приложении, мы не смогли бы обновиться. Так что после очередного релиза, которые происходят у нас раз в месяц, мы приняли решение.
Сначала мы обновились до Angular 12 и устранили все возникшие проблемы. Затем исправили всё в соответствии с breaking changes библиотек и успешно обновились до версии 13. Далее месяц шла разработка и обкатка, чтобы все команды успели выявить потенциальные проблемы, и затем в начале августа мы вышли в продакшен.
Tailwind CSS тоже был успешно обновлен сразу на третью (последнюю) версию. Со второй версии там существенно переработали процесс: теперь не генерится 12 МБ CSS-кода, а он добавляется on-demand. Как только вы добавили класс CSS из Tailwind CSS, он включается в сборку. То есть к процессу подошли с противоположной стороны: «вытряхивать» неиспользуемые классы больше не надо, поскольку добавляются только те, что используются. Одно маленькое обновление ускорило нашу сборку в 5 раз, а работу всего пайплайна — в 6 раз. Холодная сборка вместо почти двух минут занимает теперь 40–50 секунд. Обычная сборка с 10–15 минут (в зависимости от машины, на которой собирается) ускорилась до 1–2 минут.
По итогам обновления и отказа от поддержки Internet Explorer бандл после gzip-сжатия уменьшился примерно на 700–1000 КБ. На момент выкатки с Angular 13 код уже отдавался в ES2015, что помогало экономить на размере. Было выпилено большое количество полифилов, да и сам Angular тоже не стоит на месте и все лучше справляется с tree-shaking.
Размер бандла до обновления:
Размер бандла после обновления:
Ладно, а результат?
Ни один клиент из-за IE не отвалился. Возможно, кто-нибудь и был недоволен, но до разработки гнев не дошел. Эта история еще раз доказала, как важно работать с клиентами и общаться со стейкхолдерами, чтобы пропушить нужные изменения. Никто не пострадает оттого, что будет нажимать другую иконку браузера на своем рабочем столе. Часто люди просто привыкают к чему-то и потом с трудом принимают необходимость изменений. Но если принимают, то получают evergreen-браузер, который обновляется сам. Для конечного пользователя это гораздо безопасней, чем сидеть в IE, который не обновлялся уже много лет. Это хорошо и для нас как банка, потому что снижает риски от всякого рода зловредного ПО, что может находиться на компьютере.
Конечно, нам еще есть над чем работать — например, улучшать метрики Lighthouse. Но уже от одного отказа от IE выиграли все. Разработчикам больше не надо костылять баги IE, тестировщикам больше не надо тестировать на IE. Да и скорость работы за счет сборки и других факторов увеличилась. Бандл уменьшился, что улучшило работу нашего приложения для конечных пользователей.
Скорость загрузки хоть не напрямую, но прибавила несколько баллов банк-клиенту в рейтинге SUS (System Usability Scale). Все мы помним исследования Google и Amazon о том, что даже сотня миллисекунд влияет на принятие решения о покупке и пользователи охотнее прибегают к сервисам, которые быстро работают. Очки Performance до обновления:
Очки Performance после обновления:
IE также не позволял нам внедрить brotli-сжатие, которое эффективнее gzip работает с текстом и, что важно, быстрее распаковывает его в браузере. Теперь у нас развязаны руки, и внедрение дополнительно ускорит загрузку нашего интернет-клиент-банка. Также мы посматриваем на avif-изображения — как раз недавно в последний Safari TP завезли их нормальную поддержку. Правда, полное отсутствие ее в Edge пока пугает. Возможно, мы придумаем что-нибудь с фолбэком к jpeg для таких случаев, либо просто подождем еще 🙂
К чему в итоге пришли сегодня? Мы, наверно, единственный крупный банк в России, чей интернет-клиент-банк для крупного бизнеса находится в продакшене на последнем, 15-м Angular (и мы уже готовы к 16-му). Мы отдаем код в ES2022 и используем практически все библиотеки в последних версиях, а также свежайшие LTS-образы nginx и nodejs в сборочном процессе (спасибо нашим девопс-инженерам за помощь в подготовке образов!). Все это не может не радовать наших безопасников и нас самих; поддержка стала проще, а обновления — легче.
Я подробно перечислил всё, что нам дал отказ от IE, чтобы вдохновить тех, кто на это еще не решился. Если вы всё еще поддерживаете Internet Explorer, я желаю вам наладить общение со стейкхолдерами и решиться на отказ от поддержки.
Напоследок — несколько ссылок на другие посты в нашем блоге с историями масштабных изменений: