Если вы как я долгое время считали, что JavaScript – это такой «игрушечный» язык на котором пишут анимашки для менюшек и падающие снежинки на форумах под новый год, а потом очнулись в 2016 году с мыслями WTF: react, flux redux, webpack, babel,… не отчаивайтесь. Вы не одиноки. Материалов по современному фронтенду в сети много, даже слишком много. Под катом еще одно альтернативное мнение о том, каково это учить JavaScript в 2016 году.
Итак, нам потребуются: React, React Dev Tools, React-Hot-Loader, React-Router, Thunk, Redux, Redux Dev Tools, Semantic-UI, Webpack, Babel и npm.
На первый взгляд много. Сравним с бекендом: MVC-фреймворк, ORM, Data Mapper, IOC-контейнер, логер, профайлер, очереди, управление конфигурациями, сборка и выкладка… Список можно продолжить, но думаю идея понятна и так: с ростом сложности решаемых задач растет и сложность инструментов. Все чаще мы употребляем термин Web App вместо Web Site, акцентируя внимание на богатых возможностях современных веб-приложений.
Почему именно этот стек?
Если вдуматься и отбросить все лишнее, то единственный ресурс постиндустриальной эпохи, через который можно выразить все остальные – это время. Освоение каждой новой технологии требует затрат времени, а значит более перспективны инвестиции в технологии, которые не устареют в ближайшие пару месяцев. Дополнительный плюс получают технологии с пологой кривой обучения.
React + Redux VS Angular VS Yet Another JS Framework
Только ленивый не сравнил Angular с React’ом (приписав при этом, дескать, сравнение не корректно, Angular – фреймворк, React – библиотека). Пойдем от обратного. Почему бы не выбрать что-нибудь эдакое, типа Vue, Ember или, упаси боже, Elm?
- Поддержка крупных вендоров
- Размер сообщества
Благодаря этим факторам, вероятность выживания Angular и React выше. Простите, другие замечательные хипстерские решения, нам не по пути. Итак, почему React? Лично для меня выбор был не простым:
- Меня пугали jsx-файлы
- Я кое-что умел на Angular 1.x и переходить на другую технологию было психологически не комфортно
- ng2 по-умолчанию идет в комплекте с TypeScript, что мне как стороннику статической типизации ближе
- ng подкупал подходом «работает из коробки». Копаться в многообразии npm-пакетов решительно не хотелось.
Короче, я заставил себя изучить все статьи-сравнилки и написать Todo App на React, что склонило чашу весов в противоположную сторону. Ключевыми факторами для меня стали:
- HTML-шаблоны Angular — ужасны и их синтаксис меняется от версии к версии. В React шаблон – это JavaScript, потому что компонент – не более чем View. Сообщения об ошибках в React лучше.
- Как ни странно, TypeScript. При более детальном изучении оказалось, что не все так прекрасно. Во-первых TypeScript – это не полноценный язык со статической типизацией, а транспилер. Это сильно ограничивает возможности использования шаблонов и мета-программирования. Во-вторых, далеко не все npm-пакеты идут в комплекте с d.ts-файлами. Короче, Flow показался проще в прикручивании. В-третьих, у TypeScript есть как ярые фанаты, так и противники. Если фанаты TS сравнительно лояльны к ES6, то обратное – не верно. ES6 получает дополнительное очко к Bus Factor.
Если вам нравится TypeScript, ничто не мешает использовать его вместе с React. Просто конкретно мне он пока не дал критического объема преимуществ, чтобы заставить тратить время на еще один элемент в стеке.
- Доклад Дэна Абрамова про «путешествия во времени». Если ваш опыт в бекенде похож на мой, то вы без труда увидите, что новомодный flux – это CQRS и Event Sourcing «вид в профиль». Вместо проекций – редюсеры, а вместо команд и доменных событий – экшны. А если вы работали, например, с WPF, то разобраться с React – вообще дело пары вечеров.
Да, Redux можно использовать и с Angular, он никак не привязан к React, но для React уже есть react-redux и react-hot-loader. Наверное, для Angular тоже есть, но мейнтейнер Redux’а явно на стороне React.
Для React и Redux доступно два расширения Chrome. Рекомендую поставить оба, чтобы сделать отладку приятной.
Таким образом, связка React + Redux:
- Более-менее проста в изучении, потому что в основе лежит простая идея
ui = f(state => props)
, где f — это реакт-компонент, state — redux, аstate => props
— это react-redux. - Не тащит за собой дополнительных зависимостей
- Обладает лучшим на данный момент Tool Support (IDE и плагины для Chrome)
Есть еще всякие ништяки, вроде React Native, но я им не пользовался, поэтому поделиться на эту тему мне, к сожалению, нечем.
А flux и все эти модные словечки. Как это работает?
Возможно, для фронтенда flux — это некое откровение. Для бекендщика разница между CQRS и flux — не велика. React — это наше представление. Оно может зависеть от props (read-only) или state (mutable). В зависимости от state и props компонент может отображаться по-разному. Эта логика содержится в методе
render
. Компонент сам себя не перерисовывает. Вместо этого, он полагается на экосистему React. Мы можем либо изменить свое состояние (с помощью метода setState
), либо быть перерисованными извне (переданы новые props). Обработчики событий для UI-элементов передаются через props. Получается такой код<button onClick={this.props.handleClick} />
.Состояние приложение хранится в Redux и представляет собой json-объект. Redux следит за изменение объекта. Изменением считается изменение ссылки, поэтому вместо изменения текущего состояния необходимо конструировать новое путем копирования и модификации старого. Проще всего это сделать через spread-оператор:
const newState = {...prevState, somethingNew: 'blah-blah'}
Пакет react-redux осуществляет односторонний байндинг
redux state => react component
с помощью метода connect. При изменении состояния Redux сам перерисует необходимые компоненты, передав в props dispatch и часть общего state, хранимого в Redux. Какую часть состояния и какие функции на основании store.dispatch
передавать — решать вам. Я рекомендую передать все обработчики событий компонента и не «светить» dispatch в компонентах.State содержится в Redux, но и у компонентов есть свой state. Какой из них использовать?
Разработчик Redux предлагает делать как удобнее. Это не совсем формальный совет. У нас сложилась практика использовать state компонента только для форм или в целях оптимизации.
JSX
JSX – это не JavaScript. Да, React можно писать без JSX, но проще тогда без React’а. Вообще ситуация с HTML-шаблонами напоминает мне засилье шаблонизаторов для PHP лет десять назад. Самым монструозным из всех был конечно Smarty. Мне казалось, что люди сошли с ума. Как иначе можно было объяснить желание написать шаблонизатор… для шаблонизатора?
JSX – простой и понятный способ использовать JavaScript в шаблонах. Вам не нужно учить дополнительный ЯП, просто оберните теги в
(скобки)
, а код внутри тегов {в другие скобки}
. И все. Да, так просто. Если вас беспокоит разделение логики и представления, перестаньте беспокоиться прямо сейчас. React — это View-библиотека. За состояние приложения (в т.ч. поведение) отвечает Redux. React может «диспатчить» сообщения в store, а Redux будет их обрабатывать либо через редьюсеры (чистые функции), либо через специальные middleware (побочные эффекты).React позволяет создавать functional stateless-компоненты:
const StatelessComponent = props => (<div>Hello, {props.name}!</div>)
Или компоненты-классы:
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Функциональная запись короче и лучше читается. Но я рекомендую не увлекаться экономией строк и вместо:
export default props => (<div>{props.title}</div>)
использовать чуть менее лаконичное, но более безопасное:
const StatelessComponent = props => (<div>{props.title}</div>)
StatelessComponent.propTypes = {…}
export default StatelessComponent
Во-первых, если вы не передадите параметры, то React недвусмысленно намекнет в консоли, что вы не правы. Во-вторых, WebStorm умеет анализировать
PropTypes
и при авто-дополнении заботливо вставит все required props.Babel
Если вы не поняли на каком языке примеры кода выше, не расстраивайтесь. Это не JavaScript, ну не совсем JavaScript. Это ES6 + JSX. С JSX мы разобрались в параграфе выше – это просто синтаксический сахар для шаблонизации (почти как
<?=$var?>
в PHP или @Model.Param
в Razor). С ES6 ситуация чуть сложнее и запутаннее. Если коротко:
- JavaScript собрали на коленке под нужны тогдашнего интернета, который представлял собой действительно в основном гипер-текст.
- Прошло некоторое время и в сайты начали пихать все что угодно, кроме текста.
- Язык в существующем виде перестал удовлетворять нуждам рынка. Консорциум ECMA стал придумывать всякие новые фичи и стандарты языка, да с такой скоростью, что браузеры не успели все внедрить.
- В сообществе психанули и написали Babel – транспилер из JavaScript в… JavaScript. Ну в смысле из «нового» JS в «старый», который браузеры поддерживают.
Babel умеет транспилить не только JS, но и JSX, что позволяет писать React-приложения на ES6.
Да, есть React.createElement
. Можно писать на React-приложения и на ES5, но зачем?
Стоит отметить, что ES6 – это не истина в последней инстанции. Некоторые фичи до сих пор являются экспериментальными (например, генераторы) и для их использования потребуются полифиллы (библиотеки, реализующие экспериментальные фичи стандарта). Частично из-за этого мы решили отказаться от redux-saga в пользу redux-thunk, хотя и идея диспатчить функции до сих пор не кажется мне изящной (она просто работает).
Webpack
Так, то есть пишем мы на ES6 + JSX, а в бразуере выполняется минифицированный JS. Все это напоминает историю изобретения высокоуровневых ЯП. Люди могли писать более эффективные программы на ассемблере, но предпочли удобство и продуктивность. Раз есть исходники и компилятор (транспилятор в нашем случае), то потребуется и система сборки. Если в вашей пещере было достаточно тепло и уютно, возможно, названия grunt и gulp вам ничего не говорят. Что к лучшему. На данный момент, можно считать (слава богу), что для JS есть только один сборщик – Webpack — оставивший конкурентов позади. Можно считать, что webpack — аналог maven или msbuild (кому что ближе) в мире фронтэнда. Не смотря на то что, конфиги webpack’а на первый взгляд напоминают некромантские свитки, через какое-то время привыкаешь. Наверное, каждый любитель фронтенда должен хотя-бы раз в своей жизни написать tutorial по настройке webpack, также как каждый фанат ФП – tutorial по монадам.
Что нужно знать про webpack:
- Вам потребуется кто-то, кто умеет его настраивать
- npm start — для запуска dev-сервера
- npm run build для сборки фронта на продакшн
Вообще Webpack собирает не только JS, но еще и sass, svg, шрифты и вообще все что душе угодно, но я пока еще не готов написать полноценный туториал, так что поищите на просторах интернета.
Npm
У Ruby есть gem’ы, у php – composer, у .NET – nuget. Короче, JavaScript тоже потребовался пакетный менеджер. Изначально npm использовался в nodejs-разработке (отсюда и название — node package manager), а для фронта использовался bower. Потребность в последнем как-то отпала сама собой с повсеместным переходом использованием ES6, Webpack и TypeScript. Этот параграф добавлен лишь для того чтобы отметить, что npm использует файл package.json, внутри которого можно написать:
"scripts": {
"build": "webpack --config webpack/config.js -p",
"start": "webpack-dev-server --config webpack/config.js"
}
Без этих строк
npm run build
и npm start
не заработают. Было бы логичнее вызывать
npm build
, а не npm run build
, но эта команда зарезервирована для внутренних целей npm, так что ничего не выйдет.React-Hot-Loader
Ключ
--hot
запускает dev-сервер webpack’а с «горячей заменой». Согласитесь, билдить при каждом изменении – довольно уныло. HMR (hot moudle replacement) делает это за вас. react-hot-loader позволяет избежать при этом перезагрузки страницы и потери текущего состояния. Настройка hot-loader’а правда, довольно тонкая работа и вообще фича — экспериментальная и работает не всегда. Особенно сложные отношения у горячей замены с react-router. Но к хорошему быстро привыкаешь и рано или поздно вам захочется написать if(module.hot)
для того, чтобы страница не перезагружалась. React-Router и Thunk
Основная ниша React’а в Web – это конечно SPA-сайты. А какой SPA-сайт без навигации и общения с сервером. Первую задачу решает react-router. Здесь альтернатив нет. Из неприятных сюрпризов:
- Четвертая версия не совместима с третьей из-за чего у нее проблемы с пакетом history.
- Для hot-reload нужно соблюдать некоторые нюансы, иначе в консоли будут появляется предупреждения о том, что нельзя заменить route. К счастью ошибки достаточно информативные и исправить их просто.
RouterMiddleware
может сломать Redux плагин при совместном использовании.- Не очевидно, что вместо тегов
<Route />
можно использовать JavaScript-объекты и передать их в компонент роутера<Router routes={routes} />
. Это бывает полезно, при разработке модульных приложений, когда структура маршрутов не известна заранее.
Thunk – это middleware для redux, позволяющее диспатчить функции вместо объектов. Чаще всего используется для запросов к серверу. В компоненте делать запросы к серверу – не комильфо. Редьюсеры – вообще дожны быть чистыми функциями. Ничего не остается, как делегировать это middleware.
Альтернатива thunk – redux-saga. Сага предоставляет больше возможностей, но у нее довольно крутая кривая обучения, и она тащит за собой полифиллы для генераторов. При правильном проектировании логики в компонентах нет, а connect к стейту Redux в основном производится через фабричный метод. В общем, мне показалось, что нет большой разницы, как именно управление перейдет к
fetch
и будет ли написано yield
или then
. На сайте Redux пример с thunk, так что по совокупности причин сагу я отложил до лучших времен.Semantic-UI
Раз мы заговорили про SPA-приложения, то кроме навигации и запросов к серверу нужны еще компоненты, которые будут ту самую серверную информацию отображать. Для React есть обвязки Bootstrap, Material UI, Syncfusion Web Essentials (хотя эти обвязки не честные – там внутри jQuery). Наш выбор остановился на Sematic-UI. Решение удалось принять очень быстро – сначала отмели платные компоненты. Material UI не стали использовать из-за обилия анимации (сложнее модифицировать). Остались Bootstrap и Semantic. На Бутстрапе уже пол интернета сделано и в целом, Семантик показался более визуально-привлекательным. В общем, остановились на нем. Сразу оговорюсь, что использование Semantic UI – строго опционально, потому что минифицированная версия весит около 500кб.
Так что разрабатывать фронтенд в 2016 году вполне себе комфортно. Да инструментов много, многие библиотеки не совместимы, новые версии выходят очень часто. Это разумная плата за гигантский скачок в качестве фронтенд-стека.
Комментарии (181)
Fedcomp
11.04.2017 08:31> Первую задачу решает react-router. Здесь альтернатив нет
react-router-redux?marshinov
11.04.2017 08:49+2Разве эта библиотека — не построена поверх react-router? Мне казалось, что она просто дополнительно пропускает переходы через store, чтобы сохранить time travel. Нет?
VasilioRuzanni
11.04.2017 11:17+1Ну она синхронизирует текущую позицию в store и обратно — полезно для Time Travel, доступа к данным метонахождения через Redux, что позволяет компоненту в ряде случаев не зависеть от роутера. Но это совершенно точно не «альтернатива» :)
(Более чем) реальная альтернатива react-router-у — junctions.js.
redfs
11.04.2017 08:39-1Как короткое описание своего технологического стека c возможностью изучать тему «вглубь» по ссылкам — статья понравилась.
Немного критики. К сожалению, ряд моментов режет глаз.
Сравним с бекендом: MVC-фреймворк, ORM, Data Mapper, IOC-контейнер...
Почему MVC? Почему фреймворк? Почему ORM? И т.д. Это вовсе не необходимые компоненты бэкенда. Я бы назвал их «модными», но бэкенд может быть построен на совершенно других принципах. Кмк, такое сравнение тут не совсем к месту.
Самым монструозным из всех был конечно Smarty. Мне казалось, что люди сошли с ума. Как иначе можно было объяснить желание написать шаблонизатор… для шаблонизатора Perl?
Конечно же автор имеет в виду php, а не perl. Фраза о «шаблонизаторе для шаблонизатора» тоже модная, но слишком часто используется не в тему.Ogra
11.04.2017 08:48+2Автор действительно имеет в виду Perl — первая версия PHP была написана на Perl и под его сильным влиянием. $ перед каждой переменной? Это из Perl!
redfs
11.04.2017 09:02первая версия PHP была написана на Perl
Это шутка что ли или я туплю? Насколько мне помнится, php/fi сразу был написан на C.
Исправление… Понял о чем вы, о первом наборе скриптов на perl.
marshinov
11.04.2017 08:53Почему MVC? Почему фреймворк? Почему ORM? И т.д. Это вовсе не необходимые компоненты бэкенда. Я бы назвал их «модными», но бэкенд может быть построен на совершенно других принципах. Кмк, такое сравнение тут не совсем к месту.
Потому что примерно 90% кода из мира веб-разработки, который я видел или поддерживал — это MVC-фрейворк + ORM и не более. На PHP — это чаще всего Active Record, на Java — Hibernate, на .NET — Entity Framework. Да, бывают другие стеки. Но мейнстрим — именно ORM + MVC = love
VolCh
11.04.2017 09:13Как по мне, то для разработчика MV* ООП веб-бэкендов, не стоит начинать свой путь во фронте с React+Redux, достаточно одного React, или React+MobX, если практикуешь DDD. Redux — это полное разделение состояния и поведения, а не их инкапсуляция в объектах.
marshinov
11.04.2017 09:28+1Action'ы redux'а — это же не доменные объекты, а скорее просто message. Что мешает ловить экшны, создающие эффекты в middleware, обрабатывать в любом удобном виде, в т.ч. с применением DDD и по завершению операции выбрасывать
DomainEventSucceeded / Failed
и уже их обрабатывать в редюсерах?VolCh
11.04.2017 09:50Доменные объекты инкапсулируют данные и поведение. Redux же их разделят по крайней мере на уровне рекомендуемых практик. Может и можно технически хранить в store полноценные объекты с методами, реализующими доменное поведение, а потом дергать их в редюсерах, но точно это будет шоком для подавляющего большинства активно использующих Redux. А скорее всего эта возможность либо просто заблокирована (может только в дев-режиме), либо ожидаемой реакции на вызов методов объекта не будет, поскольку redux+react не заметят, что изменилось состояние объекта в редьюсере, поскольку ожидает либо возврат того же объекта без изменений, либо нового объекта. В общем не прокатит что-то вроде (state, value) => state.domainObject.setValue(value).
Можно отдельно вынести мутабельную модель и проецировать её состояние на redux-стор в миддлварах, но что нам это даст, кроме "потому что можем"?
VasilioRuzanni
11.04.2017 11:41+1А кто мешает применять DDD? Просто данные будут в сторе, а логика — отдельно — в функциях, сагах, и всяких таких штуках. Разделение ответственности же, до сих пор же идут споры о «правильном DDD» — никто не говорит, что для этого нужен один мутабельный объект модели для каждой сущности с методами поведения. Концептуально DDD никто не мешает использовать.
P.S. На практике немного сложнее, в сторе обычно находятся не только данные «domain model», а куча разного рода другого состояния приложения.VolCh
11.04.2017 12:39По-моему сложно будет говорить о выделенном слое бизнес-логики, всё будет в одной куче и бизнес, и инфраструктура, и UI.
VasilioRuzanni
11.04.2017 12:50+1Ну, этот паттерн, который называют то «saga», то «process manager» — как раз об этом. Он описывает чистую логику, результатом которой являются те или иные «action»-ы. Тем самым хранилище состояния (Redux) только тупо хранит состояния и меняет его с помощью редьюсеров, а сама логика обрабатывается специализированными функциями.
Тут не нужно придумывать отдельный паттерн для «бизнес-логики», чтобы как-то технически отделить ее от логики приложения. Просто не надо в кучу мешать (разные «саги», разные ключи в хранилище стейта, все такое).
mkusher
11.04.2017 23:52state у редакса это готовое отображение мира для UI. О том, что состояние надо поменять как раз и говорят доменные объекты, которые умеют делать dispatch. Это в общем-то типичный CQRS+ES
vintage
12.04.2017 07:34+1DOM — это готовое отоброажение мира для UI. А state у вас — это промежуточная модель.
mkusher
12.04.2017 14:10тогда замените UI в моем сообщении на DOM
vintage
12.04.2017 19:05А зачем? Может лучше state заменить на DOM?
mkusher
12.04.2017 19:40у redux'a нет DOM, либо я вас не понял
vintage
12.04.2017 20:51Вот именно. Зачем дому редукс?
VolCh
13.04.2017 09:04Он (или любая другая система хранения и управления состоянием приложения) нужен реакту, поскольку имеющаяся у него довольно примитивна.
vintage
13.04.2017 09:11А зачем дому реакт? :-D
VolCh
13.04.2017 09:42Дому реакт не нужен, реакт (или что-то подобное) нужны разработчику, чтобы управлять домом. Можно и другими средствами, но лично мне реакт кажется лучшим на сегодняшний день по балансу плюсов и минусов.
vintage
13.04.2017 10:00Это какие плюсы и минусы вы имеете ввиду?
VolCh
13.04.2017 18:12+2Плюсы, навскидку:
- декларативное описание целевого состояния DOM без учёта текущего на языке очень близком к HTML с возможностью использовать все средства JS
- средства автоматического обновления DOM при изменении состояния
- компонентная модель с высоким уровне изоляции
- исключительно односторонний биндинг
- довольно заметные оптимизации ререндеринга DOM
- минимум три способа создавать компоненты (обычные функции, наследники Component, наследники PureComponent)
Минусы: - нестандартный синтаксис, требующий компиляции в JS и изучения
- невозможность создавать компоненты, представимые в DOM типами, отличными от элементов (прежде всего напрягает отсутствие возможности представить компонент списком элементов)
- местами многословный синтаксис
- неопределенность в плане лучших практик
- доминирование отдельных не самых лучших в общем случае решений
vintage
13.04.2017 21:01декларативное описание… все средства JS
Что-то тут не так.
исключительно односторонний биндинг
Что плохого в двустороннем?
минимум три способа создавать компоненты
То есть чем больше способов создавать компоненты — тем лучше?
VolCh
14.04.2017 13:08Что-то тут не так.
Всё так. Интерфейс сугубо декларативный, а логика вполне может быть императивной. Собственно все (ну или почти все, всякие ПЛИС не рассматриваем) так называемые декларативные средства современного ИТ сводятся к императивным командам тьюринг-подобных процессоров.
Что плохого в двустороннем?
Конфликты и циклы. Да даже без них — усложнение логики в виду отсутствия единого источника правды.
То есть чем больше способов создавать компоненты — тем лучше?
В разумных пределах. Для разных ситуаций редко подходит один универсальный способ, а даже если подходит, то вносит лишний оверхид.
vintage
14.04.2017 13:26Всё так. Интерфейс сугубо декларативный, а логика вполне может быть императивной.
setState — какой-то совсем не декларативный интерфейс.
Конфликты и циклы. Да даже без них — усложнение логики в виду отсутствия единого источника правды.
Это всё особенности Ангуляра к двусторонним биндингам, в общем случае, отношения не имеющие.
Для разных ситуаций редко подходит один универсальный способ, а даже если подходит, то вносит лишний оверхид.
Например? Что за условия могут потребовать создавать компоненты по разному?
raveclassic
14.04.2017 13:32+1setState — какой-то совсем не декларативный интерфейс.
Ну вот зачем придираетесь? Под «всеми средствами» JS, очевидно, имелся полный доступ к отрисовываемым объектам, фильры, сортировки, мерджи и т.п. при непосредственном рендеринге.
Это всё особенности Ангуляра к двусторонним биндингам, в общем случае, отношения не имеющие.
WPF и компания, видимо, в общем случае тоже к двусторонним бингингам отношения не имеют? Там это давно наболевшая проблема.
Например? Что за условия могут потребовать создавать компоненты по разному?
stateless vs statefulvintage
14.04.2017 15:43Под «всеми средствами» JS, очевидно, имелся полный доступ к отрисовываемым объектам, фильры, сортировки, мерджи и т.п. при непосредственном рендеринге.
Например, взять и добавить атрибут в уже отрисованный дом?
Там это давно наболевшая проблема.
В $mol такой проблемы нет, а двусторонние биндинги есть. Что я делаю не так?
stateless vs stateful
И зачем их по разному создавать?
VolCh
14.04.2017 14:40setState — какой-то совсем не декларативный интерфейс.
Он не относится к описанию целевого состояния DOM. Целевой DOM является чистой функцией от props и state.
Это всё особенности Ангуляра к двусторонним биндингам, в общем случае, отношения не имеющие.
Это общая проблема двусторонних биндингов. В Excel/Calc попробуйте двумя ячейкам друг на друга сослаться. А лучше через десяток промежуточных.
Например? Что за условия могут потребовать создавать компоненты по разному?
- stateless компонент (чистая функция от свойств)
- stateful компонент с "нативным" setState
- stateful компонент со сторонним хранением и управлением состояния (redux, mobx, rxjs и т. п.)
vintage
14.04.2017 15:53-1Он не относится к описанию целевого состояния DOM. Целевой DOM является чистой функцией от props и state.
Зачем тут чистота, если достаточно идемпотентности?
Это общая проблема двусторонних биндингов.
Нет, это проблема кривой их реализации. В нормальной реализации таких проблем нет. Типичная ошибка при их реализации — делать двусторонний биндинг через пару противоположных реактивных зависимости. Не надо так.
В Excel/Calc попробуйте двумя ячейкам друг на друга сослаться. А лучше через десяток промежуточных.
Эксель ругается на циклические зависимости.
stateless компонент (чистая функция от свойств)
stateful компонент с "нативным" setState
stateful компонент со сторонним хранением и управлением состояния (redux, mobx, rxjs и т. п.)И зачем тут 3 разных способа создания компонента?
VolCh
15.04.2017 08:45Чистота чище :)
Именно, ругается, но позволяет их создавать.
Чтобы уменьшить накладные расходы. Не нужно состояние — не нужно его и вводить в компонент. Нужная частичная поддержка состояния — минимальное количество поддерживающего кода, полное — максимальное.
vintage
15.04.2017 09:10+1Чистота приводит лишь к ребусам в коде вида "чтобы измеить значение переменной в сторе нужно вызвать левую функцию и передать ей функцию, которая принимает стор, а возвращает копию стора с другим значением этой переменной".
Не позволяет. Формулы с циклическими зависимостями не работоают.
Ну так не определяйте состояние, если оно вам не нужно. Зачем несколько разных способов объявлять компонент?
VolCh
15.04.2017 13:49Тут я не о сторах, а о целевом состоянии DOM — оно чистая функция от состояния.
Позволяет. Не работает, но позволяет, а ошибка в рантайме выводится.
Не определять состояние в компоненте не значит что для него не будет создана инфраструктура его хранения и управления. Она будет создана просто будет простаивать. А хочется гибкости, чтобы ресурсы тратились сообразно реальному использованию, а не всем гипотетическим сценариям.
vintage
15.04.2017 16:18+1Я тоже не о сторах, а о чистоте. Погоня за чистотой порождает чудовищные решения в духе "формируем новое состояние всего дома вместо обновления пары свойств у конкретных элементов".
Позволяет ввести любую формулу. Но биндинга никакого по ней не создаёт.
Что за "инфраструктура"? Объект со ссылкой на прототип? Ну офигеть инфраструктура.
VolCh
17.04.2017 12:07Чем они чудовищные? Очень легко поддерживаются. И позволяют сделать реальное обновление парой свойств.
Формулу сохраняет — значит биндинг создаётся. Просто он не работает. Хорошо, что ошибку выводит, а не в бесконечную рекурсию уходит.
Объект с ненужными свойствами. Ну и да, разрешение методов происходит, что небесплатно.
vintage
17.04.2017 17:04+1Например, такие: https://habrahabr.ru/post/326484/#comment_10174502
Формула — это просто текст. Она сохраняется, если соответствует формальной грамматике выражений. Следующий шаг — вычисление и трекинг зависимостей — уже не проходит и падает с ошибкой.
Любое замыкание — это объект, хранящий ссылку на функцию и контекст её создания, хранящий "ненужные переменные". Проще говоря — разницы нет от слова "вообще".
VolCh
12.04.2017 09:52+1Что за доменные объекты, которые знаю о dispatch в частности и о нуждах UI вообще?
mkusher
12.04.2017 14:09про нужды UI знает reducer(в терминах cqrs+es это был бы проектор), доменный объект знает только о том, что что-то произошло
VolCh
12.04.2017 14:31Объект знает, что с ним произошло и даже может сообщить кому-то если спросят. Как об этом должен узнать redux, чтобы вычитать изменения в графе домена, создать соответствующие action и диспетчеризировать их?
VasilioRuzanni
12.04.2017 14:43Redux вообще не должен ничего «знать, чтобы диспетчеризовать». Его задача — только хранить состояние. У вас должен быть отдельный кусок программы, ответственный за перехват тех или иных action'ов и за то, чтобы на это как-то отреагировать (в том числе взять текущий стейт, проанализировать его и, вероятно, запустить какие то экшены).
Ну нужно пытаться вешать на Redux то, что выходит за рамки его ответственности.VolCh
12.04.2017 14:58Его задача не только хранение состояния (обычный JS-объект с этим справится), но и управление им.
VasilioRuzanni
12.04.2017 15:03Ну, скажем так, его задача наметить паттерн для этого (dispatch, actions, middlewares, reducers), но не обязательно предлагать непосредственный механизм для этого (вместо него — предлагается подключать что-то свое как middleware).
UPDATE: Не то, чтобы это хорошо или плохо — с одной стороны, это дает больше гибкости, но с другой — все делают кто во что горазд :) Особенно когда сложность приложения выходит за пределы разумности применения простых вещей вроде redux-thunk (который, вроде как стартовая точка и он же — первый middleware, с которым все сталкиваются).
andreymal
11.04.2017 11:04+6Для себя как бэкенд-разработчика я не увидел ответа на один очень важный вопрос: зачем это всё? Почему вдруг стало нельзя просто рисовать HTML на бэкенде и отдавать его браузеру как есть (с AJAX или без)?
(Не считая случаев, когда сайт таким образом написать в принципе невозможно и нужен интерактив на JS, но ведь такие сайты по пальцам можно пересчитать)
Quilin
11.04.2017 11:39+5Как бывший фронтенд разработчик, который ныне сидит на бэке, отвечаю =)
1. Рисовать HTML на бэкенде все еще можно, но такой подход не умеет решать ряд важных проблем. Например, если юзер взаимодействует с элементом, который меняет контент во многих разных частях приложения, скажем, отредактировал свое имя, которое должно успешно замениться повсюду на странице.
2. Для отрисовки на бэке тратятся ресурсы networking'а, и это имело бы смысл, будь генерация HTML'а чем-то сложным и непроизводительным, но это совсем не так — любой современный шаблонизатор работает с ast, и на клиенте это все будет работать во многих случаях быстрее чем один только пинг до сервера.
3. Отрисовка кусмана HTML'а на клиенте — это более трудозатратный процесс, чем использование DOM API, как раз потому, что снова выделяются ресурсы на парсинг и валидацию. Современные фреймворки/либы стараются использовать virtual dom, чтобы по возможности оптимизировать эти процессы.
4. Еще, конечно, обилие мобильных клиентов требует уменьшения трафика. Готовый HTML практически всегда будет весить больше чем JSON API.
Сайтов, которым нужны сложные взаимодействия на клиенте, точно не по пальцам пересчитать. Прикиньте, сколько в мире есть CRMок, облачных приложений, прибавьте сюда практически весь b2b-стек — почти каждая такая система выглядит и работает лучше, когда часть нагрузок передана на клиент.andreymal
11.04.2017 11:52должно успешно замениться повсюду на странице
После изменения имени страница классического сайта просто перезагрузится :)
на бэке тратятся ресурсы
Ни разу не сталкивался с тем, чтобы это становилось узким местом. Да и рисование HTML на бэкенде многими крупнейшими сайтами тоже тому пример. А вот то, что производительности браузера не хватает джаваскрипту — это постоянно, особенно на мобилках. Ну и gzip никто не отменял
Отрисовка кусмана HTML'а на клиенте — это более трудозатратный процесс
Видимо, разница в пределах погрешности, потому что видимой на глаз разницы в производительности между сайтами, активно юзающими innerHTML (PJAX/Turbolinks какие-нибудь), и сайтами на React не замечал (и на мобилках тормозят примерно одинаково).
Прикиньте, сколько в мире есть CRMок, облачных приложений
Прикиньте, сколько в мире есть сайтов-визиток, блогов, форумов, которые кроме гифок статичны чуть более чем полностью — но при этом в них в последнее время зачем-то пихают реакты и прочие ангуляры с js-бандлами в десяток-другой мегабайт :)
Quilin
11.04.2017 16:47+5После изменения имени страница классического сайта просто перезагрузится :)
Заставив нас ждать пинга сервер-клиента, рендера на сервере, рендера на клиенте, репейнта… А в нашем случае, может быть, надо было только два слова всего поменять. Конечно, раз на раз тут не приходится, но сценарий, когда действие пользователя меняет несколько достаточно удаленных друг от друга частей сайта, но незначительно — в моей практике встречался слишком часто.
на бэке тратятся ресурсы
Вы вырвали цитату из контекста. Привожу полную цитату:
Для отрисовки на бэке[ПАУЗА] тратятся ресурсы networking'а
Ресурсы бэка, конечно, не особо тратятся (зависит от). Но вот когда сервер в Лондоне, а юзер в Монголии…
видимой на глаз разницы в производительности [...] не замечал
Зависит от сайта, разумеется. Мне доводилось замечать такую разницу на всякого рода b2b-ресурсах.
(Не считая случаев, когда сайт таким образом написать в принципе невозможно и нужен интерактив на JS, но ведь такие сайты по пальцам можно пересчитать)
Прикиньте, сколько в мире есть сайтов-визиток, блогов, форумов, которые кроме гифок статичны чуть более чем полностью
То, что в мире много сайтов-визиток с морем статики, никак не подтверждает аргумент, что других сайтов — по пальцам пересчитать.
Я лично считаю, что серебряной пули нет. Реакт не панацея (и мне лично вообще Vue больше нравится например), и толкать его повсюду — это та же мода на сложение чисел с помощью jquery только в профиль. Если ваш фронт, которому надо сделать лендинг для разовой акции, тянет туда реакт, возможно, он не очень компетентен. Если ваш бэкендер, который пилит условный trello, морщится при слове реакт и хочет все сделать на GWT/ASP.NET — про него можно сказать то же самое.andreymal
11.04.2017 17:00+1А в нашем случае, может быть, надо было только два слова всего поменять
Если ограничиваться только этим случаем, то получается что-то вроде
document.querySelectorAll('.js-user-name').forEach(x => x.textContent = newName)
, и React тут как-то ни к чему)
в моей практике встречался слишком часто.
Видимо, мне чертовски повезло с тем, что в моей практике это встречалось всего один раз, и это был не сайт, а сложное веб-приложение. Впрочем, и там я без реакта успешно обошёлся, но это уже другая история)
тратятся ресурсы networking'а
В веб-приложениях всю экономию с лихвой компенсируют громадный размер js-бандлов, а при частых обновлениях кода даже браузерный кэш становится бесполезен. Зажатый gzip'ом html-код неплохо передаётся даже по EDGE (а там пинг, напомню, приближается к секунде), так что не вижу смысла экономить на спичках
VasilioRuzanni
11.04.2017 17:13+2Ну, вы как-то даже и не хотите видеть каких-то преимуществ SPA-приложений (а люди с вами зачем-то спорят). Если вам хватает ваших инструментов — у вас же их никто не отбирает. У всех задачи опять же разные. Всякие чисто «хайповые» штуки со временем уходят или находят свою нишу, а вот полезные паттерны — остаются. Неужели вы думаете, что все и каждый вокруг вас — хипстер, которому нравятся модные словечки и использует такой подход только из за своей хипстеровости (есть такое слово?) :)
Возьмем тот же Реакт и его серверный рендеринг — и вот вам вполне себе альтернативный вариант написать чисто серверный UI (можете даже не подрубать React на клиенте). Кому-то удобнее, кому-то нет, но это еще один полезный паттерн.
document.querySelectorAll('.js-user-name').forEach(x => x.textContent = newName)
Ну очень уж притянутый за уши пример. Вывести какой-то текст во все элементы с таким-то классом — элементарно. Во фронтенде реально сложно — хранить и управлять состоянием. Рендерить — чем угодно и как угодно — просто.VolCh
11.04.2017 17:19Хранить и управлять состоянием, имхо, не так уж сложно. Сложно перерендерить именно то, что нужно, когда состояние изменилось или, проще говоря, связать состояние с DOM, крайне желательно без полной очистки контейнера для рендеринга, а точечными операциями по его изменению.
VasilioRuzanni
11.04.2017 17:40Ну, я именно это имел в виду под «хранением и управлением состояния». Надо было добавить «и корректному отображению всего этого на UI». И тут становится понятно, что и virtual DOM не просто так придумали и всякие другие штуки для этого — чтобы была возможна концепция «UI — это функция от состояния». Но автор комментариев выше настаивает на том, что все это не нужно, и я, наравне со многими другими, с этим несогласен :)
vintage
11.04.2017 17:22Если ограничиваться только этим случаем, то получается что-то вроде document.querySelectorAll('.js-user-name').forEach(x => x.textContent = newName), и React тут как-то ни к чему)
А где-то в сторонке у вас на некоторых страницах список пользователей отсортированный по имени. Ваши действия?
andreymal
11.04.2017 17:32на некоторых страницах
А вот тут всплывает другая проблема: я как пользователь хочу, чтобы контент на страницах, которые я не трогаю, не менялся, и если что-то там пересортируется само по себе без явного нажатия мной F5, я сильно расстроюсь. А если на текущей странице нужно изменить что-то большее чем пару лейблов, то всё ещё нетрудно просто перезагрузить её (почти) целиком, «ресурсы networking'а» в 2017 году уже не проблема. Так что по моему личному мнению никаких действий предпринимать не нужно :)
vintage
11.04.2017 17:34Речь про одну и ту же страницу, на которую сейчас смотрит пользователь. Список пользователей на ней может быть, а может и не быть и если вы поменяли имя одного пользователя, то этот элемент списка должен перелететь в другое место, желательно с анимацией.
andreymal
11.04.2017 17:36Во-первых, её нетрудно перезагрузить. Во-вторых, что это вообще за страница такая странная, на которой есть одновременно и список пользователей, и функция изменения имени?
VasilioRuzanni
11.04.2017 17:46Страница может быть какая угодно. Вопрос в том, какая именно, и какой уровень отзывчивости и интерактивности ваши пользователи ожидают. Оба подхода имеют право на жизнь, очень зависит от конкретного сайта/приложения и того, какие действия с его помощью производят ваши пользователи. Просто не надо все под одну гребенку и говорить, что перезагрузка страницы — единственный тру-способ работы с веб-приложениями.
andreymal
11.04.2017 17:57Как я отметил ещё в скобках в своём самом первом комментарии, сайтов, на которых без интерактивщины с js вообще никак (тот же условный trello), я не касаюсь. А вот для большинства других ситуаций перезагрузка страницы таки является достаточно быстрым и достаточно интерактивным способом обновления контента. И я не понимаю, зачем жертвовать примитивностью и понятностью фронтенда ради экономии сотни миллисекунд и пары килобайт трафика. (И не забываем про PJAX.)
vintage
11.04.2017 17:57В шапке на всех страницах сайта отображается имя пользователя. Тут же можно его поменять (почему бы и нет?). В теле страницы "обсуждаем завтрашнюю стрелку" выводится список список "ребят с вашего двора", отсортированный по заданному пользователем критерию (имя, возраст, репутация, сила). В случае, если пользователь изменил имя, и на экран сейчас выводится список пользователей (а он может быть скрыт) и этот список отсортирован по имени пользователей, то в этом и только в этом случае нужно найти строку с текущим пользователем и переместить её в правильное место.
andreymal
11.04.2017 18:12Это дело всё ещё нетрудно перезагрузить. Всю страницу или только список подгрузить через AJAX — неважно, но я не вижу смысла как-то ещё усложнять фронтенд в данном случае. Даже если рассматривать экономию трафика и времени на парсинг html, она получается микроскопическая (даже если список большой — напомню, 2017 год на дворе) и, имхо, не стоит того. А анимации не нужны (если только так не скажет заказчик, с которым спорить себе невыгодно — но это уже совсем другая история)
VolCh
11.04.2017 18:14Давайте усложнять бэкенд? :)
andreymal
11.04.2017 18:17Здесь я отвечаю «давайте, потому что хороший сайт должен уметь работать без js» и меня закидывают помидорами и яйцами.)
gearbox
13.04.2017 23:31Да вы просто о разных вещах говорите, мужики. andreymal говорит о документах, VolCh vintage и прочие — о приложении. Общего у них — только броузер и html. Ваш кэп.
vintage
14.04.2017 00:10+1Давно не видел в вебе документов.
gearbox
14.04.2017 10:20-1А я вот утконосов давно не видел. Документация, статейники, форумы (да, интерактивные, но все же документы, так как предоставляют информацию, в отличие от приложений, который суть провайдеры сервиса)
vintage
14.04.2017 10:22+1Приложения тоже по большей части предоставляют информацию. Всё отличие — в интерактивности.
vintage
11.04.2017 18:20Перезагрузить-то может и не сложно, да только вот вот вы не уследите за всеми такими моментами, которые нужно предусмотреть. И будут вас долго и упорно преследовать багрепорты.
Заказчик обычно говорит "хочу быстро, модно, молодёжно, чтобы всё летало туда-сюда без перезагрузки и не дорого".
andreymal
11.04.2017 17:41А перелёт с анимацией очень спорная фича, сильно не люблю когда что-то куда-то скачет (и ещё больше не люблю, когда перед выполнением какого-то действия нужно дождаться, пока это что-то перестанет скакать). А если в обсуждаемом списке пользователей будет ещё и пагинация, то вообще караул тотальное неюзерфрендли :\
Xandrmoro
11.04.2017 18:25> Прикиньте, сколько в мире есть CRMок, облачных приложений, прибавьте сюда практически весь b2b-стек
Зачем здравомыслящему человеку перекладывать это всё с надёжных технологий на значительно менее надёжные?VolCh
11.04.2017 18:30+1Вы сейчас какую технологию ненадежной назвали?
raveclassic
11.04.2017 18:39+4этот их жабаскрипт вестимо
novoxudonoser
11.04.2017 11:57Тут достаточно просто, если вы пишите не простенький веб сайт а полноценное веб приложение с множеством динамики, то без клиентской шаблонизации не сделать многих вещей достаточно просто, ды и производительность.
andreymal
11.04.2017 11:58+1Как я намекнул в скобках, у большинства сайтов динамики просто нет (у хабра, кстати, тоже, за исключением подгрузки новых комментариев)
marshinov
11.04.2017 12:01Мы научили фронтов верстать сразу в реакт-компоненты. Это избавляет от этапа интеграции с серверной шаблонизацией, например. Можно отдельно делать фронт, отдельно бек и не разворачивать у фронта бд и все что там еще нужно. Большие возможности интерактивного UI. Думаю, это основные плюсы.
VolCh
11.04.2017 13:02+3Современные веб-приложения (а не сайты) стремятся к тому, чтобы запросов к серверу было как можно меньше и при этом в ответах отдавалось только то, что ещё не отдавалось. Минимизация количество запросов и их объёма.
Если знакомы с классическими клиент-серверными приложениями с десктопным клиентом, то у вас же не вызывает вопроса почему сервер не даёт клиенту сразу команды GDI (вроде так в винде это называется), рисующие данніе, а даёт только данные? Современные веб-приложения по сути являются полнофункциональным десктопным клиентом, не требующего установки и запускающегося с сетевого диска. Сначала ждём пока бинарник загрузится по сети, а потом общаемся с сервером только данными, делая всю работу UI на клиенте. А серверу всё равно, обычное десктоп приложение к нему стучится за данными, мобильное, веб, а может вообще другой сервер или CLI-утилита.
Labadabadubdub
11.04.2017 14:55+2Вот эти бородатые хипстеры, диалог и кнопка «Стать модным» — это ведь стёб, я правильно понимаю?
unel
11.04.2017 18:23Как ни странно, TypeScript. При более детальном изучении оказалось, что не все так прекрасно. Во-первых TypeScript – это не полноценный язык со статической типизацией, а транспилер.
Т.е. в случае с flow / babel / jsx вас это не смутило, а в случае с TypeScript — смутило?
Это сильно ограничивает возможности использования шаблонов и мета-программирования.
Не очень понял, какие? Можно хотя бы пару примеров?
Во-вторых, далеко не все npm-пакеты идут в комплекте с d.ts-файлами.
А аннотацию flow конечно же все npm-пакеты поддерживают?
Короче, Flow показался проще в прикручивании.
Для Flow надо прикрутить babel + babel-flow, а для TypeScript — только typescript… но это оказалось сложней?
ajaxtelamonid
11.04.2017 20:51+4Опять бедных бэкендеров мучают redux-ом. Бэкендеры, не слушайте их, берите mobx для управления состоянием, иначе со временем проклянёте тот день, когда решили разобраться с фронтом. И так проклянёте, конечно, но с mobx у вас хотя бы будет на руках работающее приложение и гораздо меньше седых волос.
marshinov
12.04.2017 12:38-1А что именно не нравится в редаксе? И в чем принципиальное преимущество mobx? Я когда сравнивал просто не нашел киллер-фич каких-то.
VolCh
12.04.2017 12:45Как минимум, MobX нормально оперирует привычными многим бэкендерам полноценными объектами, инкапсулирующими данные и поведение. В Redux же, по сути, есть только структуры данных типа сишных структур или паскалевских записей и функции, связанные со структурами только в голове у разработчика. Глядя на функцию нельзя однозначно сказать на работу с какой частью стора она заточена.
marshinov
12.04.2017 12:49Вообще-то можно. Для этого как раз и используется
combineReducers
. Это вопрос компоновки приложения, а не redux'а.VolCh
12.04.2017 14:03Вообще-то именно она и сбивает со следа, "анонимизируя" редьюсеру его часть стора так, что его коду нельзя понять полный путь от корня стора.
raveclassic
12.04.2017 14:08Эмм… зачем вам в редьюсере знать свой полный путь от корня стейта? Редьюсер должен оперировать только тем стейтом, который приходит в него первым аргументом.
VolCh
12.04.2017 14:39Чтобы понимать, что за стейт в него приходит.
raveclassic
12.04.2017 14:47Видимо, вы упускаете важную деталь. Редьюсер не должен знать об окружении, в котором он выполняется. Это чистая функция от стейта и экшена, приходящих в него через аргументы. Т.е. он не должен обращаться в какой-то глобальный стейт за данными.
Вот пример:
type TUser = { id: number, name: string }; type TAuthState = { user?: TUser, loggedIn: boolean }; const initial: TAuthState = { loggedIn: false }; type TLoginAction = { type: 'AUTH_LOGIN', payload: { user: TUser } }; type TLogoutAction = { type: 'AUTH_LOGOUT' }; type TAction = TLoginAction | TLogoutAction; const auth = (state: TAuthState = initial, action: TAction): TAuthState => { switch (action.type) { case 'AUTH_LOGIN': { return { ...state, user: action.payload.user, loggedIn: true }; } case 'AUTH_LOGOUT': { return { ...state, user: undefined, loggedIn: false }; } default: { return state; } } }; //compose type TAppState = { auth: TAuthState }; const app: TAppState = combineReducers({ auth });
Редьюсер auth не знает ровным счетом ничего о том, где он находится в глобальном стейте app, да и не должен.raveclassic
12.04.2017 14:52Упс, с типами напутал:
const app = combineReducers<TAppState>({ auth });
Но суть не меняется.
VolCh
12.04.2017 15:11Я не упускаю, я считаю недостатком, что в редьюсер приходит какой-то стейт, а он не знает откуда он пришёл, что редьюсер и обрабатываемый им стейт связаны через корень приложения.
raveclassic
12.04.2017 15:18+4Значит вы не понимаете концепцию redux.
Вот пишете, недостаток. А какой в этом недостаток?
В редьюсер приходит не «какой-то стейт», а тот, с которым он умеет работать. И ему абсолютно по барабану, в корне этот стейт лежит, на 20 уровне вложенности, на полке или в холодильнике. Совершенно все-равно, все что он делает — это применяет нужный экшен к своему стейту.
Можете привести пример, когда вам нужно «знать, откуда пришел стейт»?VolCh
12.04.2017 15:40Он не знает его это стейт или не его. Вот в вашем примере придёт
{ id: number, name: string };
но это может оказаться не пользователь, а клиент.
raveclassic
12.04.2017 16:04Так а редьюсеру-то какая разница? Если вам нужна разная обработка в зависимости от типа — заводите либо разные экшены, либо разные редьюсеры, либо добавляйте в структуру ее тип.
Тут есть небольшой трюк — представьте, что редьюсер — это не функция, которую где-то кто-то как-то вызывает, и может вызвать неправильно. Представьте, что это значение, которое меняется со временем при обработке приходящих экшенов. Ну, вы прямом смысле значение, объект. И из этих объектов вы собираете конечный объект стейта. Вот она функциональная композиция, вам не нужно вызывать редьюсер императивно (в большинстве случаев), достаточно обычной композиции в объект через combineReducers.
Из этого органично следует вывод, что редьюсер не может принять «не свой стейт», он даже думать не должен, «его это стейт или нет». Он сам «определяет собой» этот стейт.VolCh
12.04.2017 16:36Не редьюсеру есть разница, а разработчику, которому нужно внести изменение в логику его работы.
raveclassic
12.04.2017 16:45+1Ну так изменения должны вноситься на основании сущности, которая этим редьюсером обрабатывается. Все. Редьюсер — не зависит от окружения, какая разница, клиенты или пользователи лежат в стейте, если у них структура одинаковая?
Если вам нужно один и тот же редьюсер использовать для хранения и пользователей, и клиентов (что, хм, странно), вы при композиции кладете его под разными ключами. Если вам нужна разная обработка, вы делаете 2 разных редьюсера (что уже ближе к правде), которые реагируют на разные и/или пересекающиеся экшены по-разному и работают с разными структурами.
Когда разработчику нужно внести изменения в логику обработки тех же клиентов, он идет в редьюсер с клиентами и вносит изменения туда, основываясь только на том, что стейт в редьюсере может изменяться только этим редьюсером. Вы изолированы от окружения и реагируете строго на определенный список экшенов.VolCh
12.04.2017 16:59-1Ну вот кто-то, исходя из благих побуждений типа переиспользования кода, взял и использовал один редьюсер в разных ветках стора, когда пришла задача сделать в какой-то части поведение пользователя и клиента одинаковым, но имя оставил типа doSomthUser и в папочку положил User. А потом кому-то другому приходит задача изменить поведение для пользователей и он меняет поведение пользователя, не подозревая что меняет поведение клиентов. Когда стейт и данные, его изменяющие всегда лежат вместе, то таких проблем просто не возникает.
raveclassic
12.04.2017 17:12взял и использовал один редьюсер в разных ветках стора
Но зачем, если он обрабатывает одни и те же данные? Это обычная ошибка, и к redux не имеет никакого отношения.
doSomthUser
Видите, вы воспринимаете редьюсер, как какой-то метод, которые что-то делает со стейтом при его императивном вызове. Это не так, редьюсер — значение, которое вы можете прочитать, и все. Это значение может как-то меняться, в зависимости от описанной реакции на какие-то экшены. Ну руками снаружи вы его изменить не должны.
Если уж на то пошло, то нужно было от хранения клиентов в стейте вообще избавиться, потому как ровно такая же обработка происходит в редьюсере с пользователями. Если бы эта ошибка не была допущена, то не случилось бы и следующей с нечаянным изменением обработки клиентов при изменении обработки пользователей. Это не имеет никакого отношения к redux.VolCh
12.04.2017 17:47И у клиентов, и у пользователей есть, например, ФИО, но остальные данные различаются. И редьюсеров у каждого штук по 10 минимум, но разные, кроме вот этого проблемного.
Я его воспринимаю редьюсер как функцию от старого стейта и данных экшена с побочным эффектом в виде создания нового объекта для замены им старого стейта. Что замена происходит не непосредственно в нём — дань упрощения детектирования изменений — объекты гораздо проще сравнивать по ссылке, а не по значению.Это на основании анализа кода примеров на redux и его самого такое восприятие.
raveclassic
12.04.2017 17:56+1Почему у вас 10 редьюсеров на одну сущность вместо одного? Ну вот же ошибка. Представьте, что редьюсер — таблица в базе, вы же не заводите 10 таблиц под одну сущность?
Я его воспринимаю редьюсер как функцию от старого стейта и данных экшена с побочным эффектом в виде создания нового объекта для замены им старого стейта.
Это на основании анализа кода примеров на redux и его самого такое восприятие
Вот и неверно. Все примеры redux, которые я видел, оперируют редьюсерами как объектами, сущностями, но никак не методами или функциями от старого стейта к новому. То, что это такая функция — детали реализации.
TheShock
12.04.2017 18:09const app: TAppState = combineReducers({ auth });
У меня есть значительные замечания, но на таких примерах они проявляются слабо. Можно пример реального приложения с десятками моделей, где многие из них используются несколько раз, иногда с отличиями — ну то есть реальное приложение, а не обучающий ХеллоуВорлд? К сожалению, в интернете я таких примеров не нашел. В ООП такое вполне решается наследованием и композицией.
Я просто не хочу писать свой псевдокод, чтобы потом не было аргументов, что я придумываю, а хочу показать на чем-то реальном.marshinov
12.04.2017 18:18import Module1 from './modules/Module1' import Module2 from './modules/Module2' const combineModuleReducers = modules => { const reducers = {} for (let i in modules) { const red = modules[i].reducer if (typeof(red) !== 'function') { throw new Error('Module ' + i + ' does not define reducer!') } reducers[i] = red } return reducers } const modules = { Module1, Module2 } const store = createAppStore(combineReducers(combineModuleReducers(modules))) // код типового модуля const mdl = { title: 'Мой модуль', reducer: (state = initialState) => {/* логика редюсера*/}, path: '/module1' }
TheShock
12.04.2017 18:44И что?
marshinov
12.04.2017 19:05Пример из реального приложения с сотнями моделей. Комбинируем редьюсеры так. Хочу услышать значительные замечания. Может быть что-то узнаю и исправлю.
TheShock
12.04.2017 19:32Лучше покажите сотни редюсеров, а не код маленькой библиотеки)
marshinov
13.04.2017 16:05export const fetchReducerFactory = (name, initialState, callback) => (state = initialState, action) => { const keys = Object.keys(initialState) for(var i = 0; i < keys.length; i++){ const actionType = name + '/' + toUpperCamelCase(keys[i])+ '/Fetch' if(action.type == actionType) { const res = {...state} res[keys[i]] = Object.assign({}, res[keys[i]], {...action, isFetching: true}) if(typeof(res['ui']) == 'object' && keys[i] == 'data') { res['ui'] = Object.assign(res['ui'], action.params) } return res } const actionTypeSucceeded = (name + '/' + toUpperCamelCase(keys[i])+ '/FetchSucceeded').replace('Data/', '') if(action.type == actionTypeSucceeded) { const res = {...state} res[keys[i]] = Object.assign({}, res[keys[i]], {...action, isInitialized: true, isFetching: false}) return res } } return typeof(callback) == 'function' ? callback(state, action) : state; }
Достаточно часто используется эта фабрика редьюсеров. Отдельные пишутся, если логика какая-то другая. Этот редьюсер отвечает за обновление всех дочерних загружаемых данных в рамках компонента.
Используется так:
mdl.reducer = fetchReducerFactory( moduleId, { ...initialState, moduleId })
raveclassic
12.04.2017 18:19Ну в опенсорсе этого всего нет по понятным причинам. Но основная мысль, что доменный слой не лежит на уровне редьюсеров. Редьюсеры — это состояние, необходимое только для отрисовки UI и работы самого приложения (всякие там роутинги, состояние сессии и т.п.).
Модели же и процессы работы с ними (у нас на сагах все) лежат совершенно отдельно. А там хоть ООП, хоть ФП, хоть что угодно, главное уметь реагировать на экшены запросов (command) и выбрасывать их обратно на обработку редьюсерами (event). Чтение же из стора идет через селекторы (query). Получается этакий CQRS.
Я просто не хочу писать свой псевдокод, чтобы потом не было аргументов, что я придумываю, а хочу показать на чем-то реальном.
Ну вы предупредили, так что аргументов таких не будет, смело приводите примеры.TheShock
12.04.2017 18:54Ну в опенсорсе этого всего нет по понятным причинам
Эмс. Ну на других технологиях довольно много чего лежит в опенсорсе почему-то. Так что причин реальных я не вижу.
Я понимаю, как работает Редакс, к сожалению я успел поработать с ним.
Ну вы предупредили, так что аргументов таких не будет, смело приводите примеры.
То есть я теперь могу написать любую ересь, а вы не можете привести аргументы, что я пишу ересь?)
Я хочу показать, что в реальном коде Редакс приводит к крайне запутаному коду и куче копи-пасты. К сожалению, если я покажу пример — вы скажете, что это не так. Потому я и хочу увидеть реальный код, на котором я мог бы это доказать.
К сожалению, все примеры редакса — это что-то из разряда «я написал за пару десятков часов что-то в свое удовольствие и все, что не вписывалось в архитектуру — выбросил как ненужное».
Я же хотел бы увидеть «мы командой уже полгода пишем, активно реагируя на запросы пользователей и получили такой результат».raveclassic
12.04.2017 21:16Эмс. Ну на других технологиях довольно много чего лежит в опенсорсе почему-то. Так что причин реальных я не вижу.
Под понятными причинами я имел в виду, что у нас все closed-source. Видимо, неудачно выразился.
Ну… можно просто порассуждать о возможных проблемах. Если редьюсеры превращаются в кашу с кучей копипасты — с ними точно что-то не так.
Мы используем редьюсеры исключительно как хранилище обычных данных (грубо говоря, кэш), состояния сессии, и некоторого состояния UI (но далеко не всего). Гипотетические сотни моделей, если они являются сущностями, могут уложиться в те же сотни редьюсеров с нормализованными данными. Каких-либо проблем я тут не вижу. Если они есть — укажите.
Возможно, а у меня есть подозрение, что это так, вы имеете в виду сложный процессинг этих данных, кочующий из редьюсера в редьюсер в виде слабосвязанных функций или той же копипасты. В общем, особого процессинга или какой-либо нетривиальной логики, связанной с ним, у нас в редьюсерах нет. Их задача достать из пэйлоада экшена ненормализованные данные, нормализовать и сохранить. Вся обработка вместе с логикой лежит в сагах (процессах), и там это все очень хорошо упорядочивается и группируется.TheShock
12.04.2017 22:11Под понятными причинами я имел в виду, что у нас все closed-source. Видимо, неудачно выразился.
Ах, я не понял, что вы именно про ВАШ код. Я же говорил впринципе про любой код.
Вот видите, вы говорите, что мои рассуждения неправильные, потому что в реальном коде не так, как я и ожидал. Потому и хотел глянуть на реальный код и ткнуть пальцем, и мне не могли сказать: «но в реальном коде это не так».
А то с редаксом всегда так. Из-за Стокгольмского синдрома те, кто его используют — защищают, а показать недостатки на практике — невозможно, потому-что «код закрыт, извините, но честно-честно, там все очень клево, он нас не очень сильно насилует».raveclassic
12.04.2017 22:25Я же говорил впринципе про любой код.
Ну тут, увы и ах, более менее серьезных примеров в открытом доступе действительно нет. С сагами так вообще все плохо, кроме их документации (кстати, достаточно неплохой) и статей на медиуме из серии «саги для чайников» вообще ничего нет. Приходится искать литературу по process managers и вот этому всему и как-то перекладывать на js.
Вот видите, вы говорите, что мои рассуждения неправильные, потому что в реальном коде не так, как я и ожидал. Потому и хотел глянуть на реальный код и ткнуть пальцем, и мне не могли сказать: «но в реальном коде это не так».
А то с редаксом всегда так. Из-за Стокгольмского синдрома те, кто его используют — защищают, а показать недостатки на практике — невозможно, потому-что «код закрыт, извините, но честно-честно, там все очень клево, он нас не очень сильно насилует».
Так а вы покажите с какими недостатками столкнулись вы. А я попробую смоделировать их решение на нашем стэке.
raveclassic
12.04.2017 12:55+2Глядя на функцию нельзя однозначно сказать на работу с какой частью стора она заточена.
Ну так в этом то и смысл. Функция отвязана во всех смыслах от места компоновки. А типизация еще больше упрощает работу.VolCh
12.04.2017 14:08Ну вот это и не нравится. Мало того, что данные и функция, их мутирующая в разных местах, так она ещё и во всех смыслах полностью отвязана.
А про типизацию не понял: в сторе редакса же все объекты нетипизированные, по сути исключительно литералы, нельзя же сделать где-то в редьюсере return new User(username), а надо return {username}. Или можно?
raveclassic
12.04.2017 14:12Под типизацией я имел в виду доп. инструменты в виде ts/flow. Вы объявляете тип объекта, с которым работает ваш редьюсер и тот же тип возвращает, и все. Где, в каком месте стейта, это происходит должно быть абсолютно все-равно.
Как у вас выстроено дерево редьюсеров, что он у вас работает с данными в разных местах? Не очень понимаю.mkusher
12.04.2017 14:26из приятного в redux4 combineReducers в ts будут использовать mapped types и не надо будет прокидывать руками типы в createStore, а тайпинги эти можно юзать уже сейчас
raveclassic
12.04.2017 14:29Ага. Я было PR даже открыл им, а оказывается это уже давно в мастере.
marshinov
12.04.2017 14:27Можно, главное ссылки менять. Просто
{...state}
не удобно будет делать. А без этого не поменяется ссылка и Redux не запустит перерендер компонентов. Вам просто не нравится, что Redux функциональный.VolCh
12.04.2017 14:55-1Мне не нравится прежде всего, что связка Redux+React предполагает, что нужное состояние DOM является чистой функцией от одного параметра — глобального стора. В теории можно создавать хоть в каждом компоненте сторы как замену this.state и редьюсеры как замену this.setState(), но на практике это не просто.
VasilioRuzanni
12.04.2017 15:07+1DOM является функцией от `props` компонента — в ней может быть намешано все, что угодно, хотя обычно бОльшая часть — да, из глобального стора. Вы так говорите, будто это что-то плохое :)
P.S. Совершенно верно, редьюсеры можно использовать на любом уровне для любого стейта.VolCh
12.04.2017 15:14Никогда не доверял глобальным переменным :), а по сути стор таковой и является.
raveclassic
12.04.2017 15:20+1Если вы не хотите, чтобы ваши компоненты знали о структуре глобального стора, спрячьте чтение в селекторы, и у вас получится ну 100% CQRS.
VolCh
12.04.2017 15:47Отчасти я наоборот хочу, чтобы знали :)
А вообще, глобальный стор редакса со всеми экшенами, которые могут его изменять, у меня чётко ассоциируются с ООП-антипатерном GodObject, а то и с процедурным программированием с большим применением глобальных переменных.
raveclassic
12.04.2017 16:09+1Отчасти я наоборот хочу, чтобы знали :)
Вы про контейнеры, надеюсь? Селекторы добавляют дополнительный слой чтения, и позволяют избежать излишнего связывания.
А вообще, глобальный стор редакса со всеми экшенами, которые могут его изменять, у меня чётко ассоциируются с ООП-антипатерном GodObject, а то и с процедурным программированием с большим применением глобальных переменных.
Вот и зря, так как стор — результат функциональной композиции, а не какая-то глобальная ерунда, к которой все имеют доступ и что-то в ней меняют, как в случае с god-object'ом. То, что стор — javascript объект — это детали реализации.VolCh
12.04.2017 16:42Знание "пути" в сторе позволяет быстрее определить, а что, собственно, редьюсер делает в терминах домена.
Можно жёстко, одним файлом, например, ограничить редьюсеры, которые могут менять участок стора? Насколько я знаю нет.
raveclassic
12.04.2017 16:49+1Вы можете привести пример? Я никак не пойму, какую задачу вам это «знание пути» помогает решить. Все что вы делаете — лишь прибиваете гвоздями чистую функцию редьюсера к конкретному месту в вашем стейте, когда это прекрасно избегается по-умолчанию с сохранением самого полезного во всем этом безобразии — гибкости композиции.
ограничить редьюсеры, которые могут менять участок стора?
Я выше вам написал уже, редьюсеры не меняют участки стора, они этот стор создают при композиции.VolCh
12.04.2017 17:05Исследование чужого проекта: есть стор, который можно посмотреть в рантайме, и есть куча редьюсеров. Открываешь код редьюсера и видишь, что он, например, какое-то поле очищает, но с чем это поле связано, где находится, на что в сторе не понятно.
Формально, да, не менют, а создают новый на основе старого. По факту для большинства приложений — это лишь деталь реализации, поскольку новое значение стора просто заменяет старое.
raveclassic
12.04.2017 17:17Открываешь код редьюсера и видишь, что он, например, какое-то поле очищает, но с чем это поле связано, где находится, на что в сторе не понятно.
Ну так вы ищите, где этот редьюсер компонуется, и сразу становится понятно, какой кусок стейта к этому редьюсеру относится.
К тому же, ну и что что очищает? Это его стейт, и завязок на это поле в других кусках стейта быть не должно. Можете привести пример такого поля, чтобы понятней было?VolCh
12.04.2017 17:49Разве у редакса есть ограничение, что изменять один участок стейта может один и только один редьюсер? Если есть, то мне надо многое переосмыслить :(
raveclassic
12.04.2017 17:58Ну это же core-принцип. :) Редьюсеры компонуют стейт, а не изменяют его. Невозможно одной сужностью скомпоновать несколько участков — нельзя быть в двух местах сразу. Как только это уляжется, все станет гораздо проще. :)
VasilioRuzanni
12.04.2017 14:54+1«Как минимум, MobX нормально оперирует привычными многим бэкендерам полноценными объектами, инкапсулирующими данные и поведение.»
А кто сказал, что всем нужно именно это? Это просто другой подход. Просто mobx как решение и хранит состояние, и имеет весь инструментарий для его управления. Плюс реактивность MobX, но это немного другая тема.
Redux же очень прост, но, по сути, действительно, умеет только хранить состояние, да и все. Ему нужно что-то, что будет поддерживать весь его (потенциально) навороченный стейт в порядке и согласованности. Причем, с ростом проекта, таких сценариев становится все больше, а они сами становятся все сложнее. Нужна асинхронность, опять же. Отсюда и появляются redux-saga, redux-observable (реактивные «саги»), сам я предпочитаю вообще Cycle.js использовать для подобного рода логики.
Так что, тут просто разница подходов и, по сути, дело вкуса. Но почему то мне кажется, что Redux придется по душе многим, или даже большинству, именно из за своей прямолинейности и простоты.
frog
11.04.2017 21:42Мне кажется, в джентльменском наборе из статьи не хватает graphql (+ к нему subscriptions через сокеты)
vanburg
12.04.2017 04:47А я люблю, как вы выразились «хипстерские штучки», и именно такие как я и популяризируют elm и clojure script. Найти работу под эти мощнейшие «штучки» вполне можно. А vanilla js/ts/cs/хзс, спасибо, нет. Хочется получать удовольствие, и спать спокойно.
TheShock
12.04.2017 05:26Вчера Coffee, сегодня Elm, завтра еще что-то. Мода ради моды, которая живет только на волне хайпа.
vanburg
12.04.2017 06:15+1Сдается мне вы не вникали в элм, иначе бы не сравнивали сахар с философией.
vintage
12.04.2017 07:57Наконец-то я нашёл эксперта по Elm! Не подскажете, как повесить на элемент пассивный обработчик события? То есть нужен аналог вот этого:
el.addEventListener( 'wheel' , onScroll , { passive : true } )
caballero
12.04.2017 12:44MVC-фреймворк, ORM, Data Mapper, IOC-контейнер, логер, профайлер, очереди, управление конфигурациями, сборка и выкладка
а кто сказал что все это обязательно надо?
Не надо явскриптовое безумие переносить обратно на бэкенд.
raveclassic
12.04.2017 12:57Ну а кто сказал, что на клиенте вам обязательно надо все перечисленное в статье?
AlexMal
У меня все.
SerafimArts
IEEE 754
У меня всё.
TheShock
Что все? Это IEEE 754, во всех языках, которые его используют так будет.
А статья — очередное «вступление в мир моды на быдлокод в JS», которых, как сам автор сказал, уже очень много и потому не нужна
Xandrmoro
Вот только в нормальных языках есть decimal.
rafuck
… и long int или его аналог. В отличие от JS, в котором все, что не помещается в 4 байта преобразуется в double:
VolCh
В JS — всё double, тут никаких int нет by design, если не считать объектные расширения
rafuck
Да, извиняюсь, был неправ. Видимые отличия от использования целочисленных типов начинаются с 54 бит.
Shannon
golang:
Ogra
vintage
D:
rafuck
а так?
rafuck
Поздно заметил, что вы складываете. А вообще, никаких неожиданностей
vintage
Тем не менее:
vintage
rafuck
И все равно не понимаю, что вы пытаетесь доказать? Что операции с числами с плавающей точкой в D не соответствуют стандарту IEEE754?
vintage
Нет, что стандартная библиотека D достаточно умна, чтобы округлять последние биты при выводе. Всякие "0.020000000000000004" пользователю как правило не нужны.
rafuck
Но речь в этой ветке совсем не про округление при выводе.
vintage
Именно про округление, перечитайте её :-)
rafuck
Про округление при выводе здесь никто кроме вас не писал :)
vintage
Вам, конечно, виднее.
Shannon
В данном случае даже опаснее доверять выводу writeln, так как:
Да и в целом в любом языке так, все случае не предугадаете, когда будет 0.3, а когда 0.3000000001
Поэтому в любом случае стоит делать проверку на диапазон или любой другой хак.
VolCh
Сравнение вещественных чисел через сравнение их разности с допустимой погрешностью — не хак, а стандартный инструмент решения математических задач численными методами.
AlexMal
Я просто хотел сказать, что для каких-то важных, точных вычислений, JS абсолютно не годится. Вообще, числа с плавающей запятой — это отдельная тема.
rafuck
Он не годится в той же мере, в какой не годятся вообще все вычисления в формате IEEE754 на всех платформах и ЯП.
Captcha
Дай-ка угадаю. Гуманитарий?