使用 React 的全棧 NLP:Ionic vs Cordova vs React Native

已發表: 2022-03-11

自 Apple 發布第一款 iPhone 以來的大約 15 年裡,軟件開發格局發生了巨大變化。 隨著智能手機的廣泛採用和獨特功能的不斷增長,用戶越來越喜歡通過移動設備而不是台式機或筆記本電腦訪問軟件服務。 智能手機提供地理定位、生物認證和運動感應等功能,其中許多桌面平台現在才開始復制。 在某些人口統計數據中,智能手機或類似的移動設備是軟件消費的主要方式,完全繞過了計算機。

公司已經註意到了這種轉變,並在很大程度上加強了這種轉變。 移動應用不再是事後的想法。 從金融經紀公司 Robinhood 到社交媒體公司 Instagram,再到叫車公司 Uber,應用程序都在採用移動優先的發展戰略。 如果有桌面應用程序,它通常是作為移動應用程序的補充,而不是主要焦點。

對於全棧開發人員來說,適應這些不斷變化的趨勢至關重要。 幸運的是,有許多成熟且得到良好支持的技術可以幫助 Web 開發人員將他們的技能應用到移動開發中。 今天,我們將探索三種這樣的技術:Cordova、Ionic 和 React Native。 我們將使用最流行的前端 Web 開發框架之一 React.js 作為我們的核心開發技術。 雖然我們將專注於開發 iPhone 應用程序,但這些都是跨平台技術,可以交叉編譯到 Android 平台。

我們今天將建造什麼

我們將構建一個使用自然語言處理 (NLP) 來處理和管理 Twitter 提要的應用程序。 該應用程序將允許用戶選擇一組 Twitter 句柄,使用 Twitter API 提取最新更新,並根據情緒和主題對推文進行分類。 然後,用戶將能夠根據情緒或主題查看推文。

後端

在我們構建前端之前,我們需要構建後端。 我們暫時保持後端簡單——我們將使用基本的、現成的情感分析和詞性標記,以及一些數據清理來處理特定於數據集的問題。 我們將使用一個名為 TextBlob 的開源 NLP 庫,並通過 Flask 提供結果。

情感分析、詞性標註和 NLP:快速入門

如果您之前沒有使用過自然語言分析應用程序,那麼這些術語對您來說可能非常陌生。 NLP 是分析和處理自然人類語言數據的技術的總稱。 雖然這是一個廣泛的話題,但解決該領域的所有技術都面臨許多共同挑戰。 例如,與編程語言或數值數據不同,人類語言由於人類語言語法的許可性質而趨於鬆散結構。 此外,人類語言往往非常符合上下文,在一個上下文中說出或寫下的短語可能無法翻譯到另一個上下文中。 最後,撇開結構和語境不談,語言極其複雜。 段落中更靠後的單詞可能會改變段落開頭的句子的含義。 詞彙可以被發明、重新定義或改變。 所有這些複雜性使得數據分析技術難以交叉應用。

情感分析是 NLP 的一個子領域,專注於理解自然語言段落的情感。 雖然人類情感本質上是主觀的,因此很難在技術上確定,但情感分析是一個具有巨大商業前景的子領域。 情緒分析的一些應用包括對產品評論進行分類以識別對各種功能的正面和負面評估,檢測電子郵件或演講的情緒以及按情緒對歌詞進行分組。 如果您正在尋找更深入的情感分析解釋,您可以在此處閱讀我關於構建基於情感分析的應用程序的文章。

詞性標註或詞性標註是一個非常不同的子領域。 詞性標註的目標是使用語法和上下文信息識別句子中給定單詞的詞性。 識別這種關係是一項比最初看到的要困難得多的任務——一個詞可以根據上下文和句子結構有非常不同的詞性,而且即使對人類來說,規則也不總是清楚的。 幸運的是,當今許多現成的模型都提供了與大多數主要編程語言集成的強大且通用的模型。 如果您想了解更多信息,可以在此處閱讀我關於 POS 標記的文章。

Flask、TextBlob 和 Tweepy

對於我們的 NLP 後端,我們將使用 Flask、TextBlob 和 Tweepy。 我們將使用 Flask 構建一個小型、輕量級的服務器,使用 TextBlob 來運行我們的自然語言處理,並使用 Tweepy 從 Twitter API 中獲取推文。 在開始編碼之前,您還需要從 Twitter 獲取開發人員密鑰,以便檢索推文。

我們可以編寫更複雜的後端並使用更複雜的 NLP 技術,但就我們今天的目的而言,我們將保持後端盡可能簡單。

後端代碼

現在,我們準備開始編碼。 啟動你最喜歡的 Python 編輯器和終端,讓我們開始吧!

首先,我們要安裝必要的軟件包。

 pip install flask flask-cors textblob tweepy python -m textblob.download_corpora

現在,讓我們為我們的功能編寫代碼。

打開一個新的 Python 腳本,將其命名為 server.py,然後導入必要的庫:

 import tweepy from textblob import TextBlob from collections import defaultdict

現在讓我們編寫一些輔助函數:

 # 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

現在我們已經編寫了輔助函數,我們可以使用幾個簡單的函數將所有內容放在一起:

 # 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

