javascript

История о том, как мы перевели проект в почти четверть миллиона строк на TypeScript и остались в жив

  • суббота, 11 марта 2017 г. в 03:14:30
https://habrahabr.ru/company/ruvds/blog/323612/
  • JavaScript
  • Блог компании RUVDS.com


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


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

Coherent Labs и TypeScript


Мы начали присматриваться к типизированному JS в прошлом году, когда проект, над которым я сейчас работаю, визуальный редактор пользовательских интерфейсов для HTML-игр, написанный преимущественно на JavaScript, дорос до более чем 100 тысяч строк кода. Редактор оснащён массой возможностей, как результат, его кодовую базу стало сложнее поддерживать и подвергать рефакторингу. Именно поэтому мы решили провести некоторые исследования и найти решение для встающей перед нами проблемы.

Через некоторое время мы пришли к двум вариантам — TypeScript и Flow. Оба обещали дать нам статические типы для улучшения контроля над кодом, но одного этого было недостаточно. Мы хотели использовать всю мощь ES6 и будущих версий языка. Flow, по сути, это статический анализатор кода, что означает, что нам пришлось бы использовать транспилятор для всего нашего ES6-кода, но применяя TypeScript, который является надстройкой над JavaScript, мы получили бы и статические типы, и поддержку большинства последних возможностей ECMAScript.

Учитывая вышесказанное, было несложно принять окончательное решение. Мы остановились на TypeScript.

О популярности статически типизированного JavaScript


Давайте немного отвлечёмся и поговорим о том, что такое статическая типизация, и как она нашла своё место в динамическом мире JavaScript.

Основная разница между статически типизированными и динамически типизированными языками заключается в том, как они производят проверку типов данных. Первые делают это во время компиляции, а вторые — во время исполнения программы. Другими словами, статически типизированные языки требуют от программиста объявлять типы данных перед их использованием, в то время как в динамически типизированных языках это не нужно.

JavaScript, являясь динамически типизированным языком, позволяет использовать различные типы данных в разнообразных операциях. Для разработчиков, которые пришли в JS со статически типизированных языков, вроде Java или C++, это, в большинстве случаев, выглядит совершенно нелогично и даже странно. Подобное приводит к серьёзным неприятностям в проектах большого масштаба. Например, программисту может понадобиться потратить многие часы только на то, чтобы выяснить, отчего вдруг некая переменная оказалась NaN.

TypeScript и Flow были созданы для решения подобных проблем. Поддержкой Flow, который приобрёл популярность в последние два года и ориентирован лишь на проверку типов, занимается Facebook. TypeScript же, к которому приложила руку Microsoft, с другой стороны, является надстройкой над JavaScript. Это означает, что он не только поддерживает проверку типов, но и позволит работать с будущими возможностями ES6 и ES7. И у того, и у другого, имеются компиляторы, преобразующие код в чистый JavaScript, который можно запустить в любом браузере.

Вот некоторые преимущества использования типизированного JavaScript:

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

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

  • Значителен ли проект для вашего бизнеса?
  • Состоит ли проект из множества сложных частей?
  • Есть ли вероятность того, что вы соберётесь подвергнуть проект рефакторингу для того, чтобы он лучше соответствовал вашим потребностям?
  • Есть ли у вас большая команда, в которой разработчикам необходимо до минимума сократить время на изучение кодовой базы и поддержку собственных знаний о ней в актуальном состоянии?

Если большинство ответов на эти вопросы положительны, значит вам стоит рассмотреть возможность переноса проекта на TypeScript или Flow.

Вечная битва титанов: TypeScript и Flow


Как уже было сказано, и Flow, и TypeScript дают JS-разработчику очень важную, и, по моему мнению, весьма необходимую возможность — систему типов.

TypeScript появился в 2012-м, при содействии Microsoft, когда Андерс Хейлсберг выпустил его первый релиз. Кроме прочего, он поддерживает необязательные аннотации типов и возможности ES6 и ES7. У него имеется компилятор, который обрабатывает аннотации и генерирует код на обычном JavaScript.

