Как стать автором
Обновить

ReactJS in a nutshell. Часть 1

Время на прочтение12 мин
Количество просмотров28K

Добрый день, уважаемые читатели.


В последнее время на Хабре всё чаще упоминается такой замечательный фреймворк, как React.js. Я работаю с ним уже 4 месяца, поэтому решил поделиться опытом использования. Решено было сделать небольшую серию статей, которые должны стать максимально кратким полным руководством по фреймворку. Это моя первая публикация на Хабре, поэтому прошу не судить слишком строго. Моя главная задача – рассказать о подходах и практиках, второстепенная – узнать у людей, использовавших React, как они работают с ним и как они решали те или иные кейсы. Ну и, конечно, расширить сообщество фреймворка. Начало я оформил в виде небольшого конспекта-шпаргалки. А дальше только практика.


* >

In a nutshell
— знаменитая серия книг от издательства
O'Reilly
само выражение означает: говорить без лишних объяснений (прим. автора)


В первой статье я расскажу, как сделать самую простую страничку, которая будет содержать минимум динамики. А также покажу основные подходы, приёмы и немного мелких хитростей. По возможности весь код будет рассмотрен до мельчайших подробностей. В этом мне поможет небольшой репозиторий: https://github.com/Aetet/react-article.
У каждого изменения есть соответствующий тэг. Описания для каждого из них будут представлены по ходу дела.


Конспект


Что такое React и с чем его едят?


React – это якобы только view-фреймворк от Facebook. Да, разработчики немного кривят душой, чего не сделаешь ради маркетинга. На самом деле React является view-ориентированным MVC фреймворком, хотя и не выглядит таковым с первого взгляда. К концу статьи я продемонстрирую наличие всех трёх элементов. Для удобства записи в React используется XML-подобный синтаксис. Так как по умолчанию конструкции, подобные

