React Native является относительно простым фреймворком для разработки кроссплатформенных приложений для iOS и Android на JavaScript, но во многих введениях по нему материал даётся сложным и запутанным образом. Перед началом работы нужно что-то устанавливать, настраивать, выяснять необходимость наличия компьютер Mac и прочее. Официальная документация написана неплохо, но введение в React Native можно сделать проще и понятнее.
В самом простом случае для создания и тестирования приложений React Native требуется только браузер и Интернет-соединение. В официальной документации есть возможность редактировать код примеров и сразу видеть результат изменений, но для экспериментов удобнее использовать браузерную игровую площадку Expo Snack, при помощи которой можно создавать проекты и тестировать их в браузере, на эмуляторе или устройстве.
Нюансы фреймворка мы будем рассматривать на примерах из документации, чтобы в дальнейшем вам было удобнее с ней работать.
Рассмотрим простой пример.
Для создания приложений в React Native используется расширенный JavaScript стандарта ES2015.
Строка
import React from 'react';
необходима для поддержки JSX. JSX - это синтаксис, позволяющий встраивать XML в JavaScript. Если строку импорта убрать, то блок кода
<View>
<Text>Hello world!</Text>
</View>
будет помечен как ошибочный.
В строке
import { Component } from 'react';
происходит подключение к проекту модуля Component. Без этой строки при создании класса пришлось бы указать
export React.Component.
Далее в коде создаётся класс HelloWorldApp, который является компонентом. Обратите внимание на то, что наш класс содержит в себе дочерние компоненты - View и Text. Это нужно понимать при работе с контекстом.
В React Native используется компонентный подход для создания приложений, согласно которому приложение рекомендуется составлять из повторно используемых компонентов, код каждого из которых располагается в отдельных файлах. React Native поставляется с готовыми встроенными компонентами, функциональность которых можно расширить при помощи собственных пользовательских компонентов, как показано в примере ниже.
Здесь LotsOfGreetings является родительским пользовательским компонентом, а Greeting - дочерним пользовательским компонентом, который выводит приветствие в зависимости от параметра name.
<Greeting name='Rexxar' /> - это структурный элемент, соответствующий компоненту Greeting.
В нашем случае простой компонент располагается в главном файле приложения, но с увеличением функциональности приложения компоненты размещают в отдельных файлах и подключают к главному файлу приложения при помощи директивы import.
Передача данных от родительского компонента дочернему производится посредством параметров props. Когда родитель перерисовывает дочерний компонент, то он отправляет ему параметры. Они доступны через this.props.
Фигурные скобки {} используются для вставки кода javascript в JSX структуру. Блочный javascript комментарий в JSX также необходимо заключить в фигурные скобки:
{/* Комментарий внутри JSX */}
Если требуется расширить функциональность какого либо встроенного компонента, например, Button, то для этого мы не раздуваем его функциональность при помощи добавления атрибутов и свойств, как это происхоит в html и javascript, а создаём новый компонент, наследующийся от базового Component. Можно написать и extends Button, но это не приведёт к наследованию от кнопки и создания кастомной кнопки. Внешний вид компонента и его функциональность задаются явно, а не наследуются от родительского класса, как будет видно далее.
Вернёмся к исходному примеру.
Строка импорта
import { Text, View } from 'react-native';
подключает к проекту классы компонентов, использующиеся в нём: Text - текстовая область и View - контейнерный компонент.
В каждом классе компонента необходимо определить метод render(), предназначенный для отрисовки его содержимого. Данный метод должен вернуть либо элемент React, либо null, если ничего отрисовывать не нужно.
В контексте React Native компоненты являются виджетами - объектами, имеющими видимое представление. Под отрисовкой (рендерингом) компонента понимается преобразование движком React Native разметки JSX в нативный вид путём вызова соответствующих методов API. В результате на экране устройства мы видим нативный интерфейс, а не его имитацию средствами web. При этом полученный интерфейс на iOS и Android будет отличаться. То есть, данный фреймворк не позволяет создать интерфейс, одинаково отображающийся и на iOS, и на Android.
Вся разметка компонента должна находиться в одном корневом контейнерном элементе. Следующий код вызовет ошибку:
<View>
<Text>Hello</Text>
</View>
<View>
<Text>world!</Text>
</View>
Для устранения ошибки второй контейнерный элемент <View> нужно вложить в первый.
Контейнерные компоненты могут содержать внутри себя другие компоненты. В контексте JSX контейнерный элементом является тот, который может отображать содержимое вложенных в него элементов. Например, элемент <Button> может содержать между открывающим и закрывающим тегом другие элементы, но это не делает его контейнером, так как данное содержимое не отображается.
Контейнерные и неконтейнерные элементы можно задать в форме самозакрывающегося тега:
<Button title="Кнопка" />
<View />
Настройка компонентов происходит при помощи параметров двух видов - props (начальные неизменяемые параметры) и state (изменяемое состояние). Props часто называют свойствами, но это вносит путаницу так как под свойствами мы привыкли понимать изменяемую сущность. Тогда что это, атрибут, реквизит, свойство для чтения? Props следует понимать как параметры, которые можно передать компонентам в момент их создания и которые остаются фиксированными на протяженни всего жизненного цикла компонента. В примере
<Button title="Кнопка" />
title является встроенным обязательным параметром, который нужно указать. Значение данного параметра можно получить, но его изменение не изменит надписи на кнопке:
this.props.title = "Новая кнопка"; // надпись на кнопке останется прежней - "Кнопка"
Более того, при перерисовке компонента (которое происходит при каждом изменении его состояния, о чём будет рассказано далее, изменении контекста или получении props от родителя) значения параметров сбрасываются в начальное состояние. Например, мы решили ввести свой параметр myParams:
<Button myParams="100" title="Кнопка" />
Где-то в коде можно будет обратиться к нему и изменить его значение:
this.params.myParams = "200"
В другом месте кода можно будет его использовать, но при перерисовки компонента значение this.params.myParams останется равным "100".
Наряду со встроенными параметрами, которые изначально имеют встроенные компоненты, можно использовать пользовательские параметры при создании пользовательского компанента:
<Greeting name='Rexxar' />, где name - пользовательский параметр
или в конструкторе компонента:
constructor(props){
super(props)
this.props.title = 'myTitle'
}
Для доступа к параметру внутри JSX используются фигурные скобки:
<Text>Hello {this.props.title}!</Text>
Как изменить значение параметра (например, title), если оно является неизменяемым? Для этого используется состояние state. Данные, которые нужно изменить, необходимо преобразовать в состояния:
<Button title={this.state.myTitle}/>
title - это параметр, а myTitle - это состояние, определённое в конструкторе так:
this.state = {'myTitle':'Показать приветствие'}
Имя состояния можно не заключать в кавычки. Если состояний несколько, то они перечисляются через запятую:
this.state = {
'myTitle':'Показать приветствие',
'name':'Mate',
'age':'25'
}
Преимущество данной записи состоит в том, что в случае организации цикла по свойствам компонента, в объект state попадут все указанные состояния. Если каждое состояние будет задано в отдельной строке при помощи this.state, то в выборку попадёт состояние, определённое последним.
Для получения состояни используется запись this.state.myTitle, а для изменения состояния используется метод setState:
this.setState({myTitle:'Новая надпись'})
Изменение состояния должно производиться только при помощи указанного метода. При любом изменении состояния компонента вызывается его метод render(). Каждое каждое лишнее действие в данном методе будет влиять на скорость отрисовки. При помощи специального метода можно откелючить перерисовку компонента.
В терминах React Native компонент может иметь много изначально заданных параметров (props), но находить в каждый момент времени в одном состоянии (state). При работе с компонентом мы меняем его состояние, а не параметры (или свойства). Предположим, в момент создания кнопка имеет какую-то надпись. Это значит то, что кнопка находится в состоянии отображения данной надписи. Если при помощи состояния надпись или её цвет были изменены, то это означает то, что теперь кнопка находится в другом состоянии с изменённой надписью или её цветом. Аналогичным образом можно описать состояние всех компонентов в проекте.
Атрибут ref
Вернёмся к кнопке:
<Button title={this.state.myTitle}/>
Текущее значение параметра title в нашем случае можно получить при помощи состояния this.state.myTitle. Но как получить значение параметра title, если ему была присвоена строка? Для этого используется атрибут ref, принимающий функцию обратного вызова, параметром которого является ссылка на компонент, в котором она используется:
<Button ref={(a) => this.myButton = a} title="Кнопка" onPress={this._onPress}/>
Теперь ссылка this.myButton будет указывать на компонент Button и можно будет получить значение его параметра title так:
this.myButton.props.title
или обратиться к методу компонента:
this.myInputText.clear()
Рассмотрим более сложный пример c использованием стилей и событий.
Стили похожи на CSS, но фреймворк не использует CSS, хотя и заимствует многое из web разработки. Стиль представляет собой объект. При использовании встроенного стиля он заключается в двойные фигурные скобки, так как объекты в javascript заключаются в фигурные скобки:
<Image style={{width: 193, height: 110}}/>
Для вызова событий можно использовать обычный вызов функции, функцию стрелки и привязку.
У функции стрелки отсутствует свой контекст, поэтому внутри данной функции this будет таким же, как и снаружи. При вызове обычной функции внешний контекст this не передаётся и, соответственно, внутри функции нельзя обратиться к параметрам и состоянию компонента. Если это и не требуется, то можно использовать вызов функции без привязки контекста:
<Button title={this.state.name} onPress={this._onPress}/>
Если внутри функции требуется контекст, то используется вызов с привязкой:
<Button title={this.state.name} onPress={this._onPress.bind(this)}/>
Здесь под this понимается контекст родительского компонента, а не дочернего элемента <Button>, как может показаться. В методе _onPress компонента невозможно получить значение параметра title элемента <Button>. Для ссылки на параметр элемента (дочернего компонента) необходимо получить ссылку на него при помощи атрибута ref, как было показано выше.
Для вызова функции с контекстом и параметрами последние перечисляются после this через запятую:
<Picker selectedValue = {this.state.user} onValueChange={this.updateUser.bind(this,'Привет')}>
Пользовательские аргументы передаются функции перед системными. При возникновении события onValueChange система передаёт функции обработки один параметр, в котором находится название выбранной опции компонента Picker. Тогда в первом параметре функции updateUser будет находится строка 'Привет', а во-втором - название выбранной опции.
Для уменьшения накладных расходов привязку можно заранее сделать в конструкторе и использовать состояние в качестве параметра:
constructor(props){
super(props);
this.state = {name:'Peter'};
this.updateUser = this.updateUser.bind(this,this.state.name);
}
Обратите внимание на то, что при использовании функции стрелки можно получить оба параметра, передаваемых системой обработчику события
onValueChange - название выбранной опции и её индекс, тогда как использовании обычной функции системой передаётся один параметр - название выбранной опции.
При изменении выбранного элемента Picker система вызывает встроенный метод onValueChange. Аналогичным образом в дочернем компоненте можно создать свой метод и затем вызвать его. Для этого создаём метод:
_onMySend = (par) => {
Alert.alert(par)
}
Добавляем его дочернему элементу в качестве параметра, например, onSend:
<myComponent onSend = {this._onMySend}>
А где-нибудь в дочернем компоненте вызываем наш метод:
this.props.onSend('Параметр 1');
Упростим вызов метода при помощи деструктуризации объекта:
const {onSend} = this.props;
onSend('Параметр 1')
Параметры props используются не только для хранения в них неизменяемых данных, но и в качестве ссылок на методы. Проще говоря, в элементе:
<Button title='Кнопка' onPress={()=> this.setState({'name':'Новое имя'})}/>
title и onPress являются параметрами props.
Как динамически добавить или удалить компонент? Для этого можно изменить состояние компонента, сделав его видимым или невидимым, или настроить отображение компонентов в функции отрисовки render(), которая в зависимости от того или иного условия будет отрисовывать нужную компонентную структуру. Разметка в React Native производится при помощи Flexbox.
В основе React Native лежит JavaScript и кажется, что это позволяет использовать такие же трюки и фокусы, как и в web разработке. Но это не так. И на мой взгляд основная сложность фреймворка как раз и заключается в понимании этого. На JavaScript объекты, методы и свойства можно создавать "на лету", есть возможность напрямую взаимодействовать со свойствами и др., а здесь используется традиционный подход к разработке приложений, при котором работа происходит в рамках определённой компонентной структуры, а не динамически создаваемой и загружаемой, как в web среде.