Статьи

Создание приложения с отслеживанием состояния в реальном времени с помощью React Native и Pusher

Теперь пользователи ожидают, что приложения будут обновляться и реагировать на их действия в режиме реального времени. К счастью, сейчас существует множество языковых разновидностей и библиотек, которые помогут вам создавать эти высокодинамичные приложения. Из этого руководства вы узнаете, как создать приложение для чата в реальном времени с помощью Pusher, React-native и Redux для управления состоянием приложения.

Вы можете найти полный проект на GitHub .

Установить зависимости

толкатель

Pusher — это коммуникационная платформа в реальном времени, используемая для трансляции сообщений слушателям через их подписку на канал. Слушатели подписываются на канал, и сообщения передаются на канал, и все слушатели получают сообщения.

Сначала вам нужно будет создать учетную запись, а затем установить модуль Pusher npm с помощью следующей команды:

npm init npm install pusher -g npm install pusher-js -g 

В разделе « Ключи приложения » проекта Pusher запишите значения app_id , key и secret .

React Native

React Native — это платформа для создания многофункциональных, быстрых и собственных мобильных приложений с теми же принципами, которые используются для создания веб-приложений с помощью React.js . React (для меня) представляет собой лучший способ создания пользовательских интерфейсов, и его стоит изучить, чтобы лучше понять этот учебник и сделать вашу интерфейсную жизнь намного проще. Если вы ранее не использовали React Native, у SitePoint есть много учебных пособий , в том числе Быстрый совет для начала работы .

Redux

Redux — это простой контейнер состояний (самый простой, который я когда-либо использовал), который помогает поддерживать состояние в приложениях React.js (и React Native), используя однонаправленный поток состояний для компонентов пользовательского интерфейса и обратно из компонента пользовательского интерфейса в дерево состояний Redux. , Для более подробной информации, посмотрите этот удивительный видеоурок от человека, который создал Redux. Вы узнаете много принципов функционального программирования в Javascript, и это заставит вас увидеть Javascript в другом свете.

Бэкэнд приложения

Во-первых, приложению необходим бэкэнд для отправки сообщений чата и в качестве точки, откуда сообщения чата передаются всем слушателям. Вы создадите этот бэкенд с помощью Express.js, минималистской веб-платформы, работающей на node.js.

Установите Express с помощью следующей команды:

 npm install express -g 

Создайте папку для проекта под названием ChatServer и внутри нее файл index.js .

В index.js потребуйте необходимые библиотеки и создайте экспресс-приложение, работающее на порту 5000 .

 var express = require('express'); var Pusher = require('pusher'); var app = express(); app.set('port', (process.env.PORT || 5000)); 

Создайте свой собственный экземпляр библиотеки Pusher, передав ему значения app_id , key и secret :

 ... const pusher = new Pusher({ appId: 'YOUR PUSHER APP_ID HERE', key: 'YOUR PUSHER KEY HERE', secret: 'YOUR PUSHER SECRET HERE' }) 

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

 ... app.get('/chat/:chat', function(req,res){ const chat_data = JSON.parse(req.params.chat); pusher.trigger('chat_channel', 'new-message', {chat:chat_data}); }); app.listen(app.get('port'), function() { console.log('Node app is running on port', app.get('port')); }); 

Мобильное приложение

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

 react-native init PusherChat cd PusherChat 

Приложению нужны некоторые другие зависимости:

  • Axios — для обещаний и асинхронных запросов к бэкэнду.
  • AsyncStorage — Для хранения сообщений чата локально.
  • Момент — для установки времени отправки каждого сообщения чата и упорядочения сообщений на основе этого времени.
  • Pusher-js — для подключения к толкателю.
  • Redux — государственный контейнер
  • Redux-thunk — простое промежуточное ПО, помогающее в распределении действий.
  • React-redux — привязки React для Redux.

Вы уже должны были установить pusher-js ранее, и AsyncStorage является частью React native. Установите остальные, запустив:

 npm install --save redux redux-thunk moment axios react-redux 

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

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

Создайте новый файл в src / actions / index.js и добавьте следующее:

 import axios from 'axios' import { AsyncStorage } from 'react-native' import moment from 'moment' import Pusher from 'pusher-js/react-native'; export const SEND_CHAT = "SEND_CHAT"; export const GET_ALL_CHATS = "GET_ALL_CHATS"; export const RECEIVE_MESSAGE = " RECEIVE_MESSAGE"; 

Вам также нужны вспомогательные функции, которые инкапсулируют и возвращают соответствующий action_type при вызове, чтобы при отправке чата вы отправляли функцию sendChat и ее полезную нагрузку:

 const sendChat = (payload) => { return { type: SEND_CHAT, payload: payload }; }; const getChats = (payload) => { return { type: GET_ALL_CHATS, payload: payload }; }; const newMessage = (payload) => { return { type: RECEIVE_MESSAGE, payload: payload }; }; 