</p>
<div>Hello {this.props.name}</div>
<source lang=",">явно были бы невалидны для JS, то был придуман формат JSX. Наглядность и быстрый старт для верстальщика - главные достоинства подхода. JSX обрабатывается с помощью React-tools и превращается в обычную JS-конструкцию вида: 
```javascript
React.DOM.div(null, "Hello ", this.props.name); 

Про Реакт уже было рассказано:


  • http://andreypopp.com/moscowjs-react/#0 В этой презентации @andreypopp отлично объясняет на русском термины и основные концепции React.
  • http://habrahabr.ru/post/189230/ Очень краткое intro.
  • http://habrahabr.ru/post/217295/ Перевод замечательной статьи о внутреннем механизме работы React, который делает основную магию.

Основные термины:


Helper – объект со множеством разных методов, которые выполняют операции преобразования. Например, форматируют дату, создают текстовые представления классов для view и т.д.
Manager – объект, содержащий методы для взаимодействия с серверной частью приложения. Сохранение, получение данных с сервера — всегда значимая часть разработки.
Props – данные, которые передаются виджету при рендеринге.
State хранит внутреннее состояние виджета (например, он открыт, активен, скрыт и т.д.).
Component – блок функциональности, выполняющий специфичную для него функцию.


Основные фичи фреймворка:


  1. Декларативный подход.
  2. Stateless components.
  3. Нормализация DOMEvent.
  4. Собственная реализация DOM.
  5. Следование Unix-way.
  6. Возможность применять композицию component'ов и переиспользовать их.

Практика


Что ж, с маркетингом и вводной частью разобрались, переходим к сути: созданию виджета. "HelloWorld" можно посмотреть и на странице React. Поэтому запилим что-нибудь посложнее — страницу бронирования, например.


Постановка задачи


Что же мы на ней разместим? Что требует бизнес бронирования? Давайте для начала сделаем просто:


  1. Форма с указанием имени, фамилии, пола человека, для которого мы забронируем местечко.
  2. Кнопка Отправить.

Задача очень простая, зато на ней можно хорошо обкатать базовые кейсы и концепции. Время расчехлить инструменты и вспомнить про гитхаб https://github.com/Aetet/react-article.


Сборка проекта построена с помощью gulp, модульная система на webpack, stylus, bootstrap. Все настроено и готово к использованию. Поэтому нужно сделать минимум приготовлений:


  1. выкачать зависимости через npm install.
  2. запустить gulp.

В этой статье я не буду рассматривать настройку и использование gulp, webpack. Уверен, пытливый Хабра-читатель сам найдет информацию по этому вопросу.


Организация структуры.


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

Исходное состояние: тэг start.
Конечное состояние: тэг first-static.


Для начала наметим обычное статичное содержимое, а потом пойдем глубже.
Мы начинаем с базы:```javascript
/* jsx React.DOM /


var React = require('react');


var Index = React.createClass({
render: function () {
return (



Booking

); 

}
});
React.renderComponent(
<Index />, document.querySelector('.appl'));


Создание нового виджета состоит из нескольких частей: 
<ol>
    <li>Создаем класс для component'а Index с помощью React.createClass.</li>
    <li>Создаем функцию render, которая содержит шаблон. </li>
    <li>Рендерим класс, передав ему необходимые начальные данные props и селектор, куда будет срендерен элемент. </li>
</ol>
Так мы получили свой первый Index. 

Начинаем идти сверху вниз и производить декомпозицию нашего приложения. Корневым элементом у нас будет component Booking. Плюс у нас есть два главных блока на странице: 
<ol>
    <li>Информация о пассажире. </li>
    <li>Кнопка Submit. </li>
</ol>
Поэтому создаем component'ы PassengerInfo и Submit. Внутрь просто помещаем статичный HTML.```javascript
/** 
* @jsx React.DOM 
*/ 
var React = require('react'); 
var PassengerInfo = React.createClass({ 
  render: function () { 
    return ( 
      <div> 
        <span > 
        <label>Имя</label> 
        <input type="text" name="firstName" /> 
        </span> 
        <span > 
        <label>Фамилия</label> 
        <input type="text" name="lastName" /> 
        </span> 
        <div className="controls"> 
          <span className="btn-group"> 
            <span className="btn">M</span> 
            <span className="btn">F</span> 
          </span> 
        </div> 
      </div> 
    ); 
  } 
}); 
module.exports = PassengerInfo; 

Создание state


Я не раз замечал, что Бог, проявляя пристрастие к дешёвым литературным штампам, нередко насылает на нас погоду, отражающую наше внутреннее состояние.
Стивен Фрай

Исходное состояние: first-static
Конечное состояние: initialState-propTypes


Теперь, когда мы создали основную структуру, добавим state (состояние) для нашего виджета бронирования.
Добавим в Booking метод getInitialState.```javascript
getInitialState: function () {
return {
firstName: '',
lastName: '',
gender: ''
};
}


При первом рендеринге виджета Booking функция заполняет this.state возвращаемым объектом. Передаём значения из state в component PassengerInfo.```javascript
<PassengerInfo firstName={this.state.firstName}
                       lastName={this.state.lastName}
                       gender={this.state.gender} />

firstName, lastName, gender теперь являются props для component'а PassengerInfo.
Чтобы убедиться, что нам были переданы правильные типы props, добавляем валидацию:```javascript
propTypes: {
firstName: React.PropTypes.string,
lastName: React.PropTypes.string,
gender: React.PropTypes.string
}


Теперь, если типом одного из свойств будет не string, в консоли мы получим замечательный warning: 
<i>Warning: Invalid prop `firstName` of type `object` supplied to `PassengerInfo`, expected `string`.</i> 

Описание getInitialState в документации здесь: 
http://facebook.github.io/react/docs/component-specs.html#getinitialstate 
propTypes: 
http://facebook.github.io/react/docs/component-specs.html#proptypes 

<h4>Изменение state.</h4>
<blockquote><i>Если бы моя дочь дала мне значок со словом «идиот», я бы надел его.
</i>
<b>Хью Лори</b></blockquote>
<b>Исходное состояние:</b> тэг <i>initialState-propTypes </i>
<b>Конечное состояние:</b> тэг <i>handleChange</i>

Пожалуй, стоит немного поучиться отслеживать, что вообще творится в приложении и как правильно управлять обработчиками.

Чтобы следить за изменениями input , добавим обработчик onChange:```javascript
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 (
  ...    
      <input onChange={this.handleChange} 
               type="text" 
               name="firstName" 
               value={this.props.firstName} />
  ...
  );
  }
});

