Обновляем библиотеки на фронтенде за 24 часа - на самом деле за 120 дней
В один прекрасный осенний день, я решил, что пора. Пора обновить Lodash до 4 версии у нас в проекте.
У нас в компании я периодически слышу один и тот же холивар. Наступает такой момент, когда один из моих коллег встает и говорит: “Смотрите, там React 16 / Typescript 3 / что-то ещё — вышел с кучей крутых фишек! Давайте переезжать?”.
И тут весь офис делится на два лагеря: на тех, кто говорит “у нас миллионы строк кода и это всё как-то работает; обновить нереально!” и тех, кто говорит “ребята, ну мы точно должны это сделать!”. У каждой стороны есть аргументы в свою пользу. Эта дискуссия может продолжаться вечно, у нас так и происходит.
Забавный факт: в нашей компании такое происходит чаще всего среди фронтенд-разработчиков. Бекенд-команды в этом плане поступают проще. “Надо PostgreSQL обновить с 9 на 10 версию? Надо. Тогда давайте запланируем миграцию и создадим задачу на это”.
На фронтенде всё сложнее. Возможно, потому что у нас в одном репозитории ~350 приложений, которые были написаны разными командами из разных стран в разное время. Обновив какую-то библиотеку, ты можешь даже не заметить, как сломал что-то. Нужен герой, нужен человек, который взял бы на себя все эти заботы и ловким движением руки сделал всем хорошо. К счастью, у нас есть несколько таких людей, иногда эту роль принимаю и я.
Топ-5 аргументов, чтобы всё бросить и пойти обновлять библиотеки
У меня, конечно же, есть свои аргументы в пользу того, что мы должны держать стек зависимостей максимально свежим. Я не думаю, что нужно обновлять все библиотеки до последних версий во что бы ты не стало. Но иногда делать это всё же стоит, и вот почему:
- + Новые возможности библиотеки: здесь и действительно новые вещи, и приведение старого API к консистентному виду, а также внутренний рефакторинг библиотеки.
- + Закрытые уязвимости и баги: никто из нас не пишет идеальный код, и все разработчики стараются закрывать найденные баги. Если нет, то появляется вопрос в целесообразности использования той или иной библиотеки, ведь так?. Вы можете сказать мне, что с новой версией появятся новые баги, и будете правы. Только всем известные проблемы, которым уже год и еще немного — уже знают все подряд, а новым багам еще предстоит получить свою долю внимания.
- + Разработчики не любят работать со старыми библиотеками: предложите кандидату на вакансию писать на AngularJS 1.x в 2018 году и посмотрите реакцию. Ну, или спросите меня и посмотрите мою.
- + Вырваться из рутины: ты делаешь каждый день одинаковые задачи? Переключись и сделай что-то необычное для себя и для общего дела!
- + Сделать работу в проекте немного удобнее для команды: все люди делают ошибки, и если ты что-то сломаешь — все поймут. Ты всегда можешь всё исправить, откатить изменения. Но если ты справишься —- о тебе точно будут говорить с уважением, а может даже отправят немного bounty points.
Вот так просто, я пришёл к мысли о том, чтобы обновить Lodash до 4 версии. Хотя стойте. Не обновить! Дело в том, что у нас в сборке объявлены две версии: 3 и 4. Так что я решил, что пора… отрефакторить репозиторий и удалить Lodash@3 как зависимость в проекте.
Что именно обновляем
Все знают, для чего нужен Lodash. Эта библиотека состоит из набора методов, которые требуются программисту ежедневно: определение типа переменной, приведение строк к различным регистрам, фильтрация, сортировка и преобразование коллекций, работа с функциями и многое другое.
После того, как в свет вышел стандарт JS ES6, многие вещи появились в стандарте языка. Но важная особенность Lodash в том, что все методы в библиотеке safe. То есть они гарантируют, что если переменная не будет объявлена — всё будет хорошо и код продолжит свое выполнение.
Итак, я пошел читать, что там по изменениям. Оказалось, что ребята постарались на славу: добавили много нового (что меня не особо интересовало). Но самое главное — они изменили сигнатуры существующих методов, что переводило задачу миграции из разряда “изменить пару строчек и радоваться жизни” до “а впрочем, не очень-то и хотелось”. Но отступать было некуда, я же на весь офис сказал, что справлюсь за денёк. В день, конечно же, я не уложился.
Мигрируем
Перед тем, как начать, один мой коллега предположил, что возможно получится найти скрипт для миграции. “Они явно позаботились о разработчиках” — согласился я и полез на https://github.com/jfmengels/lodash-codemods.
(Codemods — это платформа от компании Facebook для трансформации кода, в основе которой лежит генерация и преобразование абстрактных синтаксических деревьев. Например, Prettier с помощью AST форматирует код в одном стиле, а Babel преобразует код таким образом, чтобы он мог выполняться в старых браузерах)
Отлично! Что-то есть. Запускаем: ~600 файлов изменено. Пробежавшись по изменениям глазами, вижу, что всё не так радужно, как хотелось бы. Много ложных преобразований, изменений явно мало и стойкое предчувствие, что надо брать всё в свои руки глаза руки. Запоминаем полезные преобразования и начинаем.
Я начал с того, что разделил изменения на те, где ты можешь просто переименовать метод, и на те, где тебе придется подумать, поменять местами аргументы вызова или еще какие-либо действия.
Далее я написал скрипт, который сначала ищет файлы с подключением библиотеки, потом в этом списке ищет вызовы Lodash-методов. Для методов, которые можно переименовать — переименовываем. Для тех, над которыми надо подумать — складываем в отдельный список имя модуля и строку вызова для того, чтобы я глазами понял, что происходит и принял правильное решение. Методы, которые остались неизменными — игнорируем.
Небольшая сложность возникла c _.
— я искал такие вхождения. Но библиотека позволяет создать цепочку методов с помощью _()
. Для этой же цели есть вызов _.chain()
и я не знаю, с какой целью нужно использовать первый вариант и зачем он сохранился в библиотеке. Это доставило немного неудобств, пришлось искать еще и такие специфичные цепочки (так как я изначально не знал об этом и закладывался только на основной вариант).
Запускаем: ~1000 файлов в сумме для изменений. Не так уж и плохо, да?! Просмотрев глазами автоматически примененные изменения, я обнаружил несколько ложных результатов, но в целом было хорошо. Некоторые методы были просто переименованы, например:
`_.each`
в `_.forEach`
`_.eq`
в `_.isEqual`
`_.findWhere`
стал частью `_.find`
`_.pluck`
стал частью `_.map`
`_.where`
стал часть `_.filter`
(Переименовывать легко. Понять, правильно ли ты изменил код — гораздо сложнее)
Некоторые методы в 4 версии разделились на два отдельных вызова: например _.max и _.maxBy. В таких случаях я оставлял скрипту право переименовать все вхождения и глазами пробегал полученные изменения, возвращая к первоначальному варианту места, где соответствующая сигнатура метода использовалась правильно.
`_.max`
разделился на `_.max()`
и `_.maxBy(maxFunc)`
`_.sort`
на `_.sort()`
и `_.sortBy(sortFunc)`
`_.uniq`
на `_.uniq()`
и `_.uniqBy(uniqFunc)`
Иногда мы искали в объектах вложенные значения полей`_.find(document.AdditionalReference, 'DocumentTypeCode.value', value);`
(здесь, мы пытаемся найти `item.DocumentTypeCode.value === value`
).
В 4-ой версии такой поиск не работает, но есть выход: `_.matchesProperty`
.
Такой вариант будет полностью соответствовать исходному:`_.find(document.AdditionalReference, _.matchesProperty('DocumentTypeCode.value', value))`
.
Наибольшую боль мне доставило осознание того, что в 3 версии Lodash позволял передать контекст исполнения последним параметром практически для всех методов. В 4 версии от этого отказались и нужно было явно присваивать контекст с помощью .bind(this). Я предположил, что такая форма для привязки контекста используется только когда мы передаем this в качестве контекста, и искал такие вызовы методов.
Самым распространенным вариантом оказался привязка для цикла `_.forEach`
:`_.forEach(panels, panel => this.close(panel), this)`
превратилось в`_.forEach(panels, _.bind(panel => this.close(panel), this))`
.
Весь этот процесс продвигался достаточно медленно. У меня были текущие задачи по работе, и времени на миграцию практически не появлялось. Я занимался миграцией в нерабочее время и в любой свободный момент. Так что всё затянулось на пару месяцев. Ситуация осложнялась тем, что репозиторий находится в активной разработке. И каждый день я проверял новые изменения, чтобы не упустить код от старой версии.
Что с тестами?
Итак, у меня ~2100 строк кода из 1350 файлов с изменениями и единственная мысль в голове: “Лишь бы покрытия тестами хватило, чтобы я ничего не сломал”. Когда у тебя на платформе много приложений, ты физически не можешь знать, как работает каждое из них. Одна надежда на тесты.
Юнит-тесты мне указывали на проблемы по ходу преобразований, так что с ними проблем нет. Смок-тесты, UI тесты… 15 тестов упало. Попробуй разберись!
Сложно локализовать место с ошибкой, и я закапывался всё глубже и глубже в поисках первопричины для каждого отдельного теста. Именно в этот момент я узнал что поведение `_.add`
в новой версии отличается. Аргументы метода нужно явно приводить к числам, иначе метод вернёт в результате конкатенацию входных параметров. Чтобы восстановить исходное поведение, я добавил приведение типов`_.add(_.toNumber(total.linetotal), _.toNumber(total.taxtotal))`
.
Спустя еще пару недель я наконец починил последний упавший тест. Неужели всё?
Финишная прямая
Ну, не совсем всё, конечно. Код-ревью никто не отменял. И тут мы столкнулись с новой проблемой: Google Chrome просто зависает, если открыть github-страницу с изменениями! Решение простое: разбить один коммит на десяток поменьше, чтобы гитхаб мог открыть один отдельный комит.
Пара замечаний о том, как можно было бы сделать проще. Одно из таких элегантных предложений состояло в том, что можно очень легко решить проблемы с контекстом для всех методов сразу: `_.bindAll(this)`
в 4-ой версии явно требует вторым аргументом набор функций, для которых произойдет привязка контента. Это можно сделать буквально за пару символов: `_.bindAll(this, _.functionsIn(this))`
Отлично! Тесты проходят без ошибок, апрув получен.
Что-то еще? Я решил перестраховаться и попросил всех протестировать изменения насколько это возможно. Ради этого я в течение недели обновлял бранч и держал его в актуальном состоянии. Тот факт, что команды приходили, чтобы поддержать и сказать, что все хорошо — придавал уверенности в том, что я собираюсь сделать.
И вот он момент истины. Решение принято, пора выпускать изменения. Выдох, merge, релиз на тестовый стек, релиз на сендбокс, релиз на продакшен. Первый день после релиза, второй день, третий… неделя… ты 24 часа в сутки онлайн в слаке на случай если что-то сломается. И, наконец, мой анонс в слаке о том, что мы полностью мигрировали на Lodash@4.
Стоило ли оно того?
Я оказался чертовски удачливым, и всё прошло гладко. Мне повезло? Возможно. Но даже так я рад, что всё закончилось хорошо. И да, это заняло около 4 месяцев вместо 24 часов, про которые я опрометчиво заявил перед стартом.
Не всегда есть возможность мигрировать на новую версию той или иной библиотеки и нужно трезво оценивать свои силы. Но обновление зависимостей проекта — это отличная возможность открыть для себя много нового, в том числе внутри проекта, над которым вы работаете. Ну и конечно же заработать немного уважения среди коллег, сделав их жизнь лучше.
Автор
Сергей Никитин,
Frontend Engineer