- PVSM.RU - https://www.pvsm.ru -

Создаем приложение на JavaScript с помощью React Native

В этом уроке мы будем изучать React Native – фреймворк от компании Facebook для создания нативных приложений под iOS и Android. У него много общего с другим очень популярным фреймворком от Facebook – React Javascript [1], который предназначен для построения декларативных пользовательских интерфейсов.

Создаем приложение на JavaScript с помощью React Native - 1

Примечание: это обновленный вариант урока, написанного Колином Эбергардом из iOS Team, содержащий ряд правок для версии React Native 0.22.

Но на данный момент и так существует достаточно фреймворков, использующих JavaScript для создания iOS-приложений, таких как PhoneGap [2] или Titanium [3]. Что же делает React Native особенным?

1. В отличие от PhoneGap, в React Native логика приложения пишется и работает на JavaScript, в то время как его интерфейс остается полностью нативным. Таким образом не требуется никаких компромиссов, характерных для HTML5 UI.
2. В отличие от Titanium, React вводит новый оригинальный и крайне эффективный подход к созданию пользовательских интерфейсов. Если говорить кратко, UI приложения выражается как функция текущего состояния приложения.

Ключевая особенность React Native в том, что его разработчики намерены привнести модель программирования React [1] в сферу разработки мобильных приложений. Важное уточнение: речь идет не о таком кроссплатформенном инструменте, с которым можно писать софт один раз и использовать его везде, а о таком, который можно изучить один раз и писать на нем везде. Данный урок предназначен для iOS-платформы, но, изучив весь изложенный материал, вы сможете без труда создавать также Android-приложения.

Если у вас есть опыт написания приложений на Objective-C или Swift, вы наверняка не обрадуетесь идее перехода на JavaScript. Но вместе с тем, второй пункт явно должен был заинтересовать Swift-разработчиков.

Несомненно, работая со Swift, вам приходилось изучать много новых и более эффективных способов шифрования алгоритмов, а также методик, способствующих преобразованию и неизменяемости. Тем не менее, способ построения UI здесь очень похож на тот, что используется при работе с Objective-C: он тоже основывается на UIKit и является императивным.

React за счет таких необычных понятий как Virtual DOM и согласование переносит функциональное программирование на слой пользовательского интерфейса.

В данном уроке по React Native мы будем создавать приложение по поиску недвижимости в Великобритании:

Создаем приложение на JavaScript с помощью React Native - 2

Если вы никогда прежде не работали с JavaScript, не волнуйтесь. Мы подробно разберем каждый шаг разработки. React использует для стилизации синтаксис наподобие CSS, который легко прочитать и понять, но в случае чего вы всегда можете обратиться к Mozilla Developer Network [4].

Интересно? Идем дальше.

Приступаем к работе

Для создания JavaScript-кода React Native использует Node.js [5], среду выполнения JavaScript. Если вы еще не установили себе Node.js, пора это сделать.

Сначала установим Homebrew [6], следуя инструкциям на сайте, а затем – Node.js, выполнив в окне терминала следующее:

brew install node

Затем с помощью homebrew установим watchman [7] – сервис для отслеживания изменения и поиска файлов от Facebook:

brew install watchman

React Native использует его, чтобы отслеживать изменения кода и делать соответствующие правки. Это что-то вроде Xcode, но выполняющего сборку каждый раз после сохранения файла.

Далее установим React Native Command Line Interface (CLI), используя npm:

npm install -g react-native-cli

Он использует Node Package Manager [8], чтобы вызвать и глобально установить CLI-инструмент; npm поставляется вместе с Node.js, его функция аналогична CocoaPods [9] или Carthage [10].

Для тех, кто хочет глубже разобраться с React Native, его исходный код находится в открытом доступе на GitHub [11].

Перейдите к папке, в которой вы хотите сохранить проект, и воспользуйтесь CLI-инструментом для его создания:

react-native init PropertyFinder

Эта строка создает начальный проект, в котором содержится всё необходимое для разработки и запуска приложения на React Native.

Если вы видите уведомление об устаревшей версии Node.js, убедитесь, что та, которую установил brew, является актуальной. Для этого выполните в терминале команду brew link --overwrite node.

Взглянув на созданные папки и файлы, вы обнаружите папку node_modules, в которой находится фреймворк React Native. Файл index.ios.js – это макет приложения, созданный CLI-инструментом. Обратите также внимание на папку ios – в ней содержится проект Xcode и небольшой код для интеграции с Bootstrap. Наконец, там есть и компоненты для Android, но мы не будем рассматривать их здесь.

Откройте файл проекта, сделайте его сборку и запустите. Симулятор отобразит следующее сообщение:

Создаем приложение на JavaScript с помощью React Native - 3

Примечание: На момент написания урока начальный проект, созданный CLI-инструментом React Native, выводил три предупреждения во время сборки. Потому не волнуйтесь, впервые увидев какие-либо уведомления от Xcode. Разработчики React Native знают об этой небольшой проблеме, и мы работаем вместе с ними над ее устранением в следующем релизе React Native.

Вероятно, вы также заметили всплывающее окно терминала с таким сообщением:

 ┌──────────────────────────────────────────────┐
 │  Running packager on port 8081.                                            │
 │                                                                            │
 │  Keep this packager running while developing on any JS projects. Feel      │
 │  free to close this tab and run your own packager instance if you          │
 │  prefer.                                                                   │
 │                                                                            │
 │  https://github.com/facebook/react-native                                  │
 │                                                                            │
 └──────────────────────────────────────────────┘
Looking for JS files in
   /Users/tomelliott/Desktop/Scratch/PropertyFinder
 
[6:15:40 PM] <START> Building Dependency Graph
[6:15:40 PM] <START> Crawling File System
[6:15:40 PM] <START> Loading bundles layout
[6:15:40 PM] <END>   Loading bundles layout (0ms)
[Hot Module Replacement] Server listening on /hot
 
