Глобальная область видимости (aka namespace в TypeScript) — уже давно не круто. Можно долго перечислять преимущества модулей (ES6 модулей, в частности), но лично для меня решающим стала возможность использовать SystemJS для динамической загрузки исходников и Rollup, для сборки бандла.


Однако, первое, с чем пришлось столкнуться при внедрении ES6-модулей- безумное количество import выражений, с безумным количеством точек внутри:


import { FieldGroup } from "../../../Common/Components/FieldGroup/FieldGroup";



Откуда щупальца растут?


ES6 спецификация по этому поводу ничего особо не говорит, отмахиваясь фразой, что пути к модулям "loader specific". Ну то есть, если вы используете SystemJS, то формат путей определяет SystemJS, если Webpack, то Webpack. Работа над спецификацией загрузчика идет, но, как говорит главная страница репозитория watwg:


This spec is currently undergoing potentially-large redesigns (see #147 and #149) and is not ready for implementations.

Согласие между загрузчиками пока только в том, что путь начинающийся с "./" означает, что нужно искать в той же директории, где находится текущий модуль. Двойные точки "../", соответственно, позволяют подняться на уровень выше, и посмотреть в родительской директории. При этом, даже в самом простом проекте очень легко получить пути, содержащие 3-4 двойных точки "../../../", что ужасно во всех смыслах.


Поскольку спецификации нет, то сейчас каждый решает проблему кто как может. Обычно для этих целей настраивают некоторую корневую папку и все пути указывают относительно нее. Например, babel сообщество придумали себе плагин, а webpack поддерживает настройку resolve.root.


import { BasicEvent } from '~/Common/Utils/Events/BasicEvent'

Однако, даже если вы настроите корневую папку, то это все равно не избавит вас от огромной шапки import-выражений в начале каждого файла. Правила хорошего тона говорят о том, чтобы разбивать код на как можно более маленькие модули, что и является источником проблемы (зануды сейчас скажут, что нужно лучше декомпозировать код, но реальный мир всегда не такой, как хотелось бы).




Что особенно печально, каждый раз, импортируя некоторый модуль, вы создаете жесткую привязку к расположению этого модуля в файловой системе. Поэтому 1) вам, как минимум, нужно точно помнить, где находится каждый модуль 2) если вы захотите сделать рефакторинг (например, переименовать файл), то вас ожидает много боли.


Ну и последняя боль, с которой придется столкнуться при использовании TypeScript в VisualStudio — там не работает подсветка синтаксиса, а также линтинг JSX для импортированных символов. Например:


import { FieldGroup } from "../../Components/FieldGroup/FieldGroup";
import { BasicEvent } from "../../Common/Utils/Events/BasicEvent'
...
var event = new BasicEvent(); // BasicEvent в VisualStudio не подсвечивается как класс
...
render() {
    // JSX для FieldGroup в VisualStudio не линтится (параметры компонента не проверяются), 
    // и intellicese не работает, т.к. FieldGroup импортированный символ
    return <FieldGroup name="blabla" />; 
}

В Microsoft, по-видимому, проблему решать не спешат (issue 1, issue 2).


Магические пакеты всех спасут




Решение проблемы состоит в том, чтобы отказаться от идеи отдельных модулей, беспорядочно связанных между собой, и начать использовать, хм, что-то вроде "пакетов модулей". Я не уверен, публиковалось ли уже где-то такое решение в данном контексте (UPD: gogolor подсказал, что у Angular в доках это называется barrel), однако сама идея не нова. Например, в C# у нас также есть отдельные файлы с кодом, но при этом данные файлы собираются в "сборки" (dll), которые уже явно объявляют ссылки на другие сборки.


Представим, что у нас есть следующая структура проекта (скриншот из реального проекта некоторой админ-панели):




Для того, чтобы из файла AssignmentTemplatSettings.tsx дотянуться до BasicEvent.ts, пришлось бы написать что-то вроде:


import { BasicEvent } from '../../Common/Utils/Events/BasicEvent';

Это ужасно по всем тем причинам, что я описывал выше. Однако, если вы посмотрите на структуру проекта, то легко заметить, что все модули естественным образом распределены по папкам. Чем сложнее проект, тем более разветвленной становится структура папок. Это стремление к упорядочиванию живет в каждом разработчике, и скорее всего, в вашем проекте наблюдается нечто подобное.


Хорошая новость заключается в том, что ES6-модули позволяют конвертировать такую структуру папок в структуру "пакетов", очень напоминающих dll в десктоп мире. Можно сделать каждую папку пакетом (например, Common/Utils/Events будут вложенными пакетами), можно ограничиться более крупными единицами (только Common/Utils). Для каждого пакета модулей будет четко указано, от каких пакетов он зависит и что "выставляет наружу". Все эти зависимости будут собраны в одной точке, так что модули пакета не будут ничего знать о расположении модулей других пакетов. При этом количество точек ("../../") в относительных путях будет не больше, чем вложенность папок внутри одного пакета, а количество import-выражений сократится вплоть до одного.


Реализация




Для того, чтобы конвертировать папку в пакет, достаточно добавить в нее два файла — imports и exports. В первом файле мы импортируем и делаем ре-экспорт всего того, что необходимо модулям данного пакета. Во второй файл помещается экспорт всего того, что пакет делает доступным для импорта в другие пакеты.


Реализуем экспорт


Попробуем сделать пакет из папки Events. Пусть наружу он выставляет два класса — BasicEvent и SimpleEvent. Тогда, файл @EventsExports.ts будет выглядеть следующим образом:


export * from "./BasicEvent";
export * from "./SimpleEvent";