您現在可以在要關注的句柄列表上運行函數download_analyze_tweets ,您應該會看到結果。

我運行了以下代碼:

 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)

執行此操作產生以下結果。 結果顯然是時間相關的,所以如果你看到類似的東西,你就走在了正確的軌道上。

 [{'account': '@spacex', 'tweet': 'Falcon 9… [{'account': '@nasa', 'tweet': 'Our Mars rove… {'Falcon': [{'account': '@spacex', 'tweet': 'Falc….

現在我們可以構建 Flask 服務器,這非常簡單。 創建一個名為 server.py 的空文件並編寫以下代碼:

 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)

運行服務器,您現在應該能夠使用您選擇的 HTTP 客戶端向服務器發送 post 請求。 傳入 {accounts: [“@NASA”, “@SpaceX”]} 作為 json 參數,您應該會看到 API 返回類似於 Twitter 分析代碼中返回的內容。

現在我們有了服務器,我們準備編寫前端代碼。 由於通過電話仿真器聯網的細微差別,我建議您將 API 部署在某個地方。 否則,您將需要檢測您的應用程序是否在模擬器上運行,並在模擬器中發送請求到<Your Computer IP>:5000而不是localhost:5000 。 如果您部署代碼,您可以簡單地向該 URL 發出請求。

部署服務器有很多選項。 對於需要最少設置的免費、簡單的調試服務器,我推薦使用 PythonAnywhere 之類的工具,它應該能夠開箱即用地運行該服務器。

現在我們已經編寫了後端服務器,讓我們看看前端。 我們將從 Web 開發人員最方便的選項之一開始:Cordova。

Apache Cordova 實現

科爾多瓦底漆

Apache Cordova 是一種軟件技術,可幫助 Web 開發人員瞄準移動平台。 通過利用智能手機平台上實現的 Web 瀏覽器功能,Cordova 將 Web 應用程序代碼包裝到本機應用程序容器中以創建應用程序。 然而,Cordova 不僅僅是一個花哨的網絡瀏覽器。 通過 Cordova API,Web 開發人員可以訪問許多特定於智能手機的功能,例如離線支持、定位服務和設備上的攝像頭。

對於我們的應用程序,我們將使用 React.js 作為 JS 框架和 React-Bootstrap 作為 CSS 框架來編寫一個應用程序。 因為 Bootstrap 是一個響應式 CSS 框架,它已經支持在較小的屏幕上運行。 編寫應用程序後,我們將使用 Cordova 將其編譯為 Web 應用程序。

配置應用程序

我們將首先做一些獨特的事情來設置 Cordova React 應用程序。 在一篇Medium文章中,開發人員 Shubham Patil 解釋了我們正在做什麼。 本質上,我們使用 React CLI 設置 React 開發環境,然後使用 Cordova CLI 設置 Cordova 開發環境,最後將兩者合併。

首先,在您的代碼文件夾中運行以下兩個命令:

 cordova create TwitterCurationCordova create-react-app twittercurationreact

設置完成後,我們希望將 React 應用程序的 public 和 src 文件夾的內容移動到 Cordova 應用程序。 然後,在 package.json 中,複製來自 React 項目的腳本、瀏覽器列表和依賴項。 還要在 package.json 的根目錄添加"homepage": "./"以啟用與 Cordova 的兼容性。

合併 package.json 後,我們將要更改 public/index.html 文件以使用 Cordova。 打開文件並從 www/index.html 複製元標記以及加載 Cordova.js 時 body 標記末尾的腳本。

接下來,更改 src/index.js 文件以檢測它是否在 Cordova 上運行。 如果它在 Cordova 上運行,我們將希望在 deviceready 事件處理程序中運行渲染代碼。 如果它在常規瀏覽器中運行,則立即渲染。

 const renderReactDom = () => { ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); } if (window.cordova) { document.addEventListener('deviceready', () => { renderReactDom(); }, false); } else { renderReactDom(); }

最後,我們需要設置我們的部署管道。 將以下定義添加到 config.xml 文件中:

<hook type="before_prepare" src="hooks/prebuild.js" />

並將以下腳本放入 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); }); }); }); };

這會在 Cordova 構建開始之前執行 React 構建並將構建文件夾放入適當的位置,從而使部署過程自動化。

現在,我們可以嘗試運行我們的應用程序。 在命令行中運行以下命令:

 npm install rimraf npm install npm run start

您應該會看到 React 應用程序已在瀏覽器中設置並運行。 現在添加科爾多瓦:

 cordova platform add iOS cordova run iOS

您應該會看到 React 應用程序在模擬器中運行。

路由器和軟件包設置

要設置我們構建應用程序所需的一些基礎設施,讓我們從安裝必要的包開始:

 npm install react-bootstrap react-router react-router-dom

我們現在將設置路由,在這樣做的同時,我們還將設置一個簡單的全局狀態對象,該對象將由所有組件共享。 在生產應用程序中,我們將希望使用像 Redux 或 MobX 這樣的狀態管理系統,但我們暫時保持簡單。 轉到 App.js 並配置路由:

 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> }

