Хабрахабр

Надёжный JavaScript: в погоне за мифом

JavaScript нередко называют «самым популярным языком», но, кажется, никто не отзывается о JS-разработке как о «самой безопасной», и количество подстерегающих проблем в экосистеме велико. Как эффективно их обходить?

А поскольку зрительские отзывы оказались отличными, мы теперь подготовили для Хабра текстовую версию этого доклада. Илья Климов задумался об этом, когда ошибка обошлась очень дорого (в буквальном смысле) — и в итоге сделал доклад на HolyJS. Под катом — и текст, и видеозапись.

Меня зовут Илья Климов, я из Харькова, Украина. Всем привет. Мы делаем всё, за что платят деньги… в смысле, программируем на JavaScript во всех отраслях. У меня собственная небольшая, до десяти человек, аутсорсинговая компания. Сегодня, разговаривая о надёжном JavaScript, я хочу поделиться своими наработками где-то за последний год, с тех пор как меня эта тема начала достаточно жёстко и серьёзно беспокоить.

Те, кто работает в аутсорсе, прекрасно поймут содержимое следующего слайда:

Как говорят в South Park, все персонажи спародированы, причём убого. Всё, о чем мы будем говорить, конечно же, не имеет никакого отношения к реальности. Естественно, те места, где есть подозрение на нарушение NDA, были согласованы с представителями заказчиков.

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

Иногда я даю обещания. У меня есть образовательный проект JavaScript Ninja. Я пообещал в 2017 году в рамках образовательного проекта записать видео про Kubernetes. Иногда я даже их выполняю. И я сел записывать (вот результат). Я осознал, что дал это обещание и хорошо бы его выполнить, 31 декабря.

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

Списали на сезонность: все ушли пить чай. Поскольку это было 31 декабря, часть заказов пропала вникуда. Такого дорогого продакшена у меня ещё не было. Когда заказчик очнулся, примерно 12-13 января, суммарная стоимость видео составила порядка $500 000.

Новомодная экосистема Infrastructure as a Сode: всё, что можно, описано кодом и конфигами, Kubernetes дёргается программно из JavaScript-оболочек и так далее. Пример номер два: очередной кластер Kubernetes. Немножечко изменяют процедуру развёртывания, и наступает момент, когда необходимо развёртывать новый кластер. Классно, всем нравится. Возникает следующая ситуация:

const config = {
// …
mysql: process.env.MYSQL_URI || ‘mysql://localhost:3306/foo’
// ...
}

У многих из вас наверняка тоже есть такая строчка кода в ваших конфигах. То есть забираем конфиг из mysql-переменной или берём локальную базу.

В этот раз денег было потрачено поменьше — всего $300 000. Из-за опечатки в системе деплоймента получилось так, что система опять сконфигурировалась в качестве продакшна, а вот MySQL-базу использовала тестовую — локальную, которая лежала для тестов. К счастью, это уже была не моя компания, а место, где я работал как привлечённый консультант.

Но расскажу про ещё одну ситуацию. Вы могли подумать, что вас как фронтендеров всё это не касается, ведь я рассказывал о DevOps (кстати, я восхищён названием конференции DevOops, отлично описывает суть).

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

Поскольку у нас JavaScript, а JavaScript и типы — это очень хорошо, пустые поля широты и долготы радостно приводятся к нулевым координатам. Происходит вспышка какого-то очередного ящура (я не силён в коровьих болезнях), и в спешке оператор случайно два раза нажимает кнопку «добавить», оставив поля пустыми.

Мы пытаемся объединить точки возникновения эпидемий. Нет, мы не отправили врача далеко в океан, но оказалось, что для отображения на карте, построения отчётов, аналитики, анализа размещения людей на бэкенде всё это предварительно просчитывается с точки зрения кластеризации.

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

В отчёте об этой ситуации было написано «Мы стали жертвой неудачного стечения обстоятельств». Суммарная оценка потерь — порядка миллиона долларов. Но мы-то знаем, что дело в JavaScript!

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