Собака "@" в имени файла гарантирует, что он не потеряется среди других файлов пакета и будет всегда самом верху. От других пакетов нам здесь ничего не понадобится, поэтому файла imports здесь пока не делаем. Далее конвертируем родительские папки Utils и Common в пакеты. Например, @UtilsExports.ts будет содержать:


import * as Events from "./Events/@EventsExports";
import * as ModalWindow from "./ModalWindow/@ModalWindowExports";
import * as Other from "./Other/@OtherExports";
import * as RequestManager from "./RequestManager/@RequestManagerExports";
import * as ServiceUtils from "./ServiceUtils/@ServiceUtilsExports";
export { Events, ModalWindow,  RequestManager, ServiceUtils  };



Здесь не указаны модули CachingLoader и другие, которые находились непосредственно в папке Utils. Это ограничение данного подхода, пакеты, которые экспортируют другие пакеты, не могут содержать своих модулей. Поэтому пришлось переместить все эти файлы в дочерний пакет Other. Содержимое imports-файла будет рассмотрено позже.


Аналогично делаем @CommonExports.ts:


import * as Components from "./Components/@ComponentsExports";
import * as Extensibility from "./Extensibility/@ExtensibilityExports";
import * as Models from "./Models/@ModelsExports";
import * as Services from "./Services/@ServicesExports";
import * as Utils from "./Utils/@UtilsExports";

export { Components, Extensibility, Models, Services, Utils };



Реализуем импорт


Теперь перейдем к пакету Tabs. Очевидно, что ему потребуется много классов из пакета Common. Соответственно, его файл @TabsImports.ts будет выглядеть следующим образом:


import * as Common from "../Common/@CommonExports";

export { Common };



Теперь в модуле AssignmentTemplatesSettings.tsx этого пакета достаточно написать следующее:


import { Common } from "../@TabsImports";
// Что-то вроде using для удобства обращения к вложенному пакету
var Events = Common.Utils.Events;
// Используем класс BasicEvent из модуля Common/Utils/Events/BasicEvent.ts
var basicEvent = new Events.BasicEvent();



Как видно, вместо указания полного пути к файлу BasicEvent, мы просто указываем, в каком пакете он располагается. Что особенно приятно, так это то, что при написании Events.BasicEvent подсветка синтаксиса и линтинг JSX в VisualStudio прекрасно работают!


Если пакету Tabs нужен только пакет Events, то можно переписать TabsImports.ts следующим образом:


import * as Common from "../Common/@CommonExports";
var Events =  Common.Utils.Events;
export { Events };

Либо так:


import * as Events from "../Common/Utils/@EventsExports";
export { Events };

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


Внутренние связи модулей в пакете


Связь модулей внутри пакета уже не такая страшная проблема, т.к. все они находятся рядом. Однако, по ряду причин, может понадобиться использовать такой же механизм для импорта модулей текущего пакета. Использовать exports-файл не получится, т.к. он по определению должен включать не все содержимое пакета. Однако, можно использовать его для создания третьего служебного файла internals:


export * from "./@EventsExports"; // Ре-экспортируем публичные члены
export * from "./SomeInternalEventImpl"; // Экспортируем внутренние элементы
export * from "./SomeAnotherInternalEventImpl

Соответственно, после этого мы можем использовать этот файл везде внутри пакета:


import * as Events from "./@EventsInternals";
let eventImpl = new Events.SomeInternalEventImpl();

Циклические зависимости разрешены спецификацией, поэтому с импортом internals проблем возникнуть не должно. По крайней мере, SystemJS корректно обрабатывает такие ситуации.


Результаты


  1. Мы избавились от ужасных точек "../../../" в пути импорта, при этом не прибегая к абсолютным путям, сохранив гибкость относительных.
  2. Мы избавились от необходимости импортировать каждый используемый модуль по отдельности, создавая огромную import-шапку в каждом файле. Вместо этого один раз импортируем нужные пакеты из imports-файла своего пакета.
  3. Мы вернули подсветку синтаксиса и линтинг JSX в VisualStudio.
  4. Поскольку мы используем переменные (пакеты), а не имена файлов при импорте, то рефакторинг в TypeScript окружении становится элементарным. Переименование пакета происходит автоматически, работает find-all-references и т.д.
  5. Зависимости между модулями упорядочены и сконцентрированы в специальных файлах, которых не очень много. Даже без TypeScript рефакторить такой код намного проще.
  6. При желании любой пакет легко будет выделить в отдельный проект, поскольку он в некоторой степени обособлен и все его зависимости явно прописаны. Разделение на пакеты естественным образом понуждает разработчика лучше структурировать приложение.
  7. Для часто используемых пакетов можно настроить alias в вашем загрузчике и импортировать его просто по имени, без указания пути.

Из недостатков — появились дополнительные imports/exports файлы, которые нужно постоянно актуализировать. В принципе, составление таких файлов можно автоматизировать не очень сложной gulp-задачей, главное придумать конвенцию, как различать экспортируемые и внутренние модули пакета. Ну и еще как недостаток — при обращении к импортированным символам необходимо добавлять имя пакета (Events.BasicEvent вместо BasicEvnet). Но, я считаю, с этим можно смириться, учитывая, что мы получаем взамен.


UPD: justboris обратил внимание, что exports-файл может быть удобно называть index.ts, т.к. многие сборщики и IDE считают его файлом "по умолчанию" в директории.
UPD: dzigoro отметил, что WebStorm поддерживает автоматическое добавление import-деклараций, а также их обновление при рефакторинге.