通過這個路由定義,我們引入了兩條需要實現的路由:輸入和顯示。 請注意, curatedTweets變量被傳遞給 Display,而setCuratedTweets變量被傳遞給 Input。 這意味著輸入組件將能夠調用該函數來設置curatedTweets變量,而 Display 將獲取要顯示的變量。

要開始對組件進行編碼,讓我們在 /src 下創建一個名為 /src/components 的文件夾。 在 /src/components 下,創建另一個名為 /src/components/input 的文件夾,並在下面創建兩個文件:input.js 和 input.css。 對 Display 組件執行相同的操作 - 創建 /src/components/display 及其下方:display.js 和 display.css。

在這些之下,讓我們創建存根組件,如下所示:

 import React from 'react'; import 'input.css' const Input = () => <div>Input</div>; export default Input

顯示也是如此:

 import React from 'react'; import display.css' const Display = () => <div>Display</div>; export default Display

這樣,我們的線框圖就完成了,應用程序應該可以運行了。 現在讓我們編寫輸入頁面。

輸入頁面

大圖計劃

在我們編寫代碼之前,讓我們考慮一下我們希望我們的輸入頁面做什麼。 顯然,我們希望用戶能夠輸入和編輯他們想要從中提取的 Twitter 句柄。 我們還希望用戶能夠表明他們已經完成。 當用戶表示他們完成時,我們將希望從 Python 策展 API 中提取策展推文,並最終導航到 Display 組件。

既然我們知道我們想要我們的組件做什麼,我們就可以開始編碼了。

設置文件

讓我們首先導入 React Router 庫的withRouter以訪問導航功能,即我們需要的 React Bootstrap 組件,如下所示:

 import React, {useState} from 'react'; import {withRouter} from 'react-router-dom'; import {ListGroup, Button, Form, Container, Row, Col} from 'react-bootstrap'; import './input.css';

現在,讓我們為 Input 定義存根函數。 我們知道 Input 獲取setCuratedTweets函數,並且我們還希望它能夠在它從我們的 Python API 設置策展推文後導航到顯示路線。 因此,我們將要從道具setCuratedTweets和 history 中獲取(用於導航)。

 const Input = ({setCuratedTweets, history}) => { return <div>Input</div> }

為了給它歷史 API 訪問權限,我們將在文件末尾的 export 語句中用withRouter包裝它:

 export default withRouter(Input);

數據容器

讓我們使用 React Hooks 設置數據容器。 我們已經導入了useState鉤子,因此我們可以將以下代碼添加到 Input 組件的主體中:

 const [handles, setHandles] = useState([]); const [handleText, setHandleText] = useState('');

這將為句柄創建容器和修飾符,它將保存用戶希望從中提取的句柄列表,以及handleText ,它將保存用戶用於輸入句柄的文本框的內容。

現在,讓我們編寫 UI 組件。

用戶界面組件

UI 組件將相當簡單。 我們將有一個 Bootstrap 行,其中包含輸入文本框和兩個按鈕,一個用於將當前輸入框內容添加到句柄列表中,另一個用於從 API 中提取。 我們將有另一個 Bootstrap 行,顯示用戶希望使用 Bootstrap 列表組提取的句柄列表。 在代碼中,它看起來像這樣:

 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> );

除了 UI 組件之外,我們還需要實現三個處理數據更改的 UI 事件處理程序。 調用 API 的getPull事件處理程序將在下一節中實現。

 // 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); }

現在,我們已準備好實現 API 調用。

API 調用

對於 API 調用,我們想要獲取我們想要提取的句柄,在 POST 請求中將其發送到 Python API,然後將生成的 JSON 結果放入curatedTweets變量中。 然後,如果一切順利,我們希望以編程方式導航到 /display 路由。 否則,我們會將錯誤記錄到控制台,以便我們更輕鬆地調試。

在代碼模式下,它看起來像這樣:

 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); }) }

有了這個,我們應該很高興。 隨意運行應用程序,添加幾個句柄,並向 API 發送請求。

現在,我們已經準備好編寫情緒頁面了。

情緒排序模式

因為 Python API 已經按照情緒對推文進行了排序,一旦我們從 Python API 得到結果,情緒頁面實際上並不太難。

大圖計劃

我們需要一個列表界面來顯示推文。 我們還需要幾個導航組件來切換到主題分組模式並返回輸入頁面。

首先,讓我們在 display.js 文件中定義 SentimentDisplay 模式子組件。

情緒顯示組件

SentimentDisplay 將採用curatedTweets對象並在列表中顯示情緒排序的推文。 在 React-Bootstrap 的幫助下,組件非常簡單:

 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> }

當我們這樣做的時候,讓我們也添加一些樣式。 將以下內容放入 display.css 並導入:

 .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; }

我們現在可以顯示 SentimentDisplay 組件。 像這樣更改Display功能:

 const Display = ({curatedTweets}) => { return <SentimentDisplay curatedTweets={curatedTweets} /> };

讓我們也藉此機會編寫導航組件。 我們需要兩個按鈕——“返回編輯”按鈕和主題組模式。

我們可以在 SentimentDisplay 組件正上方的單獨 Bootstrap 行中實現這些按鈕,如下所示:

 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>

運行應用程序並從幾個句柄中提取推文。 看起來很漂亮!

