Ще разгледам защо и как да мигрирам голяма кодова база на JavaScript към TypeScript. Нашият екип приключи едно петнадесетмесечно пътуване за обновяване на нашия стек от предния край. За да увеличим трудността, трябваше да направим това успоредно с предоставянето на нови функции на нашите клиенти. Накрая ще говоря и за това дали си струваше усилията или не и как се отрази на нашето развитие.

Започнете с „защо?“

Hootsuite Analytics е едностранично приложение, създадено от самото начало в JavaScript. Ние разчитаме на библиотеката React, на която бяхме много ранни осиновители. На много високо ниво нашето приложение консумира данни от набора ни от задните API. След това трансформира тези данни в няколко типа визуализации. Изобразяваме богати таблици, които показват публикации в социални медии, диаграми, графики, карти и др.

С течение на времето нашето приложение нарасна доста и сега се намира някъде на 150 хиляди реда код. В Hootsuite инвестираме много в техническите аспекти на нашите продукти. Правим това, като винаги преценяваме дали това, което правим, е добро от техническа гледна точка. Всеки голям проект трябва да започне с дефиниране на проблем, така че нека да поговорим за това какви бяха нашите технически предизвикателства.

JavaScript е труден език за моделиране на трансформации на данни. Той е динамичен език и, без да навлиза твърде много в спецификата на това какво означава това, изисква от разработчиците да направят компромис. Те не трябва да уточняват как изглеждат данните предварително, което прави първоначалното развитие много бързо. Обратната страна е, че когато един проект расте както в редовете код, така и в броя на участниците, става експоненциално по-трудно да се разсъждава за потока от данни в различните слоеве на приложението.

Този проблем донякъде се усложнява от използването на React. Тъй като е изграден на принципи на функционално програмиране като неизменност и композиция над наследяване, той насърчава разработчиците да третират компонентите като малки, многократно използвани функции. В средно или голямо приложение е типично да има стотици, хиляди (или дори „десетки хиляди“) компоненти. Данните могат да преминават през десетки компоненти наведнъж.

Представете си, че трябва да прочетете дърво от дузина компоненти на React без информация как изглеждат данните на всяка стъпка. Ето как изглеждаше светът през 2015 г. за нас. Сега, както проницателните читатели ще посочат, React предлага функция, която помага с това: PropTypes. Но те не са задължителни и доста трудни за поддръжка. Не ги използвахме от самото начало, приехме ги година по-късно и започнахме да ги добавяме към нови компоненти. Според нашия опит те не са предоставили достатъчно информация, за да балансират необходимите усилия за поддръжка. Има и по-практични проблеми: повечето IDE не се опитват да анализират PropTypes, за да предложат подсказки, имате нужда от инструменти, за да наложите тяхната коректност, и това е система с ограничен тип, недостатъчно стабилна за нашите нужди.

Друг проблем, с който се сблъскахме, беше, че поради липса на информация за потока от данни беше трудно да се извърши основно рефакторинг. Преместване на файлове, разделяне на големи компоненти на по-малки или преименуване на файлове — тези операции отнемаха време и бяха рисковани. Нашият екип прегърна модулното тестване от самото начало, така че не можахме да сринем нашето приложение чрез преместване на файлове. Но все още отнемаше много време.

Това доведе до интересна реализация: разработчиците рядко преработват кода, освен ако не е евтино и безопасно да го направят. Реалността беше, че кодовата ни база беше в ужасно състояние на „технически дълг“. Разработчиците бяха предпазливи да правят големи промени от страх да не нарушат функционалността или да не отделят твърде много време за това, което първоначално смятаха за основни задачи.

Липсата на довършване на код и контекстуални съвети беше третият и най-досаден проблем от всички. Аз и няколко членове на нашия екип идваме от фона на back-end разработката, работейки със силно типизирани езици като Java или C++. Все още помня първия път, когато написах JavaScript код в IDE. Докато работех, всичко, за което можех да мисля, беше...

Уау, писането на код без никакви IDE предложения е трудно!

След известно време свиквате, но според мен никога няма да сте толкова продуктивни, колкото при машинните езици. Твърде много се случва във всеки един момент в една JavaScript програма. Има твърде много възможности, данните могат да имат всякаква форма и могат да мутират по всяко време без предупреждение.

И така, за да обобщим, бяхме изправени пред три основни предизвикателства:

  • Трудност при проследяване на потока от данни
  • Скъп рефакторинг
  • Липса на контекстна информация в IDE

Решихме да започнем търсенето на решение и осъзнахме, че трябва да излезем извън границите на екосистемата на JavaScript, за да го намерим. Разгледахме какво предлага общността с отворен код като опции за разработка отпред. Това е невероятно време за създаване на уеб приложения, тъй като има много, много възможности за избор извън JavaScript.