Поделиться с друзьями
-->

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


  1. svekl
    03.05.2017 11:29
    +1

    А tree shaking разве нормально отработаботает, если Вы импортируете всё подряд?


    1. justboris
      03.05.2017 11:59
      +1

      1. svekl
        03.05.2017 12:04

        Здорово, спасибо


      1. svekl
        03.05.2017 12:11

        Работает даже такое

        import { Math } from './exports.js';
        console.log( Math['square']( 5 ) );
        


        1. justboris
          03.05.2017 12:44

          import { Math } from './exports.js';
          
          const prop = 'square';
          console.log( Math[prop]( 5 ) ); // 125

          А такое — уже нет.


          В общем, надурить Rollup можно, если постараться. Но основные варианты использования он поддерживает


  1. justboris
    03.05.2017 11:51
    +6

    Все хорошо, но конвенция именования файла @Exports очень странная.


    Node.js и все бандлеры поддерживают index.js файл. Когда вы укажете в импорте путь до папки, например ../Common/Utils/, то бандлер автоматически добавить index.js или index.ts в конец, если такой файл в папке имеется.


    И второй момент, вот такой код


    import * as Common from "../Common/@CommonExports";
    
    export { Common };

    Ломает вам весь tree-shaking. Теперь все содержимое Common будет включено в бандл, неважно, используется ли оно на самом деле или нет


    1. PFight77
      03.05.2017 21:17

      Хорошая мысль насчет index.ts, попробуем. Надо посмотреть, умеет ли SystemJS такое...


    1. PFight77
      04.05.2017 08:18
      +1

      Обнаружил пару проблем с index.ts (собственно, почему я отказался от просто @Exports.ts и пишу @EventsExports):


      1. При редактировании вкладка в IDE называется index.ts — трудно понять по названию вкладки, что за файл
      2. При открытии через быстрый поиск нельзя найти именно этот файл, т.к. имя не уникальное (я использую FastFind)

      Ну и возникает вопрос, как именовать imports и internals...


      1. justboris
        04.05.2017 10:41
        +2

        Во-первых, одинаковое имя лечится настройкой "показывать имя папки для одноименных файлов".
        Во-вторых, в индексных файлах у вас содержатся только импорты/экспорты, часто в них лазить, а тем более держать несколько открытых сразу не придётся.
        И в-третьих, ваш подход неэргономичен. После работы на проекте с общепринятым использованием index.js файлов приходишь в проект, где горе-архитектор не осилил настройку IDE, зато выдумал свою особую конвенцию именования, и эффективность работы только падает...


        1. PFight77
          04.05.2017 21:06

          Все-таки пока не могу решить, как лучше, но в статью добавил.


  1. justboris
    03.05.2017 11:59
    +3

    Проверил еще раз, tree-shaking все еще работает. Пример в Rollup-repl. Второй вопрос снимается, извините.


    А первый вопрос про неиспользование index.js все еще остается.


  1. dzigoro
    03.05.2017 13:25
    +3

    На что только не идут люди, чтобы не пользоваться нормальными средствами разработки. Я конечно сейчас накину на вентилятор, но ни одна религия не запрещает использовать WebStorm, в котором импорт, рефакторинг и навигация уже работает. Я вообще в импорты не смотрю, они зафолжены всегда.


    1. Anarions
      03.05.2017 15:36

      А вебшторм умеет автоматически импортировать (как решарпер по Alt+Enter) из вариантов имеющихся в проекте? Например я начинаю печатать — а вебшторм зная что у меня имеются /path1/MyComponent и /path2/MyComponent / — предлагает мне заимпортить один из двух?


      1. dzigoro
        03.05.2017 16:55

        Да, это зарелизили месяц назад буквально.


        1. thekip
          03.05.2017 17:29

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


          А так да, импорты хоть 5, хоть 10 точек, неважно. Все автоматом делается средой. Не могу представить тот гемор если бы делал бы это сам (и тот гемор на который люди себя толкают выбирая другие инструменты разработки).


    1. worldxaker
      03.05.2017 18:11

      религия то конечно позволяет, а вот финансовое состояние, нет.


      1. justboris
        03.05.2017 18:22
        +1

        Visual Studio Code – бесплатная и по импортам переходить умеет.


        1. PFight77
          04.05.2017 20:46

          Пробовал VS Code, Atom, у них у всех ужасно работают TypeScript-плагины (по сравнению с VisualStudio). Может я пробовал на слишком слабом компьютере, не знаю.


          1. vintage
            04.05.2017 21:53

            Ужасно в смысле медленно? Они все используют TypeScript Language Service написанный на JS со всеми вытекающими. Большая студия я подозреваю использует свою шуструю реализацию.


            1. PFight77
              06.05.2017 09:57

              Да, медленно обновляется кеш intellicense, медленно работает compile-on-save, глючит периодически (например, compile-on-save не замечает что были внесены правки в код, и не выполняет компиляцию). VisualStudio работает на порядок быстрее и стабильнее.


      1. dzigoro
        11.05.2017 15:36
        +1

        У разраба нет 150 баксов в год на инструмент для работы? Поработайте в выходной на фрилансе и купите себе инструмент. Напишите письмо в JB с просьбой дать скидку. Попросите лицензию на работе. *Not affiliated with JB*


        1. worldxaker
          11.05.2017 20:37

          я пока студент, так что юзаю все бесплатно


        1. PFight77
          11.05.2017 21:52
          +1

          А еще может быть так, что в компании уже куплена VisualStudio, которая во всем всех устраивает, в ней настроены все дев-процессы (сборка, отладка и т.д.) и покупать еще отдельно другую IDE только из-за того что она умеет дописывать import и в результате получить гемор с перенастройкой процессов и переучиванием разработчиков — да никто не будет таким заниматься...


          1. vintage
            11.05.2017 23:10

            Есть мнение, что если import может автоматически вставлять IDE, то он с тем же успехом может автоматически вставляться и сборщиком.


            1. PFight77
              12.05.2017 00:45

              Я смотрел агностик модули. Может это и круто, но import это стандарт, лучше все-таки придерживаться стандартных средств. Глобальная область видимости уже была, и от нее решили отказаться по многим причинам.


              1. vintage
                12.05.2017 08:51

                Для я даже не про них, а про то, что сейчас развитие языка идёт в сторону увеличения писанины, которая могла бы быть легко автоматизирована.


                Ведь можно же было посмотреть, как это сделано у других. Например, в пхп есть чудесный автолоад: http://www.php-fig.org/psr/psr-4/


                Кроме того, про отказ от глобальной области видимости вы погорячились — тот же трендовый redux хранит все данные в одной глобальной области видимости. Из-за чего люди, привыкшие к импортам и возможности использовать короткие имена, огребают, когда надо соединить несколько приложений в одном: https://habrahabr.ru/company/efs/blog/328012/


                1. PFight77
                  12.05.2017 09:53

                  Что autoloader в php, что агностик-модули, все заменяют import строгими правилами именования сущностей. То есть, конфликты имен в глобальной области видимости решаются конвенциональным путем. Проблема такого подхода очевидна — длинные имена и сложность заставить всех писать уникальные имена в правильном виде.


                  Про Redux… Там ведь не только конфликты имен могут быть, но совершенно неконтролируемое сцепление различных частей приложения, к тому же без поддержки соотв. тулинга. Мы (императивные программисты) инкапсулируем еще со времен модульного программирования, но функциональщики вообще не от мира сего ^_^


                  А про писанину, об этом как раз моя статья. Мне удалось уменьшить ее количество вплоть до одной-двух строчек import.


                  1. vintage
                    12.05.2017 10:06

                    строгими правилами именования сущностей

                    И это очень хорошая практика. Порядком задалбывает разбираться как в очередном проекте решили раскидать модули по папкам. А если ещё и с алиасами, то вообще туши свет. На одном из проектов jQuery таким образом подключался аж 3 раза разных минорных версий.


                    длинные имена

                    Вы всегда можете сделать короткий локальный алиас, если имя слишком длинное.


                    сложность заставить всех писать уникальные имена в правильном виде

                    Ничего сложного. Неправильно написал — получил ошибку.


                    функциональщики вообще не от мира сего

                    Ну так, в ФП, изменяемое состояние является внешним по отношению к приложению, а значит общим для всего кода.


  1. Drag13
    03.05.2017 13:44

    Уже пару месяцев юзаю такую штуку, полет впринципе нормальный. Единственный недостаток с которым столкнулся, если мы меняем положение index файла с молдулями, то это затрагивает всех пользователей этого файла и плодит много изменений. Но большим недостатком это не назовешь.


  1. bromzh
    03.05.2017 16:04

    А я просто настроил paths и baseUrl в конфиге тайпскрипта, импорты выглядят так:


    // PROJECT_ROOT/src/app/foo/foo.ts
    export class Foo {}
    
    // PROJECT_ROOT/src/app/fo/index.ts
    export * from './foo.ts';
    
    // PROJECT_ROOT/src/app/bar/bar.ts
    import { Foo } from 'app/foo';
    

    Вебшторм же можно настроить на импорт в таком стиле, без вездесущих ../../


    картинка


  1. nazarpc
    03.05.2017 18:21
    +2

    Мы избавились от необходимости импортировать каждый используемый модуль по отдельности, создавая огромную import-шапку в каждом файле. Вместо этого один раз импортируем нужные пакеты из imports-файла своего пакета.

    Мне кажется, прямым следствием этого будет то, что теперь браузер будет грузить огромную кучу ненужного кода.


    1. justboris
      03.05.2017 18:24

      Почитайте эту ветку. Там мы разобрали примеры, где код будет грузиться, а где лишний откинется на этапе сборки.


      1. nazarpc
        03.05.2017 18:28

        Это при условии наличия системы сборки (что по большому счёту верно на сегодняшний день). Но при нативных модулях и HTTP/2 гораздо лучше использовать явные импорты нужных вещей. Это делает зависимости более прозрачными и загрузку более точечной.


        P.S. Пока нет нормальной работы с путями и перехватом путей в браузере, чтобы можно было делать алиасы, подсовывать заглушки в тестах и прочее, я пользуюсь RequireJS (и у меня нет полноценной системы сборки, LiveScript пофайлово компилируется с помощью File Watchers во время написания)


  1. RayMan
    03.05.2017 18:29
    +2

    В typescipt можно делать так:

    "baseUrl": "./",
    "paths": {
        "pkg-*": ["./packages/pkg-*"]
    }
    


    В webpack:

    resolve: {
       modules: [
          path.join(__dirname, '..', 'packages'),
          path.join(__dirname, '..', 'node_modules')
       ]
    }
    


    После этого просто разбиваешь проект на саб пакеты и используешь:

    import { BasicEvent } from 'pkg-common/Utils/Events/BasicEvent';
    import { Button } from 'pkg-components/Button';
    


  1. gogolor
    03.05.2017 21:10

    Я не уверен, публиковалось ли уже где-то такое решение в данном контексте

    У Angular в доках это называется barrel


    1. PFight77
      03.05.2017 21:23

      Спасибо, добавил в статью.


  1. comerc
    04.05.2017 03:13

    Как-то слишком мудрено, мой велосипед попроще. Ещё посмотрите на Lerna. И про абсолютные пути — настроил для WebStorm, Atom и VSCode.


    1. PFight77
      04.05.2017 08:27

      Ваш велосипед суть половина моего. Ваши домены — отчасти мои пакеты, только "в одну сторону". Вы упорядочили импорт доменных компонентов (exports-файлы), но не упорядочили зависимости самих доменов (imports и internals — файлы). Lerna — абсолютные пути позволяют избавиться только от точек. Я же решаю массу других проблем.


  1. vintage
    04.05.2017 20:17

    Глобальная область видимости (aka namespace в TypeScript) — уже давно не круто.

    Может и "не круто", зато удобно, надёжно и практично. Взять в пример ту же Java — там импорты идут по полному пути от корня (причём даже не корня проекта, а глобального корня), которые однозначно мапятся на директории. Это к вопросу о "../../..".


    Далее, портянку импортов и rollup можно выкинуть в пользу агностик модулей. Собираться будет ровно то, что используется. Поддерживается любыми IDE и редакторами. Для вашего примера это будет так:


    /My/Pages/LogIn/Form/Form.tsx


    // Содержимое /My/Components/Field/Group/ и /My/Common/Utils/Events/Basic/ будет включено в бандл автоматически
    namespace $My.Pages.LogIn {
    
        // Хотим - напрямую используем
        var event = new $My.Common.Utils.Events.Basic();
    
        // Хотим, "импортируем" в короткий алиас
        const FieldGroup = $My.Components.Field.Group
    
        export function Form() {
            return (
                <form>
                    <FieldGroup name="blabla" />
                    <FieldGroup name="lalala" />
                </form>
            )
        }
    
    }

    /My/Components/Field/Group/Group.tsx


    namespace $My.Components.Field {
    
        export function Group() {
            return <fieldset />
        }
    
    }

    /My/Components/Field/Group/Group.css


    // Стили тоже подтянутся автоматически.
    fieldset {
        // ...
    }

    /My/Common/Utils/Events/Basic/Basic.ts


    namespace $My.Common.Utils.Events {
    
        export class Basic {
            // ...
        }
    
    }


    1. PFight77
      04.05.2017 20:44
      +1

      Собираться может и будет, но вот SystemJS вы таким образом не настроите (чтобы грузить по одному файлу во время разработки). Мы давно используем namespace c формированием списка файлов и бандла через ASP.NET. Много боли несет в себе такая практика.


      Собирать же бандл в дев-окружении, имхо, очень неудобно, т.к. приходится ждать сборки после каждой правки в коде. Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).


      1. vintage
        04.05.2017 21:59

        вот SystemJS вы таким образом не настроите

        Да как-то не вижу в нём необходимости.


        Много боли несет в себе такая практика.

        Какой?


        приходится ждать сборки после каждой правки в коде

        Она быстро происходит, так как все файлы уже есть в памяти, надо только подгрузить изменившиеся, прогнать проверку типов (а её по отдельному файлу не прогнать, только по бандлу) и сконкатенировать (плёвая операция).


        Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).

        Эм… у вас браузер поддерживает TS? :-)


        1. PFight77
          04.05.2017 22:17

          При сохранении срабатывает compile-on-save, который мгновенно компилит один единственный файл (ну а после докомпиливает остальное). Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.


          Какой?

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


          1. vintage
            05.05.2017 07:49

            При сохранении срабатывает compile-on-save

            Не всякий раз после сохранения требуется видеть результат. Зато всякий раз, когда требуется видеть результат — требуется его наиболее актуальная версия.


            мгновенно компилит один единственный файл

            Для тайпчекинга всё-равно нужны все остальные файлы.


            Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.

            Всё быстро, пока число файлов и глубина зависимостей не большие.


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

            MAM разруливает это сам, не требуя от программиста лишних телодвижений — только следование соглашению об именовании. Присмотритесь к моему примеру выше.


            1. vintage
              05.05.2017 08:54
              -1

              Собственно, видео процесса, если интересно:



  1. vintage
    05.05.2017 07:49

    .


  1. Veikedo
    05.05.2017 08:42

    Используете ли вы redux?
    Если используете, то как вы типизируете ваши actions?
    Мы используем подобие такого, но не очень удобно https://rjzaworski.com/2016/08/getting-started-with-redux-and-typescript#actions


    1. PFight77
      05.05.2017 09:16

      У нас пока не было необходимости шарить стейт между компонентами, там особая специфика предметной области. Но в целом, я вообще не понимаю, откуда столько хайпа вокруг Redux:
      1) При малейшем изменении стейта запускаются все селекторы, обновляются все компоненты (WTF?). Отсюда все эти shouldComponentUpdate и т.д.
      2) Никакой инкапсуляции — весь стейт доступен всем, причем сырые данные, без оберток.
      3) TypeScript тулинг — сплошные проблемы...


      Мне больше нравится подход Angular, со старыми добрыми сервисами, которые инкапсулируют внутри себя некоторую логику и стейт + предоставляют возможность подписки на изменения.


      1. mayorovp
        05.05.2017 09:34

        А связку react-mobx вы в таком случае не пробовали?


        1. PFight77
          05.05.2017 20:53

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


          1. mayorovp
            05.05.2017 21:13

            Хм, а whyrun не помогает?..


            1. PFight77
              05.05.2017 21:47

              Я об этой проблеме прочитал в комментах к одной статье, сам не пробовал. Помогает или нет вот у Вас хотел бы спросить...


              Вот в целом, какой смысл в MobX? Это все на случай если влом объявить событие и тригернуть его при смене свойств? То есть, просто синтаксический сахар, чтобы при объявлении свойства сразу объявлять и событие об его изменении? Если дело только в этом, то я за пол часа напишу свой декоратор, который это автоматизирует.


              Посмотрел вот еще доки, оказывается @observable нельзя объявлять для свойств с сеттерами. То есть, я буду вынужден открывать в сервисе свои сырые данные, без возможности их инкапсулировать…


              1. mayorovp
                05.05.2017 22:02

                Нет, вам не надо открывать свои сырые данные. Декоратор @observable вообще-то можно и на приватные свойства повешать.


                А на свойства с одним только геттером полагается либо вешать @computed — либо оставлять так если оно слишком простое.


                Смысл MobX — в автоматическом определении зависимостей. MobX как раз решает ту самую проблему с постоянными проверками всего подряд.


                Сам я еще MobX не пробовал, сижу пока на knockout, у которого та же идея — но не такая удобная реализация. Будет новый проект — попробую MobX.


                1. vintage
                  06.05.2017 00:40
                  -1

                  Попробуйте лучше $mol_mem, который и удобней и эффективней.


                  1. mayorovp
                    06.05.2017 07:09

                    Он слишком многословен.


                    1. vintage
                      06.05.2017 13:16

                      KnockOut


                      class Foo {
                      
                          length = ko.observable( 2 )
                      
                          squared = ko.pureComputed({
                              read : ()=> this.length() ** 2 ,
                              write : ( next: number )=> {
                                  this.length( next ** .5 )
                              } ,
                          } )
                      
                      }

                      MobX


                      class Foo {
                      
                          @observable
                          length = 2
                      
                          @computed
                          get squared() {
                              return this.length ** 2
                          }
                          set squared( next : number ) {
                              this.length = next ** .5
                          }
                      
                      }

                      $mol_mem


                      class Foo {
                      
                          @ $mol_mem()
                          length( next = 2 ) { return next }
                      
                          @ $mol_mem()
                          squared( next? : number ) {
                              return this.length( next && next ** .5 ) ** 2
                          }
                      
                      }


                      1. mayorovp
                        06.05.2017 13:20

                        Вы лучше покажите что дальше с этим делать.


                        1. vintage
                          06.05.2017 13:29

                          Я показал, что многословность приблизительно одинаковая. Что с этой информацией делать — это уж вы решайте сами :-)


                          1. mayorovp
                            06.05.2017 13:30

                            Вы показали что многословность приблизительно одинаковая в этом случае.


                            1. vintage
                              06.05.2017 13:34

                              Ну, предложите другой случай, посмотрим.


                              1. mayorovp
                                06.05.2017 13:36

                                Ваш $mol просто ужасен в части вывода.


                                1. vintage
                                  06.05.2017 13:42

                                  Вывода чего? Вы можете хоть как-то аргументировать, а не просто бросаться оценочными суждениями?


                                  1. mayorovp
                                    06.05.2017 13:43

                                    Вывода информации. Генерирования html. Показа всей этой крутой реактивной модели пользователю.


                                    1. vintage
                                      06.05.2017 13:54

                                      $mol_mem ничего такого не умеет. Это чистая реализация ОРП, которую можно взять и использовать в любом проекте. Пару лет назад, я, например, использовал его с Ангуляром.


                                      А вот $mol_view — это отдельная библиотека, использующая возможности ОРП по максимуму для построения интерфейса. Если максимум вам не нужен — можете использовать хоть Реакт, хоть Хэндлбарс.


                                      Ну, и раз уж, речь зашла про $mol_view, то не поясните, что там такого "ужасного"? Только объективно, а не "непривычно и лень вникать".


                                      1. mayorovp
                                        06.05.2017 13:55

                                        А у вас есть готовые решения для подключения $mol_mem к React?


                                        1. vintage
                                          06.05.2017 14:31

                                          А там нужны какие-то решения?


                                          Заворачиваем рендеринг реакта в атом:


                                          const ui = new $mol_atom( 'render' , ()=> React.render( <UI/> , document.body ) )
                                          
                                          ui.actualize()

                                          Добавляем декоратор перед render, чтобы результат кешировался:


                                          @ $mol_mem()
                                          render() {
                                              ...
                                          }

                                          Всё, теперь можем обращаться к любым реактивным переменным.


                                          1. mayorovp
                                            06.05.2017 14:35

                                            Вы предлагаете рендерить каждый раз дерево целиком? Мда...


                                            1. vintage
                                              06.05.2017 14:56

                                              Это не я, это Реакт так работает :-)


                                              https://facebook.github.io/react/docs/rendering-elements.html#updating-the-rendered-element


                                              1. mayorovp
                                                06.05.2017 14:59

                                                In practice, most React apps only call ReactDOM.render() once. In the next sections we will learn how such code gets encapsulated into stateful components.

                                                Ну и вот еще:


                                                https://habrahabr.ru/post/319536/
                                                https://habrahabr.ru/post/304340/
                                                https://habrahabr.ru/post/327364/


                                                1. vintage
                                                  06.05.2017 15:47

                                                  Ну так эти statefull components вызывают ReactDOM.render под капотом. А ссылки к чему? shouldComponentUpdate при использовании $mol_mem не нужен.


                                                  1. mayorovp
                                                    06.05.2017 15:49

                                                    Суть в том, что при обновлении только одного компонента все дерево целиком рендериться не должно. А вы предлагаете рендерить.


                                                    1. vintage
                                                      06.05.2017 15:51

                                                      Реакт так работает, что он на любой чих создаёт новое виртуальное дерево целиком. Потом смотрит разницу с предыдущим виртуальным деревом и применяет её к реальному дереву.


                                                      1. mayorovp
                                                        06.05.2017 15:53

                                                        Если бы это было так — то все оптимизации рендеринга не давали бы никакого прироста.


                                                        1. vintage
                                                          06.05.2017 15:55

                                                          Всё "оптимизации" Реакта сводятся к тому, чтобы render выдавал одно и то же значение, если то, от чего он реально зависит, не изменилось. Именно это и делает $mol_mem.


                                                          1. mayorovp
                                                            06.05.2017 16:03

                                                            А как ваш $mol_mem учитывает изменения в props?


                                                            1. vintage
                                                              06.05.2017 16:32

                                                              Никак, в пропсы надо передавать не сами значения, а функции получения/изменения значения. Тогда render вложенной компоненты подпишется на те атомы, от которых он реально зависит, а render владельца не подпишется. Также это позволит получать данные лениво по требованию. Кстати, независимость от props, позволяет не лепить костыли с сохранением обработчиков событий — их можно смело создавать при каждом рендеринге.


                                                              class Welcome extends React.Component {
                                                              
                                                                @ $mol_mem()
                                                                render() {
                                                                  return <h1>Hello, { this.props.name() }</h1>;
                                                                }
                                                              
                                                              }
                                                              
                                                              class App extends React.Component {
                                                              
                                                                @ $mol_mem()
                                                                name( next = 'Anon' ) { return next }
                                                              
                                                                @ $mol_mem()
                                                                render() {
                                                                  return <Welcome name={ ()=> this.name() } />
                                                                }
                                                              
                                                              }
                                                              


                                                              1. mayorovp
                                                                06.05.2017 16:38

                                                                Выглядит страшно.


                                                                1. vintage
                                                                  06.05.2017 16:54

                                                                  Зато позволяет такие штуки делать:


                                                                  class App extends React.Component {
                                                                  
                                                                    @ $mol_mem()
                                                                    profile() {
                                                                      return $mol_http_resource_json.item( '/profile.json' ).json()
                                                                    }
                                                                  
                                                                    name() {
                                                                      return this.profile().name
                                                                    }
                                                                  
                                                                    @ $mol_mem()
                                                                    render() {
                                                                      return <Welcome name={ ()=> this.name() } />
                                                                    }
                                                                  
                                                                  }```


                                                                  1. vintage
                                                                    06.05.2017 17:57

                                                                    Я тут подумал… нет, горбатого могила исправит, реактовая реконциляция тут всё испортит. Поэтому остаётся писать так:


                                                                      @ $mol_mem()
                                                                      render() {
                                                                        return <Welcome name={ this.name() } />
                                                                      }

                                                                    И прикрутить какой-нибудь костыль для пропсов.


              1. vintage
                06.05.2017 00:43

                1. PFight77
                  06.05.2017 09:55

                  Спасибо, теперь я вполне понял, почему не буду использовать все это в продакшене: ) Куча проблем и возможностей заглючить и убить производительность своего приложения, сделав в добавок практически невозможной отладку.


                  Event-Driven архитектура очень хрупкая, потому что приходится вручную следить за своевременными подписками и отписками, а человек — существо ленивое и не сильно внимательное.

                  И все-таки дело в лени: ) Имхо, лучше уж хрупкость event-driven, чем хрупкость и закадровая магия-автоматика FRP…


                  Кстати, такой вопрос. На сколько я понял, основные проблемы связаны с автотрекингом зависимостей. Что мешает сделать объявление зависимостей явным? По-моему, это сделает систему куда элегантнее, стабильне и проще.


                  1. mayorovp
                    06.05.2017 10:00

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


                    1. faiwer
                      06.05.2017 10:39

                      Ой да ладно, нет их, как же. Вышеописанный knockout немало крови у меня попил, когда я намешал deferred, trottle, и обычные computed в одном флаконе. Сказать что там всё расползлось, это всё равно, что ничего не сказать. Пришлось внутри computed лепить костыли, на случай если часть зависимостей, которые идут вначале тела-computed метода почему-то не обновились, а от них зависит логика вызова последующих зависимостей. И больше всего при этом убивает непредсказуемость такого поведения. Попытка отловить такой баг сродне попытке найти выход из лабиринта с завязанными глазами.


                      1. mayorovp
                        06.05.2017 10:52

                        Это проблема knockout, а не автотрекинга. Как будто если бы не было автотрекинга — то все эти deferred, trottle, и обычные computed заработали бы как надо… Все эти defered и trottle зачастую используются просто чтобы остановить комбинаторный взрыв при распространении изменений, и по сути являются костылями.


                        В том же mobx эта проблема решена по-другому. В Mobx помимо состояний "значение актуально" и "значение устарело" есть третье состояние, "значение могло устареть" — и ни одно производное значение не начнет вычисляться пока есть шанс что у него зависимости остались прежними.


                        Кроме того, в mobx явно разнесли зависимые значения и реакции и они никогда не вычисляются вперемешку.


                        1. PFight77
                          06.05.2017 20:57

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


                          1. mayorovp
                            06.05.2017 21:13

                            Задача достаточно сложная чтобы решать ее можно было только автоматически.


                          1. vintage
                            06.05.2017 21:29

                            Задача не тривиальная, но вполне решаемая. Думаю мне удалось запилить наиболее эффективное автоматическое решение.


      1. Veikedo
        05.05.2017 09:58

        1. Нет, не все. Как раз redux это и автоматизирует
        2. Это скорее ООП головного мозга. Нет, я не в обиду — я сам люблю и практикую C#. Но тут другой подход и он хорошо работает.
          А если смотреть ещё глубже, то тут наоборот хорошее разделение — аналоги message bus & event sourcing в мире бэкенда.
        3. Тут да, есть гемор


        1. PFight77
          05.05.2017 21:05

          Нет, не все. Как раз redux это и автоматизирует

          Поправьте, если я где-то ошибусь. Согласно вот этим докам, у нас есть container-компоненты, которые реализуют функцию mapStateToProps. Эта функция, очевидно, получает на вход state и выдает props. Она вызывается при каждом изменении стейта в сторе. Соответственно, если функция выдает те же самые props, то компонент не перерисовывается (что есть обычная логика react).


          Теперь, вопрос: чем все вот это отличается от тех самых кошмарных watch, в AngularJS? Есть некоторое глобальное состояние, и есть набор вотчеров, которые его смотрят. Единственное отличие — в AngularJS вотчеры могли еще сами менять state, здесь все стабилизируется за один проход. Но суть ведь та же — все компоненты проверяют стейт при каждом чихе…


    1. RayMan
      05.05.2017 11:41

      А в чем проблема типизирования actions?

      export interface Action<T extends string> {
      	readonly type: T;
      }
      
      export interface PayloadAction<U extends string, T> extends Action<U> {
      	readonly payload: T;
      }
      
      //
      
      export const SET_USER = 'SET_USER';
      
      export interface SetUserAction extends PayloadAction<typeof SET_USER, IUser> {}
      
      export const setUser = (user: IUser): SetUserAction => ({
      	type: SET_USER,
      	payload: user
      });
      


      1. vintage
        05.05.2017 11:52

        Ваш ребус мало кто сможет понять.


        1. RayMan
          05.05.2017 16:26

          А можно подробнее? Что тут не понятного?


          1. vintage
            05.05.2017 19:03
            -1

            Не понятно зачем такие пляски с бубном, чтобы понять которые нужно 5 минут вникать в код.


            1. RayMan
              05.05.2017 19:17

              Подождите, а вы писали что нибудь на typescript?


              1. vintage
                05.05.2017 19:19

                Я целый фреймворк на нём запилил, а что?


                1. RayMan
                  06.05.2017 01:02

                  Если вы про $mol, то тогда понятно. Типизации там очень мало, как по мне, для typescript проекта.

                  Ну а «мало кто сможет понять» слишком преувеличено.


                  1. vintage
                    06.05.2017 01:11

                    Где по вашему её не хватает?


                    Мало кто вообще TS понимает, а шаблонный код — тем более.


            1. mayorovp
              05.05.2017 21:15

              Зачем тут 5 минут в код вникать? Сразу же видно, что SetUserAction — это объект из двух полей, type и payload, причем первое строго равно 'SET_USER', а вторая имеет тип IUser.


              Это даже IDE подсказать может.


              1. vintage
                06.05.2017 00:51

                Чтобы понять что имел ввиду автор и что помешало ему написать просто:


                export function setUser( user : IUser ) {
                    return {
                        type : 'SET_USER' ,
                        payload : user ,
                    }
                }


                1. RayMan
                  06.05.2017 01:06

                  Во первых тогда type будет иметь тайп string, а не 'SET_USER';
                  Во вторых константа SET_USER нужна так же и в редьюсе, или там тоже строкой писать?


                  1. vintage
                    06.05.2017 01:23

                    1. 'SET_USER' as 'SET_USER'


                    2. А почему бы и нет?


                    1. RayMan
                      06.05.2017 01:37
                      +1

                      1. Ага и все сразу становится понятнее :)
                      2. Потому что это магия, да и опечатки сложнее искать:
                      2.1. + Интерфейс дает возможность валидировать экшн в редьюсеры и все типы отлично подсказываются в IDE.

                      export interface UserState {
                      	data: IUser;
                      }
                      
                      const initialState: UserState = {
                      	data: null
                      };
                      
                      export default createReducer({
                      	[SET_USER]: (state, { payload }: SetUserAction) => {
                      		return assign(state, {
                      			data: payload
                      		});
                      	}
                      }, initialState);
                      


                      1. vintage
                        06.05.2017 13:27

                        В этот ребус мне уже лень внимать, извините.


                      1. Veikedo
                        09.05.2017 10:36

                        Ваш вариант хорошо выглядит.
                        +Возможно вместо interface вам удобнее type было бы использовать


                        export interface SetUserAction extends PayloadAction<typeof SET_USER, IUser> {}
                        // =>
                        export type SetUserAction = PayloadAction<typeof SET_USER, IUser>


      1. Veikedo
        05.05.2017 12:45

        В том, что дублирования много — надо объявить тип, надо указать тип в actionCreator'e, а при использовании redux-thunk в dispatch вообще нетипизированный экшен отправить легко. В reducers вообще тяжко типизировать.


        Сейчас сделал так — actions это объект класса (назвал их messages). Поле type у message это имя класса.
        Затем в редьюсере с помощью декоратора handler выдираю типы message и нахожу обработчик


        // messages aka actions
        export class Message {
            type = this.constructor.name;
        }
        
        export class FetchOffenseSegments extends Message {
            constructor(public payload: NIBRSOffenseSegment[]) {
                super();
            }
        }
        
        // action creators
        export const fetchSegments = () =>
            api.getOffenseSegments()
                .then(segments => new FetchOffenseSegments(segments));
        
        // reducers
        class SegmentHandlers implements MessageReducer<NIBRSOffenseSegment[]> {
            state: NIBRSOffenseSegment[] = [];
        
            @handler handleFetchOffenseSegments(message: FetchOffenseSegments) {
                return message.payload;
            }
        
            @handler handleRemoveOffenseSegment(message: RemoveOffenseSegment) {
                return this.state.filter(x => x.id !== message.payload);
            }
        
            @handler handleImportCaseIncidents(message: ImportCaseIncidents) {
                return [...this.state, ...message.payload];
            }
        }
        
        export default combineReducers({
            segments: asReducer(SegmentHandlers),
            caseIncidents: asReducer(CaseIncidentHandlers)
        });
        


        1. PFight77
          06.05.2017 09:12
          +1

          Вы ведь знаете, что constructor.name не поддерживается в IE и изменяется при минификации?


          1. faiwer
            06.05.2017 10:40
            +1

            и изменяется при минификации?

            это отключаемая опция


          1. vintage
            06.05.2017 13:34

            constructor.name не поддерживается в IE

            Для него есть костыль:


            Function.prototype.toString.call( func ).match( /^function ([a-z0-9_$]*)/ )[ 1 ]

            Полученное таким образом имя, разумеется, нужно закешировать. Лучше всего в WeakMap.


          1. Veikedo
            09.05.2017 10:45

            Как уже написали — uglify настроили, ie заполифили