В этом уроке мы будем создавать приложение для чтения новостей с React Native. В этой серии из двух частей я собираюсь предположить, что это не ваше первое приложение React Native, и я не буду вдаваться в подробности, касающиеся настройки вашей машины и запуска приложения на устройстве. Тем не менее, я объясняю сам процесс разработки в деталях.
Несмотря на то, что мы будем развертывать на Android, код, используемый в этом руководстве, должен работать и на iOS. Вот как выглядит окончательный результат.
Вы можете найти исходный код, используемый в этом руководстве, на GitHub .
Предпосылки
Если вы новичок в React Native и еще не настроили свой компьютер, обязательно ознакомьтесь с руководством по началу работы с документацией React Native или прочитайте вводное руководство Ашраффа по Envato Tuts +. Не забудьте установить Android SDK, если вы хотите развернуть на Android или установить Xcode и SDK для iOS.
Когда вы закончите, установите NodeJS и инструмент командной строки React Native, используя npm .
1
|
npm install -g react-native-cli
|
1. Настройка проекта
Теперь мы готовы построить проект. Прежде чем мы начнем, я хотел бы дать краткий обзор того, как проект составлен. Мы создаем два пользовательских компонента:
-
NewsItems
который отображает новости - Веб-страница, которая отображает веб-страницу, когда пользователь нажимает на новость
Затем они импортируются в файл основной точки входа для Android ( index.android.js ) и для iOS ( index.ios.js ). Это все, что вам нужно знать на данный момент.
Шаг 1: Создание нового приложения
Начните с перехода к вашему рабочему каталогу. Откройте новое окно терминала внутри этого каталога и выполните следующую команду:
1
|
react-native init HnReader
|
Это создает новую папку с именем HnReader и содержит файлы, необходимые для сборки приложения.
React Native уже поставляется с несколькими компонентами по умолчанию, но есть и другие, созданные другими разработчиками. Вы можете найти их на сайте activ.parts . Однако не все компоненты работают на Android и iOS. Даже некоторые компоненты по умолчанию не являются кроссплатформенными. Вот почему вы должны быть осторожны при выборе компонентов, поскольку они могут отличаться на каждой платформе или могут не работать должным образом на каждой платформе.
Рекомендуется зайти на страницу вопросов репозитория GitHub компонента, который вы планируете использовать, и выполнить поиск поддержки Android или IOS, чтобы быстро проверить, работает ли компонент на обеих платформах.
Шаг 2: Установка зависимостей
Приложение, которое мы собираемся создать, зависит от нескольких сторонних библиотек и компонентов React. Вы можете установить их, открыв package.json в корне вашего рабочего каталога. Добавьте следующее в package.json :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
{
«name»: «HnReader»,
«version»: «0.0.1»,
«private»: true,
«scripts»: {
«start»: «react-native start»
},
«dependencies»: {
«lodash»: «^4.0.1»,
«moment»: «^2.11.1»,
«react-native»: «^0.18.1»,
«react-native-button»: «^1.3.1»,
«react-native-gifted-spinner»: «0.0.3»
}
}
|
Затем откройте окно терминала в рабочем каталоге и выполните команду npm install
чтобы установить зависимости, указанные в package.json . Вот краткое описание того, что каждая библиотека делает в проекте:
- lodash используется для обрезания строк. Это может быть немного излишним, но одна строка кода, которую вы должны написать, означает на одну ответственность меньше.
- момент используется для определения, находятся ли новости в локальном хранилище уже в течение одного дня.
- response-native — это структура React Native. Это установлено по умолчанию, когда вы ранее выполнили
react-native init
. - ответная нативная кнопка — это нативный компонент реакции, используемый для создания кнопок.
- response-native-gifted-spinner используется в качестве индикатора активности при выполнении сетевых запросов.
2. Основной компонент
Как я упоминал ранее, точкой входа для всех проектов React Native являются index.android.js и index.ios.js . Это основное внимание в этом разделе. Замените содержимое этих файлов следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
‘use strict’;
var React = require(‘react-native’);
var {
AppRegistry,
StyleSheet,
Navigator
} = React;
var NewsItems = require(‘./components/news-items’);
var WebPage = require(‘./components/webpage’);
var ROUTES = {
news_items: NewsItems,
web_page: WebPage
};
var HnReader = React.createClass({
renderScene: function(route, navigator) {
var Component = ROUTES[route.name];
return (
<Component route={route} navigator={navigator} url={route.url} />
);
},
render: function() {
return (
<Navigator
style={styles.container}
initialRoute={{name: ‘news_items’, url: »}}
renderScene={this.renderScene}
configureScene={() => { return Navigator.SceneConfigs.FloatFromRight;
);
},
});
var styles = StyleSheet.create({
container: {
flex: 1
}
});
AppRegistry.registerComponent(‘HnReader’, () => HnReader);
|
Позвольте мне сломать это. Во-первых, мы включаем строгий режим с помощью директивы use script
. Это заставляет парсер больше проверять ваш код. Например, он будет жаловаться, если вы инициализируете переменную без добавления ключевого слова var
.
1
|
‘use strict’;
|
Далее мы импортируем среду React Native. Это позволяет нам создавать собственные компоненты и добавлять стили в приложение.
1
|
var React = require(‘react-native’);
|
Затем мы извлекаем всю необходимую функциональность из объекта React
.
1
2
3
4
5
|
var {
AppRegistry,
StyleSheet,
Navigator
} = React;
|
Если вы новичок в ES6 (ECMAScript 6), приведенный выше фрагмент идентичен:
1
2
3
|
var AppRegistry = React.AppRegistry;
var StyleSheet = React.StyleSheet;
var Navigator = React.Navigator;
|
Это синтаксический сахар, введенный в ES6 для облегчения назначения свойств объекта переменным. Это называется деструктурирующим назначением .
Вот краткое описание того, что делает каждое из извлеченных нами свойств:
-
AppRegistry
используется для регистрации основного компонента приложения. -
StyleSheet
стилей используется для объявления стилей, которые будут использоваться компонентами. -
Navigator
используется для переключения между различными страницами приложения.
Далее мы импортируем пользовательские компоненты, используемые приложением. Мы будем создавать их позже.
1
2
|
var NewsItems = require(‘./components/news-items’);
var WebPage = require(‘./components/webpage’);
|
Создайте переменную ROUTES
и назначьте объект, используя два вышеуказанных компонента в качестве значения для его свойств. Это позволяет нам отображать компонент, ссылаясь на каждый из определенных нами ключей.
1
2
3
4
|
var ROUTES = {
news_items: NewsItems,
web_page: WebPage
};
|
Создайте основной компонент приложения, вызвав метод createClass
из объекта React
. Метод createClass
принимает объект в качестве аргумента.
1
2
3
|
var HnReader = React.createClass({
…
});
|
Внутри объекта находится метод renderScene
, который renderScene
при каждом изменении маршрута. route
и navigator
передаются в качестве аргумента этому методу. route
содержит информацию о текущем маршруте (например, название маршрута).
navigator
содержит методы, которые можно использовать для навигации между различными маршрутами. Внутри метода renderScene
мы получаем компонент, который хотим визуализировать, передавая имя текущего маршрута объекту ROUTES
. Затем мы визуализируем компонент и передаем route
, navigator
и url
качестве атрибутов. Позже вы увидите, как они используются внутри каждого из компонентов. А пока, просто помните, что когда вы хотите передать данные из основного компонента в дочерний компонент, все, что вам нужно сделать, это добавить новый атрибут и использовать данные, которые вы хотите передать в качестве значения.
1
2
3
4
5
6
7
8
9
|
renderScene: function(route, navigator) {
var Component = ROUTES[route.name];
//render the component and pass along the route, navigator and the url
return (
<Component route={route} navigator={navigator} url={route.url} />
);
},
|
Метод render
является обязательным методом при создании компонентов, поскольку он отвечает за рендеринг пользовательского интерфейса компонента. В этом методе мы визуализируем компонент Navigator
и передаем несколько атрибутов.
01
02
03
04
05
06
07
08
09
10
|
render: function() {
return (
<Navigator
style={styles.container}
initialRoute={{name: ‘news_items’, url: »}}
renderScene={this.renderScene}
configureScene={() => { return Navigator.SceneConfigs.FloatFromRight;
);
},
|
Позвольте мне объяснить, что делает каждый атрибут:
-
style
используется для добавления стилей к компоненту. -
initialRoute
используется для указания начального маршрута, который будет использоваться навигатором. Как видите, мы передали объект, содержащий свойствоname
значение которого установлено вnews_items
. Этот объект является тем, что передается в аргументrenderScene
методаrenderScene
, который мы определили ранее. Это означает, что этот конкретный код будет отображать компонентNewsItems
по умолчанию.
1
|
var Component = ROUTES[route.name];
|
Для url
задана пустая строка, потому что у нас нет веб-страницы для отображения по умолчанию.
-
renderScene
отвечает за визуализацию компонента для определенного маршрута. -
configureScene
отвечает за указание анимации и жестов, которые будут использоваться при навигации между маршрутами. В этом случае мы передаем функцию, которая возвращает анимациюFloatFromRight
. Это означает, что при переходе к маршруту с более высоким индексом новая страница перемещается справа налево. И когда возвращаешься, он плывет слева направо. Это также добавляет жест смахивания влево для возврата к предыдущему маршруту.
1
|
() => { return Navigator.SceneConfigs.FloatFromRight;
|
Стили определяются после определения основного компонента. Мы вызываем метод create
из объекта StyleSheet
и передаем объект, содержащий стили. В этом случае у нас есть только один, определяющий, что он займет весь экран.
1
2
3
4
5
|
var styles = StyleSheet.create({
container: {
flex: 1
}
});
|
Наконец, мы регистрируем компонент.
1
|
AppRegistry.registerComponent(‘HnReader’, () => HnReader);
|
3. Компонент NewsItem
Компонент NewsItem
используется для отображения новостей. Пользовательские компоненты хранятся в каталоге компонентов . Внутри этого каталога создайте файл news-items.js и добавьте в него следующий код:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
‘use strict’;
var React = require(‘react-native’);
var {
AppRegistry,
StyleSheet,
Text,
ListView,
View,
ScrollView,
TouchableHighlight,
AsyncStorage
} = React;
var Button = require(‘react-native-button’);
var GiftedSpinner = require(‘react-native-gifted-spinner’);
var api = require(‘../src/api.js’);
var moment = require(‘moment’);
var TOTAL_NEWS_ITEMS = 10;
var NewsItems = React.createClass({
getInitialState: function() {
return {
title: ‘HN Reader’,
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
news: {},
loaded: false
}
},
render: function() {
return (
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.header_item}>
<Text style={styles.header_text}>{this.state.title}</Text>
</View>
<View style={styles.header_item}>
{ !this.state.loaded &&
<GiftedSpinner />
}
</View>
</View>
<View style={styles.body}>
<ScrollView ref=»scrollView»>
{
this.state.loaded &&
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView>
}
</ScrollView>
</View>
</View>
);
},
componentDidMount: function() {
AsyncStorage.getItem(‘news_items’).then((news_items_str) => {
var news_items = JSON.parse(news_items_str);
if(news_items != null){
AsyncStorage.getItem(‘time’).then((time_str) => {
var time = JSON.parse(time_str);
var last_cache = time.last_cache;
var current_datetime = moment();
var diff_days = current_datetime.diff(last_cache, ‘days’);
if(diff_days > 0){
this.getNews();
}else{
this.updateNewsItemsUI(news_items);
}
});
}else{
this.getNews();
}
}).done();
},
renderNews: function(news) {
return (
<TouchableHighlight onPress={this.viewPage.bind(this, news.url)} underlayColor={«#E8E8E8»} style={styles.button}>
<View style={styles.news_item}>
<Text style={styles.news_item_text}>{news.title}</Text>
</View>
</TouchableHighlight>
);
},
viewPage: function(url){
this.props.navigator.push({name: ‘web_page’, url: url});
},
updateNewsItemsUI: function(news_items){
if(news_items.length == TOTAL_NEWS_ITEMS){
var ds = this.state.dataSource.cloneWithRows(news_items);
this.setState({
‘news’: ds,
‘loaded’: true
});
}
},
updateNewsItemDB: function(news_items){
if(news_items.length == TOTAL_NEWS_ITEMS){
AsyncStorage.setItem(‘news_items’, JSON.stringify(news_items));
}
},
getNews: function() {
var TOP_STORIES_URL = ‘https://hacker-news.firebaseio.com/v0/topstories.json’;
var news_items = [];
AsyncStorage.setItem(‘time’, JSON.stringify({‘last_cache’: moment()}));
api(TOP_STORIES_URL).then(
(top_stories) => {
for(var x = 0; x <= 10; x++){
var story_url = «https://hacker-news.firebaseio.com/v0/item/» + top_stories[x] + «.json»;
api(story_url).then(
(story) => {
news_items.push(story);
this.updateNewsItemsUI(news_items);
this.updateNewsItemDB(news_items);
}
);
}
}
);
}
});
var styles = StyleSheet.create({
container: {
flex: 1
},
header: {
backgroundColor: ‘#FF6600’,
padding: 10,
flex: 1,
justifyContent: ‘space-between’,
flexDirection: ‘row’
},
body: {
flex: 9,
backgroundColor: ‘#F6F6EF’
},
header_item: {
paddingLeft: 10,
paddingRight: 10,
justifyContent: ‘center’
},
header_text: {
color: ‘#FFF’,
fontWeight: ‘bold’,
fontSize: 15
},
button: {
borderBottomWidth: 1,
borderBottomColor: ‘#F0F0F0’
},
news_item: {
paddingLeft: 10,
paddingRight: 10,
paddingTop: 15,
paddingBottom: 15,
marginBottom: 5
},
news_item_text: {
color: ‘#575757’,
fontSize: 18
}
});
module.exports = NewsItems;
|
Шаг 1: Импорт компонентов и библиотек
Сначала мы импортируем компоненты и библиотеки, которые нам нужны для компонента NewsItem
. Мы также создаем глобальную переменную, которая хранит общее количество новостей, которые должны быть кэшированы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
‘use strict’;
var React = require(‘react-native’);
var {
AppRegistry,
StyleSheet,
Text,
ListView,
View,
ScrollView,
TouchableHighlight,
AsyncStorage
} = React;
var Button = require(‘react-native-button’);
var GiftedSpinner = require(‘react-native-gifted-spinner’);
var api = require(‘../src/api.js’);
var moment = require(‘moment’);
var TOTAL_NEWS_ITEMS = 10;
|
Мы используем несколько компонентов, которые мы не использовали ранее.
-
Text
используется для отображения текста в React Native. -
View
является основным строительным блоком для создания компонентов. Думайте об этом как оdiv
на веб-страницах. -
ListView
используется для визуализации массива объектов. -
ScrollView
используется для добавления полос прокрутки. React Native не похож на веб-страницы. Полосы прокрутки не добавляются автоматически, когда содержимое больше, чем вид или экран. Вот почему нам нужно использовать этот компонент. -
TouchableHighlight
используется для того, чтобы заставить компонент реагировать на сенсорные события. -
AsyncStorage
самом деле не является компонентом. Это API, используемый для хранения локальных данных в React Native. -
Button
является сторонним компонентом для создания кнопок. -
GiftedSpinner
используется для создания счетчиков при загрузке данных из сети. -
api
— это пользовательский модуль, который упаковываетfetch
, способ React Native для сетевых запросов. Существует много стандартного кода, необходимого для получения данных, возвращаемых сетевым запросом, и поэтому мы заключаем их в модуль. Это позволит нам не писать меньше кода при выполнении сетевых запросов. -
moment
— это библиотека, используемая для всего, что связано со временем.
Шаг 2: Создание компонента NewsItems
Далее мы создаем компонент NewsItems
:
1
2
3
|
var NewsItems = React.createClass({
…
});
|
В этом компоненте есть функция getInitialState
, которая используется для указания состояния по умолчанию для этого компонента. В React Native это состояние используется для хранения данных, доступных во всем компоненте. Здесь мы dataSource
название приложения, dataSource
для компонента ListView
, текущие news
и loaded
логическое значение, которое сообщает, loaded
ли новости в данный момент из сети или нет. loaded
переменная используется, чтобы определить, отображать или нет счетчик. Мы устанавливаем значение false
чтобы спиннер был виден по умолчанию.
После того, как новости загружены из локального хранилища или из сети, устанавливается значение true
чтобы скрыть счетчик. Источник dataSource
используется для определения проекта источника данных, который будет использоваться для компонента ListView
. Думайте об этом как о родительском классе, в котором будут наследоваться все источники данных, которые вы будете определять. Для этого требуется объект, содержащий функцию rowHasChanged
, которая сообщает ListView
повторно выполнить рендеринг при изменении строки.
Наконец, news
объект содержит начальное значение для источника данных ListView
.
01
02
03
04
05
06
07
08
09
10
|
getInitialState: function() {
return {
title: ‘HN Reader’,
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
news: {},
loaded: false
}
},
|
Шаг 3: Реализация функции render
Функция render
отображает пользовательский интерфейс для этого компонента. Сначала мы оборачиваем все в представление. Тогда внутри у нас есть заголовок и тело. Заголовок содержит заголовок и счетчик. Тело содержит ListView
. Все внутри тела обернуто внутри ScrollView
так что полоса прокрутки автоматически добавляется, если содержимое превышает доступное пространство.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
render: function() {
return (
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.header_item}>
<Text style={styles.header_text}>{this.state.title}</Text>
</View>
<View style={styles.header_item}>
{ !this.state.loaded &&
<GiftedSpinner />
}
</View>
</View>
<View style={styles.body}>
<ScrollView ref=»scrollView»>
{
this.state.loaded &&
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView>
}
</ScrollView>
</View>
</View>
);
},
|
Внутри заголовка есть два вида:
- тот, который содержит заголовок
- один, содержащий спиннер
Мы делаем это таким образом, вместо того, чтобы выводить текст и прядильщик напрямую, чтобы мы могли управлять стилем с помощью flexbox . Вы можете увидеть, как это делается в разделе стилей, позже.
Мы можем ссылаться на заголовок, сохраненный в состоянии, используя this.state
, за которым следует имя свойства. Как вы могли заметить, каждый раз, когда нам нужно обратиться к объекту, мы заключаем его в фигурные скобки. С другой стороны, мы проверяем, установлено ли для свойства loaded
в состояние значение false
и, если оно есть, мы выводим спиннер.
1
2
3
4
5
6
7
8
|
<View style={styles.header_item}>
<Text style={styles.header_text}>{this.state.title}</Text>
</View>
<View style={styles.header_item}>
{ !this.state.loaded &&
<GiftedSpinner />
}
</View>
|
Далее идет тело.
1
2
3
4
5
6
7
8
|
<ScrollView ref=»scrollView»>
{
this.state.loaded &&
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView>
}
</ScrollView>
|
Обратите внимание, что мы передали атрибут ref
в ScrollView
. ref
является предопределенным атрибутом в React Native, который позволяет нам назначать идентификатор компоненту. Мы можем использовать этот идентификатор для ссылки на компонент и вызова его методов. Вот пример того, как это работает:
1
2
3
|
scrollToTop: function(){
this.refs.scrollView.scrollTo(0);
}
|
Затем вы можете иметь кнопку и вызывать функцию при нажатии. Это автоматически прокрутит ScrollView
до самого верха компонента.
1
|
<Button onPress={this.scrollToTop}>scroll to top</Button>
|
Мы не будем использовать это в приложении, но приятно знать, что оно существует.
Внутри ScrollView
мы проверяем, установлено ли уже loaded
свойство в состоянии true
. Если это true
, это означает, что источник данных уже доступен для использования ListView
и мы можем его отобразить.
1
2
3
4
5
6
|
{
this.state.loaded &&
<ListView initialListSize={1} dataSource={this.state.news} renderRow={this.renderNews}></ListView>
}
|
Мы передали следующие атрибуты в ListView
:
-
initialListSize
используется, чтобы указать, сколько строк визуализировать при первоначальном монтировании компонента. Мы установили его на1
, что означает, что для рендеринга каждой строки потребуется один кадр. Я установил значение1
как форму оптимизации производительности, чтобы пользователь увидел что-то как можно скорее. -
dataSource
— источник данных, который будет использоваться. -
renderRow
— это функция, используемая для рендеринга каждой строки в списке.
Шаг 4: Реализация функции componentDidMount
Далее у нас есть функция componentDidMount
, которая вызывается при монтировании этого компонента:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
componentDidMount: function() {
AsyncStorage.getItem(‘news_items’).then((news_items_str) => {
var news_items = JSON.parse(news_items_str);
if(news_items != null){
AsyncStorage.getItem(‘time’).then((time_str) => {
var time = JSON.parse(time_str);
var last_cache = time.last_cache;
var current_datetime = moment();
var diff_days = current_datetime.diff(last_cache, ‘days’);
if(diff_days > 0){
this.getNews();
}else{
this.updateNewsItemsUI(news_items);
}
});
}else{
this.getNews();
}
}).done();
},
|
Внутри функции мы пытаемся получить новости, которые в данный момент хранятся в локальном хранилище. Мы используем метод getItem
из API AsyncStorage
. Он возвращает обещание, поэтому мы можем получить доступ к возвращенным данным, вызвав метод then
и передав функцию:
1
2
3
|
AsyncStorage.getItem(‘news_items’).then((news_items_str) => {
…
}).done();
|
AsyncStorage
может хранить только строковые данные, поэтому мы используем JSON.parse
для преобразования строки JSON обратно в объект JavaScript. Если значение равно null
, мы вызываем метод getNews
, который извлекает данные из сети.
1
2
3
4
5
6
7
|
var news_items = JSON.parse(news_items_str);
if(news_items != null){
…
}else{
this.getNews();
}
|
Если оно не пустое, мы используем AsyncStorage
чтобы извлечь последний раз, когда новости были сохранены в локальном хранилище. Затем мы сравниваем его с текущим временем. Если разница составляет не менее суток (24 часа), мы получаем новости из сети. Если это не так, мы используем те в локальном хранилище.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
AsyncStorage.getItem(‘time’).then((time_str) => {
var time = JSON.parse(time_str);
var last_cache = time.last_cache;
var current_datetime = moment();
//get the difference in days
var diff_days = current_datetime.diff(last_cache, ‘days’);
if(diff_days > 0){
this.getNews();
}else{
this.updateNewsItemsUI(news_items);
}
});
|
Шаг 5: Реализация функции renderNews
Следующая функция для рендеринга каждой строки в списке. Ранее в ListView
мы определили атрибут renderRow
, который имеет значение this.renderNews
. Это та функция.
Текущий элемент в итерации передается в качестве аргумента этой функции. Это позволяет нам получить доступ к title
и url
каждой новости. Все обернуто внутри компонента TouchableHighlight
и внутри мы выводим заголовок каждого новостного элемента.
Компонент TouchableHighlight
принимает атрибут onPress
, который указывает, какую функцию выполнять, когда пользователь касается элемента. Здесь мы вызываем функцию viewPage
и привязываем к ней URL. underlayColor
указывает цвет фона компонента при его нажатии.
1
2
3
4
5
6
7
8
9
|
renderNews: function(news) {
return (
<TouchableHighlight onPress={this.viewPage.bind(this, news.url)} underlayColor={«#E8E8E8»} style={styles.button}>
<View style={styles.news_item}>
<Text style={styles.news_item_text}>{news.title}</Text>
</View>
</TouchableHighlight>
);
},
|
В функции viewPage
мы получаем атрибут navigator
который мы ранее передали из index.android.js через реквизиты. В React Native реквизиты используются для доступа к атрибутам, которые передаются из родительского компонента. Мы называем это this.props
, за которым следует имя атрибута.
Здесь мы используем this.props.navigator
для ссылки на объект navigator
. Затем мы вызываем метод push
чтобы протолкнуть маршрут web_page
к навигатору вместе с URL-адресом веб-страницы, которая будет открыта компонентом WebPage
. Это делает переход приложения на компонент WebPage
.
1
2
3
|
viewPage: function(url){
this.props.navigator.push({name: ‘web_page’, url: url});
},
|
Шаг 6: Реализация функции updateNewsItemsUI
Функция updateNewsItemsUI
обновляет источник данных и состояние на основе массива элементов новостей, переданных в качестве аргумента. Мы делаем это, только если общее количество news_items
равно значению, которое мы установили ранее для TOTAL_NEWS_ITEMS
. В React Native при обновлении состояния пользовательский интерфейс запускается повторно. Это означает, что вызов setState
с новым источником данных обновляет пользовательский интерфейс новыми элементами.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
updateNewsItemsUI: function(news_items){
if(news_items.length == TOTAL_NEWS_ITEMS){
var ds = this.state.dataSource.cloneWithRows(news_items);
//update the state
this.setState({
‘news’: ds,
‘loaded’: true
});
}
},
|
Шаг 7: Обновление локального хранилища
Функция updateNewsItemDB
обновляет новости, которые хранятся в локальном хранилище. Мы используем функцию JSON.stringify
для преобразования массива в строку JSON, чтобы мы могли сохранить его с помощью AsyncStorage
.
1
2
3
4
5
6
7
|
updateNewsItemDB: function(news_items){
if(news_items.length == TOTAL_NEWS_ITEMS){
AsyncStorage.setItem(‘news_items’, JSON.stringify(news_items));
}
},
|
Шаг 8: Получение новостей
Функция getNews
обновляет элемент локального хранилища, в котором хранится последний раз, когда данные были кэшированы, извлекает элементы новостей из API Hacker News , обновляет пользовательский интерфейс и локальное хранилище на основе новых элементов, которые были извлечены.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
getNews: function() {
var TOP_STORIES_URL = ‘https://hacker-news.firebaseio.com/v0/topstories.json’;
var news_items = [];
AsyncStorage.setItem(‘time’, JSON.stringify({‘last_cache’: moment()}));
api(TOP_STORIES_URL).then(
(top_stories) => {
for(var x = 0; x <= 10; x++){
var story_url = «https://hacker-news.firebaseio.com/v0/item/» + top_stories[x] + «.json»;
api(story_url).then(
(story) => {
news_items.push(story);
this.updateNewsItemsUI(news_items);
this.updateNewsItemDB(news_items);
}
);
}
}
);
}
|
Ресурс Top Stories в API Hacker News возвращает массив, который выглядит следующим образом:
1
|
[ 10977819, 10977786, 10977295, 10978322, 10976737, 10978069, 10974929, 10975813, 10974552, 10978077, 10978306, 10973956, 10975838, 10974870…
|
Это идентификаторы топовых статей, размещенных в Hacker News. Вот почему нам нужно пройти через этот массив и сделать сетевой запрос для каждого элемента, чтобы получить фактические данные, такие как заголовок и URL.
Затем мы news_items
массив updateNewsItemsUI
и updateNewsItemDB
функции updateNewsItemsUI
и updateNewsItemDB
для обновления пользовательского интерфейса и локального хранилища.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
for(var x = 0; x <= 10; x++){
var story_url = «https://hacker-news.firebaseio.com/v0/item/» + top_stories[x] + «.json»;
api(story_url).then(
(story) => {
news_items.push(story);
this.updateNewsItemsUI(news_items);
this.updateNewsItemDB(news_items);
}
);
}
|
Шаг 9: Стиль
Добавьте следующие стили:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
var styles = StyleSheet.create({
container: {
flex: 1
},
header: {
backgroundColor: ‘#FF6600’,
padding: 10,
flex: 1,
justifyContent: ‘space-between’,
flexDirection: ‘row’
},
body: {
flex: 9,
backgroundColor: ‘#F6F6EF’
},
header_item: {
paddingLeft: 10,
paddingRight: 10,
justifyContent: ‘center’
},
header_text: {
color: ‘#FFF’,
fontWeight: ‘bold’,
fontSize: 15
},
button: {
borderBottomWidth: 1,
borderBottomColor: ‘#F0F0F0’
},
news_item: {
paddingLeft: 10,
paddingRight: 10,
paddingTop: 15,
paddingBottom: 15,
marginBottom: 5
},
news_item_text: {
color: ‘#575757’,
fontSize: 18
}
});
|
В большинстве случаев это стандартный CSS, но обратите внимание, что мы заменили тире синтаксисом верблюжьих букв. Это не потому, что мы получаем синтаксическую ошибку, если мы используем что-то вроде padding-left
. Это потому, что этого требует React Native. Также обратите внимание, что не все свойства CSS могут быть использованы .
Тем не менее, вот некоторые объявления, которые могут быть не такими интуитивными, особенно если вы раньше не использовали flexbox :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
container: {
flex: 1
},
header: {
backgroundColor: ‘#FF6600’,
padding: 10,
flex: 1,
justifyContent: ‘space-between’,
flexDirection: ‘row’
},
body: {
flex: 9,
backgroundColor: ‘#F6F6EF’
},
|
Вот упрощенная версия разметки для компонента NewsItems
чтобы помочь вам визуализировать его:
1
2
3
4
5
6
7
8
|
<View style={styles.container}>
<View style={styles.header}>
…
</View>
<View style={styles.body}>
…
</View>
</View>
|
Мы установили container
для flex: 1
, что означает, что он занимает весь экран. Внутри container
у нас есть header
и body
, которые мы установили для flex: 1
и flex: 9
соответственно. В этом случае flex: 1
не будет занимать весь экран, поскольку в header
есть одноуровневый элемент. Эти двое будут разделять весь экран. Это означает, что весь экран будет разделен на десять секций, поскольку у нас есть flex: 1
и flex: 9
. Значения для flex
для каждого из братьев и сестер суммируются.
header
занимает 10% экрана, а body
занимает 90% его. Основная идея состоит в том, чтобы выбрать число, которое будет представлять высоту или ширину всего экрана, а затем каждый брат берет часть от этого числа. Но не переусердствуйте с этим. Вы не хотите использовать 1000, если не хотите развернуть свое приложение в кинотеатре. Я нахожу десять магическим числом при работе с высотой.
Для header
мы установили следующие стили:
1
2
3
4
5
6
7
|
header: {
backgroundColor: ‘#FF6600’,
padding: 10,
flex: 1,
justifyContent: ‘space-between’,
flexDirection: ‘row’
},
|
И чтобы освежить вашу память, вот упрощенная разметка того, что находится внутри заголовка:
1
2
3
4
5
6
|
<View style={styles.header_item}>
…
</View>
<View style={styles.header_item}>
…
</View>
|
И стиль, добавленный к тем:
1
2
3
4
5
|
header_item: {
paddingLeft: 10,
paddingRight: 10,
justifyContent: ‘center’
},
|
Мы установили flexDirection
для row
и justifyContent
для space-between
justifyContent
в их родительском justifyContent
, который является header
. Это означает, что его дочерние элементы будут распределены равномерно, причем первый дочерний элемент находится в начале строки, а последний дочерний элемент — в конце строки.
По умолчанию для flexDirection
задано значение column
, что означает, что каждый дочерний flexDirection
занимает всю строку, поскольку движение является горизонтальным. Использование row
сделало бы поток вертикальным, чтобы каждый ребенок был рядом. Если вы все еще не уверены в Flexbox или хотите узнать о нем больше, ознакомьтесь с CSS: Flexbox Essentials .
Наконец, представьте компонент внешнему миру:
1
|
module.exports = NewsItems;
|
Вывод
К этому моменту у вас должно быть хорошее представление о том, как действовать в естественном стиле. В частности, вы узнали, как создать новый проект React Native, установить сторонние библиотеки через npm , использовать различные компоненты и добавить стилизацию в приложение.
В следующей статье мы продолжим добавление компонента WebPage
в приложение для чтения новостей. Не стесняйтесь оставлять любые вопросы или комментарии в разделе комментариев ниже.