React packager ready.
 
[6:15:41 PM] <END>   Crawling File System (747ms)
[6:15:41 PM] <START> Building in-memory fs for JavaScript
[6:15:42 PM] <END>   Building in-memory fs for JavaScript (653ms)
[6:15:42 PM] <START> Building in-memory fs for Assets
[6:15:42 PM] <END>   Building in-memory fs for Assets (277ms)
[6:15:42 PM] <START> Building Haste Map
[6:15:42 PM] <START> Building (deprecated) Asset Map
[6:15:42 PM] <END>   Building (deprecated) Asset Map (49ms)
[6:15:42 PM] <END>   Building Haste Map (400ms)
[6:15:42 PM] <END>   Building Dependency Graph (2094ms)

Это упаковщик React Native, работающий под управлением Node.js. Вскоре вы узнаете, для чего он нужен.

Не закрывайте окно терминала, пусть оно работает на фоне. Если вы случайно закрыли его, просто остановите и перезапустите проект с помощью Xcode.

Примечание: прежде чем мы заберемся в дебри кода, нужно определиться с выбором текстового редактора. Вам предстоит писать много JavaScript-кода, а Xcode явно не подходит для этого. Я использую Sublime Text [12], это недорогой и очень удобный инструмент. Но Atom [13], Brackets [14] или любой другой легковесный редактор тоже отлично подойдет.

Hello React Native

Прежде чем начать работу над приложением по поиску недвижимости, мы создадим приложение Hello World!.. По ходу дела я буду вводить новые компоненты и понятия.

Откройте файл index.ios.js в текстовом редакторе и удалите всё его содержимое, так как мы будем создавать приложение с нуля. Добавьте в начале файла следующее:

'use strict';

Эта директива объявляет строгий режим, который добавляет улучшенную обработку ошибок и налагает ограничения на некоторые элементы JavaScript. Проще говоря, он улучшает работу JavaScript.

Примечание: более подробную информацию о строгом режиме можно найти в статье Джона Резига под названием ECMAScript 5 Strict Mode, JSON, and More [15].

Затем добавьте эту строку:

var React = require('react-native');

Она загружает модуль react-native и присваивает его переменной React. React Native использует такую же технологию загрузки модуля, как и Node.js с функцией require, которая примерно эквивалентна подключению и импорту библиотек в Swift.

Примечание: более подробную информацию о модулях JavaScript можно найти в статье Эдди Османи о модульном JavaScript [16].

Далее добавьте следующее:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  }
});

Этот код задает единый стиль, который мы вскоре применим к тексту Hello World!.. Если у вас уже есть какой-либо опыт веб-разработки, вероятно, вы узнали эти свойства. Внешний вид класса StyleSheet, используемого для стилизации интерфейса, напоминает синтаксис широко применяемого в вебе языка Cascading Style Sheets (CSS) [4].

Итак, займемся непосредственно приложением. Добавьте следующий код прямо под переменной со стилями:

class PropertyFinderApp extends React.Component {
  render() {
    return React.createElement(React.Text, {style: styles.text}, "Hello World!");
  }
}

Да, это класс JavaScript.

Классы были добавлены в ECMAScript 6 (ES6). Поскольку JavaScript постоянно развивается, разработчики вынуждены ограничивать себя в используемых средствах ради сохранения совместимости со старыми системами или браузерами. И хотя iOS 9 не полностью поддерживает ES6, React Native использует инструмент под названием Babel [17], который автоматически переводит современный JavaScript в совместимый с устаревшими версиями JavaScript там, где это необходимо.

Примечание: если вы веб-разработчик, вы также можете использовать Babel в браузере. Так что теперь действительно не осталось оправданий для работы со старыми версиями JavaScript – даже для поддержки устаревших версий браузеров.

PropertyFinderApp расширяет React.Component, основной структурный элемент интерфейса React. Компоненты содержат неизменяемые свойства и изменяемые переменные состояния; они предоставляют метод для рендеринга. Приложение, над которым мы сейчас работаем, очень простое и нуждается только в методе рендеринга.

Компоненты React Native – это не классы UIKit, а их легковесные эквиваленты. Фреймворк обеспечивает преобразование дерева компонентов React в требуемый нативный интерфейс.

Наконец, добавим в конец файла эту строку:

React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });

AppRegistry определяет точку входа в приложение и предоставляет корневой компонент.

Сохраните изменения в index.ios.js и вернитесь к Xcode. Убедитесь, что схема PropertyFinder выбрана с одним из симуляторов iPhone, а затем соберите и запустите ваш проект. Через несколько секунд на экране отобразится ваше приложение Hello World!:

Создаем приложение на JavaScript с помощью React Native - 4

Это JavaScript-приложение, работающее на симуляторе, который отображает нативный UI – и это без помощи браузера.

Всё еще не верите? Убедитесь сами: выберите в Xcode DebugView DebuggingCapture View Hierarchy и вы увидите нативную иерархию представлений. Вы также заметите повсюду сущности UIWebView. Тест приложения отображается в RCTText. Но что это такое? Вернитесь в Xcode, выберите FileOpen Quickly… и введите RCTView.h. Обратите внимание, что RCTView наследует непосредственно от UIView. Выходит, всё работает отлично.

Создаем приложение на JavaScript с помощью React Native - 5

Хотите знать, как это работает? Откройте в Xcode AppDelegate.m и определите расположение application:didFinishLaunchingWithOptions:. Этот метод создает RCTRootView, который загружает JavaScript-приложение и рендерит результирующее представление.

Когда приложение запускается, RCTRootView загружает приложение из этого URL:

http://localhost:8081/index.ios.bundle

Вспомните окно терминала, которое было открыто, когда вы запускали это приложение. Оно запускает упаковщик и сервер, который обрабатывает запрос выше.