Система компании радостно выкупила все рейсы собственной авиакомпании, обнаружила, что мест не хватает, и отправила данные в международную систему бронирования, чтобы компенсировать нехватку. Однажды по нелепой случайности приходит заказ на бронирование билетов из Нью-Йорка в Лос-Анджелес в количестве 999 999 штук. Международная система бронирования, увидев запрос в приблизительно 950 000 билетов, радостно отключила эту авиакомпанию от своей системы.

Однако за эти семь минут стоимость штрафов, которые пришлось заплатить, составила всего-навсего $100 000. Поскольку отключение — это из ряда вон выходящее событие, после этого проблема была решена в течение семи минут.

Но эти случаи заставили меня задуматься о вопросах обеспечения надёжности и задать два исконно русских вопроса: кто виноват и что с этим делать? К счастью, это всё происходило не в один год.

Почему так происходит: юность экосистемы

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

Поскольку мне было скучно, я их решал на JavaScript с помощью функционального программирования. Более того, год назад я проходил соревнование, где надо было каждый день решать алгоритмические задачки. Это была всего лишь небольшая ошибка в оптимизаторе TurboFan, который только-только попадал в основной Chrome. Я написал абсолютно чистую функцию, и она в актуальном Chrome 1197 раз работала правильно, а 3 раза выдавала другой результат.

То есть мы выполняли код порядка 1197 раз, потом приходил оптимизатор и говорил: «Ух ты! Конечно, она была поправлена, но вы же понимаете: такое означает, например, что если ваши юнит-тесты один раз прошли, это совершенно не означает, что они будут работать в системе. Давайте её соптимизируем». Горячая функция! И в процессе оптимизации приводил к неправильному результату.

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

Из-за этого (не потому что мы не воспринимаем это серьёзно) у нас всё ещё проблемы с тем, что не хватает инструментария. Долгое время JavaScript воспринимался как игрушка.

Как говорится, «правило номер один — не говорить о надёжности». Поэтому, чтобы бороться с этой причиной, которая является фундаментальной первоосновой всего, о чем буду сегодня говорить, я попытался сформулировать правила надёжности, которые мог бы навязать в своей компании или передать в качестве консалтера в другие. А если серьёзнее, то…

Правило надёжности #1: всё, что может быть автоматизировано, должно быть автаматизировано

Включая, кстати, и проверку орфографии:

Скрытый текст

Правило надёжности #1: всё, что может быть автоматизировано, должно быть автОматизировано

Всё начинается с самых простых вещей. Казалось бы, все давно пишут на Prettier. Но только в 2018 году эта штука, которую мы все используем, хорошая и здравая, научилась работать с git add -p, когда мы частично выполняем добавление файлов в git репозиторий, и нам хочется красиво отформатировать код, допустим, перед отправкой в основной репозиторий. Абсолютно той же проблемой обладает достаточно известная утилита realinstaged, которая позволяет проверять только те файлы, которые были изменены.

Я не буду спрашивать, кто тут его использует, потому что нет смысла в том, чтобы весь зал поднимал руки (ну, я надеюсь на это и не хочу разочаровываться). Продолжим играть в Капитана Очевидность: ESLint. Лучше поднимите руки, у кого в ESLint есть собственные кастомно написанные правила.

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

Я не буду его выносить в common, потом сделаю». Мы все хотим определённого уровня изоляции, однако рано или поздно возникает ситуация: «Смотри, вот этот helper Вася реализовал где-то в директории своего компонента совсем рядышком. Это приводит к тому, что в проекте начинают появляться не вертикальные зависимости (когда верхние элементы подключают нижние, нижние никогда не лезут за верхними), а компонент A зависит от компонента B, который находится совершенно в другой ветке. Волшебное слово «потом». В итоге компонент A становится не так просто переносимым в другие компоненты.

Кстати, выражаю респект «Альфа-банку», у них очень хорошо и красиво написана библиотека компонентов на React, ей пользоваться одно удовольствие именно в плане оформления качества кода.

Банальное ESLint-правило, которое следит, откуда вы импортируете сущности, позволяет существенно увеличить качество кода и сохранить ментальную модель при code review.