В 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 ( 
      <div> 
        <PassengerInfo firstName={this.state.firstName} 
                       lastName={this.state.lastName} 
                       gender={this.state.gender} 
                       onChange={this.handleChange}/> 
        <Submit /> 
      </div> 
    ); 
  } 
}); 

Таким образом, state (состояние) верхнего виджета Booking всегда является актуальным. И state отражает состояние дочерних виджетов.


Подробнее об обработчиках событий, которые есть у элементов, можно прочитать здесь:
http://facebook.github.io/react/docs/events.html
http://facebook.github.io/react/docs/dom-differences.html


О поддержке старых браузеров:
http://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 ( 
      <span className="btn-group"> 
        <span data-type="m" className="btn" onClick={this.handleClick} >М</span> 
        <span data-type="f" className="btn" onClick={this.handleClick} >Ж</span> 
      </span> 
    ); 
  } 
}); 

Используем data-атрибут для получения информации о том, на какой именно элемент мы кликнули. Чтобы получить доступ к этим атрибутам, просто обращаемся к свойству dataset у элемента. К сожалению, я не помню точное место в официальной документации, где об этом сказано. Поэтому буду рад дополнить статью ссылкой насчёт этого момента.


ВНИМАНИЕ!!! данный подход может устареть с новой версией:
https://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.```javascript
<input onChange={this.handleChange}
type="text"
name="firstName"
value={this.props.firstName} />


<b>Внимание!!!</b> Если мы просто сделаем```javascript
<input type="text" name="firstName" value="Vasya" />
``` , то input не будет изменяться. 

Подробнее о таком поведении: 
http://facebook.github.io/react/tips/controlled-input-null-value.html 

Я рекомендую делать все input по возможности controlled, чтобы иметь полный контроль над каждым input'ом. Это поможет изменять поведение элемента, если придут новые требования. 

<h4>Утилитные функции</h4>
<blockquote> <i>Just because they serve you doesn't mean they like you.</i>
<b>Clerks</b></blockquote>
<b>Исходное состояние:</b> тэг <i>controlled</i>
<b>Конечное состояние:</b> тэг <i>viewHelper</i>

Логика для GenderSwitcher работает, однако нет визуального отображения изменения. Что ж, добавим его. 

По сути, изменение GenderSwitcher – это всего лишь изменение this.props.gender. 
Поэтому сделаем следующее: в зависимости от пришедшего this.props.gender мы будем добавлять или удалять активный класс элемента. Поскольку я все еще придерживаюсь Unix-way, то буду делать это с помощью внешней функции helper. 

Она будет очень простой:```javascript
var GenderViewHelper = { 
  generateMaleClass: function (gender, defaultClasses) { 
    var className = defaultClasses.join(' '), 
        activeClass = (gender === 'm') ? ' btn-primary' : ''; 
    return className + activeClass; 
  } 
}; 

Аналогично делаем для female.


Да, конечно, можно сделать рефакторинг, выделить глобальную функцию helper для классов, но, на мой взгляд, это преждевременная оптимизация.
Плюс есть еще одна хитрость:
http://facebook.github.io/react/docs/class-name-manipulation.html


Функция render немного преобразится:```javascript
render: function () {
var maleClass = GenderViewHelper.generateMaleClass(this.props.gender, ['btn']);
var femaleClass = GenderViewHelper.generateFemaleClass(this.props.gender, ['btn']);


return (
М
Ж

);
}