主題分組模式

現在,我們要實現主題分組模式。 它比 SentimentDisplay 複雜一點,但同樣,一些非常方便的 Bootstrap 組件極大地幫助了我們。

大圖計劃

我們將獲取所有名詞短語並將它們顯示為手風琴列表。 一旦手風琴列表展開,我們將渲染包含名詞短語的推文。

實現切換到主題分組模式

首先,讓我們實現從情感模式切換到主題分組模式的邏輯。 讓我們首先創建存根組件:

 const TopicDisplay = () => { return <div>Topic Display</div> }

並設置一些邏輯來創建一個模式來顯示它。 在主顯示組件中,添加以下行以創建顯示組件的邏輯。

 // 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'

並將 JSX 更改為以下內容以添加邏輯:

 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>

現在,您應該在切換時看到主題組顯示存根。

主題顯示組件

現在,我們準備編寫TopicDisplay組件。 如前所述,它將利用 Bootstrap Accordion List。 實現實際上相當簡單:

 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> }

運行應用程序,您應該會看到主題顯示。

現在應用程序已經完成,我們準備為模擬器構建應用程序。

在模擬器中運行應用程序

Cordova 使在模擬器中運行應用程序變得非常容易。 只需運行:

 cordova platform add ios # if you haven't done so already cordova run ios

您應該會在模擬器中看到該應用程序。 因為 Bootstrap 是一個響應式網絡應用程序,所以網絡應用程序適應 iPhone 的寬度,一切看起來都相當不錯。

完成 Cordova 應用程序後,現在讓我們看看 Ionic 實現。

離子反應實現

離子底漆

Ionic 是一個 Web 組件庫和 CLI 工具包,可以更輕鬆地構建混合應用程序。 最初,Ionic 是建立在 AngularJS 和 Cordova 之上的,但後來他們在 React.js 中發布了他們的組件,並開始支持 Capacitor,一個類似於 Cordova 的平台。 Ionic 的不同之處在於,即使您使用的是 Web 組件,這些組件的感覺也與原生移動界面非常相似。 此外,Ionic 組件的外觀和感覺會自動適應它運行的操作系統,這再次有助於應用程序的外觀和感覺更加原生和自然。 最後,雖然這超出了我們文章的範圍,但 Ionic 還提供了幾個構建工具,使您的應用程序部署更加容易。

對於我們的應用程序,我們將使用 Ionic 的 React 組件來構建 UI,同時利用我們在 Cordova 部分構建的一些 JavaScript 邏輯。

配置應用程序

首先,我們要安裝 Ionic 工具。 所以讓我們運行以下命令:

 npm install -g @Ionic/cli native-run cordova-res

安裝完成後,讓我們進入項目文件夾。 現在,我們使用 Ionic CLI 創建我們的新項目文件夾:

 ionic start twitter-curation-Ionic blank --type=react --capacitor

觀看魔術發生,現在進入文件夾:

 cd twitter-curation-Ionic

並運行空白應用程序:

 ionic serve

我們的應用程序因此設置並準備就緒。 讓我們定義一些路線。

在我們繼續之前,您會注意到 Ionic 使用 TypeScript 啟動了該項目。 雖然我不會特意使用 TypeScript,但它有一些非常好的特性,我們將在此實現中使用它。

路由器設置

對於此實現,我們將使用三個路由 - input、 sentimentDisplaytopicDisplay 。 我們這樣做是因為我們想利用 Ionic 提供的過渡和導航功能,並且因為我們使用的是 Ionic 組件,並且手風琴列表沒有與 Ionic 預打包。 當然,我們可以實現自己的,但是對於本教程,我們將繼續使用提供的 Ionic 組件。

如果您導航到 App.tsx,您應該會看到已經定義的基本路由。

輸入頁面

大圖計劃

我們將使用許多與 Bootstrap 實現類似的邏輯和代碼,但有一些關鍵區別。 首先,我們將使用 TypeScript,這意味著我們將為我們的代碼添加類型註釋,您將在下一節中看到。 其次,我們將使用 Ionic 組件,它們在樣式上與 Bootstrap 非常相似,但在樣式上將是操作系統敏感的。 最後,我們將使用歷史 API 動態導航,就像在 Bootstrap 版本中一樣,但由於 Ionic Router 實現,訪問歷史略有不同。

配置

讓我們從使用存根組件設置輸入組件開始。 在名為 input 的頁面下創建一個文件夾,並在其下創建一個名為 Input.tsx 的文件。 在該文件中,放入以下代碼來創建一個 React 組件。 請注意,因為我們使用的是 TypeScript,所以它有點不同。

 import React, {useState} from 'react'; const Input : React.FC = () => { return <div>Input</div>; } export default Input;

並將 App.tsx 中的組件更改為:

 const App: React.FC = () => ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/input" component={Input} exact={true} /> <Route exact path="/" render={() => <Redirect to="/input" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> );

現在,當您刷新應用程序時,您應該會看到 Input 存根組件。

數據容器