У нас недавно в Харьковской области большая серьезная компания PricewaterhouseCoopers закончила исследование, и средний возраст фронтендера составил порядка 24–25 лет. Я уже с точки зрения мира фронтенда старый. Поэтому я с удовольствием пишу ESLint-правила, чтобы не думать о таких вещах. Мне уже тяжело думать обо всём этом, я хочу при ревью пулл-реквеста сосредотачиваться на бизнес-логике.

И эти селекторы лежат где-то в совершенно другой иерархии, поэтому «../../../..». Казалось бы, под это можно подстроить обычные правила, но реальность обычно расстраивает куда больше, потому что, оказывается из реактовского компонента нужны какие-нибудь селекторы Redux (он, к сожалению, всё ещё жив).

К примеру, мой горячо любимый Flow. Или, ещё хуже, webpack alias, который ломает приблизительно 20% другого инструментария, потому что не все понимают, как с ним работать.

В идеальном мире вы, конечно, напишете инструкцию, которую всё равно никто не прочитает. Поэтому в следующий раз перед тем, как вам захочется нарычать на джуниора (а у программиста есть такое любимое занятие), задумайтесь, можете ли вы как-то автоматизировать это, чтобы не допускать ошибки в дальнейшем. А это те люди, с которых надо брать пример! Вот спикеры HolyJS — талантливые специалисты с большим опытом, но когда на внутреннем митинге было предложено составить инструкцию для спикеров, на это сказали «да они ж её не прочитают».

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

Ещё несколько пунктов: Если вы думаете, что это всё очень просто — как говорится, hold my beer, скоро разберёмся, что всё бывает сложнее, чем вам кажется.

Типизация

Если вы не пишете на TypeScript, возможно, вам стоит об этом задуматься. Я TypeScript не люблю, я традиционно хайплю Flow, но об этом мы можем похоливарить позже, а здесь со сцены я буду с отвращением продвигать мейнстримное решение.

У программного комитета TC39 недавно было очень большое обсуждение, куда вообще идёт язык. Почему так? Очень забавный вывод, к которому они пришли: в TC39 вечно «лебедь, рак и щука», которые тащат язык в разных направлениях, но есть одна вещь, которую хотят все и всегда, — это перформанс.

TC39 неофициально, во внутреннем обсуждении, выдал такую тираду: «мы всегда будем делать JavaScript так, чтобы он оставался производительным, а те, кому не нравится, возьмут какой-нибудь язык, который компилируется в JavaScript».

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

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

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

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

Причем язык X — это обычно какой-нибудь PHP, а не красивые Go и тому подобные. Мне регулярно приходится объяснять заказчику, почему мы взяли в качестве бэкенда JavaScript, а не язык X. К сожалению, как показывает практика, часто этот тезис остается всего лишь фразой на конференции и не находит воплощения в реальной жизни. Мне приходится объяснять, что мы способны максимально эффективно переиспользовать код, в том числе между клиентом и бэкендом, за счет того, что они написаны на одном языке программирования.

Контракты

Я уже говорил о юности экосистемы. Контрактное программирование существует более 25 лет как основной подход. Если вы пишете на TypeScript, возьмите io-ts, если вы пишете на Flow, как я, возьмите typed-contract, и получите очень важную вещь: возможность описывать runtime-контракты, из которых выводить статические типы.

Я знаю людей, которые потеряли пятизначные суммы в долларах просто из-за того, что их тип, описанный на языке со статической типизацией (они использовали TypeScript — ну, конечно, это просто совпадение), и runtime-тип (вроде бы использовали tcomb) немного различались. Хуже нет для программиста, чем наличие более чем одного источника правды.

Юнит-тестов на неё не было, потому что нам же проверил это статический типизатор. Поэтому в compile-time ошибка не было поймана, просто потому что зачем её проверять? Нет смысла проверять вещи, которые были проверены слоем ниже, все помнят иерархию тестирования.

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

Почему так происходит: изоляция

Следующая проблема — изоляция. Она многогранна и многолика. Когда я работал для компании, связанной с отелями и авиаперелетами, у них было приложение на Angular 1. Это было достаточно давно, так что простительно. Над этим приложением работала команда из 80 человек. Все было покрыто тестами. все было хорошо, пока я один прекрасный день не сделал свою фичу, не замерджил ее и обнаружил, что я сломал в рамках тестирования совершенно невероятные места системы, которых я даже не касался.

