imageХочу поделиться своим небольшом положительном опытом об проекте основанном на Angular + Typescript по прошествии года. Это далеко не новая связка, и я уверен, что уже многие её успешно используют. Конечно, уже многие ждут больше статей об React или Angular 2.0, но мне кажется, и этот опыт будет кому-то полезен.

Контекст


Я работаю в продуктовой компании. Основной продукт — это корпоративное приложение и историей 10+лет (Web, ASP.Net, C# MSSQL). Не смотря на почетный возраст и codebase 1M+ LoC, проект все еще актуальный, поддерживаемый, систематически проводятся и рефакторинг и обновления библиотек и подходов.

Но тут клиент захотел сделать замену основного UI на SPA (Angular) что бы он работал на устройствах. — Так и начался новый проект год тому назад.

До этого я работал архитектором на backend C# части, и работал над бизнес-правилами, SQL-performance, SQL-deadlocks, очередями и тп.

С Javascript активно работал только на своих pet-проектах и обьем кода был не значительным. Один свой pet-проект мне даже пришлось переводить с javascript на java (GWT траслятор) после 15k LoC! Из за того что проект перестал быть поддерживаемым. Тяжелый переход оправдал себя (пока не появился Angular).

Потому, мне было сложно представить как можно написать javascript проект с > 20k LoC и при этом сберечь его поддерживаемось, читабельность и расширяемость.

После анализа текущих похожих наших SPA проектов на Backbone, Angular я в этом еще раз убедился — они имели по 20k-35k LoC, структура была, но не четкая, любой рефакторинг в них уже был не возможен.

Требования


Требования же нового Angular проекта оценивались как 2-4 раза большие к текущим.

Более того, в требованиях также требовалась поддержка HTML5 Offline режима, при чем с поддержкой редактирования данных (а это значит что кроме REST мы еще должны иметь похожую реализацию на IndexedDB).

Старт проекта планировался через месяц-два. Потому я имел достаточно календарного времени на исследование текущего опыта текущей команды javascript и вход в новую для меня javascript — Angular экосистему.

Принятие Решений


Angular

Даже не обсуждался. У нас была команда с 1-2 годами его использования. Клиент настаивал на нем. Мне он тоже импонировал и после мого давнего опыта с чистым jQuery, GWT UI, тяжеловесными UI Toolkits — казался глотком свежего воздуха.
Knockout, и старый-добрый Backbone требовали построения модели на своих обьектах/методах. Я этого наелся таких «ограничений» сполна до этого. Возможность использования Plain Javascript Objects/Arrays в Angular перевешивали любые performance выгоды Knockout, Backbone.

С другой стороны у Angular есть свои особенности — свой DI и overdesign.

Service, Module — единица структурирования кода (подобно классу), Factory, Controller — это сахар вокруг Service. Мне не очень хотелось использовать Angular DI, который к тому-же полностью сбивал с толку WebStorm 9.

Typescript

Главное решение которое я долго обдумывал, пробовал, прототипировал и продвигал перед клиентом. Кстати, достаточно сложно тогда было объясниться клиенту (и команде) почему мы должны писать на другом языке, а не на Javascript который к тренде, тем более на стратегическом проекте рассчитанном на года.

Важно понять основной посыл — Typescript это НЕ другой язык, это расширение Javascript (так же как LESS — препроцессор CSS).
Также важно понимать зачем вообще нужен нам Compile-time Type Checking (по аналогии с C#, Java, C++):

  • Делает элементарные проверки корректности кода, иначе — нужно писать сотни элементарых Unit-test которые добавляют размер проекту и ломаются при любом изменении в приложении.
  • Делает возможным рефакторинг, изменение структуры, мерж кода возможным и контролируемым
  • Поддержка IDE обычно на порядок лучше — а это высокая скорость разработки. В любом месте кода вызываешь auto-complite после "." и видишь 5-10 вариантов, а не 100 приблизительно возможных. Find-usage реально работает, в IDE поиск по user.id не перепутает с product.id
  • Разработчик меньше запускает тяжелое приложение, IDE, grunt build сразу предупреждает об простых, а иногда сложных ошибках
  • Упрощает ревью/анализ кода
  • Особенно актуален для больших проектов (по моему опыту > 10k LOC)

С моей точки зрения, killer-фича Typescript — опциональная типизация.

Когда я работал с GWT (там ты пишешь на java в своей IDE, а компилятор конвертирует это в javascript), то меня утомляло что ВСЕ, каждую переменную нужно помечать типом, это затратно и не нужно. Обозначать типами хочется только некоторые вещи внутри приложения (контракты между модулями, данные и их конвертацию в модель, аргументы и возврат функций тп).

Кстати, C#, Java, С++ тоже движутся к опциональной типизации — var, auto, dynamic.

Если просто скопировать strict-Javascript код в Typescript файл — то компилируется без проблем. Если проверка типов не нужна — просто привели к специальному типу «any», и можно все:

var a=<any>0; //вариант 1
var a:any=0; //вариант 2
a().some().thing()[15].else();

Особенно удобна поддержка Generic-types (так же как C# и Java), совместно с ES6 Arrow functions все пребразования и работа с промисами и моделью предельно читаема и безопасна, и она сохраняет this:

function loadUsers():ng.IPromise<Array<User>>{ /*some*/}
function loadBirthDates():ng.IPromise<Array<Date>>{
    return loadUsers().then(users=>users.map(u=>u.birthDate));
}

Тут IDE четко знает что u — имеет класс User, и выдаст ошибку если birthDate там нет или у него не тип Date.

Поддержка ES6 сlass в Typescript.

На первый взгляд ES6 сlass — это обычный сахар (на примере babeljs, Typescript компилятора который генерирует prototype). Но тут дело не в runtime, тут дело в поддержке IDE и миллионах программистов мыслящих больше в OOP, а не в Functional стиле.

Пример 1:

function a(){
 function b(){
    function с(){
       function d(){}
    }
  }
}

Допустим, что этот код написан в стиле OOP, где тут модуль, где класс, а где методе класса? А может это все класс в внутри него private метод b в котором с? Может, пример и надуманный, но порою так тоже оформляют код, как и через prototype.

Пример 2:

module a{ 
  class b{
     c(){
        var d = ()=>{};
     } 
  }
}

Тут уровни в терминах OOP уже ясны без объяснений. Причем, они ясны не только человеку но и IDE. И IDE уже точно знает что есть что.
Читабельность в разы выше, хотя компилируется это во все те-же вложенные функции.

Также в отличии от GWT, Typescript компилируется по-файлово с сохранением всех отступов и комментариев (в WebStorm — по сохранению файла). Я не включаю source-maps в Chrome при просмотре Typescript файла — cгенерированный JS файл мало чем отличается.

Также в нашей Backend команде все знают С# и OOP. И использования Typescript — делает возможным ротацию их на Frontend (знания CSS и верстки не критично, повторюсь у нас корпоративное приложение).

Структура и реализация


Структура проекта выглядит так:

  • framework
  • business
    • security
      • dto
        • UserDto.ts
        • GroupDto.ts
      • dal
        • SecurityAdapter.ts
        • SecurityOfflineAdapter.ts
      • SecurityFacade.ts
  • ui
    • security
      • SecurityController.ts
      • Security.html
      • SecurityMobile.html
  • routing
    • routingStates.ts

framework «видит» библиотеки, business «видит» framework и библиотеки, ui и routing видят всё.

Эта структура и соглашения выбрана не случайно, она напоминает наш ASP.Net проект к которому все в backend-команде привыкли. Может тянуть такое из backend в SPA покажется странным, но для меня параллели и удобство разработчиков очевидны.

Используется стандартная Typescript поддержка модулей (хотя там много вариантов доступны): import(s) в начале файла и _references.d.ts файлы. Не очень удобное решение, но понятное IDE, думаю что перейдем на что то другое если IDE его будет понимать.

Ui слой обращается к business только через *Facade классы.

Типичный Facade — это класс в котором практически все методы возвращают промисы. Facade внутри отвечает за вызовы Adapters.

///<reference path="../_references.d.ts"/>
module business.security {
	export class SecurityFacade {
                loadUsers():ng.IPromise<Array<UserDto>>{}
                loadGroups():ng.IPromise<Array<GroupDto>>{}
                renameUser(userId:string, name:string):ng.IPromise<void>{}
        }
}

Offline поддержка осуществляться просто, например SecurityFacade.loadUsers реализован так: вызываем SecurityAdapter.loadUsers — если промис вернул ошибку — загружаем из IndexedDb SecurityOffineAdapter.loadUsers, если успешно то мы кешируем успешный результат в IndexedDb — UserOffineAdapter.saveUsers. Есть много деталей (например очереди на сохранение, лимит Indexed DB и тп), но грубо работает так.

///<reference path="../_references.d.ts"/>
module business.security {
	export class SecurityFacade {
                loadUsers():ng.IPromise<Array<UserDto>>{
			SecurityAdapter.loadUsers().then(data => {
				SecurityOffineAdapter.saveUsers(data);
				return data;
			}).catch(ex => {
				if (isOffline()) { return SecurityOffineAdapter.loadUsers();}
				throw ex
			});
                }
        }
}

Решение не использовать Angular DI возможно, нестандартно. Даже без особой необходимости в Angular на бизнесе, нам приходиться оборачивать все типы promice (напр jQuery) и разношостные onSuccess/onError в Angular promice.
Использование Angular promice крайне необходимо — он автоматически вызовет $apply в конце. Иначе — вам самому нужно будет наводнить свой код $apply вызовами, да еще и удостовериться что вы не в цикле дайжеста. К счастью $q, как и любой другой провайдер, легко получить вне Angular DI, например для использования Angular promice в бизнесе:
var $q = angular.element(document.body).injector().get("$q");

Любые обьекты хранимые в Json после загрузки и до отправки обязательно проверяются Adapter-ом. Typescript вам в этом не поможет. Потому в каждом Dto классе у нас есть copyAndVerify статический метод, который может внутри проверяет и очищает каждое свойство обьекта а также вызывает её у под-объектов, а также может сделать «upgrade» объекта до версии выше (например переименование поля и смена типа). Вызов copyAndVerify делает Adapter для каждого объекта в коллекции. При сохранение это также нужно — Angular навешивает свои методы на обьект которые нужно очистить до сериализации, а также Angular директивы могут сменить тип — например сохранить число как текст.

UI: SecurityController — это тоже обычный класс, только он регистрироваться в Angular, а так-же выбирает нужный view.

///<reference path="../_references.d.ts"/>
module ui.security {
	import SecurityFacade =  business.shared.SecurityFacade;

	export class SecurityController {
                users:Array<User>;
                constructor(public $scope:IScope, public $window:ng.IWindowService) {
                            new SecurityFacade().loadUsers().then(users=>this.users); //Arrow-functions позволяют не писать var _this=this; 
                }
        }
       
        // @ngInject
	function SecurityDirective ():ng.IDirective {
		return {
			scope: {},
			templateUrl: routing.isMobile() ? 'ui/security/Security.html' : 'app/security/SecurityMobile.html',
			controller:SecurityController ,
			controllerAs: 'securityCtrl'
		}
	}
        angular.module('app').directive('securityDirective', SecurityDirective);
}

Спустя год


Связкой довольны. В продакшене уже пол-года. Уже около 23k LoC и при этом проект легко пережил 7 средних рефакторингов, смену команды. Он продолжает быть легко поддерживаемым, скоро планируем добавлять очередную большую фичу. Недавно меняли IndexedDb либу на свою. Code-review проводить легко.

С поддержкой offline пришлось повозиться, но 200Mb храним легко на всех iPhone, iPad, Chome, IE10. Плюс, весь SPA (2,5Mb) загружаться из локального кеша, потому стартует шустро вне зависимости от интернет-подключения.

Производительности на Angular таки приходить периодически уделять внимание — убирать watch и заменять на события, пересматривать директивы и код views.

Autotests, кстати, написаны тоже на Typescript с использованием Protractor и PageObject подхода.

Сейчас Typescript поддерживается прекрасно в WebStorm, Visual Studio (в 2015 уже хорошо) и ряде других наверное. Если бы выбирали сейчас, может выбрали-бы React вместо Angular. Также думаю частично применять Typescript в ASP.Net на тяжелых UI контролах.

Комментарии (17)


  1. a553
    06.01.2016 15:33
    +1

    Несколько замечаний:

    var a=<any>0; //вариант 1
    var a:any=0; //вариант 2
    
    Обратите внимание, что данные варианты эквивалентны только для типа any, а для других типов вариант 1 игнорирует совместимость типов.

    var d =>{}
    
    Видимо опечатка.

    _references.d.ts файлы. Не очень удобное решение, но понятное IDE
    Для Visual Studio проектов _references.d.ts не нужен.

    Array<UserDto>
    
    Может быть это специально, но можно UserDto[].


    1. vintage
      06.01.2016 17:25
      +1

      Видимо опечатка.
      Очень сильно раздражает, что приходится брать это дело в круглые скобки. А ведь могли бы сделать хорошо, как в языке D, где есть два отдельных синтаксиса:

      Просто анонимная функция:
      var d = (){ return {} }
      


      Стрелочная нотация (справа всегда выражение):
      var d = () => {}
      



      1. a553
        06.01.2016 17:52

        Не понял проблемы. Вернуть литерал объекта из arrow function в JS можно так:

        const f = () => ({});


        1. vintage
          06.01.2016 18:13

          Проблема в том, что этот костыль вообще необходим, в то время как можно было бы обойтись без него, сделав два синтаксиса объявления лямбд менее похожими.


          1. a553
            06.01.2016 18:15

            Ну есть const f = function () {} и объявления функций, не думаю, что нужен четвертый синтаксис.


            1. vintage
              06.01.2016 18:42
              +1

              Их в любом случае два:

              var a = () => 1
              var a = () => { return 1 }
              
              

              Проблема в том, что в следующем выражении они не отличимы:
              var a = () => {}
              


    1. vitrilo
      06.01.2016 17:40

      ArrayUserDto>

      Может быть это специально, но можно UserDto[].


      TS позволяет использовать обе нотации. Но видимо, я привык C#/Java, и мне обилие Generics уже давно не «режет» глаза.
      ng.IPromise<Array<CustomField>>


  1. vitrilo
    06.01.2016 17:33

    Спасибо, да, опечатка.
    Исправил на:

    var d = ()=>{};
    


  1. RouR
    07.01.2016 14:54
    +2

    С поддержкой offline пришлось повозиться, но 200Mb храним легко на всех iPhone, iPad, Chome, IE10.
    Расскажите как


    1. vitrilo
      08.01.2016 23:38

      Ответ не будет коротким. Наверное нужно написать отдельную статью.


      1. RouR
        09.01.2016 00:03

        С удовольствием почитаю. У меня возникала подобная задача.


  1. ha7y
    07.01.2016 20:50
    +1

    Прошу прощения, нигде не нашёл. Что Вы имеете в виду под термином «LoC»?


    1. vintage
      07.01.2016 20:54
      +1

      Lines of Code — число строк.


  1. ArMikael
    10.01.2016 12:06
    +1

    Спасибо за статью, она оказалась актуальной в свете надвигающегося Angular 2.
    Пытаюсь привыкнуть к связке TypeScript + Angular.

    Мне показалось, что вы довольны принятым решением использовать такую связку, как впрочем и результатами, которые она дала.
    Но тем не менее, пишите, что сейчас возможно бы выбрали React вместо Angular. Расскажите, что вас подталкивает к этому. Что вам показалось более удобным и эффективным в React?


    1. alexanderkrass
      11.01.2016 00:23

      Поддерживаю вопрос. Тоже очень интересно почему сейчас выбрали бы React?


      1. vitrilo
        11.01.2016 06:55

        Я не исследовал React детально, могу только привести несколько мыслей:
        React — более простой, строгий, жёсткий и понятный, также его шаблоны полностью компилируемые (.tsx для Typescript) — все эти черты прекрасны для долгой enterprise разработки. Также приятный плюс (но не главный) — хорошая и прогнозируемая производительность.
        Отталкивает немного только то, что для полноценного фреймворка (замены Angular) нужно еще, что типа flux, и какой-то routing (которые лично мне, пока, менее понятны, но я исправлюсь :) ).


      1. vitrilo
        11.01.2016 07:07

        Еще раз, как уже писали другие:
        Angular — это фрейворк, и ты пишешь приложение на навороченном HTML (со всеми плюсами и минусами), то есть — декларативно. И этот навороченном HTML — можно считать новым языком (особенно видно в Angular2).
        React — это библиотека, и ты пишешь на простом и чистом js реализуя простой и понятный шаблон Builder (а virtual DOM уже дает оптимизирует все, но делает это более предсказуемо чем Angular).