現在讓我們創建數據容器。 我們想要輸入 Twitter 句柄的容器以及輸入框的當前內容。 因為我們使用的是 TypeScript,所以我們需要在組件函​​數中的useState調用中添加類型註釋:

 const Input : React.FC = () => { const [text, setText] = useState<string>(''); const [accounts, setAccounts] = useState<Array<string>>([]); return <div>Input</div>; }

我們還需要一個數據容器來保存 API 的返回值。 因為它的內容需要與其他路由共享,所以我們在 App.tsx 級別定義它們。 在 App.tsx 文件中從 React 導入useState並將應用容器函數更改為以下內容:

 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> ); }

此時,如果您正在使用像 Visual Studio Code 這樣具有語法高亮功能的編輯器,您應該會看到 CuratedTweets 亮起。 這是因為該文件不知道 CuratedTweets 界面是什麼樣的。 讓我們現在定義它。 在 src 下創建一個名為 interfaces 的文件夾,並在其中創建一個名為 CuratedTweets.tsx 的文件。 在該文件中,定義 CuratedTweets 接口,如下所示:

 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> }

現在應用程序知道了 API 返回數據的結構。 在 App.tsx 中導入 CuratedTweets 接口。 您現在應該可以看到 App.tsx 編譯沒有問題。

我們需要在這裡做更多的事情。 我們需要將setCuratedTweets函數傳遞給 Input 組件,並讓 Input 組件知道這個函數。

在 App.tsx 中,修改 Input 路由,如下所示:

 <Route path="/input" render={() => <Input setCuratedTweets={setCuratedTweets}/>} exact={true} />

現在,您應該看到編輯器標記了其他內容 - Input 不知道傳遞給它的新道具,因此我們希望在 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.

用戶界面組件

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 調用

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); }) }

添加標題

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>

現在看起來不錯!

情緒排序頁面

大圖計劃

情緒排序頁面將與 Bootstrap 頁面中的情緒排序頁面非常相似,但使用 TypeScript 和 Ionic。 我們還將主題顯示實現為單獨的路徑,以便在移動設備上運行時利用 Ionic 的導航功能,因此我們需要讓該頁面也能夠導航到主題顯示路徑。

路線設置

讓我們首先創建一個名為sentimentsorted 的新文件夾和一個名為SentimentSorted.tsx 的文件。 像這樣導出存根組件:

 import React from 'react'; const SentimentSorted: React.FC = () => { return <div>Sentiment Sorted</div> } export default SentimentSorted;

在 App.tsx 中,導入組件:

 import SentimentSorted from './pages/sentimentsorted/SentimentSorted';

並添加路線:

 <Route path="/display" render={() => <SentimentSorted curatedTweets={curatedTweets} />} />

您將收到一個 TypeScript 錯誤,指出SentimentSorted不期待 curatedTweets 道具,所以現在讓我們在下一節中處理這個問題。

用戶界面組件

讓我們從定義容器的道具開始。 很像輸入組件:

 import CuratedTweets from '../../interfaces/CuratedTweets'; interface ContainerProps { curatedTweets: CuratedTweets }

現在,更改存根顯示:

 const SentimentSorted: React.FC<ContainerProps> = ({curatedTweets}) => { return <div>Sentiment Sorted</div> }

一切都應該編譯。

顯示很簡單,就是一個帶有顯示組件的IonList:

 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>

如果您使用輸入組件保存和拉取一些推文,您應該會看到推文顯示在列表中。

現在,讓我們添加導航按鈕。 添加到離子網格:

 <IonRow> <IonCol> <IonButton color='primary' onClick={switchToInput}>Back</IonButton> <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton> </IonCol> </IonRow>

switchToInput使用歷史 API 很容易實現:

 const switchToInput = () => { history.goBack(); }

ToggleDisplayType也應該很熟悉:

 const toggleDisplayType = () => { setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment'); } const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'

現在我們已經實現了SentimentDisplay組件。 現在,在我們實現主題顯示頁面之前,我們需要實現顯示所有主題的組件。 我們將在下一節中這樣做。

主題組組件

讓我們添加一個主題列表顯示選項並有條件地顯示它。 為此,我們需要分解情緒顯示列表。 將 SentimentDisplay 重命名為 Display,我們來分解一下情緒顯示列表:

 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> }

請注意我們如何使用 CuratedTweets 接口中的類定義之一。 這是因為這些組件不需要整個 CuratedTweets 對象,而只需要一個子集。 主題列表非常相似:

 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> }

現在,條件顯示很容易在顯示組件中設置:

 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> );

確保更改默認導出,現在我們已準備好實施主題顯示頁面。

主題展示頁面

大圖計劃

主題展示頁面是類似於情感展示的列表展示,但我們會從路由參數中尋找有問題的主題。

路線設置

如果你已經走到這一步,你應該已經知道該怎麼做了。 讓我們創建一個名為 topicdisplay 的頁面文件夾和一個 TopicDisplay.tsx,編寫一個存根組件,並將其導入 App.tsx 頁面。 現在,讓我們設置路線:

 <Route path="/topicDisplay/:topic" render={() => <TopicDisplay curatedTweets={curatedTweets} /> } />

現在我們已經準備好實現 UI 組件了。

用戶界面組件

首先,讓我們創建ContainerProps定義:

 interface ContainerProps { curatedTweets: CuratedTweets } const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => { Return <div>Topic Display</div> }

