Pull to refresh

Reinforced.Typings — библиотека для автоматической генерации TypeScript-тайпингов и не только

Reading time7 min
Views8.2K
Я написал небольшую, но полезную библиотечку для любителей TypeScript и ASP.NET MVC. Очень хотелось бы про нее рассказать — возможно какому-то разработчику на вышеупомянутой комбинации технологий (а возможно и целой команде) она существенно облегчит жизнь. Писать полноценную документацию на английском пока что времени нет. К ней вообще нужно подходить осмысленно, с чувством, толком и расстановкой. Поэтому я решил написать статью на Хабрахабр, и вот прямо сейчас, под катом, я сделаю краткий обзор и покажу какую магию можно делать этой штукой. Добро пожаловать.

Лирическое введение


Вообще, меня давно и прочно будоражит идея тесной интеграции Back-End и Front-End на .NET стеке, что в свою очередь даже вылилось в попытку с наскоку написать целый транслятор из C# в JavaScript. Не скажу, что никаких результатов не было достигнуто — мой транслятор успешно перевел в JavaScript несколько C#-классов и в конечном итоге ушел в анабиоз и переосмысление архитектуры. Когда-нибудь я к нему еще вернусь. Обязательно.

Но, тем не менее, текущие задачи в проектах надо было как-то решать. А текущие задачи в почти любом web-проекте, в целом, достаточно типичны и, не побоюсь этого слова, скучны. Вот сейчас я, с вашего позволения, абстрагируюсь от всяких сложных, но давно понятных материй, вроде использования Automapper, собирания IoC-контейнера и шуршания в БД посредством EF/NH/чего-нибудь, и переключусь ближе к фронтенду. Так вот — на стыке бекенда и фронтенда тоже много скучных и типичных задач (sarcasm). А конкретно — запросы к серверу на предмет JSON с данными, их отображение и выполнение всяких-разных операций AJAX-ом. Reinforced.Typings (а именно так я назвал свою маленькую помогалку) принесет в это царство уныния толику веселья, упрощение типичных задач, избавление от шаблонного кода и несколько больше консистентности.

С тех пор, как Microsoft подарил нам TypeScript, написание клиентских JavaScript-ов стало занятием гораздо более комфортным. TypeScript принес ощущение типизируемости и прекомпилируемость туда, где его не хватало. Если вы его еще не пробовали — то обязательно попробуйте (это не реклама, нет). Можно, конечно, много спорить по вопросу «быть или не быть» TypeScript-у в вашем конкретном проекте, но давайте опустим дискуссию и перейдем " — Ближе к телу! — как говорил Ги де Мопассан".

Практический пример


Итак, рассмотрим простой, но достаточно распространенный пример — вам необходимо сделать запрос к серверу, достать информацию о заказе и отобразить её каким-либо образом на страничке в браузере.

Что мы обычно делаем для решения этой задачи? Правильно. Мы делаем POCO модели заказа и метод контроллера, который будет возвращать ее экземпляр, обернутый в JSON. Вот они, наши герои (как вы поняли, я буду убирать лишний код для экономии места):

Моделька
public class OrderViewModel
{
	public string ItemName { get; set; }
	public int Quantity { get; set; }
	public decimal Subtotal { get; set; }
	public bool IsPaid { get; set; }
	public string ClientName { get; set; }
	public string Address { get; set; }
}


Метод контроллера
public ActionResult GetOrder(int orderId)
{
	var orderVm = new OrderViewModel()
	{
		// ... тестовые данные ...
	};

	return Json(orderVm, JsonRequestBehavior.AllowGet);
}


Здесь все более-менее понятно и комментарии, думаю, излишни. Давайте переключимся на клиент. Чтобы быть предельно понятным, я буду использовать jQuery для ajax-запросов, но при необходимости вы можете заменить его на что-нибудь свое. Как и ранее, я опускаю излишний glue code, а так же создание view, TypeScript-файла, подключение его к станице, установку jQuery из NuGet — это вы все сможете сделать и без меня. Подчеркиваю самую суть (еще раз напоминаю, что этот код на TypeScript):

Код TypeScript
private btnRequest_click() {
	$.ajax({
		url: '/Home/GetOrder?orderId=10',
		success:this.handleResponse
	});
}

private handleResponse(data: any) {
	var text = `${data.ClientName}, ${data.Address} (${data.ItemName}, ${data.Quantity})`;
	$('#divInfo').text(text);
}


Здесь прекрасно все. За исключением того, что принципиально от JavaScript эта конструкция ничем не отличается. Мы получаем с сервера кусок JSON-а, в котором какой-то объект и мы, рассчитывая на то, что у него есть поля ClientName, Address и прочие — выводим его в div. Звучит не очень стабильно. Если какой-нибудь горе-джуниор удалит из ViewModel-и и из C#-кода, скажем, поле ClientName (или переименует его в целях джуниор-рефакторинга), то все места на фронтенде, где используется такая конструкция — превратятся в детонаторы и будут ждать прихода тестировщика. Ну или end user-а — тут уж кому как повезет. Что же делать? Ответ очевиден — коль скоро мы используем TypeScript, то можно написать тайпинг для этой конкретной ViewModel-и и переписать код вот таким образом:

private handleResponse(data: IOrderViewModel) {
	var text = `${data.ClientName}, ${data.Address} (${data.ItemName}, ${data.Quantity})`;
	$('#divInfo').text(text);
}

