Статьи

Как создать программу чтения новостей с помощью React Native: компонент «Настройка» и «Элемент новостей»

В этом уроке мы будем создавать приложение для чтения новостей с 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

Теперь мы готовы построить проект. Прежде чем мы начнем, я хотел бы дать краткий обзор того, как проект составлен. Мы создаем два пользовательских компонента:

  • NewsItems который отображает новости
  • Веб-страница, которая отображает веб-страницу, когда пользователь нажимает на новость

Затем они импортируются в файл основной точки входа для Android ( index.android.js ) и для iOS ( index.ios.js ). Это все, что вам нужно знать на данный момент.

Начните с перехода к вашему рабочему каталогу. Откройте новое окно терминала внутри этого каталога и выполните следующую команду:

1
react-native init HnReader

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

React Native уже поставляется с несколькими компонентами по умолчанию, но есть и другие, созданные другими разработчиками. Вы можете найти их на сайте activ.parts . Однако не все компоненты работают на Android и iOS. Даже некоторые компоненты по умолчанию не являются кроссплатформенными. Вот почему вы должны быть осторожны при выборе компонентов, поскольку они могут отличаться на каждой платформе или могут не работать должным образом на каждой платформе.

Рекомендуется зайти на страницу вопросов репозитория GitHub компонента, который вы планируете использовать, и выполнить поиск поддержки Android или IOS, чтобы быстро проверить, работает ли компонент на обеих платформах.

Приложение, которое мы собираемся создать, зависит от нескольких сторонних библиотек и компонентов 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 используется в качестве индикатора активности при выполнении сетевых запросов.

Как я упоминал ранее, точкой входа для всех проектов 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);

Компонент 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;

Сначала мы импортируем компоненты и библиотеки, которые нам нужны для компонента 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 — это библиотека, используемая для всего, что связано со временем.

Далее мы создаем компонент 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
    }
},

Функция 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 — это функция, используемая для рендеринга каждой строки в списке.

Далее у нас есть функция 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);
    }
 
});

Следующая функция для рендеринга каждой строки в списке. Ранее в 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});
},

Функция 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
        });
 
    }
     
},

Функция 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));
    }
 
},

Функция 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);
 
        }
    );
 
}

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

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 в приложение для чтения новостей. Не стесняйтесь оставлять любые вопросы или комментарии в разделе комментариев ниже.