現在,我們需要從 URL 路徑名中檢索主題。 為此,我們將使用歷史 API。 因此,讓我們導入useHistory ,實例化歷史 API,並從路徑名中提取主題。 在我們做這件事的同時,讓我們也實現切換回功能:

 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];

現在我們有了具有該特定主題的推文,顯示實際上非常簡單:

 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> );

保存並運行,事情應該看起來不錯。

在模擬器中運行應用程序

要在模擬器中運行應用程序,我們只需運行一些 Ionic 命令來添加移動平台並複制代碼,類似於我們使用 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

您應該會看到該應用程序出現。

反應本機實現

反應原生入門

React Native 採用了與前幾節基於 Web 的方法非常不同的方法。 React Native 將您的 React 代碼呈現為原生組件。 這有幾個優點。 首先,與底層操作系統的集成更加深入,這使開發人員能夠利用可能無法通過 Cordova/Capacitor 獲得的新智能手機功能和特定於操作系統的功能。 其次,由於中間沒有基於 Web 的渲染引擎,React Native 應用程序通常比使用 Cordova 編寫的應用程序更快。 最後,由於 React Native 允許集成原生組件,開發人員可以對其應用程序進行更細粒度的控制。

對於我們的應用程序,我們將使用前面部分中的邏輯,並使用一個名為 NativeBase 的 React Native 組件庫來編寫我們的 UI。

配置應用程序

首先,您需要按照此處的說明安裝 React Native 的所有必需組件。

安裝 React Native 後,讓我們啟動項目:

 react-native init TwitterCurationRN

讓安裝腳本運行,最終應該創建文件夾。 cd 進入文件夾並運行 react-native run-ios,你應該會看到模擬器彈出示例應用程序。

我們還想安裝 NativeBase,因為那是我們的組件庫。 為此,我們運行:

 npm install --save native-base react-native link

我們還想安裝 React Native 堆棧導航器。 讓我們運行:

 npm install --save @react-navigation/stack @react-navigation/native

react-native link cd ios pod-install cd

完成原生插件的鏈接和安裝。

路由器設置

對於路由,我們將使用我們在上述步驟中安裝的堆棧導航器。

導入路由器組件:

 import { NavigationContainer } from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack';

現在,我們創建一個堆棧導航器:

 const Stack = createStackNavigator();

更改 App 組件的內容以使用堆棧導航器:

 const App = () => { return ( <> <StatusBar bar /> <NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> </Stack.Navigator> </NavigationContainer> </> ); };

此時,您將收到一個錯誤,因為尚未定義 Entry。 讓我們定義一個存根元素只是為了讓它快樂。

在您的項目中創建一個 components 文件夾,創建一個名為 Entry.jsx 的文件,然後添加一個存根組件,如下所示:

 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. }

在您的應用程序中導入 Entry 組件,它應該會構建。

現在,我們已準備好對 Input 頁面進行編碼。

輸入頁面

大圖計劃

我們將實現一個與上面實現的頁面非常相似但使用 NativeBase 組件的頁面。 我們使用的大多數 JavaScript 和 React API,例如 hooks 和 fetch,都仍然可用。

不同之處在於我們使用導航 API 的方式,稍後您會看到。

用戶界面組件

我們將使用的其他 NativeBase 組件是 Container、Content、Input、List、ListItem 和 Button。 這些在 Ionic 和 Bootstrap React 中都有類似物,NativeBase 的構建者讓熟悉這些庫的人非常直觀。 像這樣簡單地導入:

 import { Container, Content, Input, Item, Button, List, ListItem, Text } from 'native-base';

組件是:

 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>

現在,讓我們實現狀態和事件處理程序:

 const [input, changeInput] = useState(''); const [handles, changeHandles] = useState([]); const inputChange = (text) => { changeInput(text); } const onAddClicked = () => { const newHandles = [...handles, {key: input}]; changeHandles(newHandles); changeInput(''); }

最後,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); }) }

請注意,此實現與 NativeBase 實現中的相同,只是我們以不同的方式導航。 堆棧導航器將一個稱為“導航”的道具傳遞到其組件中,該道具可用於使用.navigate在路線之間導航。 除了簡單的導航之外,還可以將數據傳遞給目標組件。 我們將使用這種機制來傳遞數據。