Откройте этот URL в Safari, и вы увидите JavaScript-код вашего приложения. Вы также должны обнаружить там код Hello World!, встроенный во фреймворк React Native.

Когда ваше приложение запускается, этот код загружается и выполняется фреймворком JavaScriptCore. В нашем случае он загружает компонент PropertyFinderApp и затем выстраивает нативное UIKit представление. Дальше в уроке мы поговорим об этом подробнее.

Hello World JSX

Созданное приложение использует React.createElement для построения простого интерфейса, преобразовываемого в нативный эквивалент с помощью React. И хотя текущий JavaScript-код читается легко, в случае более сложного UI со вложенными элементами он может превратиться в кашу.

Убедитесь, что приложение еще работает, затем вернитесь к редактированию файла index.ios.js и измените оператор return следующим образом:

return <React.Text style={styles.text}>Hello World (Again)</React.Text>;

Это JSX [18], расширение синтаксиса JavaScript, которое добавляет в JavaScript-код синтаксис наподобие HTML. Те, у кого уже есть опыт веб-разработки, заметят сходство с последним. Мы будем использовать JSX на протяжении всего урока.

Сохраните изменения в index.ios.js и вернитесь в симулятор. Нажмите Cmd+R, чтобы обновить сообщение на экране:

Создаем приложение на JavaScript с помощью React Native - 6

Перезапустить приложение на React Native так же просто, как обновить страницу браузера. Обратите внимание, что в таком случае отобразятся только те изменения, которые касались JavaScript-файлов. Во всех других случаях потребуется повторная сборка приложения в Xcode.

Поскольку в этом уроке мы будем работать с тем же набором JavaScript-компонентов, вы можете оставить приложение работать и обновлять его после сохранения изменений в index.ios.js.

Примечание: если вам интересно, во что преобразовывается JSX, взгляните на ‘bundle’ в браузере.

Полагаю, мы вполне наигрались с Hello World!, теперь пришло время создать настоящее приложение.

Добавляем навигацию

Приложение Property Finder использует стандартную стековую навигацию, предоставленную навигационным контроллером UIKit. Добавим это поведение.

В файле index.ios.js переименуйте класс PropertyFinderApp в HelloWorld:

class HelloWorld extends React.Component {

Оставим пока текст Hello World!, но он больше не будет корневым компонентом приложения.

Затем добавим ниже компонента HelloWorld следующий класс:

class PropertyFinderApp extends React.Component {
  render() {
    return (
      <React.NavigatorIOS
        style={styles.container}
        initialRoute={{
          title: 'Property Finder',
          component: HelloWorld,
        }}/>
    );
  }
}

Он создает навигационный контроллер, применяет стиль и устанавливает первоначальный маршрут к компоненту HelloWorld. В веб-разработке маршрутизация – это способ определения навигационной структуры приложения, где страницы – или маршруты – привязываются к соответствующим URL.

Далее подкорректируйте стили, добавив туда параметры контейнера, как показано ниже:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  },
  container: {
    flex: 1
  }
});

О том, что такое flex: 1, вы узнаете немного позже.

Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R, чтобы увидеть обновленный интерфейс:

Создаем приложение на JavaScript с помощью React Native - 7

Корневое представление навигационного контроллера соответствует тексту Hello World!.. Теперь у нас есть базовая навигационная структура текущего приложения. Пора добавить настоящий UI.

Создаем страницу поиска

Добавьте в проект новый файл под названием SearchPage.js и поместите его в одну папку с файлом index.ios.js. Добавьте в новый файл этот код:

'use strict';
 
var React = require('react-native');
var {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
  ActivityIndicatorIOS,
  Image,
  Component
} = React;

Мы уже рассматривали строгий режим и импорт в react-native, но следующий оператор присвоения – это нечто другое.

Это деструктурирующее присваивание, позволяющее извлекать множество свойств объекта и присваивать их переменным с одним оператором. Как результат, в оставшемся коде можно отбросить префикс React. К примеру, можно обращаться напрямую к StyleSheet, а не к React.StyleSheet. Деструктурирование также очень удобно использовать для управления массивами. Более подробную информацию о нем можно найти в этой статье [19].

Не закрывая файл SearchPage.js, добавьте внизу этот стиль:

var styles = StyleSheet.create({
  description: {
    marginBottom: 20,
    fontSize: 18,
    textAlign: 'center',
    color: '#656565'
  },
  container: {
    padding: 30,
    marginTop: 65,
    alignItems: 'center'
  }
});

Это тоже стандартные CSS-свойства. Возможно, данный способ задания стилей покажется вам менее удобным, чем использование Interface Builder, но этот подход определенно лучше, чем задавать свойства представления по одному в методах viewDidLoad().

Вставьте сам компонент непосредственно под стилями:

class SearchPage extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.description}>
          Search for houses to buy!
        </Text>
        <Text style={styles.description}>
          Search by place-name, postcode or search near your location.
        </Text>
      </View>
    );
  }
}

render отлично демонстрирует JSX и его структуру. Наряду со стилем вы можете очень просто визуализировать интерфейс, созданный этим компонентом: контейнер с двумя текстовыми подписями.

Напоследок добавим следующую строку в конец файла:

module.exports = SearchPage;

Она экспортирует класс SearchPage, что позволяет использовать его в других файлах.

Следующий шаг – обновить маршрутизацию приложения, чтобы установить другой первоначальный маршрут.

Откройте index.ios.js и добавьте эту строку сразу после require в начале файла:

var SearchPage = require('./SearchPage');

В функции render класса PropertyFinderApp обновите initialRoute, чтобы привязать только что созданную страницу, как показано ниже:

component: SearchPage

Теперь, если хотите, можно удалить класс HelloWorld и его стили. Они вам больше не понадобятся.
Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R, чтобы увидеть обновленный интерфейс:

Создаем приложение на JavaScript с помощью React Native - 8

Здесь используется новый компонент SearchPage.

Стилизуем с помощью Flexbox