Оказалось, что я чисто случайно назвал сервис точно так же, как другой сервис, который существовал в системе. Оказалось, у меня проблемы с креативностью. Поскольку это был Angular 1, а система сервисов там была не strictly typed, a stringly typed — строкотипизированная, Angular совершенно спокойно начал подсовывать мой сервис в совершенно другие места и по иронии судьбы парочка методов в именовании совпала.

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

В приложении был реализован code splitting, и это означает, что последовательность подключения модулей напрямую зависело от путешествия пользователя по сайту. Очевидно, когда приложение пилят 80 человек, это значит, что оно большое. Потому что вроде бы никому никогда не понадобится одновременно связываться с обоими модулями скидок. Чтобы было ещё интереснее, так сложилось, что ни один end-to-end тест, который тестировал поведение и проход пользователя по сайту, то есть определённый бизнес-сценарий, не поймал эту ошибку. Правда, это полностью парализовало работу админов сайта, но с кем не бывает.

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

Когда у вас в руках молоток, все становится подозрительно похожим на гвоздь. Lerna — это превосходный инструмент для управления несколькими npm-пакетами в репозитории. Все знают, что в unix-системах всё есть файл. Когда у вас есть unix-подобная система с правильной философией, всё становится подозрительно похоже на файл. Есть системы, где это доведено до высшей степени (чуть не сказал «до абсурда»), вроде Plan 9.

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

Просто потому что вы не можете из одного пакета нормально дотянуться до другого. Когда вы выносите какой-то элемент функциональности, будь это компонент или ещё что-то, в отдельный пакет, вы автоматически обеспечиваете слой изоляции. Особенно это зависит напрямую от версии Yarn. И ещё потому, что система работы с пакетами, которые собраны в монорепозитории через npm-link или Yarn Workspaces, устроена настолько ужасно и непредсказуемо с точки зрения того, как это организовано внутри, что вы даже не можете прибегнуть к хаку и подключить какой-нибудь файл через «node_modules что-то», просто потому, что у разных людей это всё собирается в разную структуру. Там в одной из версий втихаря полностью поменяли механизм, как Yarn Workspaces с организует работу с пакетами.

Вам может быть известен другой пакет, который реализует то же самое, — это continuation-local-storage. Вторым примером изоляции, чтобы показать, что проблема многогранна, является пакет, который я стараюсь сейчас повсеместно использовать, — это cls-hooked. Он решает очень важную проблему, с которой, к примеру, не сталкиваются, например, разработчики на PHP.

В PHP у нас, в среднем по больнице, все запросы изолированы, взаимодействовать между ними, только если вы не используете какие-нибудь извращения типа Shared Memory, мы не можем, все хорошо, мирно, красиво. Речь об изоляции каждого конкретного запроса. По сути, cls-hooked добавляет то же самое, позволяя создавать контексты выполнения, класть в них локальные переменные и потом, что самое важное, автоматически уничтожать эти контексты, чтобы они не продолжали поедать вашу память.

Есть небольшое падение по производительности, но возможность автоматической сборки мусора бесценна. Этот cls-hooked построен на async_hooks, которые в node.js всё ещё в экспериментальном статусе, но я знаю не одну и не две компании, которые используют их в достаточно суровом продакшене и счастливы.

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

Правило надёжности #2: плохой и «неправильный» код должен выглядеть неправильно

С первым критерием, который я для себя вывел, десять лет назад бы сам не согласился. Потому что пищал бы, что JavaScript — динамический язык, а вы лишаете меня всей красоты и выразительности языка. Это grep-тест.

Многие пишут в vim. Что это такое? Конечно, есть вариации на тему, но общая идея понятна. Это означает, что если вы не используете какие-нибудь относительно новомодные навороты типа Language Server, единственный способ найти упоминание какого-нибудь атома — это, грубо говоря, grep. Вы скажете, все так делают, ничего странного. Идея grep-теста в том, что вы должны найти все вызовы функций и их определение путём обычного grep.

Это самая популярная ORM для реляционных баз данных. Возьмем Sequelize. Как вы думаете, откуда появляется метод getProjects? И возьмем совершенно простой код user.getProjects(). Он появляется благодаря магии.

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