為了使應用程序編譯,我們仍然需要讓Entry知道導航組件。 為此,我們需要更改組件函數聲明:

 export default Entry = ({navigation}) => {

現在保存,您應該會看到該頁面。

情緒排序頁面

大圖計劃

我們將像前面的部分一樣實現情感頁面,但我們會稍微改變頁面的樣式,並且我們還將以不同的方式使用導航庫來獲取 API 調用返回值。

因為 React Native 沒有 CSS,我們要么需要定義一個 StyleSheet 對象,要么簡單地對樣式進行內聯編碼。 因為我們將在組件之間共享一些樣式,所以讓我們創建一個全局樣式表。 我們將在路線設置後執行此操作。

另外,因為StackNavigator有一個內置的後退導航按鈕,我們不需要實現我們自己的後退按鈕。

路線設置

StackNavigator中的路由定義非常簡單。 我們只需創建一個名為 Stack Screen 的新組件並為其提供組件,就像 React 路由器一樣。

 <NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> </Stack.Navigator> </NavigationContainer>

為了完成這項工作,我們當然需要在 components/SentimentDisplay.js 中創建一個存根組件:

 import React from 'react'; import {Text} from 'native-base'; const SentimentDisplay = () => { return <Text>Sentiment Display</Text>; } export default SentimentDisplay;

並導入它:

 import SentimentDisplay from './components/SentimentDisplay';

現在,我們準備好創建全局樣式表了。

全局樣式表

首先,創建一個名為 globalStyles.js 的文件。 然後,從 React Native 導入 StyleSheet 組件並定義樣式:

 import {StyleSheet} from 'react-native'; export default StyleSheet.create({ tweet: {paddingTop: 5}, accountName: {fontWeight: '600'}, })

我們已經準備好編寫 UI 代碼了。

用戶界面組件

UI 組件非常熟悉,除了我們如何使用路由。 我們將希望使用StackNavigator的特殊道具導航和路由來獲取當前應用程序狀態,並在用戶想要查看該頁面時導航到主題顯示。

更改組件定義以訪問導航道具:

 const SentimentDisplay = ({route, navigation}) => {

現在,我們實現了應用程序狀態讀取和導航功能:

 const {params: {data}} = route; const viewByTopicClicked = () => { navigation.navigate('TopicDisplay', { data }); }

導入全局樣式:

 import globalStyles from './globalStyles';

和組件:

 import { View } from 'react-native'; import {List, Item, Content, ListItem, Container, Text, Button} from 'native-base';

最後,組件:

 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>;

保存並嘗試提取一些推文,您應該會看到情緒顯示。 現在進入主題分組頁面。

主題分組頁面

大圖計劃

主題顯示再次非常相似。 我們將使用處理程序構建器來構建導航功能以導航到特定主題項的顯示頁面,並且我們還將定義特定於該頁面的樣式表。

我們將要做的一件新事情是實現一個 TouchableOpacity,它是一個 React Native 特定的組件,其功能很像一個按鈕。

路線設置

路由定義和之前一樣:

 <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> <Stack.Screen name="TopicDisplay" component={TopicDisplay} /> </Stack.Navigator>

存根組件 components/TopicDisplay.js:

 import React from 'react'; import {Text} from 'native-base'; const TopicDisplay = () => { return <Text>Topic Display</Text>; } export default TopicDisplay;

並導入它:

 import TopicDisplay from './components/TopicDisplay;

用戶界面組件

其中很多看起來非常熟悉。 導入庫函數:

 import { View, TouchableOpacity, StyleSheet } from 'react-native'; import {List, Item, Content, ListItem, Container, Text} from 'native-base';

導入全局樣式:

 import globalStyles from './globalStyles';

定義自定義樣式:

 const styles = StyleSheet.create({ topicName: {fontWeight: '600'}, })

定義導航道具:

 export default TopicDisplay = ({route, navigation}) => {

定義數據和操作處理程序。 請注意,我們正在使用處理程序構建器,一個返回函數的函數:

 const {params: {data}} = route; const specificItemPressedHandlerBuilder = (topic) => () => { navigation.navigate('TopicDisplayItem', { data, topic }); }

現在,組件。 請注意,我們使用的是 TouchableOpacity,它可以有一個onPress處理程序。 我們也可以使用 TouchableTransparency,但 TouchableOpacity 的單擊並按住動畫更適合我們的應用程序。

 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>;

這應該這樣做。 保存並試用該應用程序!

現在,進入主題顯示項目頁面。

主題顯示項目頁面

大圖計劃

主題顯示項目頁面非常相似,所有特性都在其他部分中處理,因此從這裡開始應該是一帆風順的。

路線設置

我們將添加路由定義:

 <Stack.Screen name="TopicDisplayItem" component={TopicDisplayItem} />

添加導入:

 import TopicDisplayItem from './components/TopicDisplayItem';

並創建存根組件。 讓我們不僅導入一個裸組件,還導入我們將使用的 NativeBase 組件並定義路由道具:

 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;

用戶界面組件

UI 組件非常簡單。 我們以前見過它,我們並沒有真正實現任何自定義邏輯。 所以,讓我們去吧! 深呼吸……

 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>;

保存,我們應該很高興! 現在我們已經準備好在模擬器中運行應用程序了……等等,不是嗎?

運行應用程序

好吧,既然你正在使用 React Native,你已經在模擬器中運行了應用程序,所以這部分已經被處理了。 這是 React Native 開發環境的一大優點。

哇! 至此,我們完成了本文的編碼部分。 讓我們看看我們從這些技術中學到了什麼。

比較技術

科爾多瓦:優點和缺點

Cordova 最棒的地方在於,熟練的 Web 開發人員可以以極快的速度編寫功能性且合理的東西。 Web 開發技能和經驗很容易轉移,因為畢竟您是在編寫 Web 應用程序。 開發過程快速簡單,訪問 Cordova API 也簡單直觀。

直接使用 Cordova 的缺點主要來自對 Web 組件的過度依賴。 用戶在使用移動應用程序時已經開始期待特定的用戶體驗和界面設計,而當應用程序感覺像移動網站時,體驗可能會有點不和諧。 此外,應用程序中內置的大多數功能,例如過渡動畫和導航實用程序,都必須手動實現。

離子:優點和缺點

Ionic 最好的部分是我“免費”獲得了多少以移動為中心的功能。 通過像編寫 Web 應用程序一樣編寫代碼,我能夠構建一個看起來比簡單地使用 Cordova 和 React-Bootstrap 更適合移動設備的應用程序。 導航動畫、具有原生樣式的按鈕以及許多用戶界面選項使用戶體驗非常流暢。

使用 Ionic 的缺點部分是由它的優勢造成的。 首先,有時很難想像應用程序在各種環境中的行為方式。 僅僅因為應用程序看起來是一種方式並不意味著相同的 UI 位置在另一個環境中會看起來相同。 其次,Ionic 位於許多底層技術之上,並且訪問某些組件被證明是困難的。 最後,這是 Ionic-React 特有的,但由於 Ionic 最初是為 Angular 構建的,因此許多 Ionic-React 功能似乎沒有多少文檔和支持。 但是,Ionic 團隊似乎非常關注 React 開發人員的需求,并快速提供了新功能。

React Native:優點和缺點

React Native 在移動端開發時擁有非常流暢的用戶體驗。 通過直接連接到模擬器,應用程序的外觀並不神秘。 基於 Web 的調試器界面在交叉應用來自 Web 應用程序世界的調試技術方面非常有幫助,並且生態系統非常健壯。

React Native 的缺點來自於它接近原生界面。 許多基於 DOM 的庫無法使用,這意味著必須學習新的庫和最佳實踐。 如果沒有 CSS 的好處,應用程序的樣式就不太直觀。 最後,有許多新的組件需要學習(例如,View 代替 div,Text 組件包裝所有內容,Buttons vs. TouchableOpacity vs. TouchableTransparency 等),如果有人進入,一開始會有一點學習曲線在對機制知之甚少的情況下反應原生世界。

何時使用每種技術

因為 Cordova、Ionic 和 React Native 都具有非常強的優缺點,所以每種技術都有一個可以享受最佳生產力和性能的環境。

如果您已經擁有一個現有的應用程序,該應用程序是網絡優先的,並且圍繞 UI 設計和總體外觀和感覺具有強大的品牌標識,那麼您最好的選擇是 Cordova,它可以讓您訪問智能手機的本機功能,同時讓您重用您的大部分Web 組件並在此過程中保留您的品牌標識。 對於使用響應式框架的相對簡單的應用程序,您可以構建一個只需要很少更改的移動應用程序。 但是,您的應用程序看起來不太像應用程序,而更像是網頁,並且人們期望從移動應用程序中獲得的一些組件將單獨編碼。 因此,如果您處於將應用程序移植到移動設備的 Web 優先項目中,我推薦使用 Cordova。

如果您開始以應用程序優先的理念編寫新應用程序,但您的團隊的技能主要是 Web 開發,我推薦 Ionic。 Ionic 的庫可讓您快速編寫外觀和感覺接近原生組件的代碼,同時讓您應用您作為 Web 開發人員的技能和直覺。 我發現 Web 開發最佳實踐很容易交叉應用到使用 Ionic 進行開發,並且使用 CSS 進行樣式設計可以無縫地工作。 此外,該網站的移動版本看起來比使用響應式 CSS 框架編碼的網站更加本地化。 但是,在此過程中的某些步驟中,我發現 React-Ionic-Native API 集成需要一些手動調整,這可能會非常耗時。 因此,如果您的應用程序是從頭開始開發的,並且您希望在支持移動的 Web 應用程序和移動應用程序之間共享大量代碼,我推薦使用 Ionic。

如果你正在編寫一個實現了一些原生代碼庫的新應用程序,你可能想嘗試 React Native。 即使您不使用本機代碼,如果您已經熟悉 React Native,或者您主要關心的是移動應用程序而不是混合應用程序,它也可能是最佳選擇。 將我的大部分前端開發工作集中在 Web 開發上後,我最初發現,由於組件組織和編碼約定的差異,開始使用 React Native 比 Ionic 或 Cordova 具有更多的學習曲線。 但是,一旦了解了這些細微差別,編碼體驗就會非常流暢,尤其是在 NativeBase 這樣的組件庫的幫助下。 鑑於開發環境的質量和對應用程序的控制,如果您的項目的前端主要是移動應用程序,我會推薦 React Native 作為您的首選工具。

未來主題

我沒有時間探索的主題之一是訪問本機 API 的便利性,例如相機、地理定位或生物識別身份驗證。 移動開發的一大好處是可以訪問通常無法在瀏覽器上訪問的豐富 API 生態系統。

在以後的文章中,我想探討使用各種跨平台技術開發這些支持原生 API 的應用程序的難易程度。

結論

今天,我們使用三種不同的跨平台移動開發技術實現了一個 Twitter 管理應用程序。 我希望這能讓您很好地了解每種技術的含義,並啟發您開發自己的基於 React 的應用程序。

感謝您的閱讀!