В этом уроке мы уже имели дело с некоторыми базовыми свойствами CSS, задающими параметры цвета, а также внутренних и внешних отступов. Тем не менее, вероятно, вы еще не слышали о flexbox. Эта технология лишь недавно была добавлена в спецификацию CSS, она очень полезна при построении макетов пользовательских интерфейсов.

React Native использует библиотеку css-layout [20], которая является JavaScript-реализацией flexbox-стандарта, скомпилированного на C (для iOS) и на Java (для Android).
Очень хорошо, что React Native создавался как отдельный проект, нацеленный на несколько языков программирования, так как это позволяет разрабатывать приложения с использованием новейших подходов, вроде применения flexbox-макетов к SVG [21].

По умолчанию контейнер в вашем приложении имеет направление потока данных в виде столбца, что соответствует параметру column – а значит, всё содержимое контейнера будет выстраиваться вертикально:

Создаем приложение на JavaScript с помощью React Native - 9

Это так называемая главная ось, или main axis, она может иметь как горизонтальное, так и вертикальное направление.

Вертикальное положение каждого дочернего элемента контейнера высчитывается с учетом его внешних и внутренних отступов, а также высоты. Контейнер также задает свойству alignItems значение center, что определяет положение дочерних элементов на главной оси. В данном случае мы получим текст с выравниванием по центру.

Теперь добавим поле ввода и кнопки. Откройте файл SearchPage.js и введите следующий код сразу после закрывающего тега второго элемента Text:

<View style={styles.flowRight}>
  <TextInput
    style={styles.searchInput}
    placeholder='Search via name or postcode'/>
  <TouchableHighlight style={styles.button}
      underlayColor='#99d9f4'>
    <Text style={styles.buttonText}>Go</Text>
  </TouchableHighlight>
</View>
<TouchableHighlight style={styles.button}
    underlayColor='#99d9f4'>
  <Text style={styles.buttonText}>Location</Text>
</TouchableHighlight>

Мы добавили два представления высшего уровня: в одном из них находится текстовое поле ввода и кнопка, а в другом – еще одна кнопка. Сейчас вы узнаете, как можно стилизовать эти элементы

Вернитесь к параметрам стилей, поставьте запятую после блока container и добавьте ниже новые стили:

flowRight: {
  flexDirection: 'row',
  alignItems: 'center',
  alignSelf: 'stretch'
},
buttonText: {
  fontSize: 18,
  color: 'white',
  alignSelf: 'center'
},
button: {
  height: 36,
  flex: 1,
  flexDirection: 'row',
  backgroundColor: '#48BBEC',
  borderColor: '#48BBEC',
  borderWidth: 1,
  borderRadius: 8,
  marginBottom: 10,
  alignSelf: 'stretch',
  justifyContent: 'center'
},
searchInput: {
  height: 36,
  padding: 4,
  marginRight: 5,
  flex: 4,
  fontSize: 18,
  borderWidth: 1,
  borderColor: '#48BBEC',
  borderRadius: 8,
  color: '#48BBEC'
}

Следите за форматированием: каждое свойство стиля или селектор следует отделять запятой.

Эти стили предназначены для только что добавленных поля ввода и кнопок.

Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R, чтобы увидеть обновленный интерфейс:

Создаем приложение на JavaScript с помощью React Native - 10

Текстовое поле и кнопка ‘Go’ находятся на одной строке, так как вы поместили их в контейнер со стилем flowRight, элементы которого выстраиваются в строку за счет свойства flexDirection: 'row'. Вместо того чтобы жестко задавать ширину каждого из этих элементов, мы выставили им относительную ширину с помощью значений свойства flex. Таким образом, в селекторе текстового поля searchInput мы имеем flex: 4, а в селекторе кнопки button — flex: 1, вследствие чего их соотношение составляет 4:1.

Возможно, вы также заметили, что элементы, которые мы называем кнопками, по сути таковыми не являются. На самом деле, кнопки в UIKit – это всего лишь интерактивные текстовые надписи. Кнопки вашего приложения используют компонент React Native под названием TouchableHighlight, который по нажатию становится прозрачным и показывает нижележащий цвет.

Наконец, добавим на страницу поиска изображение. Вы можете скачать его в нескольких разрешениях одним архивом [22]. После скачивания распакуйте архив.

Далее создадим в корневом проекте директорию под названием ’Resources’ и поместим в нее все три изображения.

Предметные каталоги: Как вам известно, специалисты из Apple рекомендуют по возможности помещать изображения в предметные каталоги. Тем не менее, для React Native это наоборот нежелательно [23]. Хранение цифровых объектов приложения рядом с его компонентами дает несколько преимуществ. Во-первых, это позволяет сохранить независимость компонентов. Во-вторых, при добавлении новых изображений не требуется повторная загрузка приложения. И в-третьих, при разработке приложения для iOS и Android это дает возможность хранить изображения для двух платформ в одном месте.

Вернитесь к файлу SearchPage.js и добавьте эту строку под закрывающим тегом компонента TouchableHighlight, отвечающего за кнопку location:

<Image source={require('./Resources/house.png')} style={styles.image}/>

Теперь добавьте соответствующий стиль изображения в блоке со стилями, не забыв поставить запятую после предыдущего селектора:

image: {
  width: 217,
  height: 138
}

Сохраните изменения. Вернитесь в симулятор и нажмите Cmd+R, чтобы увидеть новый интерфейс:

Создаем приложение на JavaScript с помощью React Native - 11

Примечание: Если изображение с домом не отображается, а вместо него показано уведомление о том, что картинка не найдена, попробуйте перезапустить упаковщик с помощью команды npm start в терминале.

Наше приложение уже смотрится симпатично, но всё же чего-то не хватает. Нужно добавить состояние приложения и выполнить кое-какие действия.

Добавляем состояние компонента