Да, теперь нам стало несколько комфортнее — мы застраховались от доступа к незадекларированному полю. Но ситуация с джуниором, переименовывающим поле лично мне не дает спокойно спать. Да и написание тайпингов для всех ViewModel-ей… руками… ночью… А если их в проекте сотни? А если тысячи? Перспективка-то, откровенно, так себе.

Вот тут-то и вступает в игру Reinforced.Typings и начинается решение задачи кардинально другим путем. Итак, открываем PM-консоль (ну или кому удобно — можете сделать это через графический интерфейс) и ставим:

PM > Install-Package Reinforced.Typings

Замечаем в корне проекта новый файл Reinforced.Typings.settings.xml. Он достаточно детально документирован и переписывать все, изложенное в нем здесь я не вижу смысла (если конечно у кого-то из аудитории у меня не все так плохо с английским), предоставив это хабралюдям. Я изменю всего один параметр — это путь к файлу, куда ляжет сгенерированный тайпинг. В моем проекте он вот такой:

	<RtTargetFile>$(ProjectDir)Scripts\app\Server.ts</RtTargetFile>

После чего, я иду в код модельки и добавляю всего две строчки кода — юзинг на Reinforced.Typings.Attributes и атрибут [TsInterface] над самим классом модельки. Примерно вот так:

using Reinforced.Typings.Attributes;
[TsInterface]
public class OrderViewModel
{
	// в самом коде модельки ничего не изменилось
}

После чего я пересобираю проект (делаю ему Rebuild) и вручную добавляю в Scripts\app\ сгенерированный согласно конфигурации Server.ts. Он уже лежит в указанной папке — просто не добавлен в проект. Давайте откроем Server.ts и посмотрим что же в нем такое:

Содержимое Server.ts
//     This code was generated by a Reinforced.Typings tool. 
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.

module Reinforced.Typings.Samples.Simple.Quickstart.Models {
	export interface IOrderViewModel {	
		ItemName: string;		
		Quantity: number;		
		Subtotal: number;		
		IsPaid: boolean;		
		ClientName: string;		
		Address: string;		
	}

}


Вот видите? Чудненько. Теперь задача по написанию тайпингов для ViewModel-ей всего проекта не кажется таким уж страшным делом, не правда ли? Да и удаление-переименование properties ViewModel-и уже не является трагедией: при следующей же сборке проекта тайпинги перегенерируются и TypeScript-код, который на них завязан просто перестанет собираться, в чем вы можете убедиться собственноручно.

Думаю, на этом практическую демонстрацию основной возможности можно закончить, оставив более сложные примеры на следующие статьи и перейти к драматическому заключению.

Драматическое заключение и немного о том-о сем


На самом деле Reinforced.Typings поддерживает много чего. Вот краткий список:
  • Автоматические тайпинги делегатов, наследников IEnumerable и IDictionary (если вы используете их в качестве properties)
  • Тайпинги для enum-ов и классов (правда, тела методов он перевести в TypeScript автоматически не может. Но это можно сделать самостоятельно — об этом ниже)
  • Тайпинги со сложными типами — Reinforced.Typings понимает, что вы используете в классе другой экспортируемый тип и автоматически ставит в том месте full-qualified имя используемого типа. В противном случае — тактично использует any
  • Можно раскидать генерируемый код по разным файлам (class-per-file) с помощью соответствующего параметра конфигурации
  • Можно добавить ///<reference ...> в генерируемые файлы посредством assembly-атрибута [TsReference]. А в случае с class-per-file, reference на соседние файлы добавляется автоматически
  • Можно генерировать, .d.ts-файлы вместо обычного .ts-кода (есть некоторые отличия в синтаксисе)
  • Вишенка на тортик — у каждого атрибута присутствует свойство CodeGeneratorType, в котором можно указать тип-наследник от Reinforced.Typings.Generators.ITsCodeGenerator<> (как вариант — унаследоваться от существующего генератора) и сделать свою генерацию шаблонного TypeScript-кода для всего, чего угодно. Таким путем можно дойти до автоматического создания knockout-овских ViewModel-ей прямо из кода серверных ViewModel-ей. В проекте по моему текущему месту работы, я перегрузил генератор кода для экшнов контроллера и сгенерировал таким образом для многих методов js-ный glue code, вызывающий соответствующий метод контроллера с указанными параметрами. Возвращают такие методы q-шный promise (просто потому что я люблю Q.js). Об этом я и расскажу в следующем посте

Из минусов: Автоматически сгенерировать тела методов классов Reinforced.Typings не может — он работает через Reflection. Ну и еще хочется отметить некие проблемы в ситуации когда серверные сущности представляют правильный TypeScript-код, но в уже сгенерированном коде содержится семантическая ошибка (например, удалено поле). В силу особенностей сборки TypeScript-а (он собирается самым первым во всем проекте), вы не сможете пересобрать проект и сгенерировать правильные тайпинги, которые исправят ошибку, пока не поправите ошибку вручную. Но я над этим работаю. Магия MSBuild творит чудеса.

Еще по проекту, как было сказано выше, крайне мало документации (эта статья, да readme на гитхабе). НО! Очень детально расписан XMLDOC и прокомментирован файл настроек. Так что, я думаю, на первое время должно хватить. А там я уже завербую студента-техписателя и сделаю нормальную книгу Reinforced.Typings Best Practices документацию в вики-формате.

В общем-то на сегодня все. Если какие-то вопросы есть — пишите в комментариях. Постараюсь ответить.

Ссылочки на проект:
Reinforced.Typings на Github
Reinforced.Typings на NuGet
Полный код рассмотренного примера

UPD: Вторая статья по Reinforced.Typings, в которой приводится гораздо больше подробностей
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+11
Comments32

Articles