Full-stack NLP z React: Ionic vs Cordova vs React Native
Opublikowany: 2022-03-11W ciągu około 15 lat, odkąd Apple wypuściło pierwszego iPhone'a, krajobraz rozwoju oprogramowania zmienił się radykalnie. W miarę jak smartfony zyskują szerokie zastosowanie i stale zyskują na unikalnych możliwościach, użytkownicy coraz częściej wolą uzyskiwać dostęp do usług oprogramowania za pośrednictwem urządzeń mobilnych niż komputerów stacjonarnych lub laptopów. Smartfony oferują takie funkcje, jak geolokalizacja, uwierzytelnianie biometryczne i wykrywanie ruchu, z których wiele platform komputerowych dopiero teraz zaczyna kopiować. W niektórych grupach demograficznych smartfon lub podobne urządzenie mobilne jest głównym sposobem korzystania z oprogramowania, całkowicie omijając komputery.
Firmy zauważyły tę zmianę i wzmocniły ją w znaczący sposób. Aplikacje mobilne nie są już refleksją. Aplikacje, od Robinhood, firmy pośrednictwa finansowego, przez Instagram, firmę zajmującą się mediami społecznościowymi, po Uber, firmę oferującą przejażdżki, przyjmują strategię rozwoju zorientowaną na urządzenia mobilne. Jeśli istnieje aplikacja komputerowa, często jest ona oferowana jako uzupełnienie aplikacji mobilnej, a nie jako główny cel.
Dla programistów zajmujących się pełnymi stosami dostosowanie się do tych zmieniających się trendów ma kluczowe znaczenie. Na szczęście istnieje wiele dojrzałych i dobrze obsługiwanych technologii, które pomagają twórcom stron internetowych wykorzystać ich umiejętności do tworzenia aplikacji mobilnych. Dzisiaj przyjrzymy się trzem takim technologiom: Cordova, Ionic i React Native. Będziemy używać React.js, jednego z najpopularniejszych frameworków do tworzenia stron internetowych, jako naszej podstawowej technologii programistycznej. Chociaż będziemy koncentrować się na tworzeniu aplikacji na iPhone'a, są to technologie wieloplatformowe i będą kompatybilne z platformą Android.
Co zbudujemy dzisiaj
Zbudujemy aplikację, która wykorzystuje przetwarzanie języka naturalnego (NLP) do przetwarzania i zarządzania kanałami Twittera. Aplikacja pozwoli użytkownikowi wybrać zestaw uchwytów Twittera, pobrać najnowsze aktualizacje za pomocą interfejsu API Twittera i kategoryzować tweety na podstawie sentymentu i tematu. Użytkownik będzie wtedy mógł przeglądać tweety na podstawie nastrojów lub tematu.
Powrót Koniec
Zanim zbudujemy front end, będziemy chcieli zbudować back end. Na razie utrzymamy prosty backend — użyjemy podstawowej, gotowej analizy sentymentu i tagowania części mowy, a także trochę czyszczenia danych, aby poradzić sobie z problemami specyficznymi dla zestawu danych. Użyjemy biblioteki NLP o otwartym kodzie źródłowym o nazwie TextBlob i będziemy wyświetlać wyniki przez Flask.
Analiza nastrojów, oznaczanie części mowy i NLP: szybki elementarz
Jeśli nie pracowałeś wcześniej z aplikacjami do analizy języka naturalnego, te terminy mogą być dla Ciebie bardzo obce. NLP to termin ogólny określający technologie, które analizują i przetwarzają dane z naturalnego języka ludzkiego. Chociaż jest to obszerny temat, istnieje wiele wyzwań, które są wspólne dla wszystkich technologii zajmujących się tym obszarem. Na przykład język ludzki, w przeciwieństwie do języka programowania lub danych liczbowych, ma zazwyczaj luźną strukturę ze względu na permisywną naturę gramatyki języka ludzkiego. Ponadto język ludzki jest bardzo kontekstowy, a fraza wypowiedziana lub napisana w jednym kontekście może nie przekładać się na inny kontekst. Wreszcie, pomijając strukturę i kontekst, język jest niezwykle złożony. Słowa znajdujące się dalej w akapicie mogą zmienić znaczenie zdania na początku akapitu. Słownictwo można wymyślić, przedefiniować lub zmienić. Wszystkie te zawiłości sprawiają, że techniki analizy danych są trudne do zastosowania krzyżowego.
Analiza sentymentu to poddziedzina NLP, która koncentruje się na zrozumieniu emocjonalności fragmentu języka naturalnego. Podczas gdy ludzkie emocje są z natury subiektywne, a zatem trudne do określenia pod względem technologicznym, analiza sentymentu jest poddziedziną, która ma ogromną obietnicę komercyjną. Niektóre zastosowania analizy sentymentu obejmują klasyfikację recenzji produktów w celu zidentyfikowania pozytywnej i negatywnej oceny różnych funkcji, wykrywanie nastroju wiadomości e-mail lub przemówienia oraz grupowanie tekstów piosenek według nastroju. Jeśli szukasz bardziej szczegółowego wyjaśnienia na temat analizy nastrojów, możesz przeczytać mój artykuł na temat tworzenia aplikacji opartej na analizie nastrojów tutaj.
Tagowanie części mowy lub tagowanie POS to zupełnie inne podpole. Celem tagowania POS jest identyfikacja części mowy danego słowa w zdaniu za pomocą informacji gramatycznych i kontekstowych. Zidentyfikowanie tego związku jest znacznie trudniejszym zadaniem niż na pierwszy rzut oka – słowo może mieć bardzo różne części mowy w zależności od kontekstu i struktury zdania, a zasady nie zawsze są jasne nawet dla ludzi. Na szczęście obecnie wiele gotowych modeli oferuje zaawansowane i wszechstronne modele zintegrowane z większością głównych języków programowania. Jeśli chcesz dowiedzieć się więcej, możesz przeczytać mój artykuł na temat tagowania POS tutaj.
Flask, TextBlob i Tweepy
Dla naszego zaplecza NLP użyjemy Flask, TextBlob i Tweepy. Użyjemy Flask do zbudowania małego, lekkiego serwera, TextBlob do obsługi naszego przetwarzania języka naturalnego oraz Tweepy do pobierania tweetów z API Twittera. Zanim zaczniesz kodować, będziesz także chciał uzyskać klucz programisty z Twittera, aby móc pobierać tweety.
Możemy napisać znacznie bardziej wyrafinowany backend i użyć bardziej złożonych technologii NLP, ale dla naszych dzisiejszych celów utrzymamy backend tak prosty, jak to tylko możliwe.
Kod zaplecza
Teraz jesteśmy gotowi do rozpoczęcia kodowania. Uruchom swój ulubiony edytor i terminal Pythona, a do dzieła!
Najpierw będziemy chcieli zainstalować wymagane pakiety.
pip install flask flask-cors textblob tweepy python -m textblob.download_corpora
Teraz napiszmy kod dla naszej funkcjonalności.
Otwórz nowy skrypt Pythona, nazwij go server.py i zaimportuj wymagane biblioteki:
import tweepy from textblob import TextBlob from collections import defaultdict
Napiszmy teraz kilka funkcji pomocniczych:
# simple, average a list of numbers with a guard clause to avoid division by zero def mean(lst): return sum(lst)/len(lst) if len(lst) > 0 else 0 # call the textblob sentiment analysis API and noun phrases API and return it as a dict def get_sentiment_and_np(sentence): blob = TextBlob(sentence) return{ 'sentiment': mean([s.sentiment.polarity for s in blob.sentences if s.sentiment.polarity != 0.0]), 'noun_phrases': list(blob.noun_phrases) } # use the tweepy API to get the last 50 posts from a user's timeline # We will want to get the full text if the text is truncated, and we will also remove retweets since they're not tweets by that particular account. def get_tweets(handle): auth = tweepy.OAuthHandler('YOUR_DEVELOPER_KEY') auth.set_access_token('YOUR_DEVELOPER_SECRET_KEY') api = tweepy.API(auth) tl = api.user_timeline(handle, count=50) tweets = [] for tl_item in tl: if 'retweeted_status' in tl_item._json: Continue # this is a retweet if tl_item._json['truncated']: status = api.get_status(tl_item._json['id'], tweet_mode='extended') # get full text tweets.append(status._json['full_text']) else: tweets.append(tl_item._json['text']) return tweets # http and https are sometimes recognized as noun phrases, so we filter it out. # We also try to skip noun phrases with very short words to avoid certain false positives # If this were a commercial app, we would want a more sophisticated filtering strategy. def good_noun_phrase(noun_phrase): noun_phrase_list = noun_phrase.split(' ') for np in noun_phrase_list: if np in {'http', 'https'} or len(np) < 3: return False return True
Teraz, gdy mamy napisane funkcje pomocnicze, możemy wszystko połączyć za pomocą kilku prostych funkcji:
# reshapes the tagged tweets into dictionaries that can be easily consumed by the front-end app def group_tweets(processed_tweets): # Sort it by sentiment sentiment_sorted = sorted(processed_tweets, key=lambda x: x['data']['sentiment']) # collect tweets by noun phrases. One tweet can be present in the list of more than one noun phrase, obviously. tweets_by_np = defaultdict(list) for pt in processed_tweets: for np in pt['data']['noun_phrases']: tweets_by_np[np].append(pt) grouped_by_np = {np.title(): tweets for np, tweets in tweets_by_np.items() if len(tweets) > 1 and good_noun_phrase(np)} return sentiment_sorted, grouped_by_np # download, filter, and analyze the tweets def download_analyze_tweets(accounts): processed_tweets = [] for account in accounts: for tweet in get_tweets(account): processed_tweet = ' '.join([i for i in tweet.split(' ') if not i.startswith('@')]) res = get_sentiment_and_np(processed_tweet) processed_tweets.append({ 'account': account, 'tweet': tweet, 'data': res }) sentiment_sorted, grouped_by_np = group_tweets(processed_tweets) return processed_tweets, sentiment_sorted, grouped_by_np
Możesz teraz uruchomić funkcję download_analyze_tweets
na liście uchwytów, które chcesz śledzić, i powinieneś zobaczyć wyniki.
Uruchomiłem następujący kod:
if __name__ == '__main__': accounts = ['@spacex', '@nasa'] processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts) print(processed_tweets) print(sentiment_sorted) print(grouped_by_np)
Wykonanie tego dało następujące wyniki. Wyniki są oczywiście zależne od czasu, więc jeśli widzisz coś podobnego, jesteś na dobrej drodze.
[{'account': '@spacex', 'tweet': 'Falcon 9… [{'account': '@nasa', 'tweet': 'Our Mars rove… {'Falcon': [{'account': '@spacex', 'tweet': 'Falc….
Teraz możemy zbudować serwer Flask, co jest dość proste. Utwórz pusty plik o nazwie server.py i napisz następujący kod:
from flask import Flask, request, jsonify from twitter import download_analyze_tweets from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/get_tweets', methods=['POST']) def get_tweets(): accounts = request.json['accounts'] processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts) return jsonify({ 'processedTweets': processed_tweets, 'sentimentSorted': sentiment_sorted, 'groupedByNp': grouped_by_np }) if __name__ == '__main__': app.run(debug=True)
Uruchom serwer, a teraz powinieneś być w stanie wysłać żądanie post do serwera za pomocą wybranego klienta HTTP. Przekaż {konta: [„@NASA”, „@SpaceX”]} jako argument json, a powinieneś zobaczyć, że API zwraca coś podobnego do tego, co zostało zwrócone w kodzie analizy Twittera.
Teraz, gdy mamy serwer, jesteśmy gotowi do kodowania frontendu. Ze względu na mały niuans w sieci na emulatorze telefonu, zalecam wdrożenie gdzieś swojego API. W przeciwnym razie będziesz chciał wykryć, czy Twoja aplikacja działa na emulatorze i wysyłać żądania do <Your Computer IP>:5000
zamiast do localhost:5000
, gdy jest w emulatorze. Jeśli wdrożysz kod, możesz po prostu wysłać żądanie do tego adresu URL.
Istnieje wiele opcji wdrożenia serwera. Aby uzyskać darmowy, prosty serwer debugowania z minimalną wymaganą konfiguracją, polecam coś takiego jak PythonAnywhere, który powinien być w stanie uruchomić ten serwer po wyjęciu z pudełka.
Teraz, gdy mamy zakodowany serwer back-end, spójrzmy na front end. Zaczniemy od jednej z najwygodniejszych opcji dla programisty WWW: Cordova.
Implementacja Apache Cordova
Podkład Cordova
Apache Cordova to technologia oprogramowania, która pomaga twórcom stron internetowych kierować reklamy na platformy mobilne. Korzystając z możliwości przeglądarki internetowej zaimplementowanych na platformach smartfonów, Cordova opakowuje kod aplikacji internetowej w natywny kontener aplikacji w celu utworzenia aplikacji. Cordova nie jest jednak po prostu wymyślną przeglądarką internetową. Za pośrednictwem interfejsu API Cordova twórcy stron internetowych mogą uzyskać dostęp do wielu funkcji specyficznych dla smartfonów, takich jak obsługa offline, usługi lokalizacyjne i kamera na urządzeniu.
Dla naszej aplikacji napiszemy aplikację używając React.js jako frameworka JS i React-Bootstrap jako frameworka CSS. Ponieważ Bootstrap jest responsywnym frameworkiem CSS, obsługuje już działanie na mniejszych ekranach. Po napisaniu aplikacji skompilujemy ją do aplikacji internetowej za pomocą Cordova.
Konfiguracja aplikacji
Zaczniemy od zrobienia czegoś wyjątkowego, aby skonfigurować aplikację Cordova React. W artykule Medium deweloper Shubham Patil wyjaśnia, co robimy. Zasadniczo konfigurujemy środowisko programistyczne React za pomocą React CLI, a następnie środowisko programistyczne Cordova za pomocą Cordova CLI, zanim ostatecznie je połączymy.
Aby rozpocząć, uruchom następujące dwa polecenia w folderze kodu:
cordova create TwitterCurationCordova create-react-app twittercurationreact
Po zakończeniu konfiguracji będziemy chcieli przenieść zawartość folderu public i src aplikacji React do aplikacji Cordova. Następnie w pliku package.json skopiuj skrypty, listę przeglądarek i zależności z projektu React. Dodaj także "homepage": "./"
w katalogu głównym package.json, aby umożliwić zgodność z Cordova.
Po scaleniu pliku package.json będziemy chcieli zmienić plik public/index.html, aby działał z Cordova. Otwórz plik i skopiuj metatagi z www/index.html, a także skrypt na końcu tagu body po załadowaniu Cordova.js.
Następnie zmień plik src/index.js, aby wykryć, czy działa na Cordova. Jeśli działa na Cordova, będziemy chcieli uruchomić kod renderowania w ramach obsługi zdarzeń deviceready. Jeśli działa w zwykłej przeglądarce, od razu renderuj.
const renderReactDom = () => { ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); } if (window.cordova) { document.addEventListener('deviceready', () => { renderReactDom(); }, false); } else { renderReactDom(); }
Na koniec musimy skonfigurować nasz potok wdrażania. Dodaj poniższą definicję do pliku config.xml:
<hook type="before_prepare" src="hooks/prebuild.js" />
I umieść następujący skrypt w prebuild.js:
const path = require('path'); const { exec } = require('child_process'); const fs = require('fs'); const rimraf = require('rimraf'); function renameOutputFolder(buildFolderPath, outputFolderPath) { return new Promise((resolve, reject) => { fs.rename(buildFolderPath, outputFolderPath, (err) => { if (err) { reject(err); } else { resolve('Successfully built!'); } }); }); } function execPostReactBuild(buildFolderPath, outputFolderPath) { return new Promise((resolve, reject) => { if (fs.existsSync(buildFolderPath)) { if (fs.existsSync(outputFolderPath)) { rimraf(outputFolderPath, (err) => { if (err) { reject(err); return; } renameOutputFolder(buildFolderPath, outputFolderPath) .then(val => resolve(val)) .catch(e => reject(e)); }); } else { renameOutputFolder(buildFolderPath, outputFolderPath) .then(val => resolve(val)) .catch(e => reject(e)); } } else { reject(new Error('build folder does not exist')); } }); } module.exports = () => { const projectPath = path.resolve(process.cwd(), './node_modules/.bin/react-scripts'); return new Promise((resolve, reject) => { exec(`${projectPath} build`, (error) => { if (error) { console.error(error); reject(error); return; } execPostReactBuild(path.resolve(__dirname, '../build/'), path.join(__dirname, '../www/')) .then((s) => { console.log(s); resolve(s); }) .catch((e) => { console.error(e); reject(e); }); }); }); };
Spowoduje to wykonanie kompilacji React i umieszczenie folderu kompilacji w odpowiednim miejscu przed rozpoczęciem kompilacji Cordova, automatyzując w ten sposób proces wdrażania.
Teraz możemy spróbować uruchomić naszą aplikację. Uruchom następujące polecenie w wierszu poleceń:
npm install rimraf npm install npm run start
Powinieneś zobaczyć skonfigurowaną i uruchomioną aplikację React w przeglądarce. Dodaj Cordova teraz:
cordova platform add iOS cordova run iOS
Powinieneś zobaczyć aplikację React działającą w emulatorze.
Konfiguracja routera i pakietów
Aby skonfigurować część infrastruktury, której potrzebujemy do zbudowania aplikacji, zacznijmy od zainstalowania wymaganych pakietów:
npm install react-bootstrap react-router react-router-dom
Teraz skonfigurujemy routing, a jednocześnie skonfigurujemy prosty globalny obiekt stanu, który będzie współdzielony przez wszystkie komponenty. W aplikacji produkcyjnej będziemy chcieli użyć systemu zarządzania stanem, takiego jak Redux lub MobX, ale na razie zachowamy prostotę. Przejdź do App.js i skonfiguruj trasę w ten sposób:
import { BrowserRouter as Router, Redirect, Route, } from "react-router-dom"; function App() { const [curatedTweets, setCuratedTweets] = useState(); return <Router> <Route path="/" exact render={() => <Input setCuratedTweets={setCuratedTweets} />} /> <Route path="/display" render={() => <Display curatedTweets={curatedTweets} />} /> <Route path="*" exact render={() => <Redirect to="/" />} /> </Router> }
W tej definicji trasy wprowadziliśmy dwie trasy, które będziemy musieli zaimplementować: wejście i wyświetlanie. Zauważ, że zmienna curatedTweets
jest przekazywana do Display, a zmienna setCuratedTweets
jest przekazywana do Input. Oznacza to, że składnik wejściowy będzie mógł wywołać funkcję, aby ustawić zmienną curatedTweets
, a Display otrzyma zmienną do wyświetlenia.
Aby rozpocząć kodowanie komponentów, utwórzmy folder w /src o nazwie /src/components. W /src/components utwórz kolejny folder o nazwie /src/components/input i dwa pliki poniżej: input.js i input.css. Zrób to samo dla komponentu Display - utwórz /src/components/display i poniżej: display.js i display.css.
Pod nimi stwórzmy komponenty pośredniczące, takie jak:
import React from 'react'; import 'input.css' const Input = () => <div>Input</div>; export default Input
I to samo w przypadku wyświetlacza:
import React from 'react'; import display.css' const Display = () => <div>Display</div>; export default Display
Dzięki temu nasz wireframing jest gotowy i aplikacja powinna działać. Zakodujmy teraz stronę Input.
Strona wejściowa
Plan z dużym obrazem
Zanim napiszemy kod, zastanówmy się, co chcemy zrobić na naszej stronie Input. Oczywiście będziemy chcieli, aby użytkownicy mogli wprowadzać i edytować uchwyty Twittera, z których chcą czerpać. Chcemy również, aby użytkownicy mogli wskazać, że skończyli. Kiedy użytkownicy wskażą, że skończyli, będziemy chcieli pobrać wyselekcjonowane tweety z naszego interfejsu API do kuracji w Pythonie i na koniec przejść do komponentu Display.
Teraz, gdy wiemy, co chcemy, aby zrobił nasz komponent, jesteśmy gotowi do kodowania.
Konfiguracja pliku
Zacznijmy od zaimportowania biblioteki React Router withRouter
, aby uzyskać dostęp do funkcji nawigacji, potrzebnych nam komponentów React Bootstrap, takich jak:
import React, {useState} from 'react'; import {withRouter} from 'react-router-dom'; import {ListGroup, Button, Form, Container, Row, Col} from 'react-bootstrap'; import './input.css';
Teraz zdefiniujmy funkcję skrótu dla Input. Wiemy, że Input otrzymuje funkcję setCuratedTweets
i chcemy również dać mu możliwość nawigowania do trasy wyświetlania po ustawieniu wyselekcjonowanych tweetów z naszego API Pythona. Dlatego będziemy chcieli pobrać z props setCuratedTweets
i historię (do nawigacji).
const Input = ({setCuratedTweets, history}) => { return <div>Input</div> }
Aby nadać mu dostęp do API historii, umieścimy go za pomocą withRouter
w instrukcji export na końcu pliku:
export default withRouter(Input);
Kontenery danych
Skonfigurujmy kontenery danych za pomocą haków reakcji. Zaimportowaliśmy już hak useState
, więc możemy dodać następujący kod do ciała komponentu Input:
const [handles, setHandles] = useState([]); const [handleText, setHandleText] = useState('');
Tworzy to kontener i modyfikatory dla uchwytów, które będą przechowywać listę uchwytów, z których użytkownik chce pobrać, oraz handleText
, który będzie zawierał zawartość pola tekstowego, którego użytkownik używa do wprowadzenia uchwytu.
Teraz zakodujmy składniki interfejsu użytkownika.
Komponenty interfejsu użytkownika
Komponenty interfejsu użytkownika będą dość proste. Będziemy mieli jeden wiersz Bootstrap, który zawiera pole tekstowe wejściowe wraz z dwoma przyciskami, jeden do dodania bieżącej zawartości pola wejściowego do listy uchwytów, a drugi do pobrania z API. Będziemy mieli kolejny wiersz Bootstrap, który wyświetla listę uchwytów, które użytkownik chce wyciągnąć za pomocą grupy list Bootstrap. W kodzie wygląda to tak:
return ( <Container className="tl-container"> <Row> <Col> <Form.Control type="text" value={handleText} onChange={changeHandler} placeholder="Enter handle to pull" /> </Col> </Row> <Row className='input-row'> <Col> <Button variant="primary" onClick={getPull}>Pull</Button> {' '} <Button variant="success" onClick={onAddClicked}>Add</Button> </Col> </Row> <Row> <Col> <ListGroup className="handles-lg"> {handles.map((x, i) => <ListGroup.Item key={i}> {x} <span onClick={groupItemClickedBuilder(i)} className="delete-btn-span"> <Button variant="danger" size="sm"> delete </Button> </span> </ListGroup.Item>)} </ListGroup> </Col> </Row> </Container> );
Oprócz komponentu UI będziemy chcieli zaimplementować trzy programy obsługi zdarzeń interfejsu użytkownika, które obsługują zmiany danych. Procedura obsługi zdarzeń getPull
, która wywołuje API, zostanie zaimplementowana w następnej sekcji.
// set the handleText to current event value const textChangeHandler = (e) => { e.preventDefault(); setHandleText(e.target.value); } // Add handleText to handles, and then empty the handleText const onAddClicked = (e) => { e.preventDefault(); const newHandles = [...handles, handleText]; setHandles(newHandles); setHandleText(''); } // Remove the clicked handle from the list const groupItemClickedBuilder = (idx) => (e) => { e.preventDefault(); const newHandles = [...handles]; newHandles.splice(idx, 1); setHandles(newHandles); }
Teraz jesteśmy gotowi do zaimplementowania wywołania API.
Wywołanie API
W przypadku wywołania API chcemy wziąć uchwyty, które chcemy wyciągnąć, wysłać je do API Pythona w żądaniu POST i umieścić wynikowy wynik JSON w zmiennej curatedTweets
. Następnie, jeśli wszystko pójdzie dobrze, chcemy programowo przejść do trasy /display. W przeciwnym razie zarejestrujemy błąd w konsoli, abyśmy mogli łatwiej debugować.
W trybie kodu wygląda to tak:
const pullAPI = (e) => { e.preventDefault(); fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts: handles }) }).then(r=>r.json()).then(j => { setCuratedTweets(j); history.push('/display'); }) .catch(e => { console.log(e); }) }
I z tym powinniśmy być gotowi do wyjścia. Zapraszam do uruchomienia aplikacji, dodania kilku uchwytów i wysłania żądania do API.
Teraz jesteśmy gotowi do zakodowania strony z sentymentem.
Tryb sortowania na podstawie opinii
Ponieważ interfejs API Pythona już sortuje tweety według nastrojów, po uzyskaniu wyniku z interfejsu API Pythona strona z nastrojami nie jest w rzeczywistości zbyt trudna.
Plan z dużym obrazem
Będziemy chcieli, aby interfejs listy wyświetlał tweety. Będziemy również chcieli, aby kilka elementów nawigacyjnych przełączyło się w tryb grupowania tematów i wróciło do strony Wprowadzanie.
Na początek zdefiniujmy podkomponent trybu SentimentDisplay w pliku display.js.
Komponent wyświetlania nastrojów
SentimentDisplay przyjmie obiekt curatedTweets
i wyświetli tweety posortowane według sentymentu na liście. Z pomocą React-Bootstrap komponent jest dość prosty:
const SentimentDisplay = ({curatedTweets}) => { return <ListGroup> {curatedTweets.sentimentSorted.map((x, i) => <ListGroup.Item key={i}> <div className="account-div">{x.account}:</div> <span className="tweet-span">{x.tweet}</span> <span className="sentiments-span">({x.data.sentiment.toPrecision(2)})</span> </ListGroup.Item> )} </ListGroup> }
Skoro już przy tym jesteśmy, dodajmy też trochę stylizacji. Umieść następujące dane w display.css i zaimportuj je:
.account-div { font-size: 12px; font-weight: 600; } .tweet-span { font-size: 11px; font-weight: 500; } .sentiments-span { font-size: 10px; } .tl-container { margin-top: 10px; } .disp-row { margin-top: 5px; }
Możemy teraz pokazać komponent SentimentDisplay. Zmień funkcję Display
w następujący sposób:
const Display = ({curatedTweets}) => { return <SentimentDisplay curatedTweets={curatedTweets} /> };
Skorzystajmy również z okazji, aby zakodować komponenty nawigacyjne. Będziemy potrzebować dwóch przycisków - przycisku „Powrót do edycji” i trybu grupy tematów.
Możemy zaimplementować te przyciski w osobnym wierszu Bootstrap tuż nad komponentem SentimentDisplay, na przykład:
Return <Container className="tl-container"> <Row> <Col> <Link to="/"><Button variant='primary'>Back</Button></Link> {' '} <Button variant='success'>View by Topic</Button> </Col> </Row> <Row className="disp-row"> <Col> <SentimentDisplay curatedTweets={curatedTweets} /> </Col> </Row> </Container>
Uruchom aplikację i wyciągnij tweety z kilku uchwytów. Wygląda całkiem fajnie!
Tryb grupowania tematów
Teraz chcemy zaimplementować tryb grupowania tematów. Jest trochę bardziej złożony niż SentimentDisplay, ale znowu, kilka bardzo przydatnych komponentów Bootstrap nam ogromnie pomaga.
Plan z dużym obrazem
Pozyskamy wszystkie frazy rzeczownikowe i wyświetlimy je jako listę akordeonową. Po rozwinięciu listy akordeonów wyrenderujemy tweety zawierające frazy rzeczownikowe.
Implementacja przełączania do trybu grupowania tematów
Najpierw zaimplementujmy logikę, aby przełączyć się z trybu sentymentu do trybu grupowania tematów. Zacznijmy od utworzenia komponentu stub:
const TopicDisplay = () => { return <div>Topic Display</div> }
I ustaw trochę logiki, aby utworzyć tryb, aby go wyświetlić. W głównym komponencie wyświetlania dodaj następujące wiersze, aby utworzyć logikę, dla której komponent zostanie wyświetlony.
// controls the display mode. Remember to import {useState} from 'react' const [displayType, setDisplayType] = useState('Sentiment'); // Switch the Display Mode const toggleDisplayType = () => { setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment'); } // determines the text on the mode switch button const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'
I zmień JSX na następujący, aby dodać logikę:
Return <Container className="tl-container"> <Row> <Col> <Link to="/"><Button variant='primary'>Back</Button></Link> {' '} <Button variant='success' onClick={toggleDisplayType}>{switchStr}</Button> </Col> </Row> <Row className="disp-row"> <Col> { displayType === 'Sentiment'? <SentimentDisplay curatedTweets={curatedTweets} />: <TopicDisplay curatedTweets={curatedTweets} /> } </Col> </Row> </Container>
Teraz po przełączeniu powinieneś zobaczyć skrót grupy tematów.
Komponent wyświetlania tematu
Teraz jesteśmy gotowi do kodowania komponentu TopicDisplay
. Jak wspomniano wcześniej, będzie wykorzystywać listę akordeonów Bootstrap. Implementacja jest właściwie dość prosta:
const TopicDisplay = ({curatedTweets}) => { return <Accordion> {Object.keys(curatedTweets.groupedByNp).map((x, i) => <Card key={i}> <Card.Header> <Accordion.Toggle as={Button} variant="link" eventKey={i}> {x} ({curatedTweets.groupedByNp[x].length}) </Accordion.Toggle> </Card.Header> <Accordion.Collapse eventKey={i}> <Card.Body> <ListGroup> {curatedTweets.groupedByNp[x].map((y, i2) => <ListGroup.Item key={i2}> <div className="account-div">{y.account}:</div> <span className="tweet-span">{y.tweet}</span> <span className="sentiments-span">({y.data.sentiment.toPrecision(2)})</span> </ListGroup.Item> )} </ListGroup> </Card.Body> </Accordion.Collapse> </Card> )} </Accordion> }
Uruchom aplikację i powinieneś zobaczyć wyświetlanie tematu.
Teraz aplikacja jest gotowa i jesteśmy gotowi do zbudowania aplikacji dla emulatora.
Uruchamianie aplikacji w emulatorze
Cordova bardzo ułatwia uruchomienie aplikacji w emulatorze. Po prostu uruchom:
cordova platform add ios # if you haven't done so already cordova run ios
Powinieneś zobaczyć aplikację w emulatorze. Ponieważ Bootstrap jest responsywną aplikacją internetową, aplikacja internetowa dostosowuje się do szerokości iPhone'a i wszystko wygląda całkiem ładnie.
Po zakończeniu aplikacji Cordova spójrzmy teraz na implementację Ionic.
Implementacja Ionic-React
Podkład jonowy
Ionic to biblioteka komponentów internetowych i zestaw narzędzi CLI, który ułatwia tworzenie aplikacji hybrydowych. Pierwotnie Ionic został zbudowany na bazie AngularJS i Cordova, ale od tego czasu wydali swoje komponenty w React.js i zaczęli wspierać Capacitor, platformę podobną do Cordova. To, co wyróżnia Ionic, to to, że nawet jeśli używasz komponentów internetowych, komponenty są bardzo podobne do natywnego interfejsu mobilnego. Dodatkowo wygląd i sposób działania komponentów Ionic automatycznie dostosowują się do systemu operacyjnego, na którym działają, co ponownie pomaga aplikacji wyglądać i działać bardziej natywnie i naturalnie. Wreszcie, chociaż wykracza to poza zakres naszego artykułu, Ionic zapewnia również kilka narzędzi do kompilacji, które ułatwiają wdrażanie aplikacji.
W naszej aplikacji będziemy używać komponentów React firmy Ionic do budowy interfejsu użytkownika, wykorzystując część logiki JavaScript, którą zbudowaliśmy w sekcji Cordova.
Konfiguracja aplikacji
Najpierw będziemy chcieli zainstalować narzędzia Ionic. Uruchommy więc następujące:
npm install -g @Ionic/cli native-run cordova-res
A po zakończeniu instalacji przejdźmy do folderu projektu. Teraz używamy Ionic CLI do stworzenia naszego nowego folderu projektu:
ionic start twitter-curation-Ionic blank --type=react --capacitor
Zobacz, jak dzieje się magia, a teraz przejdź do folderu z:
cd twitter-curation-Ionic
I uruchom pustą aplikację za pomocą:
ionic serve
Nasza aplikacja jest więc skonfigurowana i gotowa do pracy. Zdefiniujmy kilka tras.
Zanim przejdziemy dalej, zauważysz, że Ionic rozpoczął projekt za pomocą TypeScript. Chociaż nie staram się używać TypeScript, ma kilka bardzo fajnych funkcji i będziemy go używać do tej implementacji.
Konfiguracja routera
W tej implementacji użyjemy trzech tras — input, sentimentDisplay
i topicDisplay
. Robimy to, ponieważ chcemy skorzystać z funkcji przejść i nawigacji zapewnianych przez Ionic i ponieważ używamy komponentów Ionic, a listy akordeonów nie są dostarczane z Ionic. Możemy oczywiście zaimplementować własne, ale w tym samouczku pozostaniemy przy dostarczonych komponentach Ionic.
Jeśli przejdziesz do App.tsx, powinieneś zobaczyć podstawowe trasy już zdefiniowane.
Strona wejściowa
Plan z dużym obrazem
Będziemy używać podobnej logiki i kodu, co implementacja Bootstrap, z kilkoma kluczowymi różnicami. Najpierw użyjemy TypeScript, co oznacza, że będziemy mieć adnotacje typu dla naszego kodu, które zobaczysz w następnej sekcji. Po drugie, będziemy używać komponentów jonowych, które są bardzo podobne do Bootstrap, ale będą wrażliwe na system operacyjny pod względem stylizacji. Na koniec będziemy nawigować dynamicznie, używając history API, jak w wersji Bootstrap, ale dostęp do historii będzie nieco inny ze względu na implementację Ionic Router.
Konfiguracja
Zacznijmy od skonfigurowania komponentu wejściowego z komponentem pośredniczącym. Utwórz folder na stronach o nazwie input i utwórz pod nim plik o nazwie Input.tsx. Wewnątrz tego pliku umieść następujący kod, aby utworzyć komponent React. Zauważ, że ponieważ używamy TypeScript, jest trochę inaczej.
import React, {useState} from 'react'; const Input : React.FC = () => { return <div>Input</div>; } export default Input;
I zmień komponent w App.tsx na:
const App: React.FC = () => ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/input" component={Input} exact={true} /> <Route exact path="/" render={() => <Redirect to="/input" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> );
Teraz, gdy odświeżysz aplikację, powinieneś zobaczyć komponent Input stub.
Kontenery danych
Stwórzmy teraz kontenery danych. Potrzebujemy kontenerów dla wprowadzonych uchwytów Twittera, a także bieżącej zawartości pola wprowadzania. Ponieważ używamy TypeScript, musimy dodać adnotacje typu do naszego wywołania useState
w funkcji komponentu:
const Input : React.FC = () => { const [text, setText] = useState<string>(''); const [accounts, setAccounts] = useState<Array<string>>([]); return <div>Input</div>; }
Będziemy również chcieli, aby kontener danych przechowywał wartości zwracane z API. Ponieważ zawartość tego musi być udostępniona innym trasom, definiujemy je na poziomie App.tsx. Zaimportuj useState
z Reacta w pliku App.tsx i zmień funkcję kontenera aplikacji na poniższą:
const App: React.FC = () => { const [curatedTweets, setCuratedTweets] = useState<CuratedTweets>({} as CuratedTweets); return ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/input" component={Input} exact={true} /> <Route exact path="/" render={() => <Redirect to="/input" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> ); }
W tym momencie, jeśli używasz edytora z podświetlaniem składni, takiego jak Visual Studio Code, powinieneś zobaczyć świecące się CuratedTweets. Dzieje się tak, ponieważ plik nie wie, jak wygląda interfejs CuratedTweets. Zdefiniujmy to teraz. Utwórz folder w src o nazwie interfaces i utwórz w nim plik o nazwie CuratedTweets.tsx. W pliku zdefiniuj interfejs CuratedTweets w następujący sposób:
interface TweetRecordData { noun_phrases: Array<string>, sentiment: number } export interface TweetRecord { account: string, data: TweetRecordData, tweet: string } export default interface CuratedTweets { groupedByNp: Record<string, Array<TweetRecord>>, processedTweets: Array<TweetRecord>, sentimentSorted: Array<TweetRecord> }
Teraz aplikacja wie o strukturze danych zwrotnych API. Zaimportuj interfejs CuratedTweets w App.tsx. Powinieneś teraz zobaczyć kompilację App.tsx bez problemu.
Musimy zrobić jeszcze kilka rzeczy. Musimy przekazać funkcję setCuratedTweets
do komponentu Input i powiadomić komponent Input o tej funkcji.
W App.tsx zmodyfikuj trasę wejściową w następujący sposób:
<Route path="/input" render={() => <Input setCuratedTweets={setCuratedTweets}/>} exact={true} />
Teraz powinieneś zobaczyć flagę edytora opuszczoną coś innego — Input nie wie o przekazywaniu do niego nowej właściwości, więc będziemy chcieli zdefiniować ją w Input.tsx.
First, import the CuratedTweets interface, then define the ContainerProps interface like so:
interface ContainerProps { setCuratedTweets: React.Dispatch<React.SetStateAction<CuratedTweets>> }
And finally, change the Input component definition like so:
const Input : React.FC<ContainerProps> = ({setCuratedTweets}) => { const [text, setText] = useState<string>(''); const [accounts, setAccounts] = useState<Array<string>>([]); return <div>Input</div>; }
We are done defining the data containers, and now, onto building the UI components.

UI Components
For the UI component, we will want to build an input component and a list display component. Ionic provides some simple containers for these.
Let's start by importing the library components we'll be using:
import { IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';
Now, we can replace the stub component with the IonInput
, wrapped in an IonGrid:
return <IonGrid> <IonRow> <IonCol> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> </IonRow> </IonGrid>
Notice that the event listener is onIonChange
instead of onChange
. Otherwise, it should look very familiar.
When you open the app in your browser, it may not look like the Bootstrap app. However, if you set your browser to emulator mode, the UI will make more sense. It will look even better once you deploy it on mobile, so look forward to it.
Now, let's add some buttons. We will want an “Add to list” button and a “Pull API” button. For that, we can use IonButton. Change the size of the input's IonCol to 8 and add the following two buttons with columns:
<IonCol size="8"> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> <IonCol size="2"> <IonButton style={{float: 'right'}} color="primary" size="small" onClick={onAddClicked}>Add</IonButton> </IonCol> <IonCol size="2"> <IonButton style={{float: 'right'}} color="success" size="small" onClick={onPullClicked}>Pull</IonButton> </IonCol>
Since we're writing the buttons, let's write the event handlers as well.
The handler to add a Twitter handle to the list is simple:
const onAddClicked = () => { if (text === undefined || text.length === 0) { return; } const newAccounts: Array<string> = [...accounts, text]; setAccounts(newAccounts); setText(''); }
We will implement the API call in the next section, so let's just put a stub function for onPullClicked
:
const onPullClicked = () => {}
Now, we need to write the component for displaying the list of handles that has been inputted by the user. For that, we will use IonList, put into a new IonRow:
<IonRow> <IonCol> <IonList> {accounts.map((x:string, i:number) => <IonItem key={i}> <IonGrid> <IonRow> <IonCol size="8" style={{paddingTop: '12px'}}>{x}</IonCol> <IonCol><IonButton style={{float: 'right'}} color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol> </IonRow> </IonGrid> </IonItem>)} </IonList> </IonCol> </IonRow>
Each list item is displaying the handle and a delete button in its very own IonGrid. For this code to compile, we will want to implement the deleteClickedHandler
as well. It should be very familiar from the previous section but with TypeScript annotations.
const deleteClickedBuilder = (idx: number) => () => { const newAccounts: Array<string> = [...accounts]; newAccounts.splice(idx, 1); setAccounts(newAccounts); }
Save this, and you should see the Input page with all the UI components implemented. We can add handles, delete handles, and click the button to invoke the API.
As a final exercise, let's move the in-line styles to CSS. Create a file in the input folder called input.css and import it in the Input.tsx file. Then, add the following styles:
.input-button { float: right; } .handle-display { padding-top: 12px; }
Now, add className="input-button”
on all of the IonButtons and className=”handle-display”
on the handle list item IonCol that is displaying the intended Twitter handle. Save the file, and you should see everything looking quite good.
API Call
The code to pull the API is very familiar from the previous section, with one exception - we have to get access to the history component to be able to dynamically change routes. We will do this using the withHistory
hook.
We first import the hook:
import { useHistory } from 'react-router';
And then implement the handler in the input component:
const history = useHistory(); const switchToDisplay = () => { history.push('/display'); } const onPullClicked = () => { fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts }) }).then(r=>r.json()).then(j => { setCuratedTweets(j); switchToDisplay(); }) .catch(e => { console.log(e); }) }
Dodawanie nagłówka
Our Input page looks quite nice, but it looks a little bare due to Ionic's mobile-centric styling. To make the UI look more natural, Ionic provides a header feature that lets us provide a more natural user experience. When running on mobile, the header will also simulate the native OS's mobile platform, which makes the user experience even more natural.
Change your component import to:
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';
And now wrap the UI in an Ionic page with a header, like so:
return <IonPage> <IonHeader> <IonToolbar> <IonTitle>Twitter Curation App</IonTitle> </IonToolbar> </IonHeader> <IonContent> <IonHeader collapse="condense"> <IonToolbar> <IonTitle size="large">Twitter Curation App</IonTitle> </IonToolbar> </IonHeader> <IonGrid> <IonRow> <IonCol size="8"> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> <IonCol size="2"> <IonButton className="input-button" color="primary" size="small" onClick={onAddClicked}>Add</IonButton> </IonCol> <IonCol size="2"> <IonButton className="input-button" color="success" size="small" onClick={onPullClicked}>Pull</IonButton> </IonCol> </IonRow> <IonRow> <IonCol> <IonList> {accounts.map((x:string, i:number) => <IonItem key={i}> <IonGrid> <IonRow> <IonCol size="8" className="handle-display">{x}</IonCol> <IonCol><IonButton className="input-button" color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol> </IonRow> </IonGrid> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid> </IonContent> </IonPage>
Teraz to ładnie wygląda!
Strona posortowana według opinii
Plan z dużym obrazem
Strona posortowana według tonacji będzie w dużej mierze podobna do strony posortowanej według tonacji ze strony Bootstrap, ale będzie korzystała z TypeScript i Ionic. Wdrożymy również wyświetlanie tematów jako oddzielną trasę, aby skorzystać z funkcji nawigacji Ionic podczas pracy na urządzeniach mobilnych, więc będziemy musieli umożliwić tej stronie również nawigację do trasy wyświetlania tematów.
Konfiguracja trasy
Zacznijmy od utworzenia nowego folderu o nazwie Sentimentsorted i pliku o nazwie SentimentSorted.tsx pod spodem. Wyeksportuj komponent pośredniczący w następujący sposób:
import React from 'react'; const SentimentSorted: React.FC = () => { return <div>Sentiment Sorted</div> } export default SentimentSorted;
A w App.tsx zaimportuj składnik:
import SentimentSorted from './pages/sentimentsorted/SentimentSorted';
I dodaj trasę:
<Route path="/display" render={() => <SentimentSorted curatedTweets={curatedTweets} />} />
Otrzymasz błąd TypeScript mówiący, że SentimentSorted
nie oczekuje właściwości curatedTweets, więc zajmijmy się tym teraz w następnej sekcji.
Komponenty interfejsu użytkownika
Zacznijmy od zdefiniowania rekwizytów kontenera. Podobnie jak składnik wejściowy:
import CuratedTweets from '../../interfaces/CuratedTweets'; interface ContainerProps { curatedTweets: CuratedTweets }
A teraz zmień ekran skrótu:
const SentimentSorted: React.FC<ContainerProps> = ({curatedTweets}) => { return <div>Sentiment Sorted</div> }
I wszystko powinno się skompilować.
Wyświetlacz jest bardzo prosty, to po prostu IonList z komponentami wyświetlacza:
return <IonGrid> <IonRow> <IonCol> <IonList> {(curatedTweets.sentimentSorted).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid>
Jeśli zapiszesz i pobierzesz kilka tweetów za pomocą komponentu wejściowego, powinieneś zobaczyć tweety wyświetlone na liście.
Teraz dodajmy przyciski nawigacyjne. Dodaj do IonGrid:
<IonRow> <IonCol> <IonButton color='primary' onClick={switchToInput}>Back</IonButton> <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton> </IonCol> </IonRow>
switchToInput
jest bardzo łatwy do zaimplementowania za pomocą interfejsu API historii:
const switchToInput = () => { history.goBack(); }
A ToggleDisplayType
powinien być znajomy:
const toggleDisplayType = () => { setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment'); } const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'
Teraz mamy zaimplementowany komponent SentimentDisplay
. Teraz, zanim zaimplementujemy stronę wyświetlania tematu, musimy zaimplementować komponent, który wyświetla wszystkie tematy. Zrobimy to w następnej sekcji.
Składnik grup tematycznych
Dodajmy opcję wyświetlania listy tematów i wyświetlajmy ją warunkowo. Aby to zrobić, musimy rozbić listę wyświetlania sentymentu. Zmień nazwę SentimentDisplay na Display i podzielmy listę wyświetlania sentymentu:
interface SentimentDisplayProps { sentimentSorted: Array<TweetRecord> } const SentimentDisplay: React.FC<SentimentDisplayProps> = ({sentimentSorted}) => { return <IonList> {(sentimentSorted || []).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> }
Zwróć uwagę, jak używamy jednej z definicji klas w interfejsie CuratedTweets. Dzieje się tak, ponieważ te komponenty nie wymagają całego obiektu CuratedTweets, a jedynie podzbioru. Lista tematów jest bardzo podobna:
interface TopicDisplayProps { groupedByNP: Record<string, Array<TweetRecord>> } const TopicDisplay: React.FC<TopicDisplayProps> = ({groupedByNP}) => { return <IonList> {Object.keys(groupedByNP || {}).map((x, i) => <IonItem key={i} routerLink={`/topicDisplay/${encodeURIComponent(x)}`}> <IonLabel className="ion-text-wrap"> <h2>{x} ({groupedByNP[x].length})</h2> </IonLabel> </IonItem>)} </IonList> }
A teraz wyświetlanie warunkowe można łatwo skonfigurować w komponencie wyświetlania:
return ( <IonGrid> <IonRow> <IonCol> <IonButton color='primary' onClick={switchToInput}>Back</IonButton> <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton> </IonCol> </IonRow> { displayType === 'Sentiment'? <SentimentDisplay sentimentSorted={curatedTweets.sentimentSorted} /> : <TopicDisplay groupedByNP={curatedTweets.groupedByNp} /> } </IonGrid> );
Pamiętaj, aby zmienić domyślny eksport, a teraz jesteśmy gotowi do wdrożenia strony wyświetlania tematu.
Strona wyświetlania tematu
Plan z dużym obrazem
Strona wyświetlania tematu jest wyświetlaniem listy podobnym do wyświetlania sentymentu, ale będziemy szukać danego tematu na podstawie parametru trasy.
Konfiguracja trasy
Jeśli dotarłeś tak daleko, powinieneś już wiedzieć, co robić. Utwórzmy folder strony o nazwie topicdisplay i TopicDisplay.tsx, napisz składnik skrótowy i zaimportuj go na stronę App.tsx. Teraz skonfigurujmy trasy:
<Route path="/topicDisplay/:topic" render={() => <TopicDisplay curatedTweets={curatedTweets} /> } />
Teraz jesteśmy gotowi do wdrożenia komponentu UI.
Komponenty interfejsu użytkownika
Najpierw utwórzmy definicję ContainerProps
:
interface ContainerProps { curatedTweets: CuratedTweets } const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => { Return <div>Topic Display</div> }
Teraz będziemy musieli pobrać temat z nazwy ścieżki adresu URL. W tym celu użyjemy history API. Zaimportujmy więc useHistory
, stwórzmy instancję API historii i pobierzmy temat ze ścieżki. Skoro już przy tym jesteśmy, zaimplementujmy również funkcję przełączania wstecz:
const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => { const history = useHistory(); const switchToDisplay = () => { history.goBack(); } const topic = history.location.pathname.split('/')[2]; const tweets = (curatedTweets.groupedByNp || {})[topic];
Teraz, gdy mamy tweety na ten konkretny temat, wyświetlanie jest całkiem proste:
return ( <IonGrid> <IonRow> <IonCol> <IonButton color='primary' onClick={switchToDisplay}>Back</IonButton> </IonCol> </IonRow> <IonRow> <IonCol> <IonList> {(tweets || []).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid> );
Zapisz i biegnij, a wszystko powinno wyglądać dobrze.
Uruchamianie aplikacji w emulatorze
Aby uruchomić aplikację w emulatorze, po prostu uruchamiamy kilka poleceń Ionic, aby dodać platformę mobilną i skopiować kod, podobnie jak w przypadku Cordova.
ionic build # builds the app ionic cap add ios # adds iOS as one of the platforms, only have to run once ionic cap copy # copy the build over ionic cap sync # only need to run this if you added native plugins ionic cap open ios # run the iOS emulator
Powinieneś zobaczyć aplikację.
Implementacja natywna React
React Native Primer
React Native ma zupełnie inne podejście niż podejścia internetowe z poprzednich sekcji. React Native renderuje Twój kod React jako komponenty natywne. Ma to kilka zalet. Po pierwsze, integracja z podstawowym systemem operacyjnym jest znacznie głębsza, co pozwala programiście korzystać z nowych funkcji smartfonów i funkcji specyficznych dla systemu operacyjnego, które mogą nie być dostępne za pośrednictwem Cordova/Capacitor. Po drugie, ponieważ w środku nie ma silnika renderującego opartego na Internecie, aplikacja React Native jest zazwyczaj szybsza niż aplikacja napisana przy użyciu Cordova. Wreszcie, ponieważ React Native umożliwia integrację komponentów natywnych, programiści mogą sprawować znacznie dokładniejszą kontrolę nad swoją aplikacją.
W naszej aplikacji użyjemy logiki z poprzednich sekcji i użyjemy biblioteki komponentów React Native o nazwie NativeBase do zakodowania naszego interfejsu użytkownika.
Konfiguracja aplikacji
Najpierw będziesz chciał zainstalować wszystkie wymagane komponenty React Native, postępując zgodnie z instrukcjami tutaj.
Po zainstalowaniu React Native zacznijmy projekt:
react-native init TwitterCurationRN
Uruchom skrypt instalacyjny, a ostatecznie powinien zostać utworzony folder. Umieść CD w folderze i uruchom reakcję natywną run-ios. Powinieneś zobaczyć wyskakujący emulator z przykładową aplikacją.
Będziemy chcieli również zainstalować NativeBase, ponieważ jest to nasza biblioteka komponentów. W tym celu uruchamiamy:
npm install --save native-base react-native link
Chcemy również zainstalować nawigator stosu React Native. Biegnijmy:
npm install --save @react-navigation/stack @react-navigation/native
I
react-native link cd ios pod-install cd
Aby zakończyć łączenie i instalację natywnych wtyczek.
Konfiguracja routera
Do routingu użyjemy nawigatora stosu, który zainstalowaliśmy w powyższym kroku.
Zaimportuj komponenty routera:
import { NavigationContainer } from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack';
A teraz tworzymy nawigator stosu:
const Stack = createStackNavigator();
Zmień zawartość komponentu App, aby użyć nawigatora stosu:
const App = () => { return ( <> <StatusBar bar /> <NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> </Stack.Navigator> </NavigationContainer> </> ); };
W tym momencie pojawi się błąd, ponieważ Entry nie jest jeszcze zdefiniowana. Zdefiniujmy element pośredniczący tylko po to, aby był szczęśliwy.
Utwórz folder components w swoim projekcie, utwórz plik o nazwie Entry.jsx i dodaj komponent pośredniczący w następujący sposób:
import React, {useState} from 'react'; import { Text } from 'native-base'; export default Entry = ({navigation}) => { return <Text>Entry</Text>; // All text must be wrapped in a <Text /> component or <TextView /> if you're not using NativeBase. }
Zaimportuj składnik Entry do swojej aplikacji i powinien zostać skompilowany.
Teraz jesteśmy gotowi do zakodowania strony Input.
Strona wejściowa
Plan z dużym obrazem
Zaimplementujemy stronę bardzo podobną do tej zaimplementowanej powyżej, ale przy użyciu komponentów NativeBase. Większość używanych przez nas interfejsów API JavaScript i React, takich jak hooki i pobieranie, jest nadal dostępna.
Jedyną różnicą będzie sposób, w jaki pracujemy z interfejsem API nawigacji, co zobaczycie później.
Komponenty interfejsu użytkownika
Dodatkowe komponenty NativeBase, których użyjemy, to Container, Content, Input, List, ListItem i Button. Wszystkie one mają odpowiedniki w Ionic i Bootstrap React, a twórcy NativeBase sprawili, że jest to bardzo intuicyjne dla osób zaznajomionych z tymi bibliotekami. Po prostu importuj w ten sposób:
import { Container, Content, Input, Item, Button, List, ListItem, Text } from 'native-base';
A składnik to:
return <Container> <Content> <Item regular> <Input placeholder='Input Handles Here' onChangeText={inputChange} value={input} /> <Button primary onPress={onAddClicked}><Text> Add </Text></Button> <Text> </Text> <Button success onPress={onPullClicked}><Text> Pull </Text></Button> </Item> <Item> <List style={{width: '100%'}}> {handles.map((item) => <ListItem key={item.key}> <Text>{item.key}</Text> </ListItem>)} </List> </Item> </Content> </Container>
A teraz zaimplementujmy obsługę stanów i zdarzeń:
const [input, changeInput] = useState(''); const [handles, changeHandles] = useState([]); const inputChange = (text) => { changeInput(text); } const onAddClicked = () => { const newHandles = [...handles, {key: input}]; changeHandles(newHandles); changeInput(''); }
I na koniec wywołanie API:
const onPullClicked = () => { fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts: handles.map(x => x.key) }) }).then(r=>r.json()).then(data => { navigation.navigate('SentimentDisplay', { data }); }) .catch(e => { console.log(e); }) }
Zauważ, że ta implementacja jest taka sama jak w implementacji NativeBase, z wyjątkiem tego, że nawigujemy w inny sposób. Nawigator stosu przekazuje do swoich komponentów właściwość zwaną „nawigacją”, której można używać do nawigacji między trasami za pomocą .navigate
. Oprócz prostej nawigacji możesz również przekazywać dane do komponentu docelowego. Wykorzystamy ten mechanizm do przekazywania danych.
Aby aplikacja skompilowała się, nadal musimy poinformować Entry
o komponencie nawigacyjnym. W tym celu musimy zmienić deklarację funkcji komponentu:
export default Entry = ({navigation}) => {
Teraz zapisz i powinieneś zobaczyć stronę.
Strona posortowana według opinii
Plan z dużym obrazem
Będziemy implementować stronę sentymentu podobnie jak poprzednie sekcje, ale będziemy nieco inaczej stylizować stronę, a także będziemy inaczej używać biblioteki nawigacyjnej, aby uzyskać wartość zwracaną przez wywołanie API.
Ponieważ React Native nie posiada CSS, będziemy musieli albo zdefiniować obiekt StyleSheet, albo po prostu zakodować style w linii. Ponieważ podzielimy się niektórymi stylami między komponentami, stwórzmy globalny arkusz stylów. Zrobimy to po ustaleniu trasy.
Ponadto, ponieważ StackNavigator
ma wbudowany przycisk nawigacyjny Wstecz, nie musimy implementować własnego przycisku Wstecz.
Konfiguracja trasy
Definicja trasy w StackNavigator
jest bardzo prosta. Po prostu tworzymy nowy o nazwie Stack Screen i nadajemy mu komponent, podobnie jak router React.
<NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> </Stack.Navigator> </NavigationContainer>
Aby to zadziałało, będziemy oczywiście musieli stworzyć komponent stub w components/SentimentDisplay.js:
import React from 'react'; import {Text} from 'native-base'; const SentimentDisplay = () => { return <Text>Sentiment Display</Text>; } export default SentimentDisplay;
I zaimportuj:
import SentimentDisplay from './components/SentimentDisplay';
Teraz jesteśmy gotowi do stworzenia globalnego arkusza stylów.
Globalny arkusz stylów
Najpierw utwórz plik o nazwie globalStyles.js. Następnie zaimportuj komponent StyleSheet z React Native i zdefiniuj style:
import {StyleSheet} from 'react-native'; export default StyleSheet.create({ tweet: {paddingTop: 5}, accountName: {fontWeight: '600'}, })
I jesteśmy gotowi do kodowania interfejsu użytkownika.
Komponenty interfejsu użytkownika
Komponent UI jest dość znajomy, z wyjątkiem tego, jak pracujemy z trasami. Będziemy chcieli użyć specjalnych właściwości nawigacji i trasy StackNavigator
, aby uzyskać aktualny stan aplikacji i przejść do wyświetlania tematu, jeśli użytkownik chce zobaczyć tę stronę.
Zmień definicję komponentu, aby uzyskać dostęp do rekwizytów nawigacyjnych:
const SentimentDisplay = ({route, navigation}) => {
A teraz wdrażamy odczyt stanu aplikacji oraz funkcjonalność nawigacji:
const {params: {data}} = route; const viewByTopicClicked = () => { navigation.navigate('TopicDisplay', { data }); }
Importuj style globalne:
import globalStyles from './globalStyles';
A komponenty:
import { View } from 'react-native'; import {List, Item, Content, ListItem, Container, Text, Button} from 'native-base';
I wreszcie komponenty:
return <Container> <Content> <Item> <Button primary onPress={viewByTopicClicked}><Text>View By Topic</Text></Button> </Item> <Item> <List style={{width: '100%'}}> {data.sentimentSorted.map((item, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <Text style={globalStyles.accountName}>{item.account}:</Text> <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text> </View> </ListItem>)} </List> </Item> </Content> </Container>;
Zapisz i spróbuj wyciągnąć kilka tweetów, a powinieneś zobaczyć wyświetlacz nastrojów. Teraz przejdź do strony grupowania tematów.
Strona grupowania tematów
Plan z dużym obrazem
Wyświetlanie tematów jest znowu bardzo podobne. Będziemy korzystać z programu do tworzenia obsługi, aby zbudować funkcję nawigacji, aby przejść do strony wyświetlania dla określonego elementu tematu, a także będziemy definiować arkusz stylów, który jest specyficzny dla tej strony.
Jedną z nowych rzeczy, którą będziemy robić, jest zaimplementowanie TouchableOpacity, który jest komponentem specyficznym dla React Native, który działa podobnie jak przycisk.
Konfiguracja trasy
Definicja trasy jest taka sama jak poprzednio:
<Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> <Stack.Screen name="TopicDisplay" component={TopicDisplay} /> </Stack.Navigator>
Komponenty pośredniczące / TopicDisplay.js:
import React from 'react'; import {Text} from 'native-base'; const TopicDisplay = () => { return <Text>Topic Display</Text>; } export default TopicDisplay;
I zaimportuj:
import TopicDisplay from './components/TopicDisplay;
Komponenty interfejsu użytkownika
Wiele z tego będzie wyglądać bardzo znajomo. Zaimportuj funkcje biblioteczne:
import { View, TouchableOpacity, StyleSheet } from 'react-native'; import {List, Item, Content, ListItem, Container, Text} from 'native-base';
Importuj style globalne:
import globalStyles from './globalStyles';
Zdefiniuj style niestandardowe:
const styles = StyleSheet.create({ topicName: {fontWeight: '600'}, })
Zdefiniuj rekwizyty nawigacyjne:
export default TopicDisplay = ({route, navigation}) => {
Zdefiniuj procedury obsługi danych i akcji. Zauważ, że używamy programu do tworzenia obsługi, funkcji, która zwraca funkcję:
const {params: {data}} = route; const specificItemPressedHandlerBuilder = (topic) => () => { navigation.navigate('TopicDisplayItem', { data, topic }); }
A teraz komponenty. Zauważ, że używamy TouchableOpacity, które może mieć obsługę onPress
. Mogliśmy również użyć TouchableTransparency, ale animacja kliknięcia i przytrzymania TouchableOpacity była lepiej dopasowana do naszej aplikacji.
return <Container> <Content> <Item> <List style={{width: '100%'}}> {Object.keys(data.groupedByNp).map((topic, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <TouchableOpacity onPress={specificItemPressedHandlerBuilder(topic)}> <Text style={styles.topicName}>{topic}</Text> </TouchableOpacity> </View> </ListItem>)} </List> </Item> </Content> </Container>;
I to powinno wystarczyć. Zapisz i wypróbuj aplikację!
Teraz przejdź do strony wyświetlania tematu.
Temat Wyświetlany element Strona
Plan z dużym obrazem
Strona wyświetlania tematu jest bardzo podobna, a wszystkie dziwactwa są omówione w innych sekcjach, więc od tego miejsca powinno być płynnie.
Konfiguracja trasy
Dodamy definicję trasy:
<Stack.Screen name="TopicDisplayItem" component={TopicDisplayItem} />
Dodaj import:
import TopicDisplayItem from './components/TopicDisplayItem';
I utwórz komponent skrótowy. Zamiast zwykłego komponentu zaimportujmy także komponenty NativeBase, których będziemy używać, i zdefiniujmy właściwości trasy:
import React from 'react'; import {View} from 'react-native'; import {List, Item, Content, ListItem, Container, Text} from 'native-base'; import globalStyles from './globalStyles'; const TopicDisplayItem = ({route}) => { const {params: {data, topic}} = route; return <Text>Topic Display Item</Text>; } export default TopicDisplayItem;
Komponenty interfejsu użytkownika
Komponent UI jest dość prosty. Widzieliśmy to już wcześniej i tak naprawdę nie wdrażamy żadnej niestandardowej logiki. Więc po prostu zróbmy to! Weź głęboki oddech…
return <Container> <Content> <Item> <List style={{width: '100%'}}> {data.groupedByNp[topic].map((item, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <Text style={globalStyles.accountName}>{item.account}:</Text> <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text> </View> </ListItem>)} </List> </Item> </Content> </Container>;
Oszczędzaj i powinniśmy być gotowi! Teraz jesteśmy gotowi do uruchomienia aplikacji w emulatorze… czekaj, czy już nie?
Uruchamianie aplikacji
Cóż, skoro pracujesz z React Native, to już korzystasz z aplikacji w emulatorze, więc ta część jest już załatwiona. To jedna z największych zalet środowiska programistycznego React Native.
Uff! Na tym skończyliśmy z częścią tego artykułu dotyczącą kodowania. Zobaczmy, czego dowiedzieliśmy się o technologiach.
Porównanie technologii
Kordowa: plusy i minusy
Najlepszą rzeczą w Cordovie jest sama szybkość, z jaką wykwalifikowany programista może napisać coś funkcjonalnego i rozsądnie prezentującego się. Umiejętności w zakresie tworzenia stron internetowych i doświadczenie można łatwo przenosić, ponieważ w końcu programujesz aplikację internetową. Proces rozwoju jest szybki i prosty, a dostęp do Cordova API jest również prosty i intuicyjny.
Wady korzystania z Cordova wynikają głównie z nadmiernego polegania na komponentach sieciowych. Użytkownicy oczekują określonego doświadczenia użytkownika i projektu interfejsu podczas korzystania z aplikacji mobilnych, a gdy aplikacja przypomina witrynę mobilną, może to być nieco drażniące. Ponadto większość funkcji wbudowanych w aplikacje, takich jak animacje przejściowe i narzędzia nawigacyjne, należy zaimplementować ręcznie.
Ionic: plusy i minusy
Najlepszą częścią Ionic było to, ile funkcji zorientowanych na urządzenia mobilne otrzymałem „za darmo”. Dzięki kodowaniu w sposób podobny do programowania aplikacji internetowej, udało mi się zbudować aplikację, która wygląda o wiele bardziej przyjazną dla urządzeń mobilnych niż po prostu przy użyciu Cordova i React-Bootstrap. Była animacja nawigacji, przyciski z natywnymi stylami i wiele opcji interfejsu użytkownika, które sprawiły, że wrażenia użytkownika były bardzo płynne.
Wada korzystania z Ionic była częściowo spowodowana jego mocnymi stronami. Po pierwsze, czasami trudno było wyobrazić sobie zachowanie aplikacji w różnych środowiskach. Tylko dlatego, że aplikacja wyglądała w jedną stronę, nie oznaczało to, że to samo umieszczenie interfejsu użytkownika będzie wyglądało tak samo w innym środowisku. Po drugie, Ionic znajduje się na szczycie wielu podstawowych technologii, a uzyskanie dostępu do niektórych komponentów okazało się trudne. Wreszcie, jest to specyficzne dla Ionic-React, ale ponieważ Ionic został zbudowany dla Angulara, wiele funkcji Ionic-React wydawało się mieć mniej dokumentacji i wsparcia. Jednak zespół Ionic wydaje się być bardzo uważny na potrzeby programistów React i szybko dostarcza nowe funkcje.
React Native: plusy i minusy
React Native miał bardzo płynne doświadczenie użytkownika podczas tworzenia na urządzeniach mobilnych. Łącząc się bezpośrednio z emulatorem nie było tajemnicą, jak aplikacja będzie wyglądać. Internetowy interfejs debugera był niezwykle pomocny w stosowaniu technik debugowania ze świata aplikacji internetowych, a ekosystem jest dość solidny.
Wadą React Native jest bliskość interfejsu natywnego. Nie można było użyć wielu bibliotek opartych na DOM, co oznaczało konieczność poznania nowych bibliotek i najlepszych praktyk. Bez korzyści CSS stylizacja aplikacji była nieco mniej intuicyjna. Wreszcie, z wieloma nowymi komponentami do nauczenia (np. Widok zamiast div, komponent tekstowy zawijający wszystko, Buttons vs. TouchableOpacity vs. TouchableTransparency itp.), na początku jest trochę krzywej uczenia się, jeśli ktoś wchodzi do React Native world z niewielką wiedzą na temat mechaniki.
Kiedy używać każdej technologii
Ponieważ Cordova, Ionic i React Native mają bardzo mocne zalety i wady, każda technologia ma kontekst, w którym cieszyłaby się najlepszą produktywnością i wydajnością.
Jeśli masz już istniejącą aplikację internetową, która ma silną tożsamość marki otaczającą projekt interfejsu użytkownika oraz ogólny wygląd i działanie, najlepszą opcją będzie Cordova, która zapewnia dostęp do natywnych funkcji smartfona, jednocześnie umożliwiając ponowne wykorzystanie większości komponenty internetowe i zachowaj tożsamość marki w tym procesie. W przypadku stosunkowo prostych aplikacji korzystających z responsywnego frameworka możesz zbudować aplikację mobilną z bardzo małą liczbą wymaganych zmian. Jednak Twoja aplikacja będzie wyglądać mniej jak aplikacja, a bardziej jak strona internetowa, a niektóre komponenty, których ludzie oczekują od aplikacji mobilnej, będą kodowane osobno. Dlatego polecam Cordova w przypadkach, gdy jesteś w projekcie internetowym, który przenosi aplikację na telefon komórkowy.
Jeśli zaczynasz kodować nową aplikację zgodnie z filozofią app-first, ale Twój zespół posiada umiejętności przede wszystkim w tworzeniu stron internetowych, polecam Ionic. Biblioteka Ionic pozwala Ci szybko pisać kod, który wygląda i jest zbliżony do komponentów natywnych, jednocześnie pozwalając Ci zastosować swoje umiejętności i instynkt programisty WWW. Odkryłem, że najlepsze praktyki tworzenia stron internetowych można łatwo zastosować do programowania w Ionic, a stylizacja z CSS działała bezproblemowo. Dodatkowo mobilna wersja strony wygląda znacznie bardziej natywnie niż strona zakodowana za pomocą responsywnego frameworka CSS. Jednak na niektórych etapach zauważyłem, że integracja interfejsu React-Ionic-Native API wymaga ręcznego dostosowania, co może okazać się czasochłonne. Dlatego polecam Ionic w przypadkach, gdy Twoja aplikacja jest tworzona od podstaw i chcesz udostępnić znaczną ilość kodu między aplikacją internetową z obsługą urządzeń mobilnych a aplikacją mobilną.
Jeśli kodujesz nową aplikację z zaimplementowaną natywną bazą kodu, możesz spróbować React Native. Nawet jeśli nie używasz kodu natywnego, może to być również najlepsza opcja w przypadkach, gdy znasz już React Native lub gdy Twoim głównym problemem jest aplikacja mobilna, a nie aplikacja hybrydowa. Skupiwszy większość moich front-endowych prac programistycznych na tworzeniu stron internetowych, początkowo odkryłem, że rozpoczęcie pracy z React Native wymagało więcej nauki niż Ionic czy Cordova ze względu na różnice w organizacji komponentów i konwencjach kodowania. Jednak po nauczeniu się tych niuansów kodowanie jest dość płynne, szczególnie z pomocą biblioteki komponentów, takiej jak NativeBase. Biorąc pod uwagę jakość środowiska programistycznego i kontrolę nad aplikacją, jeśli frontend Twojego projektu to przede wszystkim aplikacja mobilna, polecam React Native jako narzędzie z wyboru.
Przyszłe tematy
Jednym z tematów, na które nie miałem czasu się zgłębić, była łatwość dostępu do natywnych interfejsów API, takich jak kamera, geolokalizacja czy uwierzytelnianie biometryczne. Jedną z największych zalet programowania mobilnego jest dostępność bogatego ekosystemu API, który na ogół nie jest dostępny w przeglądarce.
W przyszłych artykułach chciałbym zbadać łatwość tworzenia tych natywnych aplikacji obsługujących API przy użyciu różnych technologii międzyplatformowych.
Wniosek
Dzisiaj wdrożyliśmy aplikację kuratorską na Twitterze, korzystając z trzech różnych wieloplatformowych technologii programowania mobilnego. Mam nadzieję, że dało to dobre wyobrażenie o tym, jaka jest każda technologia i zainspirowało do opracowania własnej aplikacji opartej na React.
Dziękuję za przeczytanie!