Каждый компонент в React имеет собственный объект состояния, который используется как хранилище типа «ключ–значение». Прежде чем компонент отобразится, нужно задать начальное состояние.

В файле SearchPage.js добавьте следующий код в класс SearchPage, прямо перед render():

constructor(props) {
  super(props);
  this.state = {
    searchString: 'london'
  };
}

Теперь у вашего компонента есть переменная state, а начальным значением searchString является london.

Время воспользоваться этим состоянием компонента. Изменим элемент TextInput в render, как показано ниже:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  placeholder='Search via name or postcode'/>

Мы выставили значение свойства TextInput – то есть показываемого пользователю текста – на текущее значение переменной состояния searchString. Итак, мы позаботились о начальном состоянии. Но что будет, когда пользователь отредактирует этот текст?

Прежде всего, создадим метод, выступающий в роли обработчика событий. Перейдите к классу SearchPage и добавьте данный метод сразу после constructor:

onSearchTextChanged(event) {
  console.log('onSearchTextChanged');
  this.setState({ searchString: event.nativeEvent.text });
  console.log(this.state.searchString);
}

Он берет значение из свойства text в событии родного браузера и использует его для обновления состояния компонента. Он также добавляет код для сбора данных, которые нам вскоре пригодятся.

Чтобы данный метод вызывался каждый раз при изменении текста, вернемся к полю TextInput метода render и добавим свойство onChange. В итоге тег примет следующий вид:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  onChange={this.onSearchTextChanged.bind(this)}
  placeholder='Search via name or postcode'/>

Когда пользователь меняет текст, вызывается функция, добавленная к свойству onChange (в данном случае это onSearchTextChanged).

Примечание: Возможно, вам непонятно, для чего нужно выражение bind(this). JavaScript интерпретирует ключевое слово this немного иначе, чем большинство других языков. В Swift данному слову соответствует self. Использование bind в данном контексте гарантирует, что this внутри метода onSearchTextChanged является отсылкой к экземпляру компонента. Дополнительную информацию о ключевом слове this можно получить на MDN [24].

Прежде чем мы снова обновим приложение, добавим оператор log в начале render(), сразу перед return:

console.log('SearchPage.render');

Вам предстоит узнать нечто очень любопытное об этих операторах.

Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R. Вы увидите, что теперь начальным значением поля ввода является ‘london’, а при редактировании текста в консоль Xcode записываются какие-то выражения:

Создаем приложение на JavaScript с помощью React Native - 12

Если вы внимательно посмотрите на скриншот выше, то можете заподозрить, что порядок записи выражений немного нарушен:

1. Это первоначальный вызов render(), необходимый чтобы настроить представление.
2. При изменении текста вызывается onSearchTextChanged().
3. Затем обновляется состояние компонента, чтобы отобразить только что введенный текст, который повторно запускает render.
4. onSearchTextChanged() завершает всё, записывая новую строку поиска.

Когда приложение обновляет состояние любого компонента React, это запускает повторный рендеринг всего пользовательского интерфейса, который, в свою очередь, вызывает render всех компонентов. Это превосходная идея, так как в этом случае логика рендеринга полностью отделяется от изменений состояния, которые затрагивают UI.

В большинстве UI-фреймворков разработчику нужно либо вручную обновлять интерфейс в зависимости от изменений состояния, либо делать это посредством каких-либо вспомогательных фреймворков, которые создают неявную связь между состоянием приложения и его представлением в UI. Что касается второго варианта, по этому поводу можно ознакомиться со статьей о применении шаблона MVVM совместно с фреймворком ReactiveCocoa [25].

С React вам больше не придется волноваться о том, какие части UI могут быть затронуты изменением состояния, так как весь интерфейс выражается в виде функции состояния приложения.

На данном этапе вы, скорее всего, заметили один изъян данного подхода. Совершенно верно, дело в производительности.

Конечно, нельзя просто так отбрасывать весь интерфейс и перестраивать его каждый раз, когда что-то меняется. Вот где React по-настоящему проявляет себя. Каждый раз, когда интерфейс рендерится, он берет дерево видимых объектов, которое возвращают методы render, и согласовывает его с текущим представлением UIKit. В результате этого согласования получается список обновлений, которые React должен применить к текущему представлению. Таким образом, повторному рендерингу подвергнутся только те объекты, которые на самом деле были изменены.

Разве не здорово видеть, как в нашем iOS-приложении применяются новейшие подходы, за счет которых ReactJS является таким уникальным: virtual-DOM (объектная модель документа, визуальное дерево веб-документа) и согласование?

Позже у вас будет возможность разобраться с этим подробнее, а пока нужно еще поработать над приложением. Для начала удалим код сбора данных, который мы добавили ранее, так как он только засоряет общий код.

Настраиваем поиск

Чтобы реализовать поиск, нужно обработать нажатие кнопки ‘Go’, создать необходимый API-запрос и предоставить пользователю визуальное подтверждение того, что запрос обрабатывается.

Откройте файл SearchPage.js, найдите constructor и обновите внутри него начальное состояние:

this.state = {
  searchString: 'london',
  isLoading: false
};

Новое свойство isLoading будет следить за тем, обрабатывается ли запрос.

Добавьте такую логику в начале render:

var spinner = this.state.isLoading ?
  ( <ActivityIndicatorIOS
      size='large'/> ) :
  ( <View/>);

Это тернарный оператор if, который либо добавляет индикатор выполнения действия, либо отображает пустой экран – в зависимости от состояния компонента isLoading. Поскольку весь компонент рендерится каждый раз заново, вы можете спокойно смешивать логику JSX и JavaScript.

Чтобы добавить на страницу индикатор загрузки, перейдите к JSX, отвечающему за интерфейс поиска в return, и вставьте под Image эту строку:

{spinner}

Затем добавьте данные методы в класс SearchPage:

_executeQuery(query) {
  console.log(query);
  this.setState({ isLoading: true });
}
 
