Comments 108
Все хорошо, но конвенция именования файла @Exports
очень странная.
Node.js и все бандлеры поддерживают index.js
файл. Когда вы укажете в импорте путь до папки, например ../Common/Utils/
, то бандлер автоматически добавить index.js
или index.ts
в конец, если такой файл в папке имеется.
И второй момент, вот такой код
import * as Common from "../Common/@CommonExports";
export { Common };
Ломает вам весь tree-shaking. Теперь все содержимое Common будет включено в бандл, неважно, используется ли оно на самом деле или нет
Хорошая мысль насчет index.ts, попробуем. Надо посмотреть, умеет ли SystemJS такое...
Обнаружил пару проблем с index.ts (собственно, почему я отказался от просто @Exports.ts и пишу @EventsExports):
- При редактировании вкладка в IDE называется index.ts — трудно понять по названию вкладки, что за файл
- При открытии через быстрый поиск нельзя найти именно этот файл, т.к. имя не уникальное (я использую FastFind)
Ну и возникает вопрос, как именовать imports и internals...
Во-первых, одинаковое имя лечится настройкой "показывать имя папки для одноименных файлов".
Во-вторых, в индексных файлах у вас содержатся только импорты/экспорты, часто в них лазить, а тем более держать несколько открытых сразу не придётся.
И в-третьих, ваш подход неэргономичен. После работы на проекте с общепринятым использованием index.js файлов приходишь в проект, где горе-архитектор не осилил настройку IDE, зато выдумал свою особую конвенцию именования, и эффективность работы только падает...
Проверил еще раз, tree-shaking все еще работает. Пример в Rollup-repl. Второй вопрос снимается, извините.
А первый вопрос про неиспользование index.js
все еще остается.
Не знаю что конкретно нового они недавно зарелизили, но я еще в прошлом году мог выбирать по Alt Enter нужный символ, и добавлялся импорт именно выбранного символа.
А так да, импорты хоть 5, хоть 10 точек, неважно. Все автоматом делается средой. Не могу представить тот гемор если бы делал бы это сам (и тот гемор на который люди себя толкают выбирая другие инструменты разработки).
Visual Studio Code – бесплатная и по импортам переходить умеет.
Пробовал VS Code, Atom, у них у всех ужасно работают TypeScript-плагины (по сравнению с VisualStudio). Может я пробовал на слишком слабом компьютере, не знаю.
Ужасно в смысле медленно? Они все используют TypeScript Language Service написанный на JS со всеми вытекающими. Большая студия я подозреваю использует свою шуструю реализацию.
А еще может быть так, что в компании уже куплена VisualStudio, которая во всем всех устраивает, в ней настроены все дев-процессы (сборка, отладка и т.д.) и покупать еще отдельно другую IDE только из-за того что она умеет дописывать import и в результате получить гемор с перенастройкой процессов и переучиванием разработчиков — да никто не будет таким заниматься...
Есть мнение, что если import может автоматически вставлять IDE, то он с тем же успехом может автоматически вставляться и сборщиком.
Я смотрел агностик модули. Может это и круто, но import это стандарт, лучше все-таки придерживаться стандартных средств. Глобальная область видимости уже была, и от нее решили отказаться по многим причинам.
Для я даже не про них, а про то, что сейчас развитие языка идёт в сторону увеличения писанины, которая могла бы быть легко автоматизирована.
Ведь можно же было посмотреть, как это сделано у других. Например, в пхп есть чудесный автолоад: http://www.php-fig.org/psr/psr-4/
Кроме того, про отказ от глобальной области видимости вы погорячились — тот же трендовый redux хранит все данные в одной глобальной области видимости. Из-за чего люди, привыкшие к импортам и возможности использовать короткие имена, огребают, когда надо соединить несколько приложений в одном: https://habrahabr.ru/company/efs/blog/328012/
Что autoloader в php, что агностик-модули, все заменяют import строгими правилами именования сущностей. То есть, конфликты имен в глобальной области видимости решаются конвенциональным путем. Проблема такого подхода очевидна — длинные имена и сложность заставить всех писать уникальные имена в правильном виде.
Про Redux… Там ведь не только конфликты имен могут быть, но совершенно неконтролируемое сцепление различных частей приложения, к тому же без поддержки соотв. тулинга. Мы (императивные программисты) инкапсулируем еще со времен модульного программирования, но функциональщики вообще не от мира сего ^_^
А про писанину, об этом как раз моя статья. Мне удалось уменьшить ее количество вплоть до одной-двух строчек import.
строгими правилами именования сущностей
И это очень хорошая практика. Порядком задалбывает разбираться как в очередном проекте решили раскидать модули по папкам. А если ещё и с алиасами, то вообще туши свет. На одном из проектов jQuery таким образом подключался аж 3 раза разных минорных версий.
длинные имена
Вы всегда можете сделать короткий локальный алиас, если имя слишком длинное.
сложность заставить всех писать уникальные имена в правильном виде
Ничего сложного. Неправильно написал — получил ошибку.
функциональщики вообще не от мира сего
Ну так, в ФП, изменяемое состояние является внешним по отношению к приложению, а значит общим для всего кода.
А я просто настроил 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';
Вебшторм же можно настроить на импорт в таком стиле, без вездесущих ../../
Мы избавились от необходимости импортировать каждый используемый модуль по отдельности, создавая огромную import-шапку в каждом файле. Вместо этого один раз импортируем нужные пакеты из imports-файла своего пакета.
Мне кажется, прямым следствием этого будет то, что теперь браузер будет грузить огромную кучу ненужного кода.
Это при условии наличия системы сборки (что по большому счёту верно на сегодняшний день). Но при нативных модулях и HTTP/2 гораздо лучше использовать явные импорты нужных вещей. Это делает зависимости более прозрачными и загрузку более точечной.
P.S. Пока нет нормальной работы с путями и перехватом путей в браузере, чтобы можно было делать алиасы, подсовывать заглушки в тестах и прочее, я пользуюсь RequireJS (и у меня нет полноценной системы сборки, LiveScript пофайлово компилируется с помощью File Watchers во время написания)
"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';
Как-то слишком мудрено, мой велосипед попроще. Ещё посмотрите на Lerna. И про абсолютные пути — настроил для WebStorm, Atom и VSCode.
Ваш велосипед суть половина моего. Ваши домены — отчасти мои пакеты, только "в одну сторону". Вы упорядочили импорт доменных компонентов (exports-файлы), но не упорядочили зависимости самих доменов (imports и internals — файлы). Lerna — абсолютные пути позволяют избавиться только от точек. Я же решаю массу других проблем.
Глобальная область видимости (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 {
// ...
}
}
Собираться может и будет, но вот SystemJS вы таким образом не настроите (чтобы грузить по одному файлу во время разработки). Мы давно используем namespace c формированием списка файлов и бандла через ASP.NET. Много боли несет в себе такая практика.
Собирать же бандл в дев-окружении, имхо, очень неудобно, т.к. приходится ждать сборки после каждой правки в коде. Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).
вот SystemJS вы таким образом не настроите
Да как-то не вижу в нём необходимости.
Много боли несет в себе такая практика.
Какой?
приходится ждать сборки после каждой правки в коде
Она быстро происходит, так как все файлы уже есть в памяти, надо только подгрузить изменившиеся, прогнать проверку типов (а её по отдельному файлу не прогнать, только по бандлу) и сконкатенировать (плёвая операция).
Я привык уже нажать Ctrl+S, и сразу видеть результат, без всяких source map (которые глючат и тормозят).
Эм… у вас браузер поддерживает TS? :-)
При сохранении срабатывает compile-on-save, который мгновенно компилит один единственный файл (ну а после докомпиливает остальное). Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.
Какой?
Боль там от управления зависимостями — приходится вручную прописывать, какой файл сначала, какой потом. Плюс при наследовании для TypeScript нужно указывать reference-тэг на файл с родительским классом.
При сохранении срабатывает compile-on-save
Не всякий раз после сохранения требуется видеть результат. Зато всякий раз, когда требуется видеть результат — требуется его наиболее актуальная версия.
мгновенно компилит один единственный файл
Для тайпчекинга всё-равно нужны все остальные файлы.
Через SystemJS скомпиленный файл тут же подгружается при F5 в браузере. Никаких бандлов, все быстро и удобно.
Всё быстро, пока число файлов и глубина зависимостей не большие.
Боль там от управления зависимостями — приходится вручную прописывать, какой файл сначала, какой потом. Плюс при наследовании для TypeScript нужно указывать reference-тэг на файл с родительским классом.
MAM разруливает это сам, не требуя от программиста лишних телодвижений — только следование соглашению об именовании. Присмотритесь к моему примеру выше.
.
Используете ли вы redux?
Если используете, то как вы типизируете ваши actions
?
Мы используем подобие такого, но не очень удобно https://rjzaworski.com/2016/08/getting-started-with-redux-and-typescript#actions
У нас пока не было необходимости шарить стейт между компонентами, там особая специфика предметной области. Но в целом, я вообще не понимаю, откуда столько хайпа вокруг Redux:
1) При малейшем изменении стейта запускаются все селекторы, обновляются все компоненты (WTF?). Отсюда все эти shouldComponentUpdate и т.д.
2) Никакой инкапсуляции — весь стейт доступен всем, причем сырые данные, без оберток.
3) TypeScript тулинг — сплошные проблемы...
Мне больше нравится подход Angular, со старыми добрыми сервисами, которые инкапсулируют внутри себя некоторую логику и стейт + предоставляют возможность подписки на изменения.
А связку react-mobx вы в таком случае не пробовали?
Несколько пугает вся эта автоматика и закадровая магия. Люди говорят, что в крупном проекте становится трудно отследить, из-за чего обновляется компонент, когда по каждому чиху начинается перерисовка несвязанных компонентов.
Хм, а whyrun не помогает?..
Я об этой проблеме прочитал в комментах к одной статье, сам не пробовал. Помогает или нет вот у Вас хотел бы спросить...
Вот в целом, какой смысл в MobX? Это все на случай если влом объявить событие и тригернуть его при смене свойств? То есть, просто синтаксический сахар, чтобы при объявлении свойства сразу объявлять и событие об его изменении? Если дело только в этом, то я за пол часа напишу свой декоратор, который это автоматизирует.
Посмотрел вот еще доки, оказывается @observable
нельзя объявлять для свойств с сеттерами. То есть, я буду вынужден открывать в сервисе свои сырые данные, без возможности их инкапсулировать…
Нет, вам не надо открывать свои сырые данные. Декоратор @observable
вообще-то можно и на приватные свойства повешать.
А на свойства с одним только геттером полагается либо вешать @computed
— либо оставлять так если оно слишком простое.
Смысл MobX — в автоматическом определении зависимостей. MobX как раз решает ту самую проблему с постоянными проверками всего подряд.
Сам я еще MobX не пробовал, сижу пока на knockout, у которого та же идея — но не такая удобная реализация. Будет новый проект — попробую MobX.
Он слишком многословен.
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
}
}
Вы лучше покажите что дальше с этим делать.
Я показал, что многословность приблизительно одинаковая. Что с этой информацией делать — это уж вы решайте сами :-)
Вы показали что многословность приблизительно одинаковая в этом случае.
Ну, предложите другой случай, посмотрим.
Ваш $mol просто ужасен в части вывода.
Вывода чего? Вы можете хоть как-то аргументировать, а не просто бросаться оценочными суждениями?
Вывода информации. Генерирования html. Показа всей этой крутой реактивной модели пользователю.
$mol_mem ничего такого не умеет. Это чистая реализация ОРП, которую можно взять и использовать в любом проекте. Пару лет назад, я, например, использовал его с Ангуляром.
А вот $mol_view — это отдельная библиотека, использующая возможности ОРП по максимуму для построения интерфейса. Если максимум вам не нужен — можете использовать хоть Реакт, хоть Хэндлбарс.
Ну, и раз уж, речь зашла про $mol_view, то не поясните, что там такого "ужасного"? Только объективно, а не "непривычно и лень вникать".
А у вас есть готовые решения для подключения $mol_mem к React?
А там нужны какие-то решения?
Заворачиваем рендеринг реакта в атом:
const ui = new $mol_atom( 'render' , ()=> React.render( <UI/> , document.body ) )
ui.actualize()
Добавляем декоратор перед render, чтобы результат кешировался:
@ $mol_mem()
render() {
...
}
Всё, теперь можем обращаться к любым реактивным переменным.
Вы предлагаете рендерить каждый раз дерево целиком? Мда...
Это не я, это Реакт так работает :-)
https://facebook.github.io/react/docs/rendering-elements.html#updating-the-rendered-element
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/
Ну так эти statefull components вызывают ReactDOM.render под капотом. А ссылки к чему? shouldComponentUpdate при использовании $mol_mem не нужен.
Суть в том, что при обновлении только одного компонента все дерево целиком рендериться не должно. А вы предлагаете рендерить.
Реакт так работает, что он на любой чих создаёт новое виртуальное дерево целиком. Потом смотрит разницу с предыдущим виртуальным деревом и применяет её к реальному дереву.
Если бы это было так — то все оптимизации рендеринга не давали бы никакого прироста.
Всё "оптимизации" Реакта сводятся к тому, чтобы render выдавал одно и то же значение, если то, от чего он реально зависит, не изменилось. Именно это и делает $mol_mem.
А как ваш $mol_mem учитывает изменения в props?
Никак, в пропсы надо передавать не сами значения, а функции получения/изменения значения. Тогда 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() } />
}
}
Выглядит страшно.
Зато позволяет такие штуки делать:
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() } />
}
}```
Спасибо, теперь я вполне понял, почему не буду использовать все это в продакшене: ) Куча проблем и возможностей заглючить и убить производительность своего приложения, сделав в добавок практически невозможной отладку.
Event-Driven архитектура очень хрупкая, потому что приходится вручную следить за своевременными подписками и отписками, а человек — существо ленивое и не сильно внимательное.
И все-таки дело в лени: ) Имхо, лучше уж хрупкость event-driven, чем хрупкость и закадровая магия-автоматика FRP…
Кстати, такой вопрос. На сколько я понял, основные проблемы связаны с автотрекингом зависимостей. Что мешает сделать объявление зависимостей явным? По-моему, это сделает систему куда элегантнее, стабильне и проще.
Да нет никаких проблем с автотрекингом зависимостей в нормальных библиотеках, это очень простая технология.
Ой да ладно, нет их, как же. Вышеописанный knockout немало крови у меня попил, когда я намешал deferred, trottle, и обычные computed в одном флаконе. Сказать что там всё расползлось, это всё равно, что ничего не сказать. Пришлось внутри computed лепить костыли, на случай если часть зависимостей, которые идут вначале тела-computed метода почему-то не обновились, а от них зависит логика вызова последующих зависимостей. И больше всего при этом убивает непредсказуемость такого поведения. Попытка отловить такой баг сродне попытке найти выход из лабиринта с завязанными глазами.
Это проблема knockout, а не автотрекинга. Как будто если бы не было автотрекинга — то все эти deferred, trottle, и обычные computed заработали бы как надо… Все эти defered и trottle зачастую используются просто чтобы остановить комбинаторный взрыв при распространении изменений, и по сути являются костылями.
В том же mobx эта проблема решена по-другому. В Mobx помимо состояний "значение актуально" и "значение устарело" есть третье состояние, "значение могло устареть" — и ни одно производное значение не начнет вычисляться пока есть шанс что у него зависимости остались прежними.
Кроме того, в mobx явно разнесли зависимые значения и реакции и они никогда не вычисляются вперемешку.
- Нет, не все. Как раз redux это и автоматизирует
- Это скорее ООП головного мозга. Нет, я не в обиду — я сам люблю и практикую C#. Но тут другой подход и он хорошо работает.
А если смотреть ещё глубже, то тут наоборот хорошее разделение — аналоги message bus & event sourcing в мире бэкенда. - Тут да, есть гемор
Нет, не все. Как раз redux это и автоматизирует
Поправьте, если я где-то ошибусь. Согласно вот этим докам, у нас есть container-компоненты, которые реализуют функцию mapStateToProps. Эта функция, очевидно, получает на вход state и выдает props. Она вызывается при каждом изменении стейта в сторе. Соответственно, если функция выдает те же самые props, то компонент не перерисовывается (что есть обычная логика react).
Теперь, вопрос: чем все вот это отличается от тех самых кошмарных watch, в AngularJS? Есть некоторое глобальное состояние, и есть набор вотчеров, которые его смотрят. Единственное отличие — в AngularJS вотчеры могли еще сами менять state, здесь все стабилизируется за один проход. Но суть ведь та же — все компоненты проверяют стейт при каждом чихе…
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
});
Ваш ребус мало кто сможет понять.
Не понятно зачем такие пляски с бубном, чтобы понять которые нужно 5 минут вникать в код.
Зачем тут 5 минут в код вникать? Сразу же видно, что SetUserAction — это объект из двух полей, type и payload, причем первое строго равно 'SET_USER', а вторая имеет тип IUser.
Это даже IDE подсказать может.
Чтобы понять что имел ввиду автор и что помешало ему написать просто:
export function setUser( user : IUser ) {
return {
type : 'SET_USER' ,
payload : user ,
}
}
Во вторых константа SET_USER нужна так же и в редьюсе, или там тоже строкой писать?
'SET_USER' as 'SET_USER'
- А почему бы и нет?
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);
В том, что дублирования много — надо объявить тип, надо указать тип в 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)
});
Вы ведь знаете, что constructor.name не поддерживается в IE и изменяется при минификации?
и изменяется при минификации?
это отключаемая опция
constructor.name не поддерживается в IE
Для него есть костыль:
Function.prototype.toString.call( func ).match( /^function ([a-z0-9_$]*)/ )[ 1 ]
Полученное таким образом имя, разумеется, нужно закешировать. Лучше всего в WeakMap.
Как уже написали — uglify настроили, ie заполифили
Ужасный import кракен — как использовать ES6-модули и не сойти с ума