Ужасный import кракен — как использовать ES6-модули и не сойти с ума

    Глобальная область видимости (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-деклараций, а также их обновление при рефакторинге.

    ДоксВижн 35,61
    Компания
    Поделиться публикацией
    Комментарии 108
    • +1
      А tree shaking разве нормально отработаботает, если Вы импортируете всё подряд?
        • 0
          Здорово, спасибо
          • 0
            Работает даже такое

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

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


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

        • +6

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


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


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


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

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

          • 0

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

            • +1

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


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

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

              • +2

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

                • 0

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

            • +3

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


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

              • +3
                На что только не идут люди, чтобы не пользоваться нормальными средствами разработки. Я конечно сейчас накину на вентилятор, но ни одна религия не запрещает использовать WebStorm, в котором импорт, рефакторинг и навигация уже работает. Я вообще в импорты не смотрю, они зафолжены всегда.
                • 0
                  А вебшторм умеет автоматически импортировать (как решарпер по Alt+Enter) из вариантов имеющихся в проекте? Например я начинаю печатать — а вебшторм зная что у меня имеются /path1/MyComponent и /path2/MyComponent / — предлагает мне заимпортить один из двух?
                  • 0
                    Да, это зарелизили месяц назад буквально.
                    • 0

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


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

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

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

                      • 0

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

                        • 0

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

                          • 0

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

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

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

                            • 0

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

                              • 0

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

                                • 0

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


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


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

                                  • 0

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


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


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

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

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


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

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


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

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


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

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

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

                          А я просто настроил 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';
                          

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


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

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

                            • 0

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

                              • 0

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


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

                            • +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';
                              

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

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

                                • 0

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

                                • 0

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

                                  • 0

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

                                  • 0
                                    Глобальная область видимости (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

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


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

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

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


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

                                        Какой?


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

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


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

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

                                        • 0

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


                                          Какой?

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

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

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


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

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


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

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


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

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

                                            • –1

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


                                    • 0

                                      .

                                      • 0

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

                                        • 0

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


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

                                          • 0

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

                                            • 0

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

                                              • 0

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

                                                • 0

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


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


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

                                                  • 0

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


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


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


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

                                                    • –1

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

                                                      • 0

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

                                                        • 0

                                                          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
                                                              }
                                                          
                                                          }
                                                          • 0

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

                                                            • 0

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

                                                              • 0

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

                                                                • 0

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

                                                                  • 0

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

                                                                    • 0

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

                                                                      • 0

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

                                                                        • 0

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


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


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

                                                                          • 0

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

                                                                            • 0

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


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


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

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


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

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

                                                                              • 0

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

                                                                                • 0

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


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

                                                                                  • 0
                                                                                    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/

                                                                                    • 0

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

                                                                                      • 0

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

                                                                                        • 0

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

                                                                                          • 0

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

                                                                                            • 0

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

                                                                                              • 0

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

                                                                                                • 0

                                                                                                  Никак, в пропсы надо передавать не сами значения, а функции получения/изменения значения. Тогда 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() } />
                                                                                                    }
                                                                                                  
                                                                                                  }
                                                                                                  
                                                                                                  • 0

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

                                                                                                    • 0

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


                                                                                                      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() } />
                                                                                                        }
                                                                                                      
                                                                                                      }```
                                                                                                      • 0

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


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

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

                                                      • 0

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


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

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


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

                                                        • 0

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

                                                          • 0

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

                                                            • 0

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


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


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

                                                              • 0

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

                                                                • 0

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

                                                                  • 0

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

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

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


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

                                                • 0
                                                  А в чем проблема типизирования 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
                                                  });
                                                  
                                                  • 0

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

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

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

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

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

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

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

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


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

                                                          • 0

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


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

                                                            • 0

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


                                                              export function setUser( user : IUser ) {
                                                                  return {
                                                                      type : 'SET_USER' ,
                                                                      payload : user ,
                                                                  }
                                                              }
                                                              • 0
                                                                Во первых тогда type будет иметь тайп string, а не 'SET_USER';
                                                                Во вторых константа SET_USER нужна так же и в редьюсе, или там тоже строкой писать?
                                                                • 0
                                                                  1. 'SET_USER' as 'SET_USER'


                                                                  2. А почему бы и нет?
                                                                  • +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);
                                                                    

                                                                    • 0

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

                                                                      • 0

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


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

                                                          В том, что дублирования много — надо объявить тип, надо указать тип в 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

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

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

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

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

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


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

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

                                                                • 0

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

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

                                                          Самое читаемое