onSearchPressed() {
  var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
  this._executeQuery(query);
}

Метод _executeQuery() впоследствии будет выполнять запрос, но пока он просто заносит сообщение в консоль и необходимым образом настраивает компонент isLoading, чтобы в интерфейсе отобразилось новое состояние.

Примечание: Классы в JavaScript не имеют модификаторов доступа, потому ключевого слова ‘private’ для них тоже не существует. В силу этого многие разработчики часто дают методам префикс в виде нижнего подчеркивания, чтобы указать, что они являются private.

Метод onSearchPressed() настраивает и отправляет запрос. Он должен срабатывать по нажатию кнопки ‘Go’. Чтобы реализовать это, вернитесь к render и добавьте следующее свойство внутри открывающего тега компонента TouchableHighlight, отвечающего за текст ‘Go’:

onPress={this.onSearchPressed.bind(this)}

Наконец, добавьте эту служебную функцию над объявлением класса SearchPage:

function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;
 
  var querystring = Object.keys(data)
    .map(key => key + '=' + encodeURIComponent(data[key]))
    .join('&');
 
  return 'http://api.nestoria.co.uk/api?' + querystring;
};

Эта функция не зависит от SearchPage, потому она реализуется скорее как свободная функция, а не как метод. Сначала она создает строку запроса, основываясь на параметрах данных. Исходя из них, она преобразовывает данные в требуемый формат строки: пары name=value, разделенные амперсандами. Синтаксис => соответствует стрелочной функции, еще одному недавнему дополнению в JavaScript [26]. Стрелочные функции предоставляют более лаконичный синтаксис для создания анонимных функций.

Сохраните изменения, вернитесь к симулятору, перезагрузите страницу с помощью комбинации Cmd+R и нажмите кнопку ‘Go’. На экране отобразится индикатор загрузки. Обратите внимание на консоль Xcode:

Создаем приложение на JavaScript с помощью React Native - 13

На экране показан индикатор активности, а в логе появляется URL для требуемого запроса. Откройте этот URL в браузере, чтобы увидеть результат: большой JSON-объект. Но не волнуйтесь, вам не нужно будет вникать в него. Сейчас мы добавим код и проведем его парсинг.

Примечание: Данное приложение использует для поиска недвижимости Nestoria API [27]. JSON-ответ, который приходит из API, весьма незамысловат. Так или иначе, вы всегда можете просмотреть документацию на предмет предполагаемого формата URL-запросов и ответов.

Следующий шаг – выполнить запрос из приложения.

Выполняем API-запрос

Не закрывая файл SearchPage.js, обновите начальное положение в конструкторе класса, чтобы добавить переменную message:

this.state = {
  searchString: 'london',
  isLoading: false,
  message: ''
};

Найдите render и добавьте внутри него следующую строку:

<Text style={styles.description}>{this.state.message}</Text>

Она отвечает за отображение внизу экрана сообщений для пользователя.

Найдите класс SearchPage и добавьте в конец метода _executeQuery() данный код:

fetch(query)
  .then(response => response.json())
  .then(json => this._handleResponse(json.response))
  .catch(error =>
     this.setState({
      isLoading: false,
      message: 'Something bad happened ' + error
   }));

Он использует функцию fetch, которая является частью Web API [28] и предоставляет значительно улучшенный API по сравнению с XMLHttpRequest. Асинхронный ответ возвращается в виде объекта promise [29], и в случае успеха выполняется парсинг JSON-объекта, который затем передается в метод _handleResponse (мы добавим его немного позже).

Наконец, добавим эту функцию в SearchPage:

_handleResponse(response) {
  this.setState({ isLoading: false , message: '' });
  if (response.application_response_code.substr(0, 1) === '1') {
    console.log('Properties found: ' + response.listings.length);
  } else {
    this.setState({ message: 'Location not recognized; please try again.'});
  }
}

Она очищает isLoading и при успешном запросе добавляет в лог количество найденных объектов недвижимости.

Примечание: В Nestoria API есть ряд весьма полезных кодов ответа сервера [30]. К примеру, коды 202 и 200 возвращают список локаций, подобранных по принципу наилучшего выбора. Почему бы не воспользоваться этой опцией и не предоставить пользователям несколько дополнительных предложений?

Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R. Попробуйте ввести поисковый запрос ‘london’. Вы должны увидеть в логе сообщение о том, что было найдено 20 объектов недвижимости (стандартное для результата количество). А теперь попробуйте ввести неверный запрос, например ‘narnia’. Вы увидите следующее сообщение:

Создаем приложение на JavaScript с помощью React Native - 14

Время поработать над выводом результата поиска на экран.

Выводим результат поиска

Создайте новый файл под названием SearchResults.js и добавьте в него следующее:

'use strict';
 
var React = require('react-native');
var {
  StyleSheet,
  Image,
  View,
  TouchableHighlight,
  ListView,
  Text,
  Component
} = React;

Всё верно, это выражение require, включающее модуль react-native и деструктурирующее присваивание.

Затем добавьте сам компонент:

class SearchResults extends Component {
 
  constructor(props) {
    super(props);
    var dataSource = new ListView.DataSource(
      {rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.listings)
    };
  }
 
  renderRow(rowData, sectionID, rowID) {
    return (
      <TouchableHighlight
          underlayColor='#dddddd'>
        <View>
          <Text>{rowData.title}</Text>
        </View>
      </TouchableHighlight>
    );
  }
 
  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)}/>
    );
  }
 
}

Код выше использует специальный компонент – ListView, который отображает данные внутри контейнера прокрутки в несколько рядов, почти как в UITableView. Данные попадают в ListView через ListView.DataSource и функцию, передающую UI для каждого ряда.

Создавая источник данных, вы предоставляете функцию, которая проверяет на идентичность пару рядов. ListView затем использует результат во время процесса согласования, чтобы выявить изменения в данных списка. В нашем примере Nestoria API возвращает объекты недвижимости со свойством guid, что хорошо подходит для наших целей.

