使用 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、 sentimentDisplay
和topicDisplay
。 我们这样做是因为我们想利用 Ionic 提供的过渡和导航功能,并且因为我们使用的是 Ionic 组件,并且手风琴列表没有与 Ionic 预打包。 当然,我们可以实现自己的,但是对于本教程,我们将继续使用提供的 Ionic 组件。
如果您导航到 App.tsx,您应该会看到已经定义的基本路由。
输入页面
大图计划
我们将使用许多与 Bootstrap 实现类似的逻辑和代码,但有一些关键区别。 首先,我们将使用 TypeScript,这意味着我们将为我们的代码添加类型注释,您将在下一节中看到。 其次,我们将使用 Ionic 组件,它们在样式上与 Bootstrap 非常相似,但在样式上将是操作系统敏感的。 最后,我们将像在 Bootstrap 版本中一样使用历史 API 动态导航,但由于 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 中定义它。
首先,导入 CuratedTweets 接口,然后定义 ContainerProps 接口,如下所示:
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 的应用程序。
感谢您的阅读!