Последним оплотом в обеспечении надёжности всегда будет человек. Я очень много говорю о code review, потому что мы можем автоматизировать что угодно, но мы никогда не можем предусмотреть всего. Наша задача — максимально упростить работу этому человеку, чтобы ему пришлось думать о минимальном количестве вещей.

Это абсолютная правда, я сам такой. Я уже несколько раз повторял эту фразу, но она мне всё ещё нравится: «merge request на 20 строчек — 30 замечаний, merge request на 5000 строк — looks good to me».

Вчера запостил скриншот «перевёл половину проекта на react и redux», там было 8000 строк добавлено, 10 000 удалено» Я с нетерпением предвкушаю, как его будут реьювить, с нетерпением жду «выхода нового сезона». У нас в чатике JavaScript Ninja есть человек, которого, похоже, взяли на позицию junior-разработчика, и против нашей воли делится с нами успехами своего рефакторинга. При этом, по его словам, ему сказали «всё в порядке», и он искренне уверен, что этот merge request никак нельзя разбить на отдельные части.

Это превосходный пример репозитория, где все работают не с коммитами, а с патч-сетами. Тем, кто считает, что такие merge request оправданы, и их действительно нельзя разбить на новые части, советую взять пример с ядра Linux. Вы знаете, почта не лучшая среда для работы с патчами. То есть по почте вам присылают патчи, который вы должны накатить на свой git. В почте очень быстро начинает теряться контекст. Ревьювить большой код в почте неудобно, в частности, вам сложно оставить комментарий к конкретной строчке.

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

Мой ноутбук Microsoft Surface почти идеально работает под Linux, но там не отображается статус батарейки. Я знаю, о чем говорю. И навык разбивать большой код на отдельные мелкие патч-сеты фундаментально важен для обеспечения надёжности. И я очень внимательно наблюдаю за тем, как люди реверсят протокол и как они постепенно готовят патчи для включения в основную ветку ядра, это очень сложно.

Почему так происходит: магия

К примеру, большинство из вас вряд ли представляют всю магию реконсилера React. Магия бывает не только «запрещённой за пределами Хогвартса», но и «вредной и полезной». Потому что если мы говорим о шестнадцатом React, Fiber — это мегасложно.

Дальше они внедрили планировщик, который очень сильно напоминает код первых примитивных планировщиков операционных систем. По сути, разработчики Fiber полностью с нуля реализовали стековую машину для вызовов (которая была почти полностью содрана из OCaml) внутри JavaScript, чтобы иметь возможность асинхронно обрабатывать рендеры компонентов. Scheduler — это proposal stage 0. Дальше они поняли, что планировщик — это очень сложно, поэтому они создали proposal, чтобы внедрить это прямо в JavaScript.

То есть что он делает, понятно: мы отрендерили, он применяет виртуальный DOM. Но тем не менее, магия React в том, как он делает что-то максимально быстро. Это полезная магия, потому что она управляема. Но как он это делает быстро — это уже магия.

Кому из вас нравится Vue? Приведу пример другого фреймворка — Vue.js. Я долго восхищался Vue, теперь период большого разочарования, сейчас объясню, почему. Я рад, что вас становится меньше.

Проект, написанный джуниором на Vue, абсолютно точно работает гораздо быстрее проекта, написанного джуниором на React. Абстрактный проект на Vue работает гораздо быстрее абстрактного проекта на React.

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

Наверняка многие из вас, если не все, знакомы с философией Web Components и c философией трансклюзии, когда у нас есть слоты, и контент из дочернего объекта вставляется в дыру в другом компоненте. Вторая классная идея. Просто передаём кусок — здраво. Простейший пример применения слота: допустим, у нас есть pop-up, который содержит оверлеи, крестик и так далее, и нам надо передавать туда какой-то контент.

Они нужны, когда нам надо в слот передать еще какие-то данные. Во Vue есть слоты на стероидах — scoped slots. То есть вам нужен кастомный рендер для каждой строки. Простейший пример применения scoped slot — это когда есть таблица, и вы хотите кастомизировать каждую строку. Во Vue вы просто вставляете кусок шаблона и не задумываетесь, что на самом деле это тоже компилируется в рендер-функцию. В React умные хипстеры сейчас бы использовать для этого Render Proper: красиво, декларативно.