<h4>Вызов серверных <s>духов </s>данных </h4>
<blockquote><i>— Я духов вызывать могу из бездны!
— И я могу, и всякий это может.</i>
<i>Вопрос лишь в том, явятся ль они на зов.</i>
<b>Шекспир. Генрих IV.</b></blockquote>
<b>Исходное состояние:</b> тэг <i>viewHelper</i>
<b>Конечное состояние:</b> тэг <i>save-server</i>

Статья выходит на финишную прямую, поэтому пришла пора закреплять и сохранять. Отправим наши данные на сервер. 
Для этого добавим обработчик у кнопки по клику:```javascript
var Submit = React.createClass({
  handleClick: function () {
    this.props.onSubmit();
  },

  render: function () {
    return (
      <div className="form-actions">
        <button className="btn btn-primary"
                onClick={this.handleClick}>Submit</button>
      </div>
    );
  }
});

И будем ловить этот обработчик в корневом виджете.```javascript
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 (
...



);
}
});
```

Передавать объекты внутрь методов без предварительного копирования — плохая практика, на мой вкус. Мало ли кто изменит объект внутри функции, хак какой-нибудь наложит, а потом ищи, молись, стучи в бубен. Поэтому хорошая практика — работать с immutable data structure. Конечно, моя реализация клонирования жуть как несовершенна, но никто не мешает подключить нормальные библиотечные функции.
А как же у нас тогда будет выглядеть Manager, который сохраняет данные.
```javascript
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'а:
  1. Рендерим Views
  2. Views реагируют на handlers
  3. Handlers передают управление callbacks в корневой виджет — Controller.
  4. Сохраняем данные в хранилище state.
  5. State генерирует событие об изменении.
  6. Views начинают заново рендериться.
  7. И ситуация пошла по новому кругу.


Вместо послесловия


Все-таки ничто MVC-шное React не чуждо. M – state. Да, мы сохраняем наши данные в state, чем не привычная модель? Тем более он излучает события и вызывает изменения в жизненном цикле component'а. V – вся функция render, это одна большая View, которую скрестили вместе с шаблоном. С – корневой виджет, идеальный кандидат на роль контроллера, в нем бизнес встречает деньги данные начинают свое путешествие по props component'ов – V и лежат до востребования в M – state.

Таким образом, создатели React очень изящно подошли к реализации давно известного паттерна. Вместо разделения MVC на отдельные буквы они разделили lifecycle между ними. И получилось, честно говоря, очень здорово. Сейчас я могу с уверенностью сказать, что React — это мой любимый инструмент, работа с которым доставляет большую радость. Конечно, у него есть свои недостатки и свои болячки. Однако для каждого из них можно найти решение или hack. Ведь сейчас над ним работают люди, у которых hackathon является частью корпоративной культуры.

Если хабрасообществу понравится статья, то я напишу еще одну статью про React.
В ней я планирую осветить следующие темы:
  1. Масштабный рефакторинг.
  2. Масштабирование от простенькой формы к сложной форме со сложным динамическим поведением.
  3. Второе пришествие Менеджера, или требования опять поменялись.
  4. Подводные камни React.
  5. Как спастись от Лукавого jQuery.
  6. UPD: Stateless компоненты.
  7. Роутинг и общая организация приложения.
  8. Flux.

В своей работе я использовал следующие инструменты:
  1. Библиотека React.JS (https://github.com/facebook/react)
  2. Система сборки gulp (https://github.com/gulpjs/gulp)
  3. Модульная система webpack (https://github.com/webpack/webpack)
  4. Фреймворк connect (https://github.com/senchalabs/connect)
  5. Библиотека для promise и defer vow (https://github.com/dfilatov/vow)

P.S.

Эту статью я посвящаю своей маме – моей первой учительнице.
Теги:
Хабы:
Всего голосов 27: ↑23 и ↓4+19
Комментарии32

Публикации

Истории

Работа

Ближайшие события