Имахме много критично ограничение. Hootsuite е бизнес, милиони клиенти по целия свят зависят от нас, така че не можахме да спрем разработката, докато мигрирахме нашия код на друг език. Миграцията трябваше да бъде постепенна. Щяхме да напишем нов код на нашия нов език, докато бавно пренаписвахме по-стария код, без да се намесваме в продуктовата пътна карта, която трябваше да изпратим. Така че направихме кратък списък с опции и ги обсъдихме един по един.

Един от първите езици, които разгледахме, беше Elm. Богдан Захария, член на нашия екип, е голям привърженик на функционалното програмиране в най-чистата му форма. Той беше този, който въведе този език в нашия екип. Elm е вдъхновен от Haskell и предлага стабилна, въведена алтернатива на JavaScript. Въпреки че беше привлекателен, не можахме да направим прехода към него лесно. Не можахме да намерим лесен начин постепенно да приемем Elm в нашата кодова база. Освен това не е много удобен за начинаещи. Изисква се от разработчиците да разбират някои нетривиални концепции от света на функционалното програмиране. Също така трябваше да обмислим колко лесно би било да включим нови разработчици в нашия екип в Elm. След като преценихме всички тези недостатъци, решихме да продължим да търсим.

Други опции, които анализирахме:

  • Dart: този език отново става популярен поради инвестирането на Google в Fuchsia. Dart е неговият де факто UI технически стек. Липсата на прозрачна пътна карта и несигурното бъдеще означаваха много ясен отказ.
  • PureScript: това е Haskell за front-end разработка. Същите аргументи като използването на Elm. За да бъда честен, той има още по-стръмна крива на обучение от Elm.

Имахме двама останали кандидати в нашия списък: Flow и TypeScript.

Не беше лесно да се избере между тези двама предвид приликите им. Инвестирахме известно време в изследвания, за да видим от първа ръка как изглежда развитието и в двата случая. Това, което открихме, беше, че настройката на Flow беше по-трудна от TypeScript и че опитът на разработчиците беше муден. Това обаче се подобри през последната година, така че ако трябваше да ги анализираме отново днес, това нямаше да има толкова голяма разлика.

И двата типа системи са солидни, но много различни. TypeScript използва система от структурнитипове. Систематаs на Flow е структурна иноминална. Разликата е ключова за това как те се интегрират с по-голямата JavaScript екосистема. Ще обобщя какво означава това, но ако се интересувате, „тази статия“ навлиза в много по-голяма дълбочина. Система с номинален тип предполага, че два типа, Fooи Bar, никога не са равни, независимо от техните дефиниции. TypeScript, използвайки само структурната алтернатива, не се интересува как се наричат ​​типовете. Той анализира тяхната структура и определя дали трябва да действа като равни. На практика това означава, че писането на код е по-малко взискателно към вас. Можете лесно да използвате повторно типове и интерфейси и дори можете да ги дефинирате вградени, без да ги наименувате.

Разгледахме и общностите около тези два проекта. Flow е език, който поне преди година, когато направихме този анализ, се управляваше от Facebook по много затворен начин. Развитието никога не е било прозрачно, нямаше публична пътна карта и много малко хора извън Facebook допринасяха за проекта. TypeScript, напротив, прегърна разработката с отворен код, откакто се премести в GitHub преди няколко години. Те поддържат актуална пътна карта, приемат външни приноси и като цяло поддържат много тясна връзка с общността. Андерс Хейлсберг, сътрудник на Microsoft и главен архитект, говори на конференции (като TSConf) през цялата година, като държи всички в течение на това какви функции предстоят и как се представя проектът.

И така, както може би се досещате, избрахме да използваме TypeScript. Това беше доста дълго въведение за това защо го направихме, но служи като добър шаблон за други екипи, когато решават да променят технически стекове. Виждал съм твърде много екипи да правят технически превключватели, като започват с „как“, а не с „защо“. Вместо да заявявате, че вашият екип иска да премине към друг език (или стек), започнете с разбиране на вашите проблеми.

  1. Какви са сегашните ни трудности?
  2. Как да ги приоритизираме?
  3. Разрешими ли са в рамките на текущия стек?

Винаги трябва да преминавате през тези стъпки, за да можете да достигнете до най-доброто възможно решение.

Продължете с „как?“

В този момент бяхме постигнали консенсус, че искаме да започнем да пренаписваме кодовата си база в TypeScript. Първата стъпка, която предприехме, беше да се уверим, че наистина можем да пишем код на TypeScript.

Създадохме Hootsuite Analytics върху съвременните софтуерни практики. Разполагаме с непрекъсната интеграционна линия, която ни помага да доставяме код в продукция само след като наборите от промени преминат няколко проверки:

  • Нанасяне на мъх
  • Форматиране (с помощта на Prettier)
  • Единични тестове
  • Покритие на кода
  • Тестове за приемане

