React - относительно молодая библиотека, которая получила большую популярность, что привело к огромному числу компонентов для него. Сама библиотека React призывает писать слабосвязанный код, который является модульным и компонуемым.
В этом уроке я покажу вам как создать небольшое приложение и как разбить его на отдельные компоненты, которые общаются друг с другом. Наше веб-приложение будет предлагать людям найти некое дорогое им место и сохранить его в своем браузере с помощью localStorage. Места будут показаны на карте с помощью Google maps API.
Если вы еще не чувствуете себя способным написать первое серьезное приложение на React, рекомендую вам ознакомиться со статьей 5 практических примеров для изучения React
Подготавливаем рабочую среду
Первое, с чего нам следует начать - это устновить Node.js и менеджер пакетов Bower. Как это делать описывалось в статье Руководство по менеджеру пакетов Bower. Теперь нам нужно установить Git и выбрать директорию, в которой мы будем создавать наше приложение. У меня это директория node. Щелкаем по ней правой кнопкой мыши и выбираем в контекстном меню команду Git Bash here.
Отлично, у нас появилась консоль, которая хочет чтобы мы в ней что-нибудь написали. Создайте в вашей директории файл bower.json:
{
"name": "Vasia Pupkin",
"main": "",
"version": "0.0.1",
"homepage": "",
"authors": [
"<sss@gmail.com>"
],
"moduleType": [
"globals"
],
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jsx-requirejs-plugin": "~0.6.2",
"reflux": "~0.3.0"
}
}
После этого нам останется ввести команду bower install
и у нас установятся все необходимые пакеты.
Пишем код на React
Вот компоненты, которые мы будем с вами писать:
- App является основным компонентом. Он содержит методы для действий, которые могут быть выполнены пользователем: поиск, добавление местоположения в избранное и многое другое. Другие компоненты вложены внутри него.
- CurrentLocation показывает текущее местоположение на карте. Адреса могут быть добавлены, или удалены из избранного при нажатии на значок.
- LocationList выводит все любимые места, обращаясь при этом к LocationItem.
- LocationItem - индивидуальная локация. Нажатие на нее приводит к появлению соответствующего адреса на карте.
- Map интегрируется с библиотекой Gmaps и отображает карту из Google Maps.
- Search - это компонент, который оборачивается вокруг формы поиска. Нажатие иконку с лупой приводит к поискку требуемого адреса на карте.
Мы будем использовать Asynchronous module definition (AMD), которая позволяет создавать модули так, чтобы они и их зависимости могли быть загружены асинхронно. Асинхронная загрузка модулей позволяет улучшить скорость загрузки веб страницы в целом, поскольку модули загружаются одновременно с остальным контентом сайта. При наличии у модуля большого числа зависимостей работать с ним очень сложно. Чтобы избавиться от этой проблемы переделаем такой модуль на Simplified CommonJS. Вот как компактно он будет выглядеть
define(function(require, module, exports) {
var dep1 = require('dep1'),
dep2 = require('dep2'),
...
depN = require('depN');
module.exports = moduleName;
});
App.jsx
Создайте в вашей директории (напоминаю, что она у меня называется node) каталог app. в нем у нас будут лежать все наши модули. Первый и самый главный компонент App
мы назовем App.jsx (jsx - потому что там содержится jsx-элементы). Код в этом файле достаточно простой и не требует особых пояснений. Обратите внимание как мы подключаем модули Search, Map, CurrentLocation и LocationList.
define(function(require, exports, module) {
'use strict';
var React = require('react');
var Search = require('jsx!./Search');
var Map = require('jsx!./Map');
var CurrentLocation = require('jsx!./CurrentLocation');
var LocationList = require('jsx!./LocationList');
var App = React.createClass({
getInitialState: function(){
// Извлекаем любимые места из локального хранилища (local storage)
var favorites = [];
if(localStorage.favorites){
favorites = JSON.parse(localStorage.favorites);
}
// Т.к. Париж - столица моды, то пусть по умолчанию появляется он
return {
favorites: favorites,
currentAddress: 'Paris, France',
mapCoordinates: {
lat: 48.856614, lng: 2.3522219
}
};
},
toggleFavorite: function(address){
if(this.isAddressInFavorites(address)){
this.removeFromFavorites(address);
}
else{
this.addToFavorites(address);
}
},
addToFavorites: function(address){
var favorites = this.state.favorites;
favorites.push({address: address,timestamp: Date.now()});
this.setState({favorites: favorites});
localStorage.favorites = JSON.stringify(favorites);
},
removeFromFavorites: function(address){
var favorites = this.state.favorites;
var index = -1;
for(var i = 0; i < favorites.length; i++){
if(favorites[i].address == address){ index = i; break; }
}
// Если индекс найден в массиве favorites, то удалить его оттуда
if(index !== -1){
favorites.splice(index, 1);
this.setState({ favorites: favorites });
localStorage.favorites = JSON.stringify(favorites);
}
},
isAddressInFavorites: function(address){
var favorites = this.state.favorites;
for(var i = 0; i < favorites.length; i++){
if(favorites[i].address == address){
return true;
}
}
return false;
},
searchForAddress: function(address){
var self = this;
// Мы будем использовать функциональные
// возможности GMaps с помощью Google Maps API
GMaps.geocode({
address: address,
callback: function(results, status) {
if (status !== 'OK') return;
var latlng = results[0].geometry.location;
self.setState({
currentAddress: results[0].formatted_address,
mapCoordinates: {
lat: latlng.lat(),
lng: latlng.lng()
}
});
}
});
},
render: function() {
return (
<div>
<h1>Любимые места на карте Google с помощью React</h1>
<Search onSearch={this.searchForAddress} />
<Map lat={this.state.mapCoordinates.lat} lng={this.state.mapCoordinates.lng} />
<CurrentLocation address={this.state.currentAddress}
favorite={this.isAddressInFavorites(this.state.currentAddress)}
onFavoriteToggle={this.toggleFavorite} />
<LocationList locations={this.state.favorites} activeLocationAddress={this.state.currentAddress}
onClick={this.searchForAddress} />
</div>
);
}
});
module.exports = App;
});
В методе render мы запускаем другие компоненты. Каждый компоненты получает только нужные для работы данные, как атрибуты. В некоторых случаях мы также передаем методы, вызываемые дочерним компонентом, что является хорошим способом коммуникации компонентов, когда они изолированы друг от друга.
CurrentLocation.jsx
Следующий у нас CurrentLocation. Этот компонент представляет собой адрес, отображаемый в данный момент в теге Н4, с кликабельной звездочкой. Когда вы кликните по иконке, будет вызван метод toggleFavorite.
define(function(require, exports, module) {
'use strict';
var React = require('react');
var CurrentLocation = React.createClass({
toggleFavorite: function(){
this.props.onFavoriteToggle(this.props.address);
},
render: function(){
var starClassName = "glyphicon glyphicon-star-empty";
if(this.props.favorite){
starClassName = "glyphicon glyphicon-star";
}
return (
<div className="col-xs-12 col-md-6 col-md-offset-3 current-location">
<h4 id="save-location">{this.props.address}</h4>
<span className={starClassName}
onClick={this.toggleFavorite}
aria-hidden="true">
</span>
</div>
);
}
});
module.exports = CurrentLocation;
});
LocationList.jsx
LocationList принимает переданный ему массив из любимых местах и создает объект LocationItem для отображения ввиде Bootstrap-компонента List group:
define(function(require, exports, module) {
'use strict';
var React = require('react');
var LocationItem = require('jsx!./LocationItem');
var LocationList = React.createClass({
render: function(){
var self = this;
var locations = this.props.locations.map(function(l){
var active = self.props.activeLocationAddress == l.address;
// Обратите внимание, что мы передаем функцию обратного
// вызова onClick в LocationList каждому LocationItem.
return <LocationItem address={l.address} timestamp={l.timestamp}
active={active} onClick={self.props.onClick} />
});
if(!locations.length){ return null; }
return (
<div className="list-group col-xs-12 col-md-6 col-md-offset-3">
<span className="list-group-item active">Сохраненные локации</span>
{locations}
</div>
);
}
});
module.exports = LocationList;
});
LocationItem.jsx
LocationItem представляет собой элемент списка любимых мест. Он использует библиотеку Moment.js для расчета времени, прошедшего с момента добавления данного адреса в Избранное.
define(function(require, exports, module) {
'use strict';
var React = require('react');
var moment = require('./moment');
var LocationItem = React.createClass({
handleClick: function(){
this.props.onClick(this.props.address);
},
render: function(){
var cn = "list-group-item";
if(this.props.active){ cn += " active-location"; }
return (
<a className={cn} onClick={this.handleClick}>
{this.props.address}
<span className="createdAt">{ moment(this.props.timestamp).fromNow() }</span>
<span className="glyphicon glyphicon-menu-right"<>/span>
</a>
)
}
});
module.exports = LocationItem;
});
Map.jsx
Map - это специальный компонент. Он обрабатывает плагин Gmaps, который сам по себе не является компонентом React. Обратившись к методу componentDidUpdate, мы можем инициализировать реальную карту внутри div #map
, когда изменяется отображаемая локация.
define(function(require, exports, module) {
'use strict';
var React = require('react');
var Map = React.createClass({
componentDidMount: function(){
// Только componentDidMount вызывается когда компонент
// в первый раз добавляется на страницу. Вот почему
// следующий метод мы вызываем вручную. Это гарантирует,
// что наш код инициализации карты запускается впервые.
this.componentDidUpdate();
},
componentDidUpdate: function(){
if(this.lastLat==this.props.lat && this.lastLng==this.props.lng){
// Карта уже инициализирована по этому адресу.
// Возвращаемся из этого метода, чтобы не инициализировать
// повторно (накладно и будет мерцание).
return;
}
this.lastLat = this.props.lat;
this.lastLng = this.props.lng
var map = new GMaps({
el: '#map',
lat: this.props.lat,
lng: this.props.lng
});
// Добавляем маркер
map.addMarker({
lat: this.props.lat,
lng: this.props.lng
});
},
render: function(){
return (
<div className="map-holder">
<p>Загрузка...</p>
<div id="map"></div>
</div>
);
}
});
module.exports = Map;
});
Search.jsx
Компонент Search состоит из Bootstrap формы с input. При отправке формы вызывается метод searchForAddress.
define(function(require, exports, module) {
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var Search = React.createClass({
getInitialState: function() {
return { value: '' };
},
handleChange: function(event) {
this.setState({value: event.target.value});
},
handleSubmit: function(event){
event.preventDefault();
// При отправке формы, вызываем ф-ю обратного вызова onSearch,
// которая передается компоненту
this.props.onSearch(this.state.value);
// убираем курсор из поля ввода
ReactDOM.findDOMNode(this).querySelector('input').blur();
},
render: function() {
return (
<form id="geocoding_form" className="form-horizontal" onSubmit={this.handleSubmit}>
<div className="form-group">
<div className="col-xs-12 col-md-6 col-md-offset-3">
<div className="input-group">
<input type="text" className="form-control" id="address"
placeholder="Найти локацию..." value={this.state.value}
onChange={this.handleChange} />
<span className="input-group-btn">
<span className="glyphicon glyphicon-search" aria-hidden="true"></span>
</span>
</div>
</div>
</div>
</form>
);
}
});
module.exports = Search;
});
main.js
Осталось добавить компонент App на страницу. Добавление происходит в тег div со свойством id, равным main
(вы можете увидеть этот элемент в index.html, когда скачаете и разорхивируете наше готовое приложение на React и Google maps).
var React = require('react');
var App = require('./components/App');
React.render(
<App />,
document.getElementById('main')
);
В дополнение к этим файлам, я подключил библиотеку Gmaps и API JavaScript карт Google, от которой он зависит, с помощью тегов <script> в index.html.