Приложение реального времени позволяет пользователю получать актуальную информацию, которую он хочет знать, когда ему это нужно. Пользователю не нужно постоянно обновлять окно, чтобы получать последние обновления, сервер за приложением будет автоматически загружать обновления в приложение. В этом уроке я расскажу о разработке приложений в реальном времени, создав приложение для обмена новостями с RethinkDB и React Native .
Я предполагаю, что у вас уже есть опыт создания приложений React Native, поэтому я не буду вдаваться в подробности по каждой строке кода. Если вы новичок, я рекомендую вам прочитать мой предыдущий учебник « Создание приложения для Android с React Native ». Если вы хотите следовать, вы можете найти код на Github .
Вот как будет выглядеть финальное приложение:
Я начну с просмотра кода для мобильного приложения, а затем перейду к серверному компоненту, который использует Node, Express, Socket.io и RethinkDB.
Установить зависимости
Внутри вашего клона проекта перейдите в каталог NewsSharer и выполните npm install
чтобы установить следующие зависимости:
- response-native : структура React Native.
- lodash : используется для манипулирования массивом новостей, чтобы он был ограничен и упорядочен в соответствии с количеством голосов.
- Reaction-native-modalbox : Используется для создания модального сообщения о новостях.
- Reaction-native-button : Зависимость response-native-modalbox, используемая для создания кнопок.
- response-native-vector-icons : Используется для создания иконок с популярными наборами иконок, такими как FontAwesome и Ionicons Это в основном используется для создания иконки для кнопки голосования.
- socket.io-client : клиентский компонент Socket.io , прикладной среды реального времени.
Значки ссылок
После установки зависимостей есть еще один шаг, чтобы заставить значки работать, связывая их с приложением. Сделайте это с помощью rnpm , менеджера пакетов React Native.
Установите rnpm с помощью npm:
npm install rnpm -g
Затем выполните rnpm link
в корне каталога NewsSharer, чтобы связать значки.
Приложение
Ниже приведено содержимое файла index.android.js :
import React, { Component } from 'react'; import { AppRegistry, StyleSheet, View } from 'react-native'; import Main from './components/Main'; class NewsSharer extends Component { render() { return ( <View style={styles.container}> <Main /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', } }); AppRegistry.registerComponent('NewsSharer', () => NewsSharer);
Этот файл является файлом точки входа для приложения Android. Если вы хотите развернуть на iOS, вы можете скопировать код в новый файл index.ios.js .
Основная задача этого файла — импортировать Main
компонент, в котором находится ядро приложения. Это уменьшает повторение кода при импорте компонента вместо повторения кода для каждой платформы.
Основной компонент приложения
Внутренние компоненты / Main.js :
import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, TextInput, TouchableHighlight, Linking, ListView } from 'react-native'; import Button from 'react-native-button'; import Modal from 'react-native-modalbox'; import Icon from 'react-native-vector-icons/Octicons'; import "../UserAgent"; import io from 'socket.io-client/socket.io'; import _ from 'lodash'; var base_url = 'http://YOUR_DOMAIN_NAME_OR_IP_ADDRESS:3000'; export default class Main extends Component { constructor(props){ super(props); this.socket = io(base_url, { transports: ['websocket'] }); this.state = { is_modal_open: false, news_title: '', news_url: '', news_items_datasource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), is_news_loaded: false, news: {}, news_items: [] }; } getNewsItems(){ fetch(base_url + '/news') .then((response) => { return response.json(); }) .then((news_items) => { this.setState({ 'news_items': news_items }); var news_datasource = this.state.news_items_datasource.cloneWithRows(news_items); this.setState({ 'news': news_datasource, 'is_news_loaded': true }); return news_items; }) .catch((error) => { alert('Error occured while fetching news items'); }); } componentWillMount(){ this.socket.on('news_updated', (data) => { var news_items = this.state.news_items; if(data.old_val === null){ news_items.push(data.new_val); }else{ _.map(news_items, function(row, index){ if(row.id == data.new_val.id){ news_items[index].upvotes = data.new_val.upvotes; } }); } this.updateUI(news_items); }); } updateUI(news_items){ var ordered_news_items = _.orderBy(news_items, 'upvotes', 'desc'); var limited_news_items = _.slice(ordered_news_items, 0, 30); var news_datasource = this.state.news_items_datasource.cloneWithRows(limited_news_items); this.setState({ 'news': news_datasource, 'is_news_loaded': true, 'is_modal_open': false, 'news_items': limited_news_items }); } componentDidMount(){ this.getNewsItems(); } upvoteNewsItem(id, upvotes){ fetch(base_url + '/upvote-newsitem', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ news_id: id, upvotes: upvotes + 1 }) }) .catch((err) => { alert('Error occured while trying to upvote'); }); } openModal(){ this.setState({ is_modal_open: true }); } closeModal(){ this.setState({ is_modal_open: false }); } shareNews(){ fetch(base_url + '/save-newsitem', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ news_title: this.state.news_title, news_url: this.state.news_url, }) }) .then((response) => { alert('News was shared!'); this.setState({ news_title: '', news_url: '' }); }) .catch((err) => { alert('Error occured while sharing news'); }); } openPage(url){ Linking.canOpenURL(url).then(supported => { if(supported){ Linking.openURL(url); } }); } renderNews(news){ return ( <View style={styles.news_item}> <TouchableHighlight onPress={this.upvoteNewsItem.bind(this, news.id, news.upvotes)} underlayColor={"#E8E8E8"}> <View style={styles.upvote}> <Icon name="triangle-up" size={30} color="#666" /> <Text style={styles.upvote_text}>{news.upvotes}</Text> </View> </TouchableHighlight> <TouchableHighlight onPress={this.openPage.bind(this, news.url)} underlayColor={"#E8E8E8"}> <View style={styles.news_title}> <Text style={styles.news_item_text}>{news.title}</Text> </View> </TouchableHighlight> </View> ); } render(){ return ( <View style={styles.container}> <View style={styles.header}> <View style={styles.app_title}> <Text style={styles.header_text}>News Sharer</Text> </View> <View style={styles.header_button_container}> <Button onPress={this.openModal.bind(this)} style={styles.btn}> Share News </Button> </View> </View> { this.state.is_news_loaded && <View style={styles.body}> <ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews.bind(this)}></ListView> </View> } <Modal isOpen={this.state.is_modal_open} style={styles.modal} position={"center"} > <View style={styles.modal_body}> <View style={styles.modal_header}> <Text style={styles.modal_header_text}>Share News</Text> </View> <View style={styles.input_row}> <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} onChangeText={(text) => this.setState({news_title: text})} value={this.state.news_title} placeholder="Title" /> </View> <View style={styles.input_row}> <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} onChangeText={(text) => this.setState({news_url: text})} value={this.state.news_url} placeholder="URL" keyboardType="url" /> </View> <View style={styles.input_row}> <Button onPress={this.shareNews.bind(this)} style={[styles.btn, styles.share_btn]}> Share </Button> </View> </View> </Modal> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignSelf: 'stretch', backgroundColor: '#F5FCFF', }, header: { flex: 1, backgroundColor: '#3B3738', flexDirection: 'row' }, app_title: { flex: 7, padding: 10 }, header_text: { fontSize: 20, color: '#FFF', fontWeight: 'bold' }, header_button_container: { flex: 3 }, body: { flex: 19 }, btn: { backgroundColor: "#05A5D1", color: "white", margin: 10 }, modal: { height: 300 }, modal_header: { margin: 20, }, modal_body: { alignItems: 'center' }, input_row: { padding: 20 }, modal_header_text: { fontSize: 18, fontWeight: 'bold' }, share_btn: { width: 100 }, news_item: { paddingLeft: 10, paddingRight: 10, paddingTop: 15, paddingBottom: 15, marginBottom: 5, borderBottomWidth: 1, borderBottomColor: '#ccc', flex: 1, flexDirection: 'row' }, news_item_text: { color: '#575757', fontSize: 18 }, upvote: { flex: 2, paddingRight: 15, paddingLeft: 5, alignItems: 'center' }, news_title: { flex: 18, justifyContent: 'center' }, upvote_text: { fontSize: 18, fontWeight: 'bold' } }); AppRegistry.registerComponent('Main', () => Main);
Разбивка кода выше. Сначала импортируйте необходимые встроенные компоненты React Native и сторонние.
import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, TextInput, TouchableHighlight, Linking, ListView } from 'react-native'; import Button from 'react-native-button'; import Modal from 'react-native-modalbox'; import Icon from 'react-native-vector-icons/Octicons'; import "../UserAgent"; import io from 'socket.io-client/socket.io'; import _ from 'lodash';
Обратите внимание, что вы импортируете пользовательский код здесь:
import "../UserAgent";
Это файл UserAgent.js, который вы видите в корне каталога NewsSharer . Он содержит код для установки пользовательского агента на react-native
, необходимый для работы Socket.io, или он предполагает, что он находится в среде браузера.
window.navigator.userAgent = 'react-native';
Далее, базовый URL, к которому приложение будет отправлять запросы. Если вы проводите локальное тестирование, это может быть внутренний IP-адрес вашего компьютера. Чтобы это работало, вы должны убедиться, что ваш телефон или планшет подключен к той же сети, что и ваш компьютер.
var base_url = 'http://YOUR_DOMAIN_NAME_OR_IP_ADDRESS:3000';
Внутри конструктора инициализируйте соединение сокета:
this.socket = io(base_url, { transports: ['websocket'] });
Установите состояние приложения по умолчанию:
this.state = { is_modal_open: false, //for showing/hiding the modal news_title: '', //default value for news title text field news_url: '', //default value for news url text field //initialize a datasource for the news items news_items_datasource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), //for showing/hiding the news items is_news_loaded: false, news: {}, //the news items datasource news_items: [] //the news items };
Эта функция извлекает новости с сервера, используя встроенный метод извлечения . Он выполняет запрос GET
к маршруту news
и затем извлекает объект news_items
из ответа. Затем он используется для создания источника данных новостей, который требуется компоненту ListView
. После создания он обновляет состояние с помощью источника данных новостей, чтобы пользовательский интерфейс обновлялся с новостями в нем.
getNewsItems(){ fetch(base_url + '/news') .then((response) => { return response.json(); }) .then((news_items) => { this.setState({ 'news_items': news_items }); var news_datasource = this.state.news_items_datasource.cloneWithRows(news_items); this.setState({ 'news': news_datasource, 'is_news_loaded': true }); return news_items; }) .catch((error) => { alert('Error occured while fetching news items'); }); }
Метод componentWillMount
является одним из методов жизненного цикла React. Это позволяет вам выполнить код до того, как произойдет начальный рендеринг. Здесь вы слушаете событие news_updated, генерируемое серверным компонентом Socket.io, и когда это событие происходит, это может быть одна из двух вещей. Когда пользователи делятся новостями или когда они голосуют за существующие новости.
Канал изменений RethinkDB возвращает null
значение для old_val
если это новый элемент. Вот как вы различаете две возможности. Если пользователь поделился новым элементом новостей, news_items
массив news_items
. В противном случае, ищите проголосовавшую новость и обновите счетчик голосов. Теперь вы можете обновить пользовательский интерфейс, чтобы отразить изменения.
componentWillMount(){ this.socket.on('news_updated', (data) => { var news_items = this.state.news_items; if(data.old_val === null){ //a new news item is shared //push the new item to the news_items array news_items.push(data.new_val); }else{ //an existing news item is upvoted //find the news item that was upvoted and update its upvote count _.map(news_items, function(row, index){ if(row.id == data.new_val.id){ news_items[index].upvotes = data.new_val.upvotes; } }); } //update the UI to reflect the changes this.updateUI(news_items); }); }
Функция updateUI
упорядочивает новости по их количеству голосов, от наивысшего к низшему. После сортировки извлеките первые 30 новостей и обновите состояние.
updateUI(news_items){ var ordered_news_items = _.orderBy(news_items, 'upvotes', 'desc'); var limited_news_items = _.slice(ordered_news_items, 0, 30); var news_datasource = this.state.news_items_datasource.cloneWithRows(limited_news_items); this.setState({ 'news': news_datasource, 'is_news_loaded': true, 'is_modal_open': false, 'news_items': limited_news_items }); }
Метод componentDidMount
— это другой метод жизненного цикла React, который вызывается после первоначального рендеринга. Здесь вы можете получать новости с сервера.
Примечание . Это также можно сделать внутри метода componentWillMount
если вы хотите сделать запрос до монтирования компонента.
componentDidMount(){ this.getNewsItems(); }
Метод upvoteNewsItem
отправляет запрос новостей новостей upvote на сервер.
upvoteNewsItem(id, upvotes){ fetch(base_url + '/upvote-newsitem', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ news_id: id, upvotes: upvotes + 1 }) }) .catch((err) => { alert('Error occured while trying to upvote'); }); }
openModal
и closeModal
показывают и скрывают closeModal
для обмена новостями.
openModal(){ this.setState({ is_modal_open: true }); } closeModal(){ this.setState({ is_modal_open: false }); }
Функция shareNews
отправляет запрос на создание новости.
shareNews(){ fetch(base_url + '/save-newsitem', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ news_title: this.state.news_title, news_url: this.state.news_url, }) }) .then((response) => { alert('News was shared!'); this.setState({ news_title: '', news_url: '' }); }) .catch((err) => { alert('Error occured while sharing news'); }); }
Функция openPage
открывает URL-адрес новости в браузере.
openPage(url){ Linking.canOpenURL(url).then(supported => { if(supported){ Linking.openURL(url); } }); }
Функция renderNews
возвращает интерфейс для каждого элемента новостей. Это отображает кнопку upvote, количество upvotes и заголовок новости. Заголовок новости TouchableHighlight
компонент TouchableHighlight
. Это позволяет вам выполнить функцию openPage
чтобы открыть URL. Вы делаете то же самое для подсчета голосов.
Примечание . В коде используется компонент TouchableHighlight
вместо компонента Button
поскольку в компоненте Button
не может быть компонентов View
или Text
.
renderNews(news){ return ( <View style={styles.news_item}> <TouchableHighlight onPress={this.upvoteNewsItem.bind(this, news.id, news.upvotes)} underlayColor={"#E8E8E8"}> <View style={styles.upvote}> <Icon name="triangle-up" size={30} color="#666" /> <Text style={styles.upvote_text}>{news.upvotes}</Text> </View> </TouchableHighlight> <TouchableHighlight onPress={this.openPage.bind(this, news.url)} underlayColor={"#E8E8E8"}> <View style={styles.news_title}> <Text style={styles.news_item_text}>{news.title}</Text> </View> </TouchableHighlight> </View> ); }
Функция render
возвращает пользовательский интерфейс всего приложения.
render(){ ... }
Внутри функции render
вас есть заголовок, который содержит заголовок приложения и кнопку для открытия модального окна для публикации новости.
<View style={styles.header}> <View style={styles.app_title}> <Text style={styles.header_text}>News Sharer</Text> </View> <View style={styles.header_button_container}> <Button onPress={this.openModal.bind(this)} style={styles.btn}> Share News </Button> </View> </View>
Для тела у вас есть компонент ListView
для отображения новостей. Он имеет три обязательных параметра: initialListSize
, dataSource
и renderRow
. initialListSize
установлен в 1, так что ListView
визуализирует каждую строку одну за другой в течение нескольких кадров. Вы также можете обновить его до более высокого значения, если хотите, чтобы строки появлялись сразу. dataSource
— это элементы новостей, а renderRow
— это функция для отображения каждой отдельной строки элемента новостей.
{ this.state.is_news_loaded && <View style={styles.body}> <ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews.bind(this)}></ListView> </View> }
Далее идет модал для обмена новостями. Здесь есть два текстовых поля для ввода заголовка и URL-адреса новостей, а также кнопка для отправки их на сервер. В текстовых полях используется компонент TextInput
. Нет меток, поэтому добавьте заполнитель текста, чтобы направить пользователя к тому, что ему нужно ввести.
Оба текстовых поля имеют метод onChangeText
который обновляет значение для каждого из них. keyboardType
типа url
используется для текстового поля URL-адреса новостей, чтобы открыть клавиатуру, оптимизированную для ввода URL-адресов на устройстве. Пользователь не должен вводить его вручную, он может использовать копирование и вставку, но это «приятно иметь», если он решит ввести его вручную. Под текстовыми полями находится кнопка для обмена новостями. Это вызывает функцию shareNews
определенную ранее.
<Modal isOpen={this.state.is_modal_open} style={styles.modal} position={"center"} > <View style={styles.modal_body}> <View style={styles.modal_header}> <Text style={styles.modal_header_text}>Share News</Text> </View> <View style={styles.input_row}> <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} onChangeText={(text) => this.setState({news_title: text})} value={this.state.news_title} placeholder="Title" /> </View> <View style={styles.input_row}> <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}} onChangeText={(text) => this.setState({news_url: text})} value={this.state.news_url} placeholder="URL" keyboardType="url" /> </View> <View style={styles.input_row}> <Button onPress={this.shareNews.bind(this)} style={[styles.btn, styles.share_btn]}> Share </Button> </View> </View> </Modal>
Установите стили для компонента:
const styles = StyleSheet.create({ container: { flex: 1, alignSelf: 'stretch', backgroundColor: '#F5FCFF', }, header: { flex: 1, backgroundColor: '#3B3738', flexDirection: 'row' }, app_title: { flex: 7, padding: 10 }, header_text: { fontSize: 20, color: '#FFF', fontWeight: 'bold' }, header_button_container: { flex: 3 }, body: { flex: 19 }, btn: { backgroundColor: "#05A5D1", color: "white", margin: 10 }, modal: { height: 300 }, modal_header: { margin: 20, }, modal_body: { alignItems: 'center' }, input_row: { padding: 20 }, modal_header_text: { fontSize: 18, fontWeight: 'bold' }, share_btn: { width: 100 }, news_item: { paddingLeft: 10, paddingRight: 10, paddingTop: 15, paddingBottom: 15, marginBottom: 5, borderBottomWidth: 1, borderBottomColor: '#ccc', flex: 1, flexDirection: 'row' }, news_item_text: { color: '#575757', fontSize: 18 }, upvote: { flex: 2, paddingRight: 15, paddingLeft: 5, alignItems: 'center' }, news_title: { flex: 18, justifyContent: 'center' }, upvote_text: { fontSize: 18, fontWeight: 'bold' } });
Серверный компонент
Теперь пришло время перейти к серверному компоненту приложения, где вы узнаете, как сохранять и отправлять новости в RethinkDB, а также как информировать приложение о том, что в базе данных произошли изменения.
Создание базы данных
Я собираюсь предположить, что вы уже установили RethinkDB на свой компьютер. Если нет, следуйте инструкциям по установке и началу работы на веб-сайте RethinkDB .
После этого вы можете получить доступ к http://localhost:8080
в своем браузере, чтобы просмотреть консоль администратора RethinkDB. Нажмите на вкладку Таблицы, затем нажмите кнопку Добавить базу данных . Откроется модальное окно, в котором вы сможете ввести имя базы данных, назвать его «newssharer» и нажать « Добавить» .
Теперь создайте таблицу, в которой вы собираетесь сохранять новости. Нажмите кнопку « Добавить таблицу» , назовите ее «news_items», затем нажмите « Создать таблицу» .
Установить зависимости
Вы можете установить зависимости сервера, перейдя в корень каталога проекта (с файлами newssharer-server.js и package.json ), и выполнить npm install
для установки следующих зависимостей:
- express : веб-инфраструктура для Node.js, которая позволяет создавать веб-сервер, который отвечает на определенные маршруты.
- body-parser : позволяет легко извлекать строку JSON, передаваемую в теле запроса.
- rethinkdb : клиент RethinkDB для Node.js.
- socket.io : среда реального времени, которая позволяет вам общаться со всеми подключенными клиентами, когда кто-то делится новостями или опровергает существующие новости.
Код на стороне сервера
Внутри newssharer-server.js :
var r = require('rethinkdb'); var express = require('express'); var app = express(); var server = require('http').createServer(app); var io = require('socket.io')(server); var bodyParser = require('body-parser'); app.use(bodyParser.json()); var connection; r.connect({host: 'localhost', port: 28015}, function(err, conn) { if(err) throw err; connection = conn; r.db('newssharer').table('news_items') .orderBy({index: r.desc('upvotes')}) .changes() .run(connection, function(err, cursor){ if (err) throw err; io.sockets.on('connection', function(socket){ cursor.each(function(err, row){ if(err) throw err; io.sockets.emit('news_updated', row); }); }); }); }); app.get('/create-table', function(req, res){ r.db('newssharer').table('news_items').indexCreate('upvotes').run(connection, function(err, result){ console.log('boom'); res.send('ok') }); }); app.get('/fill', function(req, res){ r.db('newssharer').table('news_items').insert([ { title: 'A Conversation About Fantasy User Interfaces', url: 'https://www.subtraction.com/2016/06/02/a-conversation-about-fantasy-user-interfaces/', upvotes: 30 }, { title: 'Apple Cloud Services Outage', url: 'https://www.apple.com/support/systemstatus/', upvotes: 20 } ]).run(connection, function(err, result){ if (err) throw err; res.send('news_items table was filled!'); }); }); app.get('/news', function(req, res){ res.header("Content-Type", "application/json"); r.db('newssharer').table('news_items') .orderBy({index: r.desc('upvotes')}) .limit(30) .run(connection, function(err, cursor) { if (err) throw err; cursor.toArray(function(err, result) { if (err) throw err; res.send(result); }); }); }); app.post('/save-newsitem', function(req, res){ var news_title = req.body.news_title; var news_url = req.body.news_url; r.db('newssharer').table('news_items').insert([ { 'title': news_title, 'url': news_url, 'upvotes': 100 }, ]).run(connection, function(err, result){ if (err) throw err; res.send('ok'); }); }); app.post('/upvote-newsitem', function(req, res){ var id = req.body.news_id; var upvote_count = req.body.upvotes; r.db('newssharer').table('news_items') .filter(r.row('id').eq(id)) .update({upvotes: upvote_count}) .run(connection, function(err, result) { if (err) throw err; res.send('ok'); }); }); app.get('/test/upvote', function(req, res){ var id = '144f7d7d-d580-42b3-8704-8372e9b2a17c'; var upvote_count = 350; r.db('newssharer').table('news_items') .filter(r.row('id').eq(id)) .update({upvotes: upvote_count}) .run(connection, function(err, result) { if (err) throw err; res.send('ok'); }); }); app.get('/test/save-newsitem', function(req, res){ r.db('newssharer').table('news_items').insert([ { 'title': 'banana', 'url': 'http://banana.com', 'upvotes': 190, 'downvotes': 0 }, ]).run(connection, function(err, result){ if(err) throw err; res.send('ok'); }); }); server.listen(3000);
В приведенном выше коде сначала вы импортируете зависимости:
var r = require('rethinkdb'); var express = require('express'); var app = express(); var server = require('http').createServer(app); var io = require('socket.io')(server); var bodyParser = require('body-parser'); app.use(bodyParser.json());
Создайте переменную для хранения текущего соединения RethinkDB.
var connection;
Прислушиваться к изменениям
Подключитесь к базе данных RethinkDB, по умолчанию RethinkDB работает на порту 28015
поэтому вы подключаетесь к нему. Если вы использовали другой порт, замените 28015
на используемый вами порт.
r.connect({host: 'localhost', port: 28015}, function(err, conn) { if(err) throw err; connection = conn; ... });
Все еще находясь в коде соединения с базой данных, запросите таблицу newssharer
базе данных newssharer
, упорядочив элементы по их количеству голосов. Затем используйте функцию Changefeeds RethinkDB для прослушивания изменений в таблице (своего рода журнал базы данных). Каждый раз, когда в таблице происходит изменение (операции CRUD), оно уведомляется об изменении.
r.db('newssharer').table('news_items') .orderBy({index: r.desc('upvotes')}) .changes() .run(connection, function(err, cursor){ ... });
Внутри функции обратного вызова для метода run
инициализируйте соединение с сокетом и прокрутите содержимое cursor
. cursor
представляет изменения, внесенные в таблицу. Каждый раз, когда происходит изменение, он запускает функцию cursor.each
.
Примечание . Функция не содержит всех изменений данных. Предыдущие изменения заменяются при каждом новом изменении. Это означает, что в любой момент времени он проходит только по одной строке. Это позволяет отправлять изменения клиенту с помощью socket.io.
if (err) throw err; //check if there are errors and return it if any io.sockets.on('connection', function(socket){ cursor.each(function(err, row){ if(err) throw err; io.sockets.emit('news_updated', row); }); });
Каждая row
имеет следующую структуру, если новость является общей:
{ "old_val": null, "new_val": { "id": 1, "news_title": "Google", "news_url": "http://google.com", "upvotes": 0 } }
Вот почему вы проверили на null
ранее, потому что недавно опубликованный новостной элемент не будет иметь old_val
.
Если пользователь проголосует за новость:
{ "old_val": { "id": 1, "news_title": "Google", "news_url": "http://google.com", "upvotes": 0 } "new_val": { "id": 1, "news_title": "Google", "news_url": "http://google.com", "upvotes": 1 } }
Возвращает как всю структуру для старого значения, так и новое значение строки. Это означает, что вы можете обновить более одного поля в одном клиенте и отправить эти изменения всем другим подключенным клиентам. RethinkDB упрощает реализацию приложений в реальном времени благодаря функции changefeeds.
Добавление индекса в поле Upvotes
Это маршрут, который добавляет индекс в поле upvotes
:
app.get('/add-index', function(req, res){ r.db('newssharer').table('news_items').indexCreate('upvotes').run(connection, function(err, result){ res.send('ok') }); });
Это необходимо для orderBy
функции orderBy
, потому что для ее сортировки необходимо, чтобы у поля, которое вы сортируете, был индекс.
.orderBy({index: r.desc('upvotes')})
Когда сервер работает, обязательно откройте http://localhost:3000/add-index
в браузере, прежде чем тестировать приложение. Этот маршрут нужно вызывать только один раз.
Добавление фиктивных новостей
Этот маршрут вставляет фиктивные записи в таблицу news_items
. Это необязательно для целей тестирования, так что вы можете просматривать новости сразу, без добавления их через приложение.
app.get('/fill', function(req, res){ r.db('newssharer').table('news_items').insert([ { title: 'A Conversation About Fantasy User Interfaces', url: 'https://www.subtraction.com/2016/06/02/a-conversation-about-fantasy-user-interfaces/', upvotes: 30 }, { title: 'Apple Cloud Services Outage', url: 'https://www.apple.com/support/systemstatus/', upvotes: 20 } ]).run(connection, function(err, result){ if (err) throw err; res.send('news_items table was filled!'); }); });
Возвращение новостей
Этот маршрут возвращает новости:
app.get('/news', function(req, res){ res.header("Content-Type", "application/json"); r.db('newssharer').table('news_items') .orderBy({index: r.desc('upvotes')}) .limit(30) .run(connection, function(err, cursor) { if (err) throw err; cursor.toArray(function(err, result) { if (err) throw err; res.send(result); }); }); });
Новостные элементы упорядочены от наибольшего количества голосов до минимального и ограничены 30. Вместо использования cursor.each
для циклического просмотра новостных элементов используйте cursor.toArray
чтобы преобразовать его в массив со следующей структурой:
[ { "title": "A Conversation About Fantasy User Interfaces", "url": "https://www.subtraction.com/2016/06/02/a-conversation-about-fantasy-user-interfaces/", "upvotes": 30 }, { "title": "Apple Cloud Services Outage", "url": "https://www.apple.com/support/systemstatus/", "upvotes": 20 } ]
Создание новости
Этот маршрут сохраняет новость:
app.post('/save-newsitem', function(req, res){ var news_title = req.body.news_title; var news_url = req.body.news_url; r.db('newssharer').table('news_items').insert([ { 'title': news_title, 'url': news_url, 'upvotes': 100 }, ]).run(connection, function(err, result){ if (err) throw err; res.send('ok'); }); });
Это вызывается, когда пользователь делится новостью в приложении. Он принимает заголовок новости и URL из тела запроса. Начальное количество голосов установлено на 100, но вы можете выбрать другой номер.
Upvoting News News
Это маршрут для голосования новостей:
app.post('/upvote-newsitem', function(req, res){ var id = req.body.news_id; var upvote_count = req.body.upvotes; r.db('newssharer').table('news_items') .filter(r.row('id').eq(id)) .update({upvotes: upvote_count}) .run(connection, function(err, result) { if (err) throw err; res.send('ok'); }); });
Это вызывается, когда пользователь поднимает новость в приложении. Он использует идентификатор элемента новостей, чтобы получить и затем обновить его.
Примечание . Вы уже upvotes
число upvotes
внутри приложения, поэтому upvotes
значение, которое содержится в теле запроса.
Тестовое сохранение и обновление новостей
Я также включил несколько маршрутов для тестирования сохранения и обновления новостей. Лучшее время для доступа к ним, когда приложение уже запущено на вашем устройстве. Таким образом, вы увидите, что пользовательский интерфейс обновляется. Как запустить приложение, будет рассказано в следующем разделе.
Это маршрут для тестирования сохранения новости:
app.get('/test/save-newsitem', function(req, res){ r.db('newssharer').table('news_items').insert([ { 'title': 'banana', 'url': 'http://banana.com', 'upvotes': 190, 'downvotes': 0 }, ]).run(connection, function(err, result){ if(err) throw err; res.send('ok'); }); });
И это маршрут для тестирования новостей. Обязательно замените идентификатор на идентификатор существующей новости, чтобы он работал.
app.get('/test/upvote', function(req, res){ var id = '144f7d7d-d580-42b3-8704-8372e9b2a17c'; var upvote_count = 350; r.db('newssharer').table('news_items') .filter(r.row('id').eq(id)) .update({upvotes: upvote_count}) .run(connection, function(err, result) { if (err) throw err; res.send('ok'); }); });
Запуск сервера
На данный момент я предполагаю, что RethinkDB все еще работает в фоновом режиме. Запустите его, если он еще не запущен. Когда он запустится, запустите node newssharer-server.js
в корне каталога проекта, чтобы запустить серверный компонент приложения.
Запуск приложения
Вы можете запустить приложение так же, как и любое другое приложение React Native. Ниже приведены ссылки для запуска приложения на выбранной вами платформе:
Если у вас возникли проблемы с запуском приложения, вы можете проверить раздел « Общие проблемы » в моей предыдущей статье « Создание приложения для Android с помощью React Native» .
Как только приложение запустится, попробуйте его или получите доступ к любому из тестовых маршрутов в вашем браузере.
Что дальше
Вот несколько советов по дальнейшему улучшению приложения:
- Вместо того, чтобы открывать новости в приложении веб-браузера по умолчанию на устройстве, используйте компонент WebView React Native для создания веб-просмотра, используемого в приложении.
- Приложение позволяет пользователям многократно нажимать на кнопку upvote, добавлять функцию, чтобы проверить, не проголосовал ли текущий пользователь за новость.
- Настройте сервер на прием только запросов, поступающих из приложения.
Вот и все! В этом руководстве вы создали приложение для обмена новостями в реальном времени и узнали, как использовать изменения Socket.io и RethinkDB для создания приложения в реальном времени.