JS Код, написанный C#-разработчиком (ровно как и C++, Java) обычно выглядит не очень. Чтобы нормально писать на JS, надо основательно этим заниматься, а не наскоками-набегами. TypeScript как раз предоставляет мостик из мира C# в мир JS. Этот мостик позволяет писать код, похожий на нормальный, привычный ООП в C#, затем компилировать его в JS. НО от знания и понимания JS кода, TypeScript не освобождает, он лишь помогает человеку из не JS мира писать структурированный код в привычном стиле.
Лично мой код на JS, несмотря на то, что я старался использовать опыт/мозг/google все равно похож на макароны. А чем дальше идет разработка системы, тем больше хочется накрутить именно интерфейсных функций. После попытки добавить парочку таких функций, код стал еще менее понимаемым. Typescript и был создан, чтобы дать возможность .net разработчикам писать код, в более привычном стиле.
Моя интерпретация терминов понимаемый-расширяемый
- надо написать более-менее объектно-ориентированный код. Нужно, чтобы был объект контекста страницы, чтобы каждая диаграмма была объектом, содержащим все свои внутренности (chart, коллекцию точек, возможность открыть и закрывать каждый чарт отдельно, разрешить удалять последние точки с графика/или собирать точки бесконечно долго);
- Хотелось получить контроль над типами, т.к. очень неприятно разбираться в runtime, почему в объекте нет свойства, которое мне нужно (можно перечислить все стандартные проблемы отсутствия строгой типизации).
Пути решения поставленной задачи
Можно было пойти длинным/правильным путем и изучить, как в JS пишется ООП код (на JS я немного писал, так что совсем уж нубом себя не считаю).
Однако, я решил пойти более коротким (как я считал) путем, хотя истинные JS-gays меня, наверно, заклюют, что «кодогенерация отстой, надо писать нормальный код, самому; вложите свои силы, и будет вам вечная польза». Я согласен с этими высказываниями, но посчитал, что на TypeScript напишу и разберусь быстрее.
Большая часть ошибок, с которыми я сталкивался во время отладки приложения, — это javascript ошибки. Соответственно гуглить ответы надо было именно для JS, а не для TypeScript. По этому, знание JS ни кто не отменял!
Код до переписывания
Во время написания кода на js были постоянные проблемы из серии «отсутствие контроля типов». Например, в runtime получаешь ошибку, что у объекта нет такого свойства, и после разбирательства ты понимаешь, что тебе нужен вложенный объект (или наоборот родительский). А еще хуже — описки в именах или большие-малые буквы.
В итоговом коде получились: вложенные цепочки вызовов, глобальные переменные. На каждое действие производится работа с глобальными переменными, никаких тебе объектов.
Кода не много — ~156 стро. Я понимал, что на TypeScript кода будет больше, да и TypeScript сгенерирует сильно больше JavaScript кода, чем сейчас есть, и это было понятно с самого начала.
Комплект используемых JS библиотек
- Jquery — вызов webservice, dom;
- Jquery.SignalR — работа с SignalR;
- CanvasJS — отрисовка графики.
Ко всем библиотекам, кроме CanvasJS, есть TypeScript заголовки, которые пришлось написать самому. Это несложно, если понимаешь, как их писать.
Реализация
Первое, что надо было сделать — это написать на TypeScript объектную модель сообщений. То, что выходит с сервера должно быть 1х1 отображено в TypeScript и писать код, с контролем типов.
TypeScript позволяет полностью (1х1) сделать модели, т.к. поддерживает Enum, Class, Namespace (module), Interface, Generics.
Несколько примеров:
Объём кода на TypeScript только для моделей данных получился в 209 строк (если убрать пропуски строк и скобки, будет 60строк). JS кода было сгенерировано еще больше: 259 строк (пробелов не было, но были закрывающие скобки).
Реализация объектной модели страницы
Далее дергаем метод init, в котором производим все операции инициализации: подключение к SignalR hub, получение данных с API, а также подписываемся на событие diagramHubNotify.
Тонкости написания кода на typescript
Проблема SignalR + TypeScript и ее решение.
JQuery.SignalR написан на javascript, это framework, позволяющий работать с SignalR, не залезая в тонкости реализации работы с websockets, или другими транспортами.
После старта SignalRHub генерируется proxy на JS для более приятной работы с SignalR со стороны JS — это великолепно.
Можно работать и без этой proxy, но тогда наш код будет немного менее строго типизирован, и придется брать объект по имени, а не свойство объекта.
Я хочу использовать этот сгенерированный JS proxy, но вот незадача: каждый раз он генерируется заново и не генерирует никаких TypeScript артефактов — просто голый JS код. Таким образом, получается, что proxy есть, а использовать ее в строго типизированном TypeScript коде невозможно без дополнительных трудозатрат. Здесь человек описывает как это обойти.
Я смог понять, как это работает только со второго подхода, поэтому сначала написал работу без proxy, а потом просто переписал.
TypeDefinition для CanvasJS
Как я уже говорил ранее, для CanvasJS нет ни каких typedefinition файлов. Значит, придется самому сесть, разобраться и написать его.
Давайте взглянем на внутренности *d.ts файла от jquery, чтобы понять, как нам писать самим. В интернете очень плохо с описанием, как это делать.
Что мы видим:
Мы видим огромный файл с интерфейсами, по которому не генерируется JS. Фактически этот файл нужен, чтобы “Make the compiler happy”. Компилятор видит интерфейсы, описывающие классы и методы, и генерирует вызовы JS версии jquery на основе этих интерфейсов. На развернутом сайте никаких упоминаний о d.ts файлах нет. На деле — это сродни C++ .h файлам, а на целевой системе нужно надеяться, что будут реализации этих описанных классов. В нашем случае будет загружена jquery библиотека.
Пишем CanvasJS d.ts
Я из всего CanvasJS использую только 2 вещи:
- Конструктор класса chart;
- Метод render класса chart.
Мы создаем интерфейс Chart, в котором должен быть конструктор и метод.
Дальше — немного черной магии, чтобы этот интерфейс использовать.
Создаем интерфейс CanvasJSStatic. Его единственная задача — хранить объект chart. Именно этот интерфейс мы теперь высовываем наружу из нашего d.ts файла в качестве переменной CanvasJS типа CanvasJSStatic. Это все нужно, чтобы компилятор TypeScript подставил в результирующий JS код строчку «new CanvasJS.Chart()»
Если возникнут вопросы по этому кусочку кода, советую лично прочитать про d.ts файлы (про это можно отдельную статью написать, но моя цель не в этом).
Итог:
Мы полностью написали весь интерфейс на TypeScript и 3 строчки на JS.
У меня получилось 279 строк логики на TypeScript + 259 строк описания классов входных данных от API+hub.
Итого: ~500 строк на TypeScript. На JS кода получилось больше, но зато мы получили возможность писать объектно-ориентированный код.
Затраченное время:-1.5 дня я потратил на написание этого кода. Плюс еще день писал статью.
Мой уровень JS/TypeScript до написания проекта был не велик. По TypeScript я смотрел курс , а опыта не было вообще. По JS я смотрел много разных курсов, но из практического опыта на js я писал совсем чуть-чуть (poi на яндекс карте рисовал)
Наставление от прошедшего по минам тем, кто по ним только пойдет…
Ниже я опишу несколько проблем, которые мой
Проблема Compilation time vs RunTime
Все разработчики знакомы с проблемой «у меня работает, а на сервере не работает» и с её разновидностью «у меня компилится, а где-то еще не компилится» (build server/другие разработчики). В TypeScript есть похожая проблема.
В первых строчках кода, ты подключаешь другой файл. Код компилируется, а в RunTime ты видишь совершенно невнятную ошибку: ”Uncaught TypeError: undefined is not a function”
Как человеку, который не очень силен в JS, мне она поначалу мало что сказала: очевидно, что класс почему-то не виден в RunTime. Начал гуглить и нашел первую подсказку
«Незаданная переменная — это вам не функция!»
Означает это, что скобочки "()" приставлены к тому, чего нет.
Она мне пока не выдала решение, но я понял куда надо копать. На всякий случай решил посмотреть видео по TypeScript на тему module.- тут автор произнес шикарную фразу, смысл которой был таков: надо проверять, в каком порядке загружены наши JS файлы на html страницу из-за потенциальных зависимостей.
И тут меня осенило, что решение-то тривиальное. Я заглянул в свой index.html и обнаружил, что подключил всего 1 JS файл, а второй — нет. Первая мысль была: «Я идиот»! Включил второй файл, и полетело.
После этого я понял… Есть compile time у файлов TypeScript, а есть runtime у javascript. В Compile Time в .ts файле подключены все нужные зависимости и все компилируется, а в runtime этих зависимостей не было.
Пример далекий от JS. Во время компиляции на вашей машине была нужная библиотека (на пример в gac или просто под ногами). Затем вы запустили программу на другой машине, словили runtime exception, потому что на этой машине нет этой библиотеки. Ваш браузер и ваш текстовый редактор — это абсолютно разные системы, как машина на которой код компилировался и та, где он запускался.
This или не this
В общем, пока пишешь простой линейный код, без каких-либо promises (для любителей .net – это аналог task.continuewith они же continuation), — все ок. Проблема возникает, как только появляется асинхронность. Хождение к веб сервису, например.
Код отлично компилируется, работает intellisence и типы выводятся корректно. Проблема начинается в runtime.
У меня 2 вызова одной и той же функции: на первичное получение данных от API controller и на последующее, когда из SignalR hub прилетают обновления. В первом случае все отлично работает. Во втором — нет. Валится ошибка при попытке получить любой объект из контекста страницы. Логика подсказывает, что мы не в объекте страницы. Открываем отладчик (этот пример пусть будет в chrome)
Если присмотреться, то они отличаются. Отличаются тем, что в замыкании хранятся совершенно разные объекты. В первом случае — это класс-контекст страницы, во втором — SignalRproxy. TypeScript во время разработки ничего плохого не сказал. Вызов, по мнению компилятора, был совершенно корректен. А вот в runtime у нас не тот объект.
Если посмотреть внимательно, станет видно, что я подписываю на событие this.onUpdateRecive, а на самом деле надо было создать другую функцию, которая вызывает onUpdateRecieve, и тогда все будет хорошо. В замыкание в качестве this попадает правильный контекст переменной. Проблема тривиальная, но контроль типов нас не спас, и выстрелить себе в ногу можно как и в js.
Особенно внимательные заметят, что ниже есть получение через jquery списка диаграмм из API controller, и он написан уже правильно. Если написать его в том стиле this.onGetDiagrams вместо (data)=>{this.onGetDiagram}, то мы получим ту же самую проблему, но уже при вызове JQuery.
Проблемы с контролем, типом или Type Any
Чистый TypeScript код в реальном приложении вряд ли может быть. Для того, чтобы связать TypeScript код с JS кодом, нужны type definitions файлы, в которых будут описаны интерфейсы JS кода. TypeDefinitions *.t.ds файлы чем-то похожи по своей сути на *.h файлы в C/C++). Дело в том, что как только мы выходим в не строго типизированный мир, нас ждут все его радости.
Если к нему присмотреться, то мы увидим кучу методов, которые принимают any[], т.е. что угодно. И здесь, в отличие от C#, нам никто на этапе компиляции не даст по рукам, если мы подставим странный тип на вход. C# предупредит, если мы попытаемся подписать на делегат метод с не верной сигнатурой. А здесь — пожалуйста, ибо сигнатура фактически отсутствует.
В общем, как только мы выходим за границу TypeScript, мы вступаем в JS мир, а там уже действуют JS законы.
Автор: SychevIgor