Имаме три различни среди: разработка, постановка и, разбира се, производство. Когато обмисляхме да добавим TypeScript, трябваше да се уверим, че той не пречи на тръбопровода. Освен това трябваше да го направим безпроблемно за разработчиците.

Няма да навлизам твърде много в конкретните технически подробности за това как интегрирахме TypeScript в нашата кодова база. Има твърде много отлични статии и уроци, които описват процеса по-добре, отколкото аз мога, и ви предлагам да ги разгледате:

Аз обаче ще опиша нашата философия, нашите болезнени точки и това, което сме научили по пътя.

Едно от най-важните решения, които взехме в началото, беше да наложим строг режим. Строгият режим е комбинация от няколко флага на компилатора, които, когато са включени, извършват допълнителни проверки за безопасност на типа на вашия код. Бих препоръчал на всеки екип, който стартира TypeScript проект, да използва строг режим от самото начало. Преместването на код, написан в нестрог режим, към строг режим всъщност е наистина трудно и ще си спестите време в дългосрочен план, като активирате този режим рано.

Един от потенциалните недостатъци на стриктния режим е, че вече не можете да импортирате JavaScript код, освен ако той не предоставя файл с декларация на TypeScript. Ако не сте запознати с този термин, това е като заглавка на C/C++. Трябва да напишете типове за вашите структури от данни, класове, методи и функции. Много проекти включват декларационни файлове, написани от поддържащите. Ние обаче зависехме от няколко, които не го направиха.

За да разреши този проблем, общността свърши невероятна работа с хранилището DefinitelyTyped, попълвайки за автори, които нямат нито време, интерес или опит да пишат и поддържат свои собствени файлове с декларации. Нямахме проблеми с намирането на декларации за всички наши зависимости, след като се консултирахме с него. За съжаление се сблъскахме с няколко проблема при надграждането им надолу. Поради причини, по-добре обяснени „в този проблем с GitHub“, може да бъде предизвикателство да се намери правилната комбинация от версии. С проекти като Redux (които ние използваме и обичаме), има цяла екосистема от зависими проекти, всеки със свои собствени управлявани от общността декларационни файлове. Обикновено трябва да надстроите всички наведнъж, така че, както можете да си представите, това може да стане разочароващо.

За да направи нещата по-лоши, нашето приложение зависеше от няколко вътрешно написани и хоствани пакета и всички те бяха JavaScript. В резултат на това импортирането им би се счупило при строг режим, така че имахме нов проблем. Имахме две възможности: или да игнорираме грешките на компилатора (използвайки @ts-ignore), или сами да напишем декларационните файлове.

За да не заседнем в този конкретен проблем, ние игнорирахме грешките при импортиране. Поехме ангажимент да се върнем и да напишем файловете с декларации (което направихме навреме). Бих препоръчал този подход и на други екипи, тъй като ако тепърва започвате с TypeScript, е трудно да пишете декларационни файлове. В зависимост от това колко динамични са вашите API, може да са необходими сложни познания за типовата система, за да завършите. В първите дни на осиновяване, когато овладявате нещата, не искате да бъдете разпръснати твърде тънко.

Друго предизвикателство, което имахме, беше мъхът. По време на нашето проучване открихме два начина за линтинг на TypeScript код: TSlint, инструмент, разработен вътрешно в Palantir и впоследствие с отворен код, и ESlint, де факто инструментът за линтинг за JavaScript, заедно с typescript-eslint -парсер.

Вече използвахме ESLint в нашия процес, така че нашето решение беше да приемем двойна настройка на ESlint за проверка на JavaScript код и TSlint за новия код. Това се почувства правилно в момента, тъй като мислехме да използваме най-добрите инструменти за работата, но донесе много недостатъци. Трябваше да поддържаме два конфигурационни файла за linting, два списъка с правила, имахме повече зависимости, които трябваше да следим, и имахме два различни изхода за linting, когато имаше грешки.

Бързо напред няколко месеца, които консолидирахме в ESlint за проверка както на JavaScript, така и на TypeScript кода. Приехме, че въпреки че загубихме някои по-усъвършенствани специфични правила за TSlint, не си струваше усилието да запазим и двата инструмента в нашия процес.

След като инструментите бяха решени, трябваше да решим как да подходим към миграцията на кода. Както споменах по-рано, нямаше да има огромно еднократно пренаписване, трябваше да продължим да поддържаме нашата продуктова пътна карта, което означаваше предоставяне на функции и поддържане на приложението. Завършихме с две правила, които ще ръководят нашата работа.

  • Новият код трябва винагида бъде написан на TypeScript. Без изключения.
  • Кодовата база ще се третира като структура от данни за копиране при запис. Ако задачата ви изисква да промените JavaScript кода, трябва да го пренапишете.