Прям в исходниках Vue написано: проверить, если у child-компонента, который мы обновляем, есть scoped slots, сделать forceUpdate. Как только у вас в компоненте появляются рендер-слоты, ваша хвалёная система реактивности Vue превращается в тыкву. Это становится катастрофой для производительности в отдельных ситуациях. Как только родитель обновляется, child тоже перерисовывается.

У нас есть, допустим, shouldComponentUpdate(), который запретит компоненту перерисовываться. В React со всей его магией мы можем этой магией управлять. Приходится изобретать совершенно феноменальные костыли для этого. С Vue нам остаётся долгий пристальный взгляд, чтобы на это посмотреть, у нас нет механизма, например, чтобы запретить компоненту перрерисоваться. Это отдельная история, когда-нибудь он точно выйдет. Это поправлено в третьем Vue.

Превосходный фреймворк тестирования от Facebook. Последний пункт — это Jest. Он прекрасно умеет мокать импорты. Я абсолютно честно считаю его на данный момент лидирующим фреймворком в мире тестирования JavaScript: красивый, выразительный, эффективный. Это работает превосходно до тех пор, пока это работает.

Вы не можете подменять реализацию импортов, вы не можете объявить импорт в if, в зависимости от require. Проблема в том, что импорты по определению по спецификации к ECMAScript 2015 являются статическими. Импорты, в принципе, не могут быть подменяемы, они должны быть вычислены ещё до процесса вычисления компонента. Require вы можете применить, а импорт нет. Jest компилирует с помощью Babel импорты в Require и дальше их подменяет.

Потому что это противоестественно иметь в мире JavaScript две системы управления модулями: одну с Require, вторую с импортами. В один прекрасный день вы решаете побыть совсем хипстером и воспользоваться NGS-модулями в Node, которые являются proposal, но совсем скоро станут стандартом. И тут вы обнаруживаете, что Jest c NGS-модулями бессилен, потому что нодовцы решили строго следовать спецификации, и подменять импорты нельзя в принципе. Хочется везде писать импорты. То есть результат вычисления импорта является замороженным объектом, который разморозить нельзя.

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

IoC/DI

Причём, как вы понимаете, эта философия уже давно есть на фронтенде. Angular целиком построен вокруг философии IoC/DI. В React сам господин Абрамов… кстати, знаете, почему React круче Vue? Потому что сайт dan.church зареган, а сайта evan.church пока нет.

В самом Vue даже слова называются inject и provide. Так вот, Абрамов говорил, что контекст в React является по сути реализацией паттерна Dependency Injection, позволяет вам добавлять в контекст какие-то зависимости и вытягивать их на уровни ниже. То есть на фронте этот паттерн уже давно присутствует в неявном виде.

К примеру, NestJS. Что касается бэкенда, у нас тоже начинают появляться вещи, которые реализуют это. Он построен с любовью к TypeScript, что, впрочем, не мешает ему работать с использованием обычного JavaScript. Я использую InversifyJS. И тут мы упираемся в то, что наша экосистема все еще недостаточно взрослая.

Посмотрим на конструктор кода, в котором мы говорим: inject(TYPES. Код на Typescript, взят из документации Inversify. Как вы думаете, на каком этапе будет проверяться, что объект типа TYPES. Weapon) katana: Weapon. Ответ — никогда. Weapon удовлетворяет классу Weapon?

Грубо говоря, если вы опечатаетесь в том, что вы вставляете в объект, хвалёный TypeScript (хваленый Flow поведет себя точно так же) не сможет никак это проверить, потому что процедура inject и весь dependency injection работает в runtime, а там информации о типах почти нет.

Если вы будете использовать Weapon не как интерфейс, а как абстрактный класс, TypeScript сможет это проверить, потому что у нас есть абстрактные классы. Что значит «почти нет»? А вот интерфейсы являются эфемерными сущностями, которые испаряются, и в runtime у вас уже не будет никакой информации о том, что элемент katana должен удовлетворять интерфейсу Weapon. Абстрактные классы в TypeScript компилируются в классы, а классы являются first-class citizen в JavaScript, они остаются после компиляции. И это проблема.