Добавьте в конец файла экспорт модуля:

module.exports = SearchResults;

А эту строку нужно вставить в начале файла SearchPage.js, ниже вызова require для React:

var SearchResults = require('./SearchResults');

Это позволит нам использовать класс SearchResults изнутри класса SearchPage:

Измените метод _handleResponse, заменив выражение console.log на следующее:

this.props.navigator.push({
  title: 'Results',
  component: SearchResults,
  passProps: {listings: response.listings}
});

Этот код переходит к компоненту SearchResults и передает объекты недвижимости из API-запроса. Использование push-метода обеспечивает загрузку результатов поиска в стек переходов, вследствие чего у вас появится кнопка ‘Back’, чтобы вернуться в корневой каталог.

Сохраните изменения, вернитесь в симулятор, нажмите Cmd+R и попробуйте выполнить поиск. Вы увидите список объектов недвижимости:

Создаем приложение на JavaScript с помощью React Native - 15

Наконец-то мы получили настоящий список. Правда, выглядит он пока скучновато. Давайте немного преобразим его.

Применяем стилизацию

Постепенно код React Native начинает приобретать знакомый вам вид, поэтому мы немного ускорим темп работы.

Добавим данное определение стиля сразу после деструктурирующего присваивания в файле SearchResults.js:

var styles = StyleSheet.create({
  thumb: {
    width: 80,
    height: 80,
    marginRight: 10
  },
  textContainer: {
    flex: 1
  },
  separator: {
    height: 1,
    backgroundColor: '#dddddd'
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    color: '#656565'
  },
  rowContainer: {
    flexDirection: 'row',
    padding: 10
  }
});

Здесь содержатся все стили для отображения каждого ряда данных.

Заменим renderRow() следующим кодом:

renderRow(rowData, sectionID, rowID) {
  var price = rowData.price_formatted.split(' ')[0];
 
  return (
    <TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
        underlayColor='#dddddd'>
      <View>
        <View style={styles.rowContainer}>
          <Image style={styles.thumb} source={{ uri: rowData.img_url }} />
          <View  style={styles.textContainer}>
            <Text style={styles.price}>£{price}</Text>
            <Text style={styles.title}
                  numberOfLines={1}>{rowData.title}</Text>
          </View>
        </View>
        <View style={styles.separator}/>
      </View>
    </TouchableHighlight>
  );
}

Этот код обрабатывает данные цен, полученных в формате ‘300,000 GBP’ и убирает оттуда GBP. Затем он отображает строку интерфейса, используя подход, с которым вы, скорее всего, уже хорошо знакомы. Изображение (Image) загружается из возвращенного URL (rowData.img_url), который React Native берет из основного потока, и добавляется в ряд.

Также обратите внимание на использование стрелочной функции в свойстве onPress компонента TouchableHighlight. С ее помощью захватывается guid для ряда.

Последний шаг – добавить этот метод в класс, чтобы управлять нажатием на экран:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
}

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

Сохраните изменения, вернитесь в симулятор и нажмите Cmd+R, чтобы увидеть обновленный список:

Создаем приложение на JavaScript с помощью React Native - 16

Выглядит отлично… если закрыть глаза на цены.

Наконец, пришло время добавить последнюю страницу.

Добавляем страницу просмотра информации о недвижимости

Создайте в проекте новый файл под названием PropertyView.js и введите туда этот код:

'use strict';
 
var React = require('react-native');
var {
  StyleSheet,
  Image,
  View,
  Text,
  Component
} = React;

Уверен, вы уже способны набрать его даже с закрытыми глазами.

Теперь добавим стили:

var styles = StyleSheet.create({
  container: {
    marginTop: 65
  },
  heading: {
    backgroundColor: '#F8F8F8',
  },
  separator: {
    height: 1,
    backgroundColor: '#DDDDDD'
  },
  image: {
    width: 400,
    height: 300
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    margin: 5,
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    margin: 5,
    color: '#656565'
  },
  description: {
    fontSize: 18,
    margin: 5,
    color: '#656565'
  }
});

И сам компонент:

class PropertyView extends Component {
 
  render() {
    var property = this.props.property;
    var stats = property.bedroom_number + ' bed ' + property.property_type;
    if (property.bathroom_number) {
      stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
        ? 'bathrooms' : 'bathroom');
    }
 
    var price = property.price_formatted.split(' ')[0];
 
    return (
      <View style={styles.container}>
        <Image style={styles.image}
            source={{uri: property.img_url}} />
        <View style={styles.heading}>
          <Text style={styles.price}>£{price}</Text>
          <Text style={styles.title}>{property.title}</Text>
          <View style={styles.separator}/>
        </View>
        <Text style={styles.description}>{stats}</Text>
        <Text style={styles.description}>{property.summary}</Text>
      </View>
    );
  }
}

Часто случается так, что API возвращает данные низкого качества и с пропущенными полями. Потому первая часть render() проводит обработку данных для частичного улучшения их качества.

Остальная его часть весьма очевидна: это функция неизменяемого состояния данного компонента.

Теперь добавим экспорт в конец файла:

module.exports = PropertyView;

Вернитесь к SearchResults.js и добавьте выражение require в начало файла, сразу после строки React require:

var PropertyView = require('./PropertyView');

Затем обновите rowPressed(), чтобы перемещаться по PropertyView:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
 
  this.props.navigator.push({
    title: "Property",
    component: PropertyView,
    passProps: {property: property}
  });
}

Порядок действий вам знаком: сохраняем, возвращаемся в симулятор и нажимаем Cmd+R. Теперь можно выполнить поиск, выбрать любой объект и перейти к просмотру информации о нем:

Создаем приложение на JavaScript с помощью React Native - 17

Эх, вот что я называю доступным жильем!

Приложение почти готово, осталось только добавить опцию поиска по геолокации.

