Пишем UI системы RealTime мониторинга на typescript

в 3:55, , рубрики: Без рубрики

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 кода, чем сейчас есть, и это было понятно с самого начала.
Пишем UI системы RealTime мониторинга на typescript
Пишем UI системы RealTime мониторинга на typescript
Комплект используемых JS библиотек

  • Jquery — вызов webservice, dom;
  • Jquery.SignalR — работа с SignalR;
  • CanvasJS — отрисовка графики.

Ко всем библиотекам, кроме CanvasJS, есть TypeScript заголовки, которые пришлось написать самому. Это несложно, если понимаешь, как их писать.

Реализация

Первое, что надо было сделать — это написать на TypeScript объектную модель сообщений. То, что выходит с сервера должно быть 1х1 отображено в TypeScript и писать код, с контролем типов.

В итоге это выглядело так:
Пишем UI системы RealTime мониторинга на typescript
Пишем UI системы RealTime мониторинга на typescript

TypeScript позволяет полностью (1х1) сделать модели, т.к. поддерживает Enum, Class, Namespace (module), Interface, Generics.

Несколько примеров:

Enums

Пишем UI системы RealTime мониторинга на typescript

Наследование и Generics

Пишем UI системы RealTime мониторинга на typescript

Объём кода на TypeScript только для моделей данных получился в 209 строк (если убрать пропуски строк и скобки, будет 60строк). JS кода было сгенерировано еще больше: 259 строк (пробелов не было, но были закрывающие скобки).

Реализация объектной модели страницы

Страница представляла из себя объект, который содержит коллекцию диаграмм + proxy до SignalR.

Пишем UI системы RealTime мониторинга на typescript

Далее дергаем метод init, в котором производим все операции инициализации: подключение к SignalR hub, получение данных с API, а также подписываемся на событие diagramHubNotify.

А вот каждая диаграмма уже отдельно содержит в себе все свои данные: chart + коллекцию точек.

Пишем UI системы RealTime мониторинга на typescript

Код инициализации chart абсолютно идентичен JS коду.

Пишем UI системы RealTime мониторинга на typescript

Обработчик на добавление новой точки в chart

Пишем UI системы RealTime мониторинга на typescript

Класс самой line на chart

Пишем UI системы RealTime мониторинга на typescript

Точка

Пишем UI системы RealTime мониторинга на typescript
Тонкости написания кода на typescript

Проблема SignalR + TypeScript и ее решение.

JQuery.SignalR написан на javascript, это framework, позволяющий работать с SignalR, не залезая в тонкости реализации работы с websockets, или другими транспортами.
После старта SignalRHub генерируется proxy на JS для более приятной работы с SignalR со стороны JS — это великолепно.

Выглядит это так

Пишем UI системы RealTime мониторинга на typescript

Можно работать и без этой proxy, но тогда наш код будет немного менее строго типизирован, и придется брать объект по имени, а не свойство объекта.

Я хочу использовать этот сгенерированный JS proxy, но вот незадача: каждый раз он генерируется заново и не генерирует никаких TypeScript артефактов — просто голый JS код. Таким образом, получается, что proxy есть, а использовать ее в строго типизированном TypeScript коде невозможно без дополнительных трудозатрат. Здесь человек описывает как это обойти.
Я смог понять, как это работает только со второго подхода, поэтому сначала написал работу без proxy, а потом просто переписал.

Мой вариант *.d.ts файла.

Пишем UI системы RealTime мониторинга на typescript

И соответствующий ему SignalR hub с 2 серверными методами и 1 клиентским.

Пишем UI системы RealTime мониторинга на typescript
TypeDefinition для CanvasJS

Как я уже говорил ранее, для CanvasJS нет ни каких typedefinition файлов. Значит, придется самому сесть, разобраться и написать его.
Давайте взглянем на внутренности *d.ts файла от jquery, чтобы понять, как нам писать самим. В интернете очень плохо с описанием, как это делать.

Открываем d.ts файл от jquery.

Пишем UI системы RealTime мониторинга на typescript