В C# и Java есть reflection, тоже позволяющий получать необходимую информацию. В каком-нибудь C++, который существует больше лет, чем я живу, существует RTTI: run-time type information, позволяющий во время выполнения получать информацию о том, как это работает. Разработчики TypeScript, у которых есть отдельный раздел «что мы НЕ будем реализовывать», сказали, что не намерены предоставлять никакие инструменты для RTTI.

Пятнадцать секунд инсайдов. Доходит до смешного. Очевидно, что тип передаваемых props и тип объектов, появляющихся на this, должен быть одинаковым. Разработчики Vue переписывают Vue 3 на TypeScript, и обнаружили, что подход Vue несовместим с философией TypeScript настолько, что они попросили Microsoft: «А можно, мы напишем свой плагин к TypeScript, чтобы это работало?» Те, кто пишет на Vue, поймут: когда вы в компонент передаёте props, они магически появляются на this. Сюрприз. Но в TypeScript вы не можете это ни проверить, ни описать.

Им же свой компилятор пришлось дописывать, чтобы нормально обойти ограничения TypeScript. Разработчики Microsoft отказали, а то знаем мы вас: сейчас разрешим Vue это сделать, потом потянется Angular, у которых очень давно болит от этого. Потом подтянется React, и у нас получится не система типизации, а непонятно что.

Как ни странно, мне в этом плане импонирует Dart, который имеет mirrors, работающие везде, кроме Flutter. Но тем не менее, проблема все еще остается. Даже если не доступны mirrors, есть инструменты, с которыми можно через кодогенерацию на этапе компиляции вытащить всю необходимую информацию, сохранить её и всё ещё получить получить доступ в рантайме.

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

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

Более того, есть определённые инструменты, которые позволяют вытащить из V8, если собрать его с определёнными патчами, информацию о типах, которую он собрал. V8 начинает собирать информацию о типах, потому что она фундаментально важна для работы оптимизирующего компилятора. Очень забавно сравнивать выводы в типах, которые получились в V8 и выводы, которые были у вас в TypeScript.

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

Правило надежности #3: должно быть проще писать «правильный» код, чем «неправильный»

Когда я говорю о словах «правильный» или «неправильный» код, в данном контексте, «правильный» — это код, который устраивает команду. В каждой команде есть определённые конвенции и соглашения, особенно это важно во фреймворках типа Vue, где одну задачу можно решить множеством путей. Кто-то использует слоты, кто-то передаёт props’ами функцию и так далее.

Кто использует typed-css-modules? Здесь всеми красками сияет кодогенерация. О чём идёт речь: вы пишете CSS, потом подключаете его в CSS Modules, а потом обращаетесь к конкретным полям полученного объекта.

Существует готовое решение typed-css-modules, которое с помощью кодогенерации позволит проверить, что вы обращаетесь классом css-файла, который там точно есть.

И в 11 из 12 я нашел на сгенерированной странице класс undefined, потому что CSS-стили развивались, изменялись и так далее, а в итоге радостно подключается из объекта стилей класс, которого не существует, и получается undefined, бывает. Я за последний год участвовал в 12 проектах в качестве консультанта, которые использовали CSS-модули и существовали по крайней мере, полгода.

Чтобы применить его для конкретного проекта, приходится очень много думать, страдать и пытаться. Yeoman хорошо известен всем, но проблема его и всего подобного инструментария в том, что попытка построить слишком общий инструмент для кодогенерации приводят к тому, что он почти бесполезен в частных случаях. Поэтому мне гораздо больше импонирует инициатива Angular CLI, которая называет Blueprints (чёрт, я два года хаял Angular, а через два дня на GDG SPB буду рассказывать, какой он классный, и как я его полюбил).

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

Вообще эталонный пример, когда «правильный» код писать проще — функциональное программирование. Ещё один пример, когда «правильный» код проще писать, чем «неправильный» — это хорошие, большие фреймворки. Как только вы познали дао функционального программирования, поняли, что такое монада, попутно лишившись возможности объяснить это остальным, вы понимаете, что в функциональном стиле вы или пишете правильный код, или он у вас не получается вовсе.