Flow — это статический анализатор кода, которым занимается Facebook. Он спроектирован так, чтобы быстро находить ошибки в JavaScript-приложениях. Это — не компилятор, а анализатор. Он может работать вообще без каких-либо аннотаций типов, отлично показывая себя в задаче вывода типов переменных.

Взглянем на простой пример. Имеется следующая функция, которая, принимая имя, возвращает приветствие:

function greet = function(name) {
   return `Hello, ${name}!`;
}

И с использованием TypeScript, и с применением Flow, эта функция будет выглядеть примерно так:

function greet(name: string): string {
   return `Hello, ${name}!`;
}

«String» после параметра функции — это способ задания типа в TypeScript и Flow. Типы могут быть как простыми, так и составными. Это означает, что если мы попытаемся использовать аргумент, тип которого отличается от заданного, будет выдано сообщение об ошибке. Рассмотрим пример использования функции.

const message = greet(43);

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

Argument of type 'number' is not assignable to parameter of type 'string.'

Вылавливание подобных ошибок во время компиляции может весьма положительно сказаться на производительности труда и даже на настроении разработчика. TypeScript, кроме того, поддерживает современные возможности JS, такие, как стрелочные функции, импорт и экспорт, генераторы, механизмы async/await, классы, и так далее. В итоге оказывается, что TypeScript позволяет сформировать отличную экосистему разработки приложений на JavaScript.

Итак, теперь позвольте мне рассказать о том, как мы перевели проект, содержащий более 200 тысяч строк кода, на TypeScript.

Шаг первый — выбор технологии


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

  • Использовать Flow и Babel для транспиляции ES6.
  • Использовать TypeScript и его компилятор для ES6/ES7 компиляции.

Мы выбрали именно TypeScript по нескольким причинам:

  • В те времена у TypeScript было более обширное сообщество, по нему было больше материалов.
  • Нам не хотелось усложнять процесс сборки приложения, который и так был довольно тяжёлым. Добавление ещё двух шагов при использовании Flow (компиляция Flow и транспиляция Babel) замедлили бы работу ещё сильнее. TypeScript же позволял обойтись одним дополнительным этапом сборки, что показалось нам вполне приемлемым.
  • TypeScript лучше поддерживают интегрированные среды разработки. Вся наша команда пользуется WebStorm, которая обеспечивает отличную поддержку TypeScript.
  • Поддержка ES5. Ядро нашей системы построено на WebKit и JavaScriptCore, с учётом определённых ограничений. Некоторые из стандартных возможностей ES5 не поддерживаются. В результате, возможности TypeScript сыграли ведущую роль при принятии окончательного решения.

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

Шаг второй — начало


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

Далее была настройка сборочного инструмента (Grunt) с использованием grunt-ts, и, дня через полтора, первый TypeScript модуль был готов. Главной трудностью тогда была работа с глобальными и внешними модулями. TypeScript имеет жёсткие правила, касающиеся типизации. Как результат, компилятор TypeScript не распознаёт вспомогательные библиотеки, вроде jQuery и Kendo UI, и их методы. Это сразу же дало более 200 ошибок компиляции, указывающих на то, что TypeScript не может найти переменную типа «$». Решение было довольно простым — мы воспользовались инструкциями declare:

declare var $;
declare var kendo;
...

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

Шаг третий — переход


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

Для начала мы переименовали все файлы, дав им расширение .ts, и тут же столкнулись со шквалом ошибок компиляции. Затем добавили необходимые объявления TypeScript и назначили некоторым переменным тип данных Any. Это позволяет записывать в переменную данные различных типов. Таким способом объявляли переменные, которые могут хранить данные разных типов, скажем, строки, числа, логические значения.

После двухдневной борьбы с типами и объявлениями, мы вышли на первую полную компиляцию TypeScript. Однако, работы оставалось ещё очень много. Надо было улучшить проверку типов, добавить соответствующие объявления для внешних библиотек и подвергнуть ES5-код переработке под стандарты ES6.