Что мы видим:
Мы видим огромный файл с интерфейсами, по которому не генерируется JS. Фактически этот файл нужен, чтобы “Make the compiler happy”. Компилятор видит интерфейсы, описывающие классы и методы, и генерирует вызовы JS версии jquery на основе этих интерфейсов. На развернутом сайте никаких упоминаний о d.ts файлах нет. На деле — это сродни C++ .h файлам, а на целевой системе нужно надеяться, что будут реализации этих описанных классов. В нашем случае будет загружена jquery библиотека.

Пишем CanvasJS d.ts

Я из всего CanvasJS использую только 2 вещи:

  • Конструктор класса chart;
  • Метод render класса chart.

Отлично: наша жизнь оказывается не такой уж и сложной.

Пишем UI системы RealTime мониторинга на typescript

Мы создаем интерфейс Chart, в котором должен быть конструктор и метод.
Дальше — немного черной магии, чтобы этот интерфейс использовать.
Создаем интерфейс CanvasJSStatic. Его единственная задача — хранить объект chart. Именно этот интерфейс мы теперь высовываем наружу из нашего d.ts файла в качестве переменной CanvasJS типа CanvasJSStatic. Это все нужно, чтобы компилятор TypeScript подставил в результирующий JS код строчку «new CanvasJS.Chart()»
Если возникнут вопросы по этому кусочку кода, советую лично прочитать про d.ts файлы (про это можно отдельную статью написать, но моя цель не в этом).

Стартовая точка приложения

Пишем UI системы RealTime мониторинга на typescript

Итог:

Мы полностью написали весь интерфейс на TypeScript и 3 строчки на JS.

И все работает

Пишем UI системы RealTime мониторинга на typescript

У меня получилось 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”

на тривиальной строке:

Пишем UI системы RealTime мониторинга на typescript

Как человеку, который не очень силен в 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)

Нормальный пример:

Пишем UI системы RealTime мониторинга на typescript

Не работающий пример:

Пишем UI системы RealTime мониторинга на typescript

Если присмотреться, то они отличаются. Отличаются тем, что в замыкании хранятся совершенно разные объекты. В первом случае — это класс-контекст страницы, во втором — SignalRproxy. TypeScript во время разработки ничего плохого не сказал. Вызов, по мнению компилятора, был совершенно корректен. А вот в runtime у нас не тот объект.

Код до решения ошибки:

Пишем UI системы RealTime мониторинга на typescript

Если посмотреть внимательно, станет видно, что я подписываю на событие this.onUpdateRecive, а на самом деле надо было создать другую функцию, которая вызывает onUpdateRecieve, и тогда все будет хорошо. В замыкание в качестве this попадает правильный контекст переменной. Проблема тривиальная, но контроль типов нас не спас, и выстрелить себе в ногу можно как и в js.

Код до решения после:

Пишем UI системы RealTime мониторинга на typescript

Особенно внимательные заметят, что ниже есть получение через jquery списка диаграмм из API controller, и он написан уже правильно. Если написать его в том стиле this.onGetDiagrams вместо (data)=>{this.onGetDiagram}, то мы получим ту же самую проблему, но уже при вызове JQuery.

Проблемы с контролем, типом или Type Any

Чистый TypeScript код в реальном приложении вряд ли может быть. Для того, чтобы связать TypeScript код с JS кодом, нужны type definitions файлы, в которых будут описаны интерфейсы JS кода. TypeDefinitions *.t.ds файлы чем-то похожи по своей сути на *.h файлы в C/C++). Дело в том, что как только мы выходим в не строго типизированный мир, нас ждут все его радости.

Вот пример t.ds файла от SignalR

Пишем UI системы RealTime мониторинга на typescript

Если к нему присмотреться, то мы увидим кучу методов, которые принимают any[], т.е. что угодно. И здесь, в отличие от C#, нам никто на этапе компиляции не даст по рукам, если мы подставим странный тип на вход. C# предупредит, если мы попытаемся подписать на делегат метод с не верной сигнатурой. А здесь — пожалуйста, ибо сигнатура фактически отсутствует.
В общем, как только мы выходим за границу TypeScript, мы вступаем в JS мир, а там уже действуют JS законы.

Автор: SychevIgor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js