Както можете да си представите, това не винаги е минавало гладко. Докато писането на нов код беше забавно в TypeScript, импортирането на наследен код остава проблем. Първите ни няколко заявки за изтегляне бяха необичайно големи и по този начин трудни за партньорска проверка. Опитахме се да стигнем до дъното на проблема по време на нашите седмични срещи на JavaScript Guild. Оказа се, че често разработчиците трябваше да влязат в наследените модули и да коригират проблеми, които точно тогава бяха открити от компилатора. Така заявките за изтегляне, които трябваше да включват един или два файла, сега бяха с размер на стотици или дори хиляди редове поради нашето второ правило за миграция.

Все още искахме да поддържаме визията, така че решихме да разгледаме основните нарушители, които причиняват големите промени. Това бяха предимно по-стари основни React компоненти или помощни функции. Тези модули бяха използвани повторно в целия проект, така че промените, направени в тях, се разпространиха каскадно в цялата кодова база. Вместо да отделяме време предварително за пренаписването им, което потенциално би причинило забавяне в продуктовата ни пътна карта, ние написахме декларационни файлове за тях. Това помогна много за намаляване на размера на последващите заявки за изтегляне. Забавихме пренаписването на тези модули, докато не продължим с миграцията.

Ако вашият екип направи това, предупреждаваме – вие на практика добавяте технически дълг. Ще трябва да поддържате тези файлове, което не е лесно. Много е лесно да промените JavaScript кода, без да актуализирате съответните файлове с декларации. Вашето приложение ще се компилира, но ще се срине и ще изгори по време на изпълнение. На практика много рядко имахме този проблем, поради нашето прилично покритие с модулен тест. Опитайте се обаче да не пазите декларационните файлове завинаги и се ангажирайте да пренапишете кода си в даден момент.

Разбира се, ние също така доставихме някои грешки в производството по време на пренаписване. Удивителната стойност в системите за типове е, че можете да хванете толкова много грешки по време на компилиране, вместо по време на изпълнение. Това обаче никога не е било така за нас преди миграцията, така че имахме много остатъчни грешки, които чакаха подходящите условия да се проявят.

Заслужаваше ли си?

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

Броят пъти, които пускаме в производство, се е удвоил в сравнение с 2017 г.

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

Броят на фронтенд разработчиците в нашия екип до голяма степен остава постоянен. Така че макар да има някаква връзка между броя на служителите и увеличаването на производството, това не е достатъчно, за да се обясни тенденцията.

Ние не поставихме Analytics като продукт на пауза, нашата пътна карта беше постоянен поток от функции, така че това също е изключено. Има някои сезонни влияния. Радваме се на мирно замразяване на кода по време на коледните празници всяка година. През това време не можем да изпращаме никакъв код до производство. Това обяснява ниската активност в края на всяка година.

Все още сме в началото на анализирането на въздействието, което тази миграция е оказала върху нашата кодова база. Количествените данни показват, че изпращаме по-често от всякога. Качествената информация също е много ценна. Нашите разработчици обичат контекстната информация, която типовете предлагат (особено сега, когато всички в екипа използват VSCode или WebStorm). Те пишат по-малко тестове, защото грешките вече се откриват по време на компилация. Като цяло те са по-доволни от качеството на своя код. Включването на нови разработчици в проекта, което исторически е било много трудно, сега е по-бързо. Планираме да направим нов преглед след шест месеца и да видим дали тези първоначални резултати все още са валидни.

Бихте си помислили, че тази миграция е повлияла на нашата продуктова пътна карта поради цялата работа по рефакторинг. При нас не беше така. Успяхме да доставим всички наши проекти в срок. Уговорката за това е, че в Hootsuite ние никога не работим под натиска на фиксирани срокове, които не подлежат на обсъждане. Мога също така да потвърдя факта, че някои функции, които предоставихме, щяха да отнемат много повече време, ако трябваше да работим в JavaScript. Части от нашата кодова база наближават четиригодишна възраст. Новите функции около тях със сигурност биха повишили оценките.

В заключителната

Тази миграция не би била възможна без страстта, решителността, вниманието към детайла и упоритата работа на нашите разработчици в офиса на Hootsuite в Букурещ. Бих искал да благодаря на Bogdan Zaharia, Flavius ​​Tîrnăcop, Gabriel Ilie, Sergiu Buciuc, и Sînziana Nicolae за поддържането на мечтата за TypeScript жива и като цяло за една невероятна изминала година. Щастлив съм и съм горд, че работя в екип, който може да бъде мотивиран, когато се сблъска с проект като този. Очакваме следващите ни постижения.

Мигрирали ли сте някога кодова база на друг език? Споделете опита си в коментарите и ни уведомете, ако сте намерили тази статия за полезна.