Файл объявлений описывает устройство библиотеки. Применяя их (ещё их называют файлами .d.ts), можно избежать неправильного использования библиотек и получить удобства вроде автозавершения команд в редакторе кода. При работе с TypeScript 1.8 для того, чтобы добавить новый файл объявлений, нужно установить отдельные npm-пакеты глобально, найти файлы объявлений для конкретной библиотеки и сохранить все файлы описаний библиотек в папке «typings». Честно говоря, решение это было не самое элегантное. В TypeScript 2.0 имеется новый способ управления объявлениями библиотек — использование реестра npm. Делается это всё теперь парой команд. Сначала — установка:

npm install --save @types/jquery

Потом — импорт там, где это нужно:

import * as $ from 'jquery'

В результате — нет больше скучной возни с файлами и папками.
Итак, с внешними библиотеками мы разобрались. Следующим шагом было изменение всех объявлений с типом Any на подходящие типы данных. Эта задача заняла несколько дней. Мы решили не торопиться и поэтапно перерабатывать код. В частности, выполняли рефакторинг под ES6 и изменение типов. После пары недель мы инкрементально перевели на ES6 80% кода, снабдив его осмысленными аннотациями типов. На данном этапе можно было переходить к следующей стадии работы — тестированию.

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


Наша среда разработки была близка к идеалу и мы были готовы к следующему большому этапу работы над проектом — сквозному (E2E) тестированию. Мы решили использовать средства JavaScript. Наш арсенал — это Mocha, Chai, Selenium и WebDriver Selenium. Мы изолировали код тестирования — фреймворк Selenium, тесты и все зависимости, предоставив им отдельный tsconfig.json.

Присутствие файла tsconfig.json в директории указывает на то, что директория является корнем проекта TypeScript. Этот файл задаёт корневые файлы и параметры компилятора.

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

Структура фреймворка состоит из семи основных и пяти вспомогательных модулей. Вот основные модули:

  • Обработчик драйвера, который запускает и останавливает Selenium и управляет тайм-аутами.
  • Модуль селектора, содержащий все элементы, которые используются в тестах.
  • Главный файл действий, который ответственен за организацию работы с различными частями приложения и выполнение взаимодействий с ним, вроде щелчков мышью, выделения элементов, ввода данных.
  • Модуль для экспорта и удаления ресурсов.
  • Модуль ведения журналов.
  • Модуль создания копий экрана, который делает скриншот после каждого проваленного теста.
  • Модуль для имитации API бэкенда.

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

Кроме того, мы добавили средство создания отчётов — mochawesome, которое выводило полный отчёт по всем тестам после их завершения.

Благодаря TypeScript в проекте удалось задействовать возможности async/await, в результате, мы пришли к более чистому и лучше организованному коду.

Планы на будущее


На самом деле, то, о чём я рассказал, это только начало. Переход на TypeScript — лишь капля в море. Сделать нам предстоит ещё очень много. Вот некоторые из наших планов:

  • Произвести рефакторинг всей архитектуры проекта.
  • Добавить новые функции, такие, как 3D-выделение и ограничительные рамки, визуальные указатели привязки данных элементов и многое другое.
  • Улучшить общее время исполнения тестов. Сейчас для выполнения 440 тестов требуется порядка 30 минут.

Кроме того, мы экспериментируем с библиотеками, поддерживающими работу с виртуальным DOM, наподобие React и InfernoJS. И та и другая отлично поддерживают TypeScript, проект может значительно выиграть от прироста производительности, который даёт работа с виртуальным DOM.
Мы внимательно следим за развитием TypeScript. Последние релизы представляют новые возможности, вроде отображения типов, операторов spread и rest для работы с объектами, поддержки асинхронных функций при компиляции в ES3 и ES5. В будущих релизах ожидается поддержка генераторов для ES3/ES5, улучшение средств обобщённого программирования, асинхронные итераторы.

Итоги


Решение использовать TypeScript в нашем проекте оказалось правильным, принесло много хорошего. В частности, это — повышение производительности труда, улучшение управляемости кода, поддержка ES6. Изначально я с недоверием относился к TypeScript, но после работы с ним могу сказать, что он — это то, чего мне очень долго не хватало.

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

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

Уважаемые разработчики! А как вы относитесь к средствам статической типизации в JavaScript? Если бы вы решили переработать большой JS-проект, что бы вы выбрали?