Реализуем поиск по местоположению

Откройте в Xcode файл Info.plist и добавьте напротив NSLocationWhenInUseUsageDescription следующее значение:

PropertyFinder would like to use your location to find nearby properties – 

Вот как будет выглядеть ваш plist-файл после добавления нового значения:

Создаем приложение на JavaScript с помощью React Native - 18

Это информация, которую приложение будет отображать пользователям при попытке запросить их местоположение.

Откройте SearchPage.js, перейдите к компоненту TouchableHighlight, отвечающему за отображение кнопки ‘Location’, и добавьте это значение:

onPress={this.onLocationPressed.bind(this)}

По нажатию кнопки вызывается метод onLocationPressed (мы сейчас его добавим).

Вставьте внутрь класса SearchPage следующий код:

onLocationPressed() {
  navigator.geolocation.getCurrentPosition(
    location => {
      var search = location.coords.latitude + ',' + location.coords.longitude;
      this.setState({ searchString: search });
      var query = urlForQueryAndPage('centre_point', search, 1);
      this._executeQuery(query);
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your location: ' + error
      });
    });
}

Данные о текущем местоположении берутся с помощью navigator.geolocation. Это интерфейс Web API [31], потому он должен быть понятен каждому, кто имел дело с браузерными сервисами определения геолокации. React Native предоставляет собственную реализацию данного API, используя нативные средства iOS.

В случае успешного определения текущего местоположения будет вызвана первая стрелочная функция. Она отправит запрос в Nestoria. Если же что-то пойдет не так, отобразится стандартное сообщение.

Так как мы редактировали plist, потребуется перезапустить приложение, чтобы увидеть изменения. На этот раз, увы, без Cmd+R. Остановите приложение в Xcode и выполните его повторную сборку. Затем запустите проект.

Прежде чем использовать поиск по геолокации, стоит убедиться, что база данных Nestoria содержит информацию о вашем регионе. Выберите в меню симулятора DebugLocationCustom Location … и введите широту с долготой: к примеру, 55.02 и -1.42 соответственно. Это координаты одного живописного городка на побережье северной части Англии, откуда я родом.

Теперь нажмите кнопку Location, разрешите приложению определять местоположение и смотрите результат.

Создаем приложение на JavaScript с помощью React Native - 19

Примечание от Рэя: Поиск по местоположению сработал не у всех. Как правило, возникала ошибка доступа, несмотря на то, что определение местоположения было разрешено. Мы пока не до конца разобрались с этой ситуацией. Возможно, это проблема самого React Native. Если кому-либо удалось исправить эту ошибку, пожалуйста, напишите нам.

Конечно, это не Лондон, но цены гораздо приятнее.

Что дальше?

Поздравляю с созданием вашего первого приложения на React Native. Вы можете скачать себе весь проект [32], если хотите просмотреть мой код.

Если вы веб-разработчик, теперь вам должно быть понятнее, как можно без труда создать полностью нативный интерфейс и навигацию приложения, используя JavaScript и React. Если вы разработчик нативных приложений, надеюсь, вы оценили по достоинству такие преимущества React Native, как быстрое взаимодействие с приложением, современный JavaScript и понятные правила стилизации в духе CSS.

Возможно, ваше следующее приложение будет написано на этом фреймворке? А может быть, вы всё равно продолжите работать со Swift или Objective-C? Так или иначе, надеюсь, вам удалось найти в этой статье что-нибудь новое и полезное для будущих проектов.

Если же у вас есть вопросы или комментарии по поводу данного урока, присоединяйтесь к обсуждению.

Автор: Plarium

Источник [33]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/139867

Ссылки в тексте:

[1] React Javascript: https://facebook.github.io/react/

[2] PhoneGap: http://phonegap.com/

[3] Titanium: http://www.appcelerator.com/mobile-app-development-products/

[4] Mozilla Developer Network: https://developer.mozilla.org/en-US/docs/Web/CSS

[5] Node.js: https://nodejs.org/en/

[6] Homebrew: http://brew.sh/

[7] watchman: https://facebook.github.io/watchman/

[8] Node Package Manager: https://www.npmjs.com/

[9] CocoaPods: https://cocoapods.org/

[10] Carthage: https://github.com/Carthage/Carthage

[11] в открытом доступе на GitHub: https://github.com/facebook/react-native

[12] Sublime Text: http://www.sublimetext.com/

[13] Atom: https://atom.io/

[14] Brackets: http://brackets.io/

[15] ECMAScript 5 Strict Mode, JSON, and More: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/

[16] статье Эдди Османи о модульном JavaScript: https://addyosmani.com/writing-modular-js/

[17] Babel: https://babeljs.io/

[18] JSX: https://facebook.github.io/react/docs/jsx-in-depth.html

[19] можно найти в этой статье: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

[20] css-layout: https://github.com/facebook/css-layout

[21] flexbox-макетов к SVG: http://blog.scottlogic.com/2015/02/02/svg-layout-flexbox.html

[22] одним архивом: http://www.raywenderlich.com/wp-content/uploads/2015/03/ReactNative-HouseImage.zip

[23] наоборот нежелательно: http://facebook.github.io/react-native/docs/images.html#content

[24] MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

[25] шаблона MVVM совместно с фреймворком ReactiveCocoa: https://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1

[26] недавнему дополнению в JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

[27] поиска недвижимости Nestoria API: http://www.nestoria.co.uk/help/api

[28] частью Web API: https://fetch.spec.whatwg.org/

[29] promise: http://www.html5rocks.com/en/tutorials/es6/promises/

[30] кодов ответа сервера: http://www.nestoria.co.uk/help/api-return-codes

[31] интерфейс Web API: https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation

[32] весь проект: https://cdn4.raywenderlich.com/wp-content/uploads/2016/03/ReactNative-PropertyFinder.zip

[33] Источник: https://habrahabr.ru/post/303328/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best