У меня в команде есть junior’ы, и они важны мне для того, чтобы не терять отрыв от реальности, понимать, как вообще в реальном мире люди пишут код. Другое дело, что там порог входа — проще повеситься. Поэтому, к примеру, React-хуки не стали брать концепцию чистого функционального программирования.

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

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

Я не буду сейчас критиковать Sails за совершенно убогую ORM, которая идет в комплекте. Казалось бы, и тот и другой фреймворк, решают одну и ту же задачу, они навязывают конкретную архитектуру, и описывают, как вы должны решать то-то и то-то. Но проблема в том, что при всем этом Sails.js обеспечивает слишком много магии. Waterline — это первая штука, которую все выбрасывают. Это не просто работает медленно, а архимедленно, причем это дико сложно отлаживать другим. Вы можете делать blueprint контроллеров, из которых потом генерируется куча магических свойств и вещей.

У вас есть Dependency Injection, который лежит в основе всего, и есть правило описания контроллеров middleweight и так далее. А вот если мы говорим о Nest, то здесь отличный компромисс — фреймворк просто дает компоненты, каждый из которых необязательно использовать.

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

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

Никто не пилит ничего за пределами экосистемы Vue, поэтому если вы будете выбирать что-то из этих двух вещей, вывод достаточно очевиден — берите Nest. Если мы берём какой-нибудь Vue (извините, сегодня получается лёгкий хейт в его сторону), то обнаружим, что за пределами экосистемы мейнстрима Vue начинается выжженная земля. Он крутой, мне очень нравится, куда он развивается, несмотря на то, что TypeScript.

Почему так: общее

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

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

Я был свидетелем, когда платформа для создания чат-ботов не учла один интересный сценарий, который позволял зациклить бота. Мы используем Grafana, мне очень нравится, но что вы выберете — абсолютно неважно. Бот пытался распознать фразу, распознавал, уходил в ошибку, начинал заново пытаться сделать что-то. Поскольку каждая его фраза обращалась к speech recognition API от Microsoft, к тому моменту, когда обнаружили ошибку, счёт составлял порядка 20 тысяч долларов.

Нам нравится GitLab, но это дело вкуса, у каждого свои впечатления. Следующий важный пункт: CI/CD. Но сейчас GitLab, по моим ощущениям, это наиболее JavaScript-friendly environment с точки зрения понимания, как это работает, что с этим делать и так далее.

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

Если эти слова вам незнакомы, загуглите, потом сможете продать себя подороже! Blue/Screen deployment: Если у вас хоть сколько-нибудь важный сервис или микросервис, задумайтесь о том, чтобы держать в облаке два инстанса.

Что мы имеем в итоге

  • Экосистема JavaScript очень юная, и это открывает очень большие возможности тем, кто хочет круто хайпануть. Вы берете любой другой язык программирования, ищете, как та или иная проблема решена в нём, перекладываете решение на JavaScript и гребёте звездочки на GitHub. Рецепт рабочий — я так сам, конечно, не делал, но тем не менее.
  • Обеспечение надёжности JavaScript упирается в то, что разработчики не хотят переучиваться (особенно актуально для аутсорсинговых компаний, где мотивация разработчиков ниже продуктовых). Есть отличная фраза Андрея Листочкина «Хорошо не жили, нечего и начинать». Необходимость построения надёжного кода требует полного изменения подхода к тому, как этот код пишется. Одно внедрение DI требует кардинального изменения в философии, вида «Где же мои уютненькие импорты?»
  • Язык и низлежащая экосистема не готовы полностью к написанию надёжного кода, что бы вы не выбрали. Выберите TypeScript, Flow, даже если выберете Rizen — вы столкнётесь с тем, что рано или поздно настигнет runtime exception, который вы не сумеете обработать, и так далее.

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

Среди спикеров — например, Мишель Вестстрате (создатель MobX) и Алексей Козятинский (работает в команде Chrome DevTools). Если вам понравился этот доклад с HolyJS, обратите внимание: в следующий раз HolyJS состоится 24-25 мая в Петербурге. Вся актуальная информация и билеты — на сайте.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»