[Из песочницы] ReactJS in nutshell. Часть 1
Добрый день, уважаемые читатели.
В последнее время на Хабре все чаще упоминается такой замечательный фреймворк, как React.js. Я работаю с ним уже 4 месяца, поэтому решил поделиться опытом использования. Решено было сделать небольшую серию статей, которые должны стать максимально кратким полным руководством по фреймворку. Это моя первая публикация на Хабре, поэтому прошу не судить слишком строго. Моя главная задача рассказать о подходах и практиках, второстепенная — узнать у людей, использовавших React, как они работают с ним и как они решали те или иные кейсы, которые возникали в их работе. Ну и конечно расширить сообщество фреймворка. Начало я оформил в виде небольшого конспекта-шпаргалки. А дальше только практика.* in nutshell знаменитая серия книг от издательства o’Reilly, само выражение означает: говорить без лишних объяснений (прим. автора)
В первой статье я покажу как сделать самую простую страничку, которая будет содержать самый минимум динамики. Основные подходы, приемы и немного мелких хитростей. По возможности весь код будет рассмотрен до мельчайших подробностей. В этом мне поможет небольшой репозиторий: https://github.com/Aetet/react-article.У каждого изменения есть соответствующий тэг. Описания для каждого из них будут объявлены по ходу дела.
Конспект Что это такое и с чем это едят? React — это якобы только View фреймворк от Facebook. Да, разработчики немного кривят душой, чего не сделаешь ради маркетинга. На самом деле React является полноценным MVC фреймворком, хотя и не выглядит таковым с первого взгляда. К концу статьи я продемонстрирую наличие всех трех элементов. Для удобства записи в React используется XML-подобный синтаксис. Так как по умолчанию конструкции, подобные:
выкачать зависимости через npm install. запустить gulp. В этой статье я не буду рассматривать настройку и использование gulp, webpack. Уверен, пытливый Хабра-читатель сам найдет информацию по этому вопросу.Организация структуры. Хаос всегда побеждает порядок, поскольку лучше организован.Терри Пратчетт
Исходное состояние: тэг start.Конечное состояние: тэг first-static.Для начала наметим обычное статичное содержимое, а потом пойдем глубже: Мы начинаем с базы:
/** @jsx React.DOM */ var React = require ('react'); var Index = React.createClass ({ render: function () { return (
Создаем класс для component’а Index с помощью React.createClass . Создаем функцию render, которая содержит шаблон. Рендерим класс, передав ему необходимые начальные данные props и селектор, куда будет срендерен элемент. Так мы получили свой первый Index.Начинаем идти сверху вниз и производить декомпозицию нашего приложения. Корневым элементом у нас будет component Booking. Плюс, у нас есть два главных блока на странице:
Информация о пассажире. Кнопка Submit. Поэтому создаем component’ы PassengerInfo и Submit. Внутрь просто помещаем статичный HTML. /** * @jsx React.DOM */ var React = require ('react'); var PassengerInfo = React.createClass ({ render: function () { return (
Исходное состояние: first-staticКонечное состояние: initialState-propTypesТеперь, когда мы создали основную структуру, добавим state (состояние) для нашего виджета бронирования.Добавим в Booking метод getInitialState.
getInitialState: function () { return { firstName: '', lastName: '', gender: '' }; } При первом рендеринге виджета букинга функция заполняет this.state возвращаемым объектом. Передаем значения из state в component PassengerInfo.
Изменение state. Если бы моя дочь дала мне значок со словом «идиот», я бы надел его.Хью Лори
Исходное состояние: тэг initialState-propTypesКонечное состояние: тэг handleChangeПожалуй, стоит немного поучиться отслеживать, что вообще творится в приложении и как правильно управлять обработчиками.
Чтобы следить за изменениями input, добавим обработчик onChange:
var PassengerInfo = React.createClass ({ handleChange: function (e) { var target = e.target, name = target.name, value = target.value; this.props.onChange (name, value); }, render: function () { return ( … … ); } }); В React он немного отличается от поведения обычного onChange. Подробнее
Аргумент (e) в данном случае является SynteticReactEvent`ом, а не DOMEvent`ом, как может показаться на первый взгляд. Объект был специальным образом приготовлен и выращен в лабораторных условиях для кроссбраузерной работы. С некоторыми оговорками.
Хм. Видимо, настал очень важный момент передачи данных в родительский виджет. Нет ничего проще. Нам всего лишь надо постучать соседу сверху шваброй в потолок. Для этого в Booking в props PassengerInfo добавляем функцию onChange.
var Booking = React.createClass ({ handleChange: function (name, value) { var state = {}; state[name] = value; this.setState (s tate); }, render: function () { return (
Подробнее об обработчиках событий, которые есть у элементов можно прочитать здесь: facebook.github.io/react/docs/events.htmlfacebook.github.io/react/docs/dom-differences.html
О поддержке старых браузеров: facebook.github.io/react/docs/working-with-the-browser.html#browser-support-and-polyfills
Переиспользуемые виджеты. Мы ведь не знаем, что внутри у щенка и что он чувствует. Может, это тоже притворство.Айзек Азимов
Исходное состояние: тэг handleChange.Конечное состояние: тэг Gender-Switcher.Лично я люблю небольшие view с очень простым взаимодействием внутри. В PassengerInfo, на мой вкус, многовато верстки. Поэтому я выделю виджет выбора пола в отдельный GenderSwitcher и сделаю его переиспольуемым. Переиспользуемые виджеты обычно храню в Common/Widgets.
Чтобы виджет был по настоящему переиспользуемым, ему нужно иметь четкие входные и выходные данные. Очевидно, что для этого виджета входными будет пол по умолчанию, выходными — выбранный пол. Что ж, все просто. Действуем по аналогии с прошлым примером и получаем:
var GenderSwitcher = React.createClass ({ propTypes: { gender: React.PropTypes.string }, handleClick: function (e) { var target = e.target, type = target.dataset.type; this.props.handleGender (type); }, render: function () { return ( М Ж ); } }); Используем data-атрибут для получения информации о том, на какой именно элемент мы кликнули. Чтобы получить доступ к этим атрибутам, просто обращаемся к свойству dataset у элемента. К сожалению, я не помню точное место в официальной документации, где об этом сказано. Поэтому буду рад дополнить статью ссылкой насчет этого момента.
ВНИМАНИЕ!!! данный подход может устареть с новой версией: github.com/facebook/react/issues/1259
Конечно, можно всю обработку делать через один и тот же обработчик handleChange, но я предпочитаю придерживаться Unix-way. Одна функция — одно назначение. Поэтому добавим обработчик handleGender.
var PassengerInfo = React.createClass ({ … handleGender: function (type) { this.props.onChange ('gender', type); }, … }); Теперь, чтобы убедиться, что при изменении пола он сохраняется в state, поставим console.log в верхнем виджете Booking.jsx. Откроем страничку, кликнем на М, и действительно — в консоли изменился пол.
Время Controlled-component’ов. Люди слишком восприимчивы. Стоит кому-то попытаться вас контролировать, и вы подчиняетесь. Иногда мне кажется, вам это нравится.Доктор Кто
Исходное состояние: тэг GenderSwitcherКонечное состояние: тэг controlled.Сделаем так, чтобы input отображал изменения и был controlled.
Для этого к input добавим value, в который будем записывать значение пришедших props.
Внимание!!! Если мы просто сделаем
, то input не будет изменяться.Подробнее о таком поведении: facebook.github.io/react/tips/controlled-input-null-value.html
Я рекомендую делать все input по возможности controlled, чтобы иметь полный контроль над каждым input’ом. Это поможет изменять поведение элемента, если придут новые требования.
Утилитные функции. Just because they serve you doesn’t mean they like you.Clerks
Исходное состояние: тэг controlledКонечное состояние: тэг viewHelperЛогика для GenderSwitcher работает, однако нет визуального отображения изменения. Что ж, добавим его.
По сути, изменение GenderSwitcher — это всего лишь изменение this.props.genderПоэтому сделаем следующее: в зависимости от пришедшего this.props.gender мы будем добавлять или удалять активный класс элемента. Поскольку я все еще придерживаюсь Unix-way, то буду делать это с помощью внешней функции helper.
Она будет очень простой:
var GenderViewHelper = { generateMaleClass: function (gender, defaultClasses) { var className = defaultClasses.join (' '), activeClass = (gender === 'm') ? ' btn-primary' : ''; return className + activeClass; } }; Аналогично делаем для female.
Да, конечно, можно сделать рефакторинг, выделить глобальную функцию helper для классов, но, на мой взгляд, это преждевременная оптимизация.Плюс есть еще одна хитрость: facebook.github.io/react/docs/class-name-manipulation.html
Функция render немного преобразится:
render: function () { var maleClass = GenderViewHelper.generateMaleClass (this.props.gender, ['btn']); var femaleClass = GenderViewHelper.generateFemaleClass (this.props.gender, ['btn']); return ( М Ж ); } Вызов серверных духов данных — Я духов вызывать могу из бездны! — И я могу, и всякий это может.Вопрос лишь в том, явятся ль они на зов.Шекспир. Генрих IV.
Исходное состояние: тэг viewHelperКонечное состояние: тэг save-serverСтатья выходит на финишную прямую, поэтому пришла пора закреплять и сохранять. Отправим наши данные на сервер.Для этого добавим обработчик у кнопки по клику:
var Submit = React.createClass ({ handleClick: function () { this.props.onSubmit (); }, render: function () { return (
var Booking = React.createClass ({
…
handleSubmit: function () {
var dataForServer = clone (this.state);
BookingManager.saveData (dataForServer)
.then (function (successMsg) {
alert (successMsg);
})
.fail (function (err) {
console.log ('err when save', err);
});
},
render: function () {
return (
…
var Vow = require ('vow'); var BookingManager = { saveData: function (data) { var dfd = Vow.defer (); setTimeout (function () { dfd.resolve ('Hello, Habra!' + JSON.stringify (data)); }, 1000); return dfd.promise (); } }; module.exports = BookingManager; Как видим, saveData — простая эмуляция ответа от сервера. Defer нужны в основном для удобной работы, если вдруг понадобится ждать несколько событий, и для полной поддержки асинхронности.
У нас начинает вырисовываться четкий жизненный цикл component’а:
Рендерим Views Views реагируют на handlers Handlers передают управление callbacks в корневой виджет — Controller. Сохраняем данные в хранилище state. State генерирует событие об изменении. Views начинают заново рендерится. И ситуация пошла по новому кругу. Вместо послесловия. Все-таки ничто MVC-шное React не чуждо. M — state. Да мы сохраняем наши данные в state, чем не привычная модель? Тем более он излучает события и вызывает изменения в жизненном цикле component’а. V — вся функция render, это одна большая View, которую скрестили вместе с шаблоном. С — корневой виджет, идеальный кандидат на роль контроллера, в нем Бизнес встречает деньги данные начинают свое путешествие по props component’ов — V и лежат до востребования в M — state.Таким образом, создатели React очень изящно подошли к реализации давно известного паттерна. Вместо разделения MVC на отдельные буквы они разделили lifecycle между ними. И получилось, честно говоря, очень здорово. Сейчас я могу с уверенностью сказать, что React — это мой любимый инструмент, работа с которым доставляет большую радость. Конечно, у него есть свои недостатки и свои болячки. Однако для каждого из них можно найти решение или hack. Ведь сейчас над ним работают люди, у которых hackathon является частью корпоративной культуры.
Если хабрасообществу понравится статья, то я напишу еще одну статью про React.В ней я планирую осветить следующие темы:
Масштабный рефакторинг. Масштабирование от простенькой формы к сложной форме со сложным динамическим поведением. Второе пришествие Менеджера или требования опять поменялись. Подводные камни React. Как спастись от Лукавого jQuery. В своей работе я использовал следующие инструменты: Библиотека React.JS (https://github.com/facebook/react) Система сборки gulp (https://github.com/gulpjs/gulp) Модульная система webpack (https://github.com/webpack/webpack) Фреймворк connect (https://github.com/senchalabs/connect) Библиотека для promise и defer vow (https://github.com/dfilatov/vow) P.S. Эту статью я посвящаю своей маме — моей первой учительнице.
