Глобальная область видимости (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 корректно обрабатывает такие ситуации.
Результаты
- Мы избавились от ужасных точек "../../../" в пути импорта, при этом не прибегая к абсолютным путям, сохранив гибкость относительных.
- Мы избавились от необходимости импортировать каждый используемый модуль по отдельности, создавая огромную import-шапку в каждом файле. Вместо этого один раз импортируем нужные пакеты из imports-файла своего пакета.
- Мы вернули подсветку синтаксиса и линтинг JSX в VisualStudio.
- Поскольку мы используем переменные (пакеты), а не имена файлов при импорте, то рефакторинг в TypeScript окружении становится элементарным. Переименование пакета происходит автоматически, работает find-all-references и т.д.
- Зависимости между модулями упорядочены и сконцентрированы в специальных файлах, которых не очень много. Даже без TypeScript рефакторить такой код намного проще.
- При желании любой пакет легко будет выделить в отдельный проект, поскольку он в некоторой степени обособлен и все его зависимости явно прописаны. Разделение на пакеты естественным образом понуждает разработчика лучше структурировать приложение.
- Для часто используемых пакетов можно настроить alias в вашем загрузчике и импортировать его просто по имени, без указания пути.
Из недостатков — появились дополнительные imports/exports файлы, которые нужно постоянно актуализировать. В принципе, составление таких файлов можно автоматизировать не очень сложной gulp-задачей, главное придумать конвенцию, как различать экспортируемые и внутренние модули пакета. Ну и еще как недостаток — при обращении к импортированным символам необходимо добавлять имя пакета (Events.BasicEvent вместо BasicEvnet). Но, я считаю, с этим можно смириться, учитывая, что мы получаем взамен.
UPD: justboris обратил внимание, что exports-файл может быть удобно называть index.ts, т.к. многие сборщики и IDE считают его файлом "по умолчанию" в директории.
UPD: dzigoro отметил, что WebStorm поддерживает автоматическое добавление import-деклараций, а также их обновление при рефакторинге.
Комментарии (108)
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 будет включено в бандл, неважно, используется ли оно на самом деле или нет
PFight77
03.05.2017 21:17Хорошая мысль насчет index.ts, попробуем. Надо посмотреть, умеет ли SystemJS такое...
PFight77
04.05.2017 08:18+1Обнаружил пару проблем с index.ts (собственно, почему я отказался от просто @Exports.ts и пишу @EventsExports):
- При редактировании вкладка в IDE называется index.ts — трудно понять по названию вкладки, что за файл
- При открытии через быстрый поиск нельзя найти именно этот файл, т.к. имя не уникальное (я использую FastFind)
Ну и возникает вопрос, как именовать imports и internals...
justboris
04.05.2017 10:41+2Во-первых, одинаковое имя лечится настройкой "показывать имя папки для одноименных файлов".
Во-вторых, в индексных файлах у вас содержатся только импорты/экспорты, часто в них лазить, а тем более держать несколько открытых сразу не придётся.
И в-третьих, ваш подход неэргономичен. После работы на проекте с общепринятым использованием index.js файлов приходишь в проект, где горе-архитектор не осилил настройку IDE, зато выдумал свою особую конвенцию именования, и эффективность работы только падает...
justboris
03.05.2017 11:59+3Проверил еще раз, tree-shaking все еще работает. Пример в Rollup-repl. Второй вопрос снимается, извините.
А первый вопрос про неиспользование
index.js
все еще остается.
dzigoro
03.05.2017 13:25+3На что только не идут люди, чтобы не пользоваться нормальными средствами разработки. Я конечно сейчас накину на вентилятор, но ни одна религия не запрещает использовать WebStorm, в котором импорт, рефакторинг и навигация уже работает. Я вообще в импорты не смотрю, они зафолжены всегда.
Anarions
03.05.2017 15:36А вебшторм умеет автоматически импортировать (как решарпер по Alt+Enter) из вариантов имеющихся в проекте? Например я начинаю печатать — а вебшторм зная что у меня имеются /path1/MyComponent и /path2/MyComponent / — предлагает мне заимпортить один из двух?
dzigoro
03.05.2017 16:55Да, это зарелизили месяц назад буквально.
thekip
03.05.2017 17:29Не знаю что конкретно нового они недавно зарелизили, но я еще в прошлом году мог выбирать по Alt Enter нужный символ, и добавлялся импорт именно выбранного символа.
А так да, импорты хоть 5, хоть 10 точек, неважно. Все автоматом делается средой. Не могу представить тот гемор если бы делал бы это сам (и тот гемор на который люди себя толкают выбирая другие инструменты разработки).
worldxaker
03.05.2017 18:11религия то конечно позволяет, а вот финансовое состояние, нет.
justboris
03.05.2017 18:22+1Visual Studio Code – бесплатная и по импортам переходить умеет.
PFight77
04.05.2017 20:46Пробовал VS Code, Atom, у них у всех ужасно работают TypeScript-плагины (по сравнению с VisualStudio). Может я пробовал на слишком слабом компьютере, не знаю.
vintage
04.05.2017 21:53Ужасно в смысле медленно? Они все используют TypeScript Language Service написанный на JS со всеми вытекающими. Большая студия я подозреваю использует свою шуструю реализацию.
PFight77
06.05.2017 09:57Да, медленно обновляется кеш intellicense, медленно работает compile-on-save, глючит периодически (например, compile-on-save не замечает что были внесены правки в код, и не выполняет компиляцию). VisualStudio работает на порядок быстрее и стабильнее.
dzigoro
11.05.2017 15:36+1У разраба нет 150 баксов в год на инструмент для работы? Поработайте в выходной на фрилансе и купите себе инструмент. Напишите письмо в JB с просьбой дать скидку. Попросите лицензию на работе. *Not affiliated with JB*
PFight77
11.05.2017 21:52+1А еще может быть так, что в компании уже куплена VisualStudio, которая во всем всех устраивает, в ней настроены все дев-процессы (сборка, отладка и т.д.) и покупать еще отдельно другую IDE только из-за того что она умеет дописывать import и в результате получить гемор с перенастройкой процессов и переучиванием разработчиков — да никто не будет таким заниматься...
vintage
11.05.2017 23:10Есть мнение, что если import может автоматически вставлять IDE, то он с тем же успехом может автоматически вставляться и сборщиком.
PFight77
12.05.2017 00:45Я смотрел агностик модули. Может это и круто, но import это стандарт, лучше все-таки придерживаться стандартных средств. Глобальная область видимости уже была, и от нее решили отказаться по многим причинам.
vintage
12.05.2017 08:51Для я даже не про них, а про то, что сейчас развитие языка идёт в сторону увеличения писанины, которая могла бы быть легко автоматизирована.
Ведь можно же было посмотреть, как это сделано у других. Например, в пхп есть чудесный автолоад: http://www.php-fig.org/psr/psr-4/
Кроме того, про отказ от глобальной области видимости вы погорячились — тот же трендовый redux хранит все данные в одной глобальной области видимости. Из-за чего люди, привыкшие к импортам и возможности использовать короткие имена, огребают, когда надо соединить несколько приложений в одном: https://habrahabr.ru/company/efs/blog/328012/
PFight77
12.05.2017 09:53Что autoloader в php, что агностик-модули, все заменяют import строгими правилами именования сущностей. То есть, конфликты имен в глобальной области видимости решаются конвенциональным путем. Проблема такого подхода очевидна — длинные имена и сложность заставить всех писать уникальные имена в правильном виде.
Про Redux… Там ведь не только конфликты имен могут быть, но совершенно неконтролируемое сцепление различных частей приложения, к тому же без поддержки соотв. тулинга. Мы (императивные программисты) инкапсулируем еще со времен модульного программирования, но функциональщики вообще не от мира сего ^_^
А про писанину, об этом как раз моя статья. Мне удалось уменьшить ее количество вплоть до одной-двух строчек import.
vintage
12.05.2017 10:06строгими правилами именования сущностей
И это очень хорошая практика. Порядком задалбывает разбираться как в очередном проекте решили раскидать модули по папкам. А если ещё и с алиасами, то вообще туши свет. На одном из проектов jQuery таким образом подключался аж 3 раза разных минорных версий.
длинные имена
Вы всегда можете сделать короткий локальный алиас, если имя слишком длинное.
сложность заставить всех писать уникальные имена в правильном виде
Ничего сложного. Неправильно написал — получил ошибку.
функциональщики вообще не от мира сего
Ну так, в ФП, изменяемое состояние является внешним по отношению к приложению, а значит общим для всего кода.
Drag13
03.05.2017 13:44Уже пару месяцев юзаю такую штуку, полет впринципе нормальный. Единственный недостаток с которым столкнулся, если мы меняем положение index файла с молдулями, то это затрагивает всех пользователей этого файла и плодит много изменений. Но большим недостатком это не назовешь.
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';
Вебшторм же можно настроить на импорт в таком стиле, без вездесущих
../../
картинкаnazarpc
03.05.2017 18:21+2Мы избавились от необходимости импортировать каждый используемый модуль по отдельности, создавая огромную import-шапку в каждом файле. Вместо этого один раз импортируем нужные пакеты из imports-файла своего пакета.
Мне кажется, прямым следствием этого будет то, что теперь браузер будет грузить огромную кучу ненужного кода.
justboris
03.05.2017 18:24Почитайте эту ветку. Там мы разобрали примеры, где код будет грузиться, а где лишний откинется на этапе сборки.
nazarpc
03.05.2017 18:28Это при условии наличия системы сборки (что по большому счёту верно на сегодняшний день). Но при нативных модулях и HTTP/2 гораздо лучше использовать явные импорты нужных вещей. Это делает зависимости более прозрачными и загрузку более точечной.
P.S. Пока нет нормальной работы с путями и перехватом путей в браузере, чтобы можно было делать алиасы, подсовывать заглушки в тестах и прочее, я пользуюсь RequireJS (и у меня нет полноценной системы сборки, LiveScript пофайлово компилируется с помощью File Watchers во время написания)
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';
comerc
04.05.2017 03:13Как-то слишком мудрено, мой велосипед попроще. Ещё посмотрите на Lerna. И про абсолютные пути — настроил для WebStorm, Atom и VSCode.
PFight77
04.05.2017 08:27Ваш велосипед суть половина моего. Ваши домены — отчасти мои пакеты, только "в одну сторону". Вы упорядочили импорт доменных компонентов (exports-файлы), но не упорядочили зависимости самих доменов (imports и internals — файлы). Lerna — абсолютные пути позволяют избавиться только от точек. Я же решаю массу других проблем.
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 { // ... } }
PFight77
04.05.2017 20:44+1Собираться может и будет, но вот SystemJS вы таким образом не настроите (чтобы грузить по одному файлу во время разработки). Мы давно используем namespace c формированием списка файлов и бандла через ASP.NET. Много боли несет в себе такая практика.
Собирать же бандл в дев-окружении, имхо, очень неудобно, т.к. приходится ждать сборки после каждой правки в коде. Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).
vintage
04.05.2017 21:59вот SystemJS вы таким образом не настроите
Да как-то не вижу в нём необходимости.
Много боли несет в себе такая практика.
Какой?
приходится ждать сборки после каждой правки в коде
Она быстро происходит, так как все файлы уже есть в памяти, надо только подгрузить изменившиеся, прогнать проверку типов (а её по отдельному файлу не прогнать, только по бандлу) и сконкатенировать (плёвая операция).
Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).
Эм… у вас браузер поддерживает TS? :-)
PFight77
04.05.2017 22:17При сохранении срабатывает compile-on-save, который мгновенно компилит один единственный файл (ну а после докомпиливает остальное). Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.
Какой?
Боль там от управления зависимостями — приходится вручную прописывать, какой файл сначала, какой потом. Плюс при наследовании для TypeScript нужно указывать reference-тэг на файл с родительским классом.
vintage
05.05.2017 07:49При сохранении срабатывает compile-on-save
Не всякий раз после сохранения требуется видеть результат. Зато всякий раз, когда требуется видеть результат — требуется его наиболее актуальная версия.
мгновенно компилит один единственный файл
Для тайпчекинга всё-равно нужны все остальные файлы.
Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.
Всё быстро, пока число файлов и глубина зависимостей не большие.
Боль там от управления зависимостями — приходится вручную прописывать, какой файл сначала, какой потом. Плюс при наследовании для TypeScript нужно указывать reference-тэг на файл с родительским классом.
MAM разруливает это сам, не требуя от программиста лишних телодвижений — только следование соглашению об именовании. Присмотритесь к моему примеру выше.
Veikedo
05.05.2017 08:42Используете ли вы redux?
Если используете, то как вы типизируете вашиactions
?
Мы используем подобие такого, но не очень удобно https://rjzaworski.com/2016/08/getting-started-with-redux-and-typescript#actionsPFight77
05.05.2017 09:16У нас пока не было необходимости шарить стейт между компонентами, там особая специфика предметной области. Но в целом, я вообще не понимаю, откуда столько хайпа вокруг Redux:
1) При малейшем изменении стейта запускаются все селекторы, обновляются все компоненты (WTF?). Отсюда все эти shouldComponentUpdate и т.д.
2) Никакой инкапсуляции — весь стейт доступен всем, причем сырые данные, без оберток.
3) TypeScript тулинг — сплошные проблемы...
Мне больше нравится подход Angular, со старыми добрыми сервисами, которые инкапсулируют внутри себя некоторую логику и стейт + предоставляют возможность подписки на изменения.
mayorovp
05.05.2017 09:34А связку react-mobx вы в таком случае не пробовали?
PFight77
05.05.2017 20:53Несколько пугает вся эта автоматика и закадровая магия. Люди говорят, что в крупном проекте становится трудно отследить, из-за чего обновляется компонент, когда по каждому чиху начинается перерисовка несвязанных компонентов.
mayorovp
05.05.2017 21:13Хм, а whyrun не помогает?..
PFight77
05.05.2017 21:47Я об этой проблеме прочитал в комментах к одной статье, сам не пробовал. Помогает или нет вот у Вас хотел бы спросить...
Вот в целом, какой смысл в MobX? Это все на случай если влом объявить событие и тригернуть его при смене свойств? То есть, просто синтаксический сахар, чтобы при объявлении свойства сразу объявлять и событие об его изменении? Если дело только в этом, то я за пол часа напишу свой декоратор, который это автоматизирует.
Посмотрел вот еще доки, оказывается
@observable
нельзя объявлять для свойств с сеттерами. То есть, я буду вынужден открывать в сервисе свои сырые данные, без возможности их инкапсулировать…mayorovp
05.05.2017 22:02Нет, вам не надо открывать свои сырые данные. Декоратор
@observable
вообще-то можно и на приватные свойства повешать.
А на свойства с одним только геттером полагается либо вешать
@computed
— либо оставлять так если оно слишком простое.
Смысл MobX — в автоматическом определении зависимостей. MobX как раз решает ту самую проблему с постоянными проверками всего подряд.
Сам я еще MobX не пробовал, сижу пока на knockout, у которого та же идея — но не такая удобная реализация. Будет новый проект — попробую MobX.
vintage
06.05.2017 00:40-1Попробуйте лучше $mol_mem, который и удобней и эффективней.
mayorovp
06.05.2017 07:09Он слишком многословен.
vintage
06.05.2017 13:16KnockOut
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 } }
mayorovp
06.05.2017 13:20Вы лучше покажите что дальше с этим делать.
vintage
06.05.2017 13:29Я показал, что многословность приблизительно одинаковая. Что с этой информацией делать — это уж вы решайте сами :-)
mayorovp
06.05.2017 13:30Вы показали что многословность приблизительно одинаковая в этом случае.
vintage
06.05.2017 13:34Ну, предложите другой случай, посмотрим.
mayorovp
06.05.2017 13:36Ваш $mol просто ужасен в части вывода.
vintage
06.05.2017 13:42Вывода чего? Вы можете хоть как-то аргументировать, а не просто бросаться оценочными суждениями?
mayorovp
06.05.2017 13:43Вывода информации. Генерирования html. Показа всей этой крутой реактивной модели пользователю.
vintage
06.05.2017 13:54$mol_mem ничего такого не умеет. Это чистая реализация ОРП, которую можно взять и использовать в любом проекте. Пару лет назад, я, например, использовал его с Ангуляром.
А вот $mol_view — это отдельная библиотека, использующая возможности ОРП по максимуму для построения интерфейса. Если максимум вам не нужен — можете использовать хоть Реакт, хоть Хэндлбарс.
Ну, и раз уж, речь зашла про $mol_view, то не поясните, что там такого "ужасного"? Только объективно, а не "непривычно и лень вникать".
mayorovp
06.05.2017 13:55А у вас есть готовые решения для подключения $mol_mem к React?
vintage
06.05.2017 14:31А там нужны какие-то решения?
Заворачиваем рендеринг реакта в атом:
const ui = new $mol_atom( 'render' , ()=> React.render( <UI/> , document.body ) ) ui.actualize()
Добавляем декоратор перед render, чтобы результат кешировался:
@ $mol_mem() render() { ... }
Всё, теперь можем обращаться к любым реактивным переменным.
mayorovp
06.05.2017 14:35Вы предлагаете рендерить каждый раз дерево целиком? Мда...
vintage
06.05.2017 14:56Это не я, это Реакт так работает :-)
https://facebook.github.io/react/docs/rendering-elements.html#updating-the-rendered-element
mayorovp
06.05.2017 14:59In 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/vintage
06.05.2017 15:47Ну так эти statefull components вызывают ReactDOM.render под капотом. А ссылки к чему? shouldComponentUpdate при использовании $mol_mem не нужен.
mayorovp
06.05.2017 15:49Суть в том, что при обновлении только одного компонента все дерево целиком рендериться не должно. А вы предлагаете рендерить.
vintage
06.05.2017 15:51Реакт так работает, что он на любой чих создаёт новое виртуальное дерево целиком. Потом смотрит разницу с предыдущим виртуальным деревом и применяет её к реальному дереву.
mayorovp
06.05.2017 15:53Если бы это было так — то все оптимизации рендеринга не давали бы никакого прироста.
vintage
06.05.2017 15:55Всё "оптимизации" Реакта сводятся к тому, чтобы render выдавал одно и то же значение, если то, от чего он реально зависит, не изменилось. Именно это и делает $mol_mem.
mayorovp
06.05.2017 16:03А как ваш $mol_mem учитывает изменения в props?
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() } /> } }
mayorovp
06.05.2017 16:38Выглядит страшно.
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() } /> } }```
vintage
06.05.2017 17:57Я тут подумал… нет, горбатого могила исправит, реактовая реконциляция тут всё испортит. Поэтому остаётся писать так:
@ $mol_mem() render() { return <Welcome name={ this.name() } /> }
И прикрутить какой-нибудь костыль для пропсов.
vintage
06.05.2017 00:43PFight77
06.05.2017 09:55Спасибо, теперь я вполне понял, почему не буду использовать все это в продакшене: ) Куча проблем и возможностей заглючить и убить производительность своего приложения, сделав в добавок практически невозможной отладку.
Event-Driven архитектура очень хрупкая, потому что приходится вручную следить за своевременными подписками и отписками, а человек — существо ленивое и не сильно внимательное.
И все-таки дело в лени: ) Имхо, лучше уж хрупкость event-driven, чем хрупкость и закадровая магия-автоматика FRP…
Кстати, такой вопрос. На сколько я понял, основные проблемы связаны с автотрекингом зависимостей. Что мешает сделать объявление зависимостей явным? По-моему, это сделает систему куда элегантнее, стабильне и проще.
mayorovp
06.05.2017 10:00Да нет никаких проблем с автотрекингом зависимостей в нормальных библиотеках, это очень простая технология.
faiwer
06.05.2017 10:39Ой да ладно, нет их, как же. Вышеописанный knockout немало крови у меня попил, когда я намешал deferred, trottle, и обычные computed в одном флаконе. Сказать что там всё расползлось, это всё равно, что ничего не сказать. Пришлось внутри computed лепить костыли, на случай если часть зависимостей, которые идут вначале тела-computed метода почему-то не обновились, а от них зависит логика вызова последующих зависимостей. И больше всего при этом убивает непредсказуемость такого поведения. Попытка отловить такой баг сродне попытке найти выход из лабиринта с завязанными глазами.
mayorovp
06.05.2017 10:52Это проблема knockout, а не автотрекинга. Как будто если бы не было автотрекинга — то все эти deferred, trottle, и обычные computed заработали бы как надо… Все эти defered и trottle зачастую используются просто чтобы остановить комбинаторный взрыв при распространении изменений, и по сути являются костылями.
В том же mobx эта проблема решена по-другому. В Mobx помимо состояний "значение актуально" и "значение устарело" есть третье состояние, "значение могло устареть" — и ни одно производное значение не начнет вычисляться пока есть шанс что у него зависимости остались прежними.
Кроме того, в mobx явно разнесли зависимые значения и реакции и они никогда не вычисляются вперемешку.
PFight77
06.05.2017 20:57Вот, вот, "комбинаторный взрыв", "значение могло устареть" и еще куча страшных слов в той статье и комментариях к ней. По-моему, задача слишком сложная, что бы ее решать вот так автоматически.
vintage
06.05.2017 21:29Задача не тривиальная, но вполне решаемая. Думаю мне удалось запилить наиболее эффективное автоматическое решение.
Veikedo
05.05.2017 09:58- Нет, не все. Как раз redux это и автоматизирует
- Это скорее ООП головного мозга. Нет, я не в обиду — я сам люблю и практикую C#. Но тут другой подход и он хорошо работает.
А если смотреть ещё глубже, то тут наоборот хорошее разделение — аналоги message bus & event sourcing в мире бэкенда. - Тут да, есть гемор
PFight77
05.05.2017 21:05Нет, не все. Как раз redux это и автоматизирует
Поправьте, если я где-то ошибусь. Согласно вот этим докам, у нас есть container-компоненты, которые реализуют функцию mapStateToProps. Эта функция, очевидно, получает на вход state и выдает props. Она вызывается при каждом изменении стейта в сторе. Соответственно, если функция выдает те же самые props, то компонент не перерисовывается (что есть обычная логика react).
Теперь, вопрос: чем все вот это отличается от тех самых кошмарных watch, в AngularJS? Есть некоторое глобальное состояние, и есть набор вотчеров, которые его смотрят. Единственное отличие — в AngularJS вотчеры могли еще сами менять state, здесь все стабилизируется за один проход. Но суть ведь та же — все компоненты проверяют стейт при каждом чихе…
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 });
vintage
05.05.2017 11:52Ваш ребус мало кто сможет понять.
RayMan
05.05.2017 16:26А можно подробнее? Что тут не понятного?
vintage
05.05.2017 19:03-1Не понятно зачем такие пляски с бубном, чтобы понять которые нужно 5 минут вникать в код.
mayorovp
05.05.2017 21:15Зачем тут 5 минут в код вникать? Сразу же видно, что SetUserAction — это объект из двух полей, type и payload, причем первое строго равно 'SET_USER', а вторая имеет тип IUser.
Это даже IDE подсказать может.
vintage
06.05.2017 00:51Чтобы понять что имел ввиду автор и что помешало ему написать просто:
export function setUser( user : IUser ) { return { type : 'SET_USER' , payload : user , } }
RayMan
06.05.2017 01:06Во первых тогда type будет иметь тайп string, а не 'SET_USER';
Во вторых константа SET_USER нужна так же и в редьюсе, или там тоже строкой писать?vintage
06.05.2017 01:23'SET_USER' as 'SET_USER'
- А почему бы и нет?
RayMan
06.05.2017 01:37+11. Ага и все сразу становится понятнее :)
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);
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>
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) });
PFight77
06.05.2017 09:12+1Вы ведь знаете, что constructor.name не поддерживается в IE и изменяется при минификации?
vintage
06.05.2017 13:34constructor.name не поддерживается в IE
Для него есть костыль:
Function.prototype.toString.call( func ).match( /^function ([a-z0-9_$]*)/ )[ 1 ]
Полученное таким образом имя, разумеется, нужно закешировать. Лучше всего в WeakMap.
svekl
А tree shaking разве нормально отработаботает, если Вы импортируете всё подряд?
justboris
Нормально отработает
svekl
Здорово, спасибо
svekl
Работает даже такое
justboris
А такое — уже нет.
В общем, надурить Rollup можно, если постараться. Но основные варианты использования он поддерживает