Вам также нужна функция, которая подписывается на pusher и прослушивает новые сообщения. Для каждого нового сообщения, получаемого этой функцией, добавьте его на устройство AsyncStorage и AsyncStorage новое действие сообщения, чтобы обновить состояние приложения.

 // function for adding messages to AsyncStorage const addToStorage = (data) => { AsyncStorage.setItem(data.convo_id+data.sent_at, JSON.stringify(data), () => {}) } // function that listens to pusher for new messages and dispatches a new // message action export function newMesage(dispatch){ const socket = new Pusher("3c01f41582a45afcd689"); const channel = socket.subscribe('chat_channel'); channel.bind('new-message', (data) => { addToStorage(data.chat); dispatch(newMessage(data.chat)) } ); } 

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

 export function apiSendChat(sender,message){ const sent_at = moment().format(); const chat = {sender:sender,message:message, sent_at:sent_at}; return dispatch => { return axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`).then(response =>{ }).catch(err =>{ console.log("error", err); }); }; }; 

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

 export function apiGetChats(){ //get from device async storage and not api return dispatch => { dispatch(isFetching()); return AsyncStorage.getAllKeys((err, keys) => { AsyncStorage.multiGet(keys, (err, stores) => { let chats = []; stores.map((result, i, store) => { // get at each store's key/value so you can work with it chats.push(JSON.parse(store[i][1])) }); dispatch(getChats(chats)) }); }); }; } 

Следующим шагом является создание редуктора. Самый простой способ понять, что делает редуктор, — это рассматривать его как кассира банка, который выполняет действия на вашем банковском счете в зависимости от того, какой бланк (тип действия) вы им предоставляете. Если вы предоставляете им квитанцию ​​о снятии (Тип действия) с установленной суммой (полезная нагрузка) для вывода (действие), они удаляют сумму (полезную нагрузку) с вашего банковского счета (штат). Вы также можете добавить деньги (действие + полезная нагрузка) со счетом депозита (Тип действия) на свой счет (штат).

Таким образом, редуктор — это функция, которая влияет на состояние приложения на основе отправленного действия, а действие содержит его тип и полезную нагрузку. В зависимости от типа действия редуктор влияет на состояние приложения.

Создайте новый файл с именем src / redurs / index.js и добавьте следующее:

 import { combineReducers } from 'redux'; import { SEND_CHAT, GET_ALL_CHATS, RECEIVE_MESSAGE} from './../actions' // THE REDUCER const Chats = (state = {chats:[]}, actions) => { switch(actions.type){ case GET_ALL_CHATS: return Object.assign({}, state, { process_status:"completed", chats:state.chats.concat(actions.payload) }); case SEND_CHAT: case NEW_MESSAGE: return Object.assign({}, state, { process_status:"completed", chats:[...state.chats,actions.payload] }); default: return state; } }; const rootReducer = combineReducers({ Chats }) export default rootReducer; 

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

Создайте новый файл src / store / configureStore.js и добавьте следующее:

 import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import rootReducer from '../reducers' const createStoreWithMiddleware = applyMiddleware( thunkMiddleware, createLogger() )(createStore) export default function configureStore(initialState) { const store = createStoreWithMiddleware(rootReducer, initialState) return store } 

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

Создайте новый файл src / Screens / convationscreen.js и добавьте следующее:

 import React, { Component, View, Text, StyleSheet, Image, ListView, TextInput, Dimensions} from 'react-native'; import Button from './../components/button/button'; import { Actions } from 'react-native-router-flux'; import KeyboardSpacer from 'react-native-keyboard-spacer'; import { connect } from 'react-redux'; import moment from 'moment'; import { apiSendChat, newMesage } from './../actions/'; const { width, height } = Dimensions.get('window'); const styles = StyleSheet.create({ container: { flex: 1 }, main_text: { fontSize: 16, textAlign: "center", alignSelf: "center", color: "#42C0FB", marginLeft: 5 }, row: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: "#42C0FB", marginBottom: 10, padding:5 }, back_img: { marginTop: 8, marginLeft: 8, height: 20, width: 20 }, innerRow: { flexDirection: "row", justifyContent: "space-between" }, back_btn: {}, dp: { height: 35, width: 35, borderRadius: 17.5, marginLeft:5, marginRight:5 }, messageBlock: { flexDirection: "column", borderWidth: 1, borderColor: "#42C0FB", padding: 5, marginLeft: 5, marginRight: 5, justifyContent: "center", alignSelf: "flex-start", borderRadius: 6, marginBottom: 5 }, messageBlockRight: { flexDirection: "column", backgroundColor: "#fff", padding: 5, marginLeft: 5, marginRight: 5, justifyContent: "flex-end", alignSelf: "flex-end", borderRadius: 6, marginBottom: 5 }, text: { color: "#5c5c5c", alignSelf: "flex-start" }, time: { alignSelf: "flex-start", color: "#5c5c5c", marginTop:5 }, timeRight: { alignSelf: "flex-end", color: "#42C0FB", marginTop:5 }, textRight: { color: "#42C0FB", alignSelf: "flex-end", textAlign: "right" }, input:{ borderTopColor:"#e5e5e5", borderTopWidth:1, padding:10, flexDirection:"row", justifyContent:"space-between" }, textInput:{ height:30, width:(width * 0.85), color:"#e8e8e8", }, msgAction:{ height:29, width:29, marginTop:13 } }); const username = 'DUMMY_USER'; function mapStateToProps(state) { return { Chats: state.Chats, dispatch: state.dispatch } } class ConversationScreen extends Component { constructor(props) { super(props); const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 != r2}); this.state = { conversation: ds, text:"", username } } componentDidMount(){ const {dispatch, Chats} = this.props; const chats = Chats; chats.sort((a,b)=>{ return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf(); }); this.setState({ conversation: this.state.conversation.cloneWithRows(chats) }) } componentWillReceiveProps(nextProps) { const {dispatch, Chats} = this.props; const chats = Chats; chats.sort((a,b)=>{ return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf(); }); this.setState({ conversation: this.state.conversation.cloneWithRows(chats) }) } renderSenderUserBlock(data){ return ( <View style={styles.messageBlockRight}> <Text style={styles.textRight}> {data.message} </Text> <Text style={styles.timeRight}>{moment(data.time).calendar()}</Text> </View> ) } renderReceiverUserBlock(data){ return ( <View style={styles.messageBlock}> <Text style={styles.text}> {data.message} </Text> <Text style={styles.time}>{moment(data.time).calendar()}</Text> </View> ) } renderRow = (rowData) => { return ( <View> {rowData.sender == username ? this.renderSenderUserBlock(rowData) : this.renderReceiverUserBlock(rowData)} </View> ) } sendMessage = () => { const message = this.state.text; const username = this.state.username; const {dispatch, Chats} = this.props; dispatch(apiSendChat(username,message)) } render() { return ( <View style={styles.container}> <View style={styles.row}> <Button style={styles.back_btn} onPress={() => Actions.pop()}> <Image source={require('./../assets/back_chevron.png')} style={styles.back_img}/> </Button> <View style={styles.innerRow}> <Image source={{uri:"https://avatars3.githubusercontent.com/u/11190968?v=3&s=460"}} style={styles.dp}/> <Text style={styles.main_text}>GROUP CHAT</Text> </View> </View> <ListView renderRow={this.renderRow} dataSource={this.state.conversation}/> <View style={styles.input}> <TextInput style={styles.textInput} onChangeText={(text) => this.setState({username:text})} placeholder="Send has?"/> <TextInput style={styles.textInput} onChangeText={(text) => this.setState({text:text})} placeholder="Type a message"/> <Button onPress={this.sendMessage}> <Image source={require('./../assets/phone.png')} style={styles.msgAction}/> </Button> </View> <KeyboardSpacer/> </View> ) } } export default connect(mapStateToProps)(ConversationScreen) 

React Native предоставляет вам функцию жизненного цикла componentWillReceiveProps(nextProps) вызываемую всякий раз, когда компонент собирается получить новые свойства (реквизиты), и именно в этой функции вы обновляете состояние компонента с помощью сообщений чата.

Функция renderSenderUserBlock отображает сообщение чата, отправленное пользователем, а функция renderReceiverUserBlock отображает сообщение чата, полученное пользователем.

Функция sendMessage получает сообщение из состояния, которое пользователь намеревается отправить, имя пользователя получателя и отправляет действие для отправки сообщения чата.

Функция renderRow переданная компоненту Listview содержит свойства и отображает данные каждой строки в Listview .

Вам нужно передать состояние компонентам приложения и использовать для этого библиотеку React-redux. Это позволяет подключать компоненты для редукции и доступа к состоянию приложения.

React-Redux предоставляет вам 2 вещи:

  1. Компонент «Провайдер», который позволяет передать хранилище в качестве свойства.
  2. Функция «Соединить», которая позволяет компоненту подключаться к избыточному. Он передает избыточное состояние, к которому компонент подключается, как свойства для компонента.

Наконец, создайте app.js, чтобы связать все вместе:

 import React, { Component, StyleSheet, Dimensions} from 'react-native'; import { Provider } from 'react-redux' import configureStore from './store/configureStore' const store = configureStore(); import ConversationScreen from './screens/conversation-screen'; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#fff", }, tabBarStyle: { flex: 1, flexDirection: "row", backgroundColor: "#95a5a6", padding: 0, height: 45 }, sceneStyle: { flex: 1, backgroundColor: "#fff", flexDirection: "column", paddingTop:20 } }) export default class PusherChatApp extends Component { render() { return ( <Provider store={store}> <ConversationScreen /> </Provider> ) } } 

И ссылаться на app.js в index.android.js и index.ios.js , заменяя любое текущее содержимое:

 import React, { AppRegistry, Component, StyleSheet, Text, View } from 'react-native'; import PusherChatApp from './src/app' AppRegistry.registerComponent('PusherChat', () => PusherChatApp); 

Поговори со мной

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