ReacteNative 示例(三)
原文:
zh.annas-archive.org/md5/6c72813d39d3c6a9eb1e81035f8a1a7b译者:飞龙
第五章:第三项目 - Facebook 客户端
到目前为止,我们主要构建了仅处理用户提供的信息的应用程序。然而,许多应用程序倾向于从网络上的其他来源发送和接收数据。在本书的第三个也是最后一个项目中,我们将构建一个可以访问外部 Facebook API 的应用程序,以便用户可以访问他们的个人资料。
在本章中,您将完成以下任务:
-
计划我们的 Facebook 应用“朋友”,决定它应该具备哪些关键因素
-
获取访问 Facebook API 的权限并安装 iOS 和 Android 的官方 SDK
-
使用 Facebook API 的登录SDK 授予应用适当的权限
-
使用
GraphRequest和GraphRequestManager从 Facebook API 获取信息 -
使用
ActivityIndicator让用户直观地知道数据正在加载 -
开始构建我们 Facebook 应用的基本功能
规划应用
“朋友”将是我们将构建的第一个完整的示例,展示 React Native 的强大功能。它将涉及许多动态部分,因此深入规划应用是很好的。在基本层面上,访问 Facebook Graph API 给我们以下权限:
-
登录
-
查看您的动态
-
查看您动态上的帖子列表及其评论和点赞
-
在您的动态上添加新的帖子或评论
-
浏览您上传到 Facebook 个人资料的照片及其评论和点赞
-
查看您已确认参加的活动
-
重新发现您喜欢的页面列表
如前几章所述,我们希望将其分解为小规模的成就。到本章结束时,“朋友”应用应实现以下功能:
-
提醒用户(如果尚未登录)登录 Facebook,并使用 SDK 自动保存其身份验证令牌
-
在动态加载时,显示旋转动画以可视化数据正在加载
-
显示用户的动态
-
对于动态上的每篇帖子,显示帖子的内容以及评论和点赞的数量
-
点击时,加载并显示该特定帖子的评论链
-
允许读者对特定帖子的评论进行回复或创建新的帖子
关于 Facebook API
在我们继续之前,关于我们可以通过 Facebook API 获得的访问级别做一个说明——您只能获取已登录用户的个人信息。具体用户的好友列表通过 Facebook 的 API 无法访问,但可以访问一小部分也安装了相同应用的好友。由于在我们的项目中这并不很有用,我故意省略了它。
虽然用户的帖子和个人照片肯定会有一个包含发表评论的人的姓名和照片的评论列表,但使用当前版本的 Facebook API 无法访问这些好友的个人资料。
获取 Facebook API 凭证
这看起来是一个很好的起点。然而,在我们开始之前,我们需要将我们的应用注册到 Facebook 上。请访问 Facebook 的开发者网站并选择添加新应用。撰写本文时,网址是 developers.facebook.com。
一旦您注册了您的应用,请从 developers.facebook.com/docs/ios/ 下载 iOS 的 Facebook SDK,并将其内容解压缩到您的 Documents 文件夹中,命名为 FacebookSDK。请保持此文件夹打开;我们很快就会用到它。
之后,前往您应用的仪表板并注意 App ID。您稍后也需要这个信息。您可以在以下位置找到它:
在下一节中,我们将探讨如何安装官方的 React Native Facebook SDK。
在 iOS 和 Android 上安装 Facebook SDK
使用以下命令行初始化一个新的 React Native 项目:
react-native init Friends
然后,使用命令行导航到您刚刚创建的新项目。
React Native 的 Facebook SDK 通过 npm 在名为 react-native-fbsdk 的包中提供。我们将这样安装它:
npm install --save react-native-fbsdk
现在,按照以下步骤链接 SDK:
react-native link react-native-fbsdk
现在,按照 GitHub 上 react-native-fbsdk 仓库中的详细说明操作,该仓库位于 github.com/facebook/react-native-fbsdk。由于安装说明可能会随时更改,我强烈建议您使用该仓库中的说明。
之后,使用我们之前看到的流程(如需复习,请参阅第四章,使用 Expenses 应用的高级功能)安装 react-native-vector-icons 库。
一旦您为该项目初始化了应用并安装了 Facebook SDK 和 react-native-vector-icons 库,就到了开始玩耍的时候了。
使用 Facebook SDK 登录
我们可以在应用中尝试的第一件事是登录用户。FSBDK 有一个内置的组件称为 LoginButton,当按下时,它将使用 WebView 在应用内部将用户发送到登录屏幕。如果登录成功,将为您保存一个访问令牌,供您的应用使用,而无需您亲自跟踪它。
首先,将 FBSDK 仓库的 README 中的 LoginButton 片段添加到您的应用的 index 文件中。您将得到类似以下的内容:
// Friends/index.ios.js
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View
} from 'react-native';
import {
AccessToken,
LoginButton
} from 'react-native-fbsdk';
从 react-native-fbsdk 仓库导入 AccessToken 和 LoginButton 模块,使用解构符号。
export default class Friends extends Component {
render() {
return (
<View style={ styles.container }>
<LoginButton
readPermissions={["public_profile", "user_photos",
"user_posts", "user_events", "user_likes"]}
readPermissions 属性接受一个字符串数组,并请求用户特定的只读权限,这些权限等于传入的数组。
Facebook API 有很多不同的权限可以请求,为了本项目的目的,我们将请求以下权限:
-
public_profile:这提供了访问用户公共 Facebook 资料中的一部分内容。这包括他们的 ID、姓名、个人资料图片等。 -
user_events:这是一个列表,其中包含一个人正在举办或已响应的事件。 -
user_likes:这是用户点击赞的 Facebook 页面的集合。 -
user_photos:这是用户上传或标记的照片。 -
user_posts:这是用户时间线上的帖子。
onLoginFinished方法被编写为异步的:
async onLoginFinished={
async (error, result) => {
if (error) {
} else if (result.isCancelled) {
alert("login is cancelled.");
} else {
const data = await AccessToken.getCurrentAccessToken()
alert(data);
}
}
}
onLogoutFinished={() => alert("logout.")}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('Friends', () => Friends);
尽管LoginButton还有一些其他属性可用,但在前面代码中展示的三个是我们需要关注的。以下是每个属性的含义:
-
publishPermissions:这表示在按钮按下时请求登录用户的发布权限。 -
onLoginFinished:这是一个在登录请求完成或产生错误时被调用的回调。 -
onLogoutFinished:这是一个在注销请求完成后调用的回调。
如果一切顺利,你将看到以下带有 Facebook 登录按钮的屏幕--居中的“使用 Facebook 登录”:
通过点击此标志,你将被带到WebView组件内的登录页面,该组件处理 Facebook 登录。
登录后,用户将看到一个提示,要求读取权限等于我们通过LoginButton组件作为属性传递的readPermissions数组中请求的权限:
一旦您的用户获得授权,您将能够从 Facebook 的 Graph API 中获取数据。
使用 Facebook Graph API
FBSDK 允许我们使用GraphRequest和GraphRequestManager类来创建请求并执行这些请求。
GraphRequest用于创建对 Graph API 的请求,而GraphRequestManager用于执行该请求。
GraphRequest
要实例化一个新的GraphRequest,我们可以传递最多三个参数:
-
graphPath:这是一个与 Graph API 端点相关的字符串,表示我们希望触发的端点。例如,要获取登录用户的信息,将使用graphPath为/me。 -
config:这是一个可选的对象,可以配置请求。该对象接受的属性都是可选的:-
httpMethod:这是一个描述此请求 HTTP 方法的字符串,例如GET或POST。 -
version:这是一个描述要使用的特定 Graph API 版本的字符串。 -
parameters:这是一个包含请求参数的对象。 -
accessToken:这是请求使用的访问令牌的字符串版本。
-
-
callback:这是一个在请求完成或失败时触发的回调函数。
一个示例GraphRequest实例将看起来像这样:
const requestMyPhotos = new GraphRequest('/me/photos/uploaded',
null, this._responseInfoCallback);
_responseInfoCallback (error, result) {
if (error) {
console.log('Error fetching data: ' + error.toString())
} else {
console.log(result);
}
}
为了执行此请求,我们将使用GraphRequestManager。
GraphRequestManager
GraphRequestManager队列请求 Facebook Graph API,并在被指示时执行它。
它可以访问以下方法:
-
addRequest: 这是一个接受GraphRequest实例并将请求推入GraphRequestManager队列中的函数。它还将回调推入一个单独的requestCallbacks队列,以便在请求完成或失败时执行。 -
addBatchCallback: 这个方法接受一个可选的回调,在请求批次完成时执行。每个GraphRequestManager实例只能接受一个回调,调用该回调并不表示批次中的每个图请求都成功--它唯一表明的是整个批次已完成执行。 -
start: 这个方法接受一个可选的数字,其值等于超时时间。如果没有传入,则默认超时时间为 0。当调用GraphRequestManager.start时,GraphRequestManager将按照先入先出的顺序向 Facebook Graph API 发起一系列请求,并在适用的情况下执行每个请求的回调函数。
在前面的示例中添加,一个GraphRequestManager请求看起来像这样:
new GraphRequestManager().addRequest(requestMyPhotos).start();
此请求创建了一个新的GraphRequestManager实例,包括其自己的新批次,将前面的requestMyPhotos任务添加到批次中,然后启动它。从这里开始,Facebook Graph API 将返回某种形式的数据。
在GraphRequest的requestMyPhotos实例中传递的回调将执行,记录错误或请求的结果。
创建我们的第一个请求
是时候创建我们的第一个请求来验证我们收到的访问令牌是否有效了。
在index.ios.js中的Friends组件内,让我们做以下几件事情:
-
创建一个名为
_getFeed的方法,该方法创建一个针对您的 Facebook 动态的GraphRequest。此方法应从/me/feed端点获取数据,并引用一个回调函数,当该GraphRequest完成时执行。您可以跳过GraphRequest可以可选接受的config对象。 -
在相同的方法
_getFeed中,创建一个新的GraphRequestManager实例,并将GraphRequest实例添加到其中;然后启动GraphRequestManager。 -
对于由
_getFeed引用的回调,当您的GraphRequest完成时,记录它接收到的错误或结果。 -
在
LoginButton的onLoginFinished回调中调用_getFeed。
当您完成时,结果应该看起来像这样:
// Friends/index.ios.js
...
import {
...
GraphRequest,
GraphRequestManager,
} from 'react-native-fbsdk';
export default class Friends extends Component {
render() {
return (
<View style={ styles.container }>
<LoginButton
...
onLoginFinished={
async (error, result) => {
...
} else {
await AccessToken.getCurrentAccessToken();
this._getFeed();
我不是在提醒访问令牌,而是在调用_getFeed。
}
}
}
...
/>
</View>
);
}
通过传递期望的端点和请求完成后要触发的回调来创建一个新的GraphRequest实例:
_getFeed () {
const infoRequest = new GraphRequest('/me/feed', null,
this._responseInfoCallback);
现在,创建一个新的GraphRequestManager实例,将infoRequest对象添加到其中,然后启动请求:
new GraphRequestManager().addRequest(infoRequest).start();
}
请求完成后,它将记录结果或遇到的错误:
_responseInfoCallback (error, result) {
if (error) {
console.log('Error fetching data: ', error.toString());
return;
}
console.log(result);
}
}
...
在你的 iOS 模拟器和远程调试已打开的情况下,登录时查看浏览器控制台:
这太棒了!这表明我们已经与 Graph API 建立了联系,并且它接受我们给出的访问令牌。现在,让我们创建一个单独的graphMethods.js实用文件,我们可以在不同的组件中使用。
图形方法
此文件的目标是创建一些与 Facebook Graph API 交互的常用方法,并将它们导出,以便我们可以在应用程序的不同组件中使用。
就像我们为Expenses创建的实用文件一样,这个graphMethods文件应该位于一个名为utils的文件夹中,该文件夹位于项目根目录下的app文件夹内:
创建此实用文件,并让它执行以下操作:
-
创建一个名为
makeSingleGraphRequest的函数,该函数接受一个请求作为参数,创建一个新的GraphRequestManager实例,将请求传递给GraphRequestManager,然后调用GraphRequestManager的start方法。 -
创建并导出一个名为
getFeed的函数,该函数接受一个回调,创建一个新的指向/me/feed的GraphRequest,并使用该回调,然后调用makeSingleGraphRequest。
一旦你的版本完成,请查看下面的我的版本:
// Friends/app/utils/graphMethods.js
import {
GraphRequest,
GraphRequestManager
} from 'react-native-fbsdk';
const makeSingleGraphRequest = (request) => {
return new GraphRequestManager().addRequest(request).start();
}
export const getFeed = (callback) => {
const request = new GraphRequest('/me/feed', null, callback);
makeSingleGraphRequest(request)
}
NavigatorIOS 和 App 组件
现在,让我们使用App.js文件创建一个App组件。在项目的app文件夹中创建此文件:
此组件应包含与之前我们在index.ios.js中拥有的类似逻辑--我们将很快用NavigatorIOS组件替换index.ios.js文件。
你的新App组件应该是本章早期编写的index.ios.js文件的反映,除了它应该导入并使用graphMethods文件而不是特定组件的_getFeed方法。
完成此任务后,请参考我的版本:
// Friends/app/App.js
import React, { Component } from 'react';
import {
View
} from 'react-native';
import {
AccessToken,
LoginButton
} from 'react-native-fbsdk';
由于GraphRequest和GraphRequestManager在graphMethods中被导入,我可以在前面的代码中的import语句中省略它们。
我正在使用解构符号从graphMethods导入getFeed方法。这将在未来很有用,因为该文件将填充更多的辅助方法:
import { getFeed } from './utils/graphMethods';
由于GraphRequest的回调包含error和result参数,我传递它们,这样_responseInfoCallback就可以使用它们:
import styles from './styles';
export default class App extends Component {
render() {
return (
<View style={ styles.container }>
<LoginButton
readPermissions={["public_profile", "user_photos",
"user_posts", "user_events", "user_likes"]}
onLoginFinished={
async (error, result) => {
if (error) {
} else if (result.isCancelled) {
alert("login is cancelled.");
} else {
await AccessToken.getCurrentAccessToken();
getFeed((error, result) =>
this._responseInfoCallback(error, result))
}
}
}
onLogoutFinished={() => alert("logout.")}
/>
</View>
);
}
_responseInfoCallback (error, result) {
if (error) {
console.log('Error fetching data: ', error.toString());
return;
}
console.log(result);
}
}
这里是App组件的基本样式:
// Friends/app/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
export default styles;
干得好!下一步是将项目根目录下的index.ios.js进行重构,执行以下操作:
-
从 React Native SDK 导入
NavigatorIOS以及你刚刚创建的App组件 -
渲染根
NavigatorIOS组件,将其App组件作为其初始路由传递
当你完成这部分后,可以查看我的解决方案:
// Friends/index.ios.js
import React, { Component } from 'react';
import {
AppRegistry,
NavigatorIOS,
StyleSheet,
} from 'react-native';
import App from './app/App';
export default class Friends extends Component {
render() {
return (
<NavigatorIOS
initialRoute={{
component: App,
title: 'Friends'
}}
style={ styles.container }
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('Friends', () => Friends);
现在是时候为用户创建一个登录提示,这样他们只有在未登录时才能看到LoginButton组件。
创建登录提示
我们首先应该考虑我们的应用将如何表现。当它启动时,我们应该使用 FBSDK 的AccessToken API 检查是否有可用的访问令牌。如果没有,那么我们的用户未登录,我们应该显示登录按钮,就像我们在之前的Expense项目中需要预算一样。
如果/当用户登录时,我们应该获取他们的数据源,将其加载到组件状态中,然后将其记录到控制台以显示我们已获取它。
我们首先应该做的是修改App组件,使其:
-
在
componentWillMount事件中,我们使用AccessTokenAPI 的getCurrentAccessToken方法检查用户是否已登录。-
如果用户未登录,我们应该提醒用户他们未登录。在下一节中,我们将用我们创建的登录界面替换这部分内容。
-
如果用户已登录,我们应该调用
graphMethods的getFeed方法。
-
-
此外,它应该不再渲染
LoginButton组件——这部分内容将在稍后放入不同的组件中。相反,让我们让App组件暂时渲染一个字符串,显示“已登录”。
需要花费时间进行这些更改,然后检查下面的代码以查看我的工作示例:
// Friends/app/App.js
...
import {
Text,
...
} from 'react-native';
import {
...
} from 'react-native-fbsdk';
我已经移除了LoginButton对App的导入,因为它将被拆分为不同的组件。
componentWillMount逻辑调用_checkLoginStatus方法:
...
export default class App extends Component {
componentWillMount () {
this._checkLoginStatus();
}
App组件的render方法中的LoginButton组件已被替换为Text块。_responseInfoCallback函数没有更改也没有被删除:
render() {
return (
<View style={ styles.container }>
<Text>Logged In</Text>
</View>
);
}
async _checkLoginStatus函数与之前渲染的LoginButton组件的onLoginFinished回调类似:
async _checkLoginStatus ( ){
const result = await AccessToken.getCurrentAccessToken();
if (result === null) {
alert('You are not logged in!');
return;
}
getFeed((error, result) => this._responseInfoCallback(error,
result));
}
...
}
如果用户在刷新应用时未登录,他们将看到以下消息:
在你的进步上做得很好!对于下一步,在app文件夹中创建一个名为components的文件夹,在该文件夹中创建一个包含index和styles文件的LoginPage文件夹:
现在,在我们创建LoginPage的同时,让我们再次修改App组件。App组件应该执行以下操作:
-
导入
LoginPage组件 -
当用户未登录时,使用导航器的
push方法推送LoginPage组件;用此逻辑替换代码中提醒用户未登录的部分 -
将
_checkLoginStatus回调传递给LoginPage组件,以便当用户登录时,我们可以使用App组件检查登录状态,并在/me/feed中记录他们的帖子列表。
LoginPage组件应该执行以下操作:
-
包含一个视图,该视图围绕我们在本章中之前渲染的
LoginButton组件。 -
有一个
onLoginFinished回调,它执行以下操作:-
如果登录操作被取消,将错误记录到控制台。
-
如果登录操作成功,调用传递给它的
getFeed回调以及导航器的pop方法。
-
当你完成时,你的结果应该看起来像这样:
// Friends/app/App.js
...
import LoginPage from './components/LoginPage';
export default class App extends Component {
...
async _checkLoginStatus ( ){
...
if (result === null) {
this.props.navigator.push({
component: LoginPage,
title: 'Log In to Facebook',
navigationBarHidden: true,
passProps: {
getFeed: () => getFeed()
}
});
return;
}
...
}
...
}
而不是提醒用户他们未登录,我现在如果用户未登录,将通过应用导航器推送LoginPage组件。这是我编写的LoginPage组件的方式:
// Friends/app/components/LoginPage/index.js
import React, { Component } from 'react';
import {
View
} from 'react-native';
import {
LoginButton
} from 'react-native-fbsdk';
import styles from './styles';
export default class LoginPage extends Component {
render() {
return (
<View style={ styles.container }>
<LoginButton
readPermissions={["public_profile", "user_photos",
"user_posts", "user_events", "user_likes"]}
onLoginFinished={
(error, result) => {
if (error) {
console.log('Error logging in: ', error.toString());
return;
}
前面的部分如果在登录过程中发生错误,将记录错误。
在以下代码中,我们记录了用户取消登录过程的事实:
if (result.isCancelled) {
console.log('login was cancelled');
return;
}
然而,如果登录成功,我们调用getFeed和navigator.pop方法。
this.props.getFeed();
this.props.navigator.pop();
}
}
onLogoutFinished={() => alert("logout.")}
/>
</View>
);
}
}
LoginPage的样式表与Expenses/app/styles.js中找到的完全相同,因此为了简洁起见,我将其省略。
很大的进步!在下一节中,我们将创建一些存储方法来处理 Facebook 的 Graph API 的速率限制。
优化 API
Facebook 的 Graph API 当前的限制是每小时每个用户 200 次调用。这意味着如果你的应用有 100 个用户,你每小时可以调用 20,000 次。这个限制是总体的,这意味着任何单个用户可以在那个小时内消耗掉所有的 20,000 次调用。
为了减少我们对 API 发出的网络调用次数,我们应该调整我们的App组件,将 feed 数据保存在AsyncStorage中,并且只有在用户手动提示时才刷新其数据。
我们可以开始创建与Expenses中相似的AsyncStorage方法:
// Friends/app/utils/storageMethods.js
import { AsyncStorage } from 'react-native';
export const getAsyncStorage = async (key) => {
let response = await AsyncStorage.getItem(key);
let parsedData = JSON.parse(response) || {};
return parsedData;
}
export const setAsyncStorage = async (key, value, callback) => {
await AsyncStorage.setItem(key, JSON.stringify(value));
if (callback) {
return callback();
}
return true;
}
对于这个应用,我们将在AsyncStorage中存储不同的键值对;因此,我们希望明确传递getAsyncStorage和setAsyncStorage方法一个键。
resetAsyncStorage和logAsyncStorage方法与我们之前在Expenses中使用的方法保持相同:
export const resetAsyncStorage = (key) => {
return setAsyncStorage(key, {});
}
export const logAsyncStorage = async (key) => {
let response = await getAsyncStorage(key);
console.log('Logging Async Storage');
console.table(response);
}
接下来,修改App.js中的_checkLoginStatus方法,使其执行以下操作:
-
如果用户已登录,调用
storageMethods中的getAsyncStorage方法来检查feed属性中是否存在数据。-
如果存在
feed属性,我们应该将其结果保存到App组件的状态中,名称相同。在这种情况下,我们不会调用getFeed。 -
如果键不存在,我们应该调用
getFeed。
-
现在,让我们修改App.js中的_requestInfoCallback方法,以便如果它不包含错误,它将执行以下操作:
-
使用
storageMethods中的setAsyncStorage方法保存response.data数组,使用feed作为传入的键。 -
将相同的数组保存到
App组件的本地状态中。
我的版本看起来是这样的:
// Friends/app/App.js
...
import { getAsyncStorage, setAsyncStorage } from './utils
/storageMethods';
...
export default class App extends Component {
...
async _checkLoginStatus () {
...
const feed = await getAsyncStorage('feed');
if (feed && feed.length > 0) {
this.setState({
feed
});
return;
}
如果存在feed数组,将其设置为本地状态。
否则,调用getFeed:
getFeed((error, result) => this._responseInfoCallback
(error, result));
}
_responseInfoCallback (error, result) {
...
setAsyncStorage('feed', result.data);
this.setState({
feed: result.data
});
}
}
这个更改首先检查我们在应用中保存的任何 feed 数据,然后再求助于为该数据发出外部 API 调用。在下一章中,我们将探讨一个允许我们按需刷新此数据的组件。
我们下一步应该采取的措施是让用户知道数据正在加载,这样他们就不会长时间看到一个静态屏幕。我们将使用 ActivityIndicator 组件来实现这一点。
使用 ActivityIndicator
ActivityIndicator 组件显示一个圆形加载指示器,可以让用户可视化一个 加载 动作。这对于整体用户体验很有帮助,因为用户不应该感觉他们的操作没有达到他们的目的。
我们将在本应用中使用以下两个 ActivityIndicator 属性:
-
animating:这是一个布尔值,用于显示或隐藏组件。它默认为true。 -
size:这是组件的物理大小。在 iOS 上,你的选项是两个字符串之一:small和large。在 Android 上,除了这两个字符串外,你还可以传递一个数字。此属性默认为small。
我们应该修改我们的应用程序,以便在从 Graph API 加载数据时显示这个 ActivityIndicator。
让我们修改 App 组件,以便在数据尚未保存到 App 组件状态的 feed 属性时,条件性地渲染 ActivityIndicator 组件。
我想出的解决方案如下:
// Friends/app/App.js
...
import {
ActivityIndicator,
...
} from 'react-native';
...
export default class App extends Component {
constructor (props) {
super (props);
this.state = {
feed: undefined,
spinning: true
}
}
在初始化时设置 App 组件状态中的 feed 和 spinning 值。
调用新的 _renderView 方法来条件性地确定要渲染的内容:
...
render() {
return (
<View style={ styles.container }>
{ this._renderView() }
</View>
);
}
修改 _checkLoginStatus 以在加载数据时将 spinning 属性设置为 false:
async _checkLoginStatus () {
...
if (feed && feed.length > 0) {
this.setState({
feed,
spinning: false
});
return;
}
...
}
检查 ActivityIndicator 是否仍然需要旋转。如果是,则返回 ActivityIndicator 组件。如果不是,则返回原始的 Text 组件:
_renderView () {
if (this.state.spinning) {
return (
<ActivityIndicator
animating={ this.state.spinning }
size={ 'large' }
/>
);
}
return (
<Text>Logged In</Text>
)
}
与 _checkLoginStatus 类似,修改 _responseInfoCallback 以将 spinning 设置为 false:
_responseInfoCallback (error, result) {
...
setAsyncStorage('feed', result.data);
this.setState({
feed: result.data,
spinning: false
});
}
}
现在,我们应该将我们从 Graph API 收到的数据显示在 ListView 中。
创建一个标准的 ListView
下一步是获取从 Graph API 收到的数据并将其渲染到视图中。
目前,App 组件状态中的 feed 数组包含 25 个对象。每个对象包含以下键值对:
-
created_time:这是帖子创建的日期和时间 -
id:这是一个标识符,它将使我们能够获取帖子的详细信息 -
story:这是一个可选的帖子描述,它添加了上下文,例如帖子是否包含基于位置的签到,是否是共享记忆或链接等 -
message:这是用户为这个帖子亲自写的可选消息
每个帖子都包含几个边,就像图数据结构中的节点一样。对于 Friends,我们将访问以下边:
-
/likes:这是喜欢这个特定帖子的用户列表 -
/comments:这些是对该帖子的评论 -
/attachments:这些是与该帖子关联的媒体附件
在我们可以访问边之前,我们应该渲染一个 ListView 组件,以连贯的方式显示这 25 个帖子。花些时间创建一个 ListView,使其执行以下操作:
-
以单独的行渲染 25 篇帖子
-
有条件逻辑,仅在故事和消息存在时显示
如果你已经完成了这本书中的前两个项目,ListView 对你来说不是什么新鲜事。
在你的 components 文件夹内创建一个名为 FeedList 的新组件。在这个文件中,创建一个 ListView 组件,它从传入的 prop 中获取数组并渲染一个标准的 ListView。
然后,创建一个新的辅助文件,称为 dateMethods。它应该包含一个接受日期字符串并返回格式化日期的函数。我喜欢用 MomentJS 做这类事情,但你可以随意这样做。
此外,创建另一个名为 FeedListRow 的组件,它将负责渲染 FeedList 的每一行。
之后,在 App.js 中,导入你创建的 FeedList 组件,并在 _renderData 中当前放置 Text 组件的位置渲染它。确保传递 feed 数组,以便它有数据可以渲染。用 FeedList 替换旧的 Text 组件:
// Friends/app/App.js
...
import FeedList from './components/FeedList';
...
Text 不再导入:
export default class App extends Component {
...
_renderView () {
if (this.state.spinning) {
...
}
return (
<FeedList
feed={ this.state.feed }
navigator={ this.props.navigator }
/>
);
}
...
}
接下来,FeedList 组件从 App 组件的状态中接收 feed 数组,并渲染一个标准的 ListView,明确传递每篇帖子的详细信息:
// Friends/app/components/FeedList/index.js
import React, { Component } from 'react';
import {
ListView,
View
} from 'react-native';
import FeedListRow from '../FeedListRow';
import styles from './styles';
export default class FeedList extends Component {
实例化一个新的 ListView.DataSource 对象:
constructor (props) {
super (props);
this.state = {
ds: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
}
}
使用从 App 传入的 feed 数组来渲染 ListView,如下所示:
render () {
const dataSource = this.state.ds.cloneWithRows
(this.props.feed || []);
使用 FeedListRow 为每个单独的行渲染一个 ListView 组件,如下所示:
return (
<View style={ styles.container }>
<ListView
automaticallyAdjustContentInsets={ false }
dataSource={ dataSource }
renderRow={ (rowData, sectionID, rowID) =>
<FeedListRow
createdTime={ rowData.created_time }
message={ rowData.message }
navigator={ this.props.navigator }
postID={ rowData.id }
story={ rowData.story }
/>
}
renderSeparator={ (sectionID, rowID) =>
<View
key={ rowID }
style={ styles.separator }
/>
}
/>
</View>
)
}
}
separator 获得了自己的样式,用于分隔每一篇帖子,如下所示:
// Friends/app/components/FeedList/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 65
},
separator: {
flex: 1,
height: StyleSheet.hairlineWidth,
marginLeft: 15,
marginRight: 15,
backgroundColor: '#1d2129'
}
});
export default styles;
使用从 Facebook API 获取的日期字符串,然后用 moment 格式化它:
// Friends/app/utils/dateMethods.js
import moment from 'moment';
export const getDateTimeString = (date) => {
return moment(date).format('lll');
}
在 FeedListRow 中,从刚刚创建的 dateMethods 文件中导入 getDateTimeString 方法:
// Friends/app/components/FeedListRow/index.js
import React, { Component } from 'react';
import {
Text,
TouchableHighlight,
View
} from 'react-native';
import { getDateTimeString } from '../../utils/dateMethods';
为了未来的导航目的,将 TouchableHighlight 组件包裹起来,如下所示:
import styles from './styles';
export default class FeedListRow extends Component {
render () {
return (
<View style={ styles.container }>
<TouchableHighlight
onPress={ () => this._navigateToPostView() }
underlayColor={ '#D3D3D3' }
>
<View>
<Text style={ styles.created }>
{ this._renderCreatedString() }
</Text>
{ this._renderStoryString() }
<Text style={ styles.message }>
{ this._renderMessageString() }
</Text>
</View>
</TouchableHighlight>
</View>
)
}
现在是一个占位函数,我们稍后会修改它。
_navigateToPostView () {
// TODO: Push to navigator
console.log('pushed');
}
渲染帖子数据某些部分的方法。
_renderCreatedString () {
return 'Posted ' + getDateTimeString(this.props.createdTime);
}
_renderMessageString () {
return this.props.message
}
_renderStoryString () {
if (this.props.story) {
return (
<Text style={ styles.story }>
{ this.props.story }
</Text>
)
}
}
}
这是为 FeedListRow 构建的样式:
// Friends/app/components/FeedListRow/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
margin: 10
},
created: {
color: '#365899',
fontWeight: 'bold',
marginBottom: 5
},
story: {
marginBottom: 5,
textDecorationLine: 'underline'
}
});
export default styles;
你会注意到这个组件的 _navigateToPostView 方法有一个注释的任务要处理。这是本练习下一步的基础,我们将在下一章中直接跳到那里。
摘要
这是一个很长的章节,感谢你一直陪伴着我!在本章中,我们获得了访问 Facebook Graph API 的权限,为 iOS 和 Android 安装了 Facebook SDK,并开始使用 Facebook SDK 来让用户登录应用,并使用他们的访问令牌获取他们的帖子数据并将其渲染到屏幕上。
在此过程中,你还使用了一个 ActivityIndicator 组件来向用户直观地传达我们正在加载数据。
在下一章中,我们将大幅增加内容。那里见。
第六章:高级 Facebook 应用功能
现在我们已经获得了访问 Facebook 的 Graph API 的权限,是时候完成我们应用的构建了。
在本章中,我们将:
-
通过从 Graph APIs 获取更多数据来继续构建我们的 Facebook 连接应用
Friends,例如从我们动态中的每个现有帖子获取媒体附件、评论和点赞数量 -
为我们的应用添加一个下拉刷新机制,允许用户重新加载数据
-
了解
Image组件,它将允许我们在应用中渲染图片 -
发现 WebView,在本地可用的
View组件中打开链接 -
为应用添加一个注销屏幕
-
对应用进行修改以构建 Android 版本
让我们继续上一章的遗留内容,并扩展我们的FeedListRow组件。
创建 PostView
在第五章的结尾,“第三项目 - Facebook 客户端”,我们创建了一个带有TouchableHighlight的FeedListRow组件,当按下时会触发以下函数:
// Friends/app/components/FeedListRow/index.js
...
_navigateToPostView () {
console.log('pushed');
}
...
我们将构建一个PostView组件,当在FeedListRow中按下TouchableHighlight组件时,用户将导航到该组件,并在_navigfateToPostView函数中替换当前的登录以处理该导航。
这个PostView组件在加载时应该在AsyncStorage中查找该帖子的详细信息,并在存在的情况下加载它们。如果不存在,则应向 Facebook Graph API 请求帖子的详细信息并将它们保存到AsyncStorage中供将来使用。
我们感兴趣的是帖子的附件、评论和点赞。由于 Facebook 上的每个帖子都分配了一个唯一的帖子 ID,我们还可以在AsyncStorage中将包含附件、评论和点赞数据的对象保存到该帖子 ID 下作为其键。
首先,我们将在storageMethods.js中创建一个新的函数,该函数执行以下功能:
-
接受帖子 ID 和批处理回调作为参数
-
创建三个单独的
GraphRequest实例,每个实例对应我们将获取的三个边缘(附件、评论和点赞),并将返回的数据保存到对象中 -
启动一个
GraphRequestManager,链接三个GraphRequest实例,并传入批处理回调,从而将返回的数据对象传递给批处理回调函数
然后,创建一个PostView组件,执行以下操作:
-
它渲染与
FeedListRow创建的相同的故事和消息字符串,以便用户保留他们点击的内容的上下文。 -
它使用一种存储方法来检查与该特定帖子 ID 相关的数据是否存在。如果存在,则
PostView将使用它。如果不存在,则应使用我们新的存储方法来获取该帖子 ID 的附件、评论和点赞。 -
传入我们新存储方法的批处理回调应包括将结果保存到
AsyncStorage中,其键与帖子 ID 相同。 -
它将帖子的评论和点赞数以视觉形式显示在一行中。
最后,修改 FeedListRow 组件,使其使用其现有的 _navigateToPostView 方法导航到 PostView,并传递任何必要的属性。
创建一个 resultsObject 来存储每个独特的 GraphRequest 的结果:
// Friends/app/utils/graphMethods.js
...
export const getPostDetails = (id, batchCallback) => {
let resultsObject = {
attachments: undefined,
comments: undefined,
likes: undefined
}
在前面的代码中的三个 GraphRequest 实例中,使用给定的帖子 ID 调用其相应的 attachments、comments 和 likes 边从 API。然后,将这些结果保存到 resultsObject 中,其键对应于相应的键:
const attachmentsRequest = new GraphRequest('/' + id +
'/attachments', null, (error, response) => {
if (error) {
console.log(error);
}
resultsObject.attachments = response.data;
});
const commentsRequest = new GraphRequest('/' + id + '/comments',
null, (error, response) => {
if (error) {
console.log(error);
}
resultsObject.comments = response.data;
});
const likesRequest = new GraphRequest('/' + id + '/likes', null,
(error, response) => {
if (error) {
console.log(error);
}
resultsObject.likes = response.data;
});
最后,创建一个新的 GraphRequestManager 实例,并将所有三个请求以及作为参数传递给此函数的 batchCallback 添加到其中。将 resultsObject 传递给 batchCallback 以使该回调能够访问从 attachments、comments 和 likes 边获得的数据:
new GraphRequestManager()
.addRequest(attachmentsRequest)
.addRequest(commentsRequest)
.addRequest(likesRequest)
.addBatchCallback(() => batchCallback(resultsObject))
.start();
}
然后,导入将在该组件中使用到的各种不同的辅助方法,如下所示:
// Friends/app/components/PostView/index.js
import React, { Component } from 'react';
import {
ActivityIndicator,
Text,
TouchableHighlight,
View
} from 'react-native';
import { getAsyncStorage, setAsyncStorage } from '../../utils/storageMethods';
import { getDateTimeString } from '../../utils/dateMethods';
import { getPostDetails } from '../../utils/graphMethods';
import styles from './styles';
将状态中的 loading 布尔值设置为 true 以用于 ActivityIndicator:
export default class PostView extends Component {
constructor (props) {
super (props);
this.state = {
loading: true
}
}
在 componentWillMount 期间,获取存储在此帖子 ID 键下的对象。检查数据是否存在:如果不存在数据,getAsyncStorage 被配置为返回一个空对象。如果这是 true,则调用 _getPostDetails;否则,将详细信息保存到本地状态:
async componentWillMount () {
const result = await getAsyncStorage(this.props.postID);
if (Object.keys(result).length === 0) {
this._getPostDetails();
return;
}
this._savePostDetailsToState(result);
}
就像 FeedListRow 一样,如果适用,渲染创建日期、故事和信息。有条件地调用 _renderActivityIndicator 或 _renderDetails,取决于 loading 布尔值。最后,渲染一个分隔符,以期待向此组件添加评论:
render () {
return (
<View style={ styles.container }>
<View>
<Text style={ styles.created }>
{ this._renderCreatedString() }
</Text>
{ this._renderStoryString() }
<Text>
{ this._renderMessageString() }
</Text>
</View>
<View>
{ this.state.loading ? this._renderActivityIndicator() :
this._renderDetails() }
</View>
<View style={ styles.separator } />
</View>
)
}
调用我们在 graphMethods 中刚刚创建的 getPostDetails 方法,并传递一个回调,该回调使用 getPostDetails 的结果对象将内容保存到状态中;然后将其保存到 AsyncStorage 中,键等于此帖子的 ID:
async _getPostDetails () {
await getPostDetails(this.props.postID, (result) => {
this._savePostDetailsToState(result);
setAsyncStorage(this.props.postID, result);
});
}
渲染一个 ActivityIndicator 组件:
_renderActivityIndicator () {
return (
<ActivityIndicator
animating={ this.state.spinning }
size={ 'large' }
/>
)
}
按如下方式渲染此帖子拥有的 Likes 和 Comments 数量:
_renderCreatedString () {
return 'Posted ' + getDateTimeString(this.props.createdTime);
}
_renderDetails () {
return (
<View style={ styles.detailsContainer }>
<Text style={ styles.detailsRow }>
{ this.state.likes.length } Likes, {
this.state.comments.length } Comments
</Text>
</View>
)
}
_renderCreatedString、_renderMessageString 和 _renderStoryString 方法与 FeedListRow 中的方法保持不变:
_renderMessageString () {
return this.props.message
}
_renderStoryString () {
if (this.props.story) {
return (
<Text style={ styles.story }>
{ this.props.story }
</Text>
)
}
}
将此帖子的 attachments、comments 和 likes 边的数据保存到状态中,并关闭旋转的 ActivityIndicator:
_savePostDetailsToState (data) {
this.setState({
attachments: data.attachments,
comments: data.comments,
likes: data.likes,
loading: false
});
}
}
这是 PostView 的样式:
// Friends/app/components/PostView/index.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
margin: 10,
marginTop: 75,
},
created: {
color: '#365899',
fontWeight: 'bold',
marginBottom: 5
},
detailsContainer: {
flexDirection: 'row',
justifyContent: 'space-between'
},
detailsRow: {
color: '#365899',
marginBottom: 15,
marginTop: 15,
textAlign: 'left'
},
separator: {
height: 2,
marginLeft: 15,
marginRight: 15,
backgroundColor: '#365899'
},
story: {
marginBottom: 5,
textDecorationLine: 'underline'
}
});
export default styles;
最后,修改 FeedListRow 中的 _navigateToPostView 函数:
// Friends/app/components/FeedListRow/index.js
...
export default class FeedListRow extends Component {
...
_navigateToPostView () {
this.props.navigator.push({
component: PostView,
passProps: {
createdTime: this.props.createdTime,
message: this.props.message,
postID: this.props.postID,
story: this.props.story
}
});
}
...
}
接下来,我们将添加一个 ListView 来填充该帖子的评论,并在 PostView 中的分隔线下方渲染它。
向 PostView 添加评论
在这一步中,我们将编辑 PostView 以包含一个 ListView 来渲染所有评论。由于 PostView 会在 componentWillMount 生命周期方法加载信息后将其评论数据保存到其状态中,我们可以使用这些数据来渲染评论。
首先,创建一个组件来容纳这个 ListView;让我们称它为 CommentList。它应该执行以下操作:
-
包含一个由
PostView通过属性传递给它的评论列表 -
使用这些评论渲染一个
ListView: -
行应该由子组件
CommentListRow渲染
您的CommentListRow组件应该执行以下操作:
-
每行应包含其发布者的名字和他们写的消息
-
使用
ListView组件分隔每个评论
最后,更新PostView,使其在PostView的render方法中直接在分隔符下方渲染CommentList。实例化一个新的ListView.DataSource实例:
// Friends/app/components/CommentList/index.js
import React, { Component } from 'react';
import {
ListView,
Text,
View
} from 'react-native';
import CommentListRow from '../CommentListRow';
import styles from './styles';
export default class CommentList extends Component {
constructor (props) {
super (props);
this.state = {
ds: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
}
}
设置dataSource常量,传入comments属性或一个空数组:
render () {
const dataSource = this.state.ds.cloneWithRows(this.props.comments || []);
每一行应该是一个新的CommentListRow组件:
return (
<View style={ styles.container }>
<ListView
automaticallyAdjustContentInsets={ false }
dataSource={ dataSource }
renderRow={ (rowData, sectionID, rowID) =>
<CommentListRow
message={ rowData.message }
name={ rowData.from.name } />
}
为每个评论渲染一个分隔符:
renderSeparator={ (sectionID, rowID) =>
<View
key={ rowID }
style={ styles.separator } />
} />
</View>
)
}
}
这是CommmentList样式块的样式:
// Friends/app/components/CommentList/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1
},
separator: {
flex: 1,
height: StyleSheet.hairlineWidth,
marginLeft: 15,
marginRight: 15,
backgroundColor: '#1d2129'
}
});
export default styles;
接下来,让我们看看CommentListRow:
// Friends/app/components/CommentListRow/index.js
import React, { Component } from 'react';
import {
Text,
View
} from 'react-native';
import styles from './styles';
export default (props) => {
return (
<View style={ styles.container }>
<View style={ styles.header }>
<Text style={ styles.name }>
{ props.name }
</Text>
</View>
<View style={ styles.body }>
<Text style={ styles.comment }>
{ props.message }
</Text>
</View>
</View>
)
}
一个简单的无状态函数组件返回带有发布者名字和他们的评论的评论行。以下代码块包含CommentListRow的样式:
// Friends/app/components/CommentListRow/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
body: {
marginBottom: 20,
marginLeft: 30,
marginRight: 30,
marginTop: 10,
},
comment: {
color: '#1d2129'
},
container: {
flex: 1
},
header: {
marginTop: 5,
marginLeft: 10,
marginRight: 10
},
name: {
color: '#1d2129',
fontWeight: 'bold'
}
});
export default styles;
最后,让我们看看对PostView所做的更改:
// Friends/app/components/PostView/index.js
...
import CommentList from '../CommentList';
export default class PostView extends Component {
...
render () {
return (
<View style={ styles.container }>
...
<View style={ styles.separator } />
<View style={ styles.commentListContainer }>
<CommentList comments={ this.state.comments } />
</View>
</View>
)
}
...
}
上述代码导入并渲染CommentList在分隔符下方。
commentListContainer样式看起来是这样的:
// Friends/app/components/PostView/styles.js
commentListContainer: {
flex: 1,
marginTop: 20
}
在这一点上,我们应该继续完善PostView,添加我们在本章开头描述的其他功能。在下一节中,我们将探讨如何在用户帖子或单个帖子中添加更多数据时刷新我们已有的现有数据。
使用 RefreshControl 重新加载数据
下拉刷新交互最初是在 2008 年创建的流行 Twitter iOS 应用Tweetie中构思的。这种交互涉及用户将屏幕向下拉,直到达到某个阈值,然后释放以表示他们想要刷新屏幕内容。
使用 React Native SDK,我们可以使用RefreshControl来获得相同的下拉刷新交互,并允许我们的用户在应用中随意重新加载数据。
本章我们将使用以下RefreshControl属性:
-
onRefresh:这是一个在执行刷新操作时被调用的函数 -
refreshing:这是一个布尔值,表示视图是否应该被动画化 -
tintColor:这是刷新指示器的颜色 -
title:这是一个在刷新指示器下方显示的字符串 -
titleColor:这是标题的颜色
要使用RefreshControl,将其渲染到具有refreshControl属性的ListView或ScrollView组件中。
对于我们的实现,我们首先想要修改App.js,使其执行以下操作:
-
在其状态中包含一个
refreshControlSpinning布尔值 -
修改当前
_checkLoginStatus函数,将获取存储中数据逻辑移动到其自己的函数_getFeedData中;新的_getFeedData函数在完成后也应该将refreshControlSpinning布尔值切换到false -
包含一个函数
_refreshFeedList,用于刷新帖子,将refreshControlSpinning设置为true,然后调用新的_getFeedData函数 -
将
refreshControlSpinning布尔值和_refreshFeedList函数传递给它渲染的FeedList组件
然后,修改 FeedList 以执行以下操作:
-
将
RefreshControl组件渲染到ListView的refreshControl属性中 -
将其旋转属性指向
App.js中的refreshControlSpinning布尔值 -
将
onRefresh属性指向App.js中的_refreshFeedList函数。
这里是我的对 App 组件的修改:
// Friends/app/App.js
...
export default class App extends Component {
constructor (props) {
...
this.state = {
...
refreshControlSpinning: false
}
}
我们在状态中添加了一个新的 refreshControlSpinning 布尔值。旧 spinner 布尔值被重命名为 activityIndicatorSpinning。在 _checkLoginStatus 的最后一行被拆分成自己的方法,以便稍后在以下片段中重用。同时,更新传递给 LoginPage 的 getFeed 属性,以反映新的拆分方法:
async _checkLoginStatus () {
...
if (result === null) {
this.props.navigator.push({
...
passProps: {
getFeed: () => _getFeed()
}
});
...
}
this._getFeed();
}
_getFeed () {
getFeed((error, result) => this._responseInfoCallback
(error, result));
}
让我们将 refreshControlSpinning 和 _refreshFeedList 传递给 FeedList:
_renderView () {
...
return (
<FeedList
...
refreshControlSpinning={ this.state.refreshControlSpinning }
refreshFeedList={ () => this._refreshFeedList() }
/>
);
}
将 refreshControlSpinning 布尔值设置为 true 并调用 _getFeed:
_refreshFeedList () {
this.setState({
refreshControlSpinning: true
});
this._getFeed();
}
一旦数据已加载到状态和 AsyncStorage 中,将 refreshControlSpinning 设置为 false:
_responseInfoCallback (error, result) {
...
this.setState({
refreshControlSpinning: false
...
});
}
}
向 ListView 添加一个 refreshControl 属性,它指向 _renderRefreshControl:
// Friends/app/components/FeedList/index.js
import {
...
RefreshControl,
} from 'react-native';
...
export default class FeedList extends Component {
...
render () {
...
return (
<View style={ styles.container }>
<ListView
refreshControl={ this._renderRefreshControl() }
...
/>
</View>
)
}
返回 RefreshControl 组件。它的 onRefresh 属性指向 App.js 中的 _refreshFeedList 方法,并且它刷新布尔值也指向 App.js 中的 refreshControlSpinning 属性:
_renderRefreshControl () {
return (
<RefreshControl
onRefresh={ () => this.props.refreshFeedList() }
refreshing={ this.props.refreshControlSpinning }
tintColor={ '#365899' }
title={ 'Refresh Feed' }
titleColor={ '#365899' }
/>
)
}
}
下一步是将任何图像附件渲染到 PostView 中。
渲染图像
要使用 React Native 显示图像,我们使用 Image 组件。它允许我们从本地和远程源显示图像。你还可以像为任何其他 React 组件添加样式一样为图像添加样式。
在本章中,我们将使用以下属性来为我们的 Image 组件设置样式:
-
resizeMode: 我们将使用以下字符串之一:-
cover: 这会均匀地缩放图像并保持其宽高比,使得图像的宽度和高度将等于或大于封装Image组件的视图。 -
contain: 这个字符串也会均匀地缩放图像并保持其宽高比,使得图像的宽度和高度将等于或小于封装Image组件的视图。 -
stretch: 这会独立地缩放宽度和高度,并可以改变源图像的宽高比。 -
repeat: 这会将图像重复以覆盖封装视图的整个框架。此选项在 iOS 上也保持原始大小和宽高比,但在 Android 上则不保持。 -
center: 这会将图像居中。
-
-
source: 这将是渲染的图像的远程 URL 或本地路径。 -
style: 这是一个样式对象。
在基本层面上,你可以这样加载静态图像资源:
<Image source={ require('../images/my-icon.png') } />
此外,你也可以对远程的做同样的处理:
<Image
source={{ uri: 'https://www.link-to-my-image.com/image.png' }}
style={{
width: 400,
height: 400
}} />
用户动态中每一条带有图像的帖子都可以使用 Image 组件来渲染该图像。
从 Facebook Graph API 结构图像的方式如下:
attachments: [{
media: {
image: {
height: 400,
src: 'https://www.link-to-my-image.com/image.png',
width: 400
}
}
}]
在此基础上,让我们首先创建一个名为imageMethods的新工具文件。此文件应执行以下操作:
-
从 React Native 导入
DimensionsAPI。 -
导出
getHeightRatio函数,该函数接受图像的高度和宽度,并返回图像应有的高度。我们可以通过执行以下操作来计算它:-
获取用户设备的宽度尺寸,并从中减去一定的量以适应左右边距。
-
使用这个边距偏移量,并将其除以图像的原始宽度以获得所需的比率。
-
返回将高度乘以比例得到正确图像高度的乘积结果。
- 导出另一个函数
getWidthOffset,它接受用户的设备宽度并返回它,减去一定的量以适应左右边距。为了代码重用,我们应该将其用作getHeightRatio的第一个要点的一部分。
- 导出另一个函数
-
修改PostView以执行以下操作:
-
考虑到较长的图片,顶级
View应替换为ScrollView组件。 -
如果帖子已加载完成并且
attachments数组中包含任何图像,则渲染帖子attachments数组中的第一张图像。 -
Image组件应将其resizeMode属性设置为contain,以便图像不会超出屏幕。它应该有一些左和右边距,以便它不会接触到屏幕边缘,其宽度和高度应由imageMethods文件计算。 -
这种渲染应放置在帖子的详细信息(时间、消息和故事)下方,但在点赞和评论数量上方。
获取gridWidthOffset,将其除以图像的width,然后将图像的height除以这个结果,如下所示:
// Friends/app/utils/imageMethods.js
import { Dimensions } from 'react-native';
export const getHeightRatio = (height, width) => {
return height * (getWidthOffset()/width);
}
获取用户的width,然后从中减去20像素:
export const getWidthOffset = () => {
return Dimensions.get('window').width - 20;
}
将Image、ScrollView和imageMethods导入到PostView组件中:
// Friends/app/components/PostView/index.js
import {
...
Image,
ScrollView,
} from 'react-native';
import { getHeightRatio, getWidthOffset } from '../../utils/imageMethods';
预计到较长的帖子,将顶级视图替换为ScrollView。添加条件逻辑以触发_renderAttachments,将其放在调用_renderDetails之前:
...
export default class PostView extends Component {
...
render () {
return (
<ScrollView style={ styles.container }>
...
<View>
{ !this.state.loading && this._renderAttachments() }
</View>
...
</ScrollView>
)
}
为涉及某些照片/相册的非常特定边缘情况分配subattachments:
...
_renderAttachments () {
let attachment = this.state.attachments[0]
let media;
if (attachment && attachment.hasOwnProperty('subattachments')) {
attachment = attachment.subattachments.data[0];
}
检查media属性的存在,如下所示:
if (attachment && attachment.hasOwnProperty('media')) {
media = attachment.media;
}
如果media属性存在并且包含image属性,则渲染Image:
if (media && media.image) {
返回具有确定属性的Image组件:
const imageObject = media.image;
return (
<Image
resizeMode={ 'contain' }
source={{ uri: imageObject.src }}
style={{
marginRight: 10,
marginTop: 30,
width: getWidthOffset(),
height: getHeightRatio(imageObject.height,
imageObject.width)
}}
/>
)
}
}
...
}
PostView的container样式已更改,省略了marginTop属性:
// Friends/app/components/PostView/styles.js
commentListContainer: {
flex: 1,
marginTop: 20
}
commentListContainer样式与新的ScrollView组件相匹配。
现在图像已经渲染,我们应该处理其他类型的附件--链接。
使用 WebView 渲染链接
当用户选择一个链接时,在您的应用程序中渲染该链接是有益的,这样用户就不会被抛出应用程序并进入他们的浏览器。为了使用 React Native 完成此任务,我们将使用WebView组件。
WebView组件在原生、应用程序包含的视图中渲染 Web 内容。对于这个应用程序,我们将使用其众多属性中的其中一个:
source:这将在WebView中加载带有可选头部的 URI 或静态 HTML。
渲染WebView组件很简单:
import {
WebView
} from 'react-native';
class WebViewSample extends Component {
render () {
return (
<WebView
source={{uri: 'https://www.google.com'}} />
)
}
}
并非所有帖子都包含附件中的链接。当它们包含链接时,其层次结构如下:
attachments: [{
title: 'Link to Google'
url: 'https://www.google.com'
}]
让我们做一些修改以适应WebView。首先,创建一个名为WebViewComponent的新组件;它应该是一个无状态的函数,返回一个WebView,并将其source设置为它接收的属性中的任何链接。
然后,修改PostView,使其执行以下功能:
-
如果帖子中包含图片,则直接在渲染图片的地方渲染按钮。
-
该按钮仅在帖子的第一个附件与链接相关联时渲染。按钮应包含链接的标题,并且当点击时,导航到您的
WebViewComponent以打开链接。
从 iOS 9 开始,未加密的 HTTP 链接被 iOS 的 App Transport Security 自动阻止。您可以在 Xcode 项目中项目文件的Info.plist文件中逐个案例地将这些链接列入白名单。苹果公司不推荐这样做,并将在不久的将来要求所有提交的应用遵守这项新政策。
以下是一个无状态的函数组件,它只返回一个带有source URI 的WebView:
// Friends/app/components/WebViewComponent/index.js
import React, { Component } from 'react';
import {
WebView
} from 'react-native';
export default (props) => {
return (
<WebView
source={{ uri: props.url }}
/>
)
}
导入Button和WebViewComponent依赖项:
// Friends/app/components/PostView/index.js
import {
Button,
...
} from 'react-native';
import WebViewComponent from '../WebViewComponent';
如果PostView已加载完成,则条件调用_renderLink:
...
export default class PostView extends Component {
...
render () {
return (
<ScrollView style={ styles.container }>
...
<View>
{ !this.state.loading && this._renderLink() }
</View>
...
</ScrollView>
)
}
获取第一个附件对象:
...
_renderLink () {
let attachment = this.state.attachments[0];
let link;
let title;
再次检查subattachments:
if (attachment && attachment.hasOwnProperty('subattachments')) {
attachment = attachment.subattachments.data[0];
}
如果title是空字符串或未定义,则将其通用地命名为Link:
if (attachment && attachment.hasOwnProperty('url')) {
link = attachment.url;
title = attachment.title || 'Link';
渲染一个在按下时调用_renderWebView的Button:
return (
<Button
color={ '#365899' }
onPress={ () => this._renderWebView(link) }
title={ title }
/>
)
}
}
将用户导航到WebViewComponent,并发送提供的 URL。
_renderWebView (url) {
this.props.navigator.push({
component: WebViewComponent,
passProps: {
url
}
});
}
...
}
我们对这个应用程序的最后一点润色是让用户能够从应用程序中注销。
使用 TabBarIOS 注销
我们的最后一步是为用户添加一个注销页面。使用TabBarIOS组件和react-native-vector-icons,我们将创建一个标签视图,允许用户注销。
让我们为此进行一些修改。首先,我们需要修改App.js,使其执行以下功能:
-
导入
TabBarIOS和react-native-vector-icons依赖项 -
如果活动指示器没有旋转,则在
_renderView方法中返回一个TabBarIOS组件 -
在
App组件的状态中添加一个selectedTab字符串以跟踪当前选择的标签,默认为FeedList组件 -
有单独的函数来渲染
FeedList和LoginPage组件而不进行导航 -
向
LoginPage传递一个回调,该回调执行_checkLoginStatus方法 -
修改其
container样式,不再对任何项目进行居中或对齐
然后,修改LoginPage组件,使其onLogoutFinished回调执行_checkLoginStatus。将新依赖项导入到项目中:
// Friends/app/App.js
import {
TabBarIOS,
...
} from 'react-native';
...
import Icon from 'react-native-vector-icons/FontAwesome';
在状态中存储selectedTab字符串,默认为feed:
...
export default class App extends Component {
constructor (props) {
...
this.state = {
...
selectedTab: 'feed'
}
}
使用之前相同的逻辑渲染FeedList组件:
...
_renderFeedList () {
return (
<FeedList
feed={ this.state.feed }
navigator={ this.props.navigator }
refreshControlSpinning={ this.state.refreshControlSpinning }
refreshFeedList={ () => this._refreshFeedList() }
/>
)
}
渲染LoginPrompt组件,传递_checkLoginStatus方法:
_renderLoginPrompt () {
return (
<LoginPage checkLoginStatus={ () => this._checkLoginStatus() } />
)
}
当用户使用以下代码注销时,这将导致应用导航回LoginPage:
_renderView () {
...
return (
<View style={ styles.container }>
<TabBarIOS>
<Icon.TabBarItemIOS
title={ 'Feed' }
selected={ this.state.selectedTab === 'feed' }
iconName={ 'newspaper-o' }
iconSize={ 20 }
onPress={ () => this._setSelectedTab('feed') }
>
{ this._renderFeedList() }
</Icon.TabBarItemIOS>
<Icon.TabBarItemIOS
title={ 'Sign Out' }
selected={ this.state.selectedTab === 'signOut' }
iconName={ 'sign-out' }
iconSize={ 20 }
onPress={ () => this._setSelectedTab('signOut') }
>
{ this._renderLoginPrompt() }
</Icon.TabBarItemIOS>
</TabBarIOS>
</View>
)
}
_renderView中之前存在_renderFeedList内容的地方现在渲染TabBarIOS组件。
...
_setSelectedTab (selectedTab) {
this.setState({
selectedTab
});
}
}
之前的代码将状态中的selectedTab属性设置为用户点击的任何标签:
// Friends/app/styles.js
container: {
flex: 1,
backgroundColor: '#F5FCFF',
}
之前的代码从container属性中移除了所有其他样式,这样标签栏的图标就不会被强制居中显示在屏幕上:
// Friends/app/components/LoginPage/index.js
...
export default class LoginPage extends Component {
render() {
return (
<View style={ styles.container }>
<LoginButton
...
onLogoutFinished={() => this.props.checkLoginStatus() }
/>
</View>
);
}
}
在LoginButton的onLogoutFinished属性中的上一个警报调用已被替换为触发checkLoginStatus。
在这个应用中,你所有的进步都做得很好!下一步是针对 Android 开发进行修改。
移植到 Android
我们将为这个应用进行的 Android 修改与为Expenses所做的修改类似,这将在第九章,额外的 React Native 组件中稍后讨论。我们对Friends所做的修改如下:
-
将
TabBarIOS替换为DrawerLayoutAndroid和ToolbarAndroid -
创建
Drawer和DrawerRow组件以支持DrawerLayoutAndroid -
在根级别的
index.android.js文件中使用Navigator -
创建
App组件的 Android 特定版本 -
为 Android 特定的样式更新
FeedList -
修改
FeedListRow以支持 Android 导航 -
向
PostView添加BackAndroid和Navigator支持
关于DrawerLayoutAndroid和ToolbarAndroid的深入解释可以在第九章,额外的 React Native 组件中找到。
添加DrawerLayoutAndroid和ToolbarAndroid
让我们从为 Android 版本的“朋友”添加基于工具栏/抽屉的导航开始。我们需要首先创建一个名为Drawer的组件,该组件执行以下功能:
-
这接受一个作为属性的路线数组。
-
这将返回一个包含每个路线作为行的
ListView。每一行都应该包含一个TouchableHighlight组件,当点击时,将调用一个名为navigateTo的属性,我们最终将其传递到Drawer中。
我们还应该将Drawer渲染的行拆分成一个名为DrawerRow的独立组件。这个组件应该执行以下操作:
-
接受行的名称作为属性并在
Text元素中渲染该名称 -
调用
setNativeProps,以便其父TouchableHighlight组件将渲染此自定义组件
实例化一个新的ListView.DataSource:
// Friends/app/components/Drawer/index.js
import React, { Component } from 'react';
import {
ListView,
Text,
TouchableHighlight,
View
} from 'react-native';
import DrawerRow from '../DrawerRow';
import styles from './styles';
export default class Drawer extends Component {
constructor (props) {
super (props);
this.state = {
ds: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
}
}
渲染一个带有分隔符的ListView组件。将我们行的渲染委托给_renderDrawerRow方法:
render () {
const dataSource = this.state.ds.cloneWithRows
(this.props.routes || []);
return (
<View style={ styles.container }>
<ListView
automaticallyAdjustContentInsets={ false }
dataSource={ dataSource }
enableEmptySections={ true }
renderRow={ (rowData, sectionID, rowID) =>
this._renderDrawerRow(rowData, sectionID, rowID) }
renderSeparator={ (sectionID, rowID) =>
<View
key={ rowID }
style={ styles.separator } />
} />
</View>
)
}
在自定义DrawerRow组件周围包裹一个TouchableHighlight,传递给它路由的名称。在TouchableHighlight的onPress方法中调用 props 中的navigateTo方法,传递给它row的index:
_renderDrawerRow (rowData, sectionID, rowID) {
return (
<View>
<TouchableHighlight
style={ styles.row }
onPress={ () => this.props.navigateTo(rowData.index) }>
<DrawerRow
routeName={ rowData.title } />
</TouchableHighlight>
</View>
)
}
}
接下来,创建了DrawerRow组件:
// Friends/app/components/Drawer/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1
},
separator: {
height: StyleSheet.hairlineWidth,
marginLeft: 10,
marginRight: 10,
backgroundColor: '#000000'
}
})
export default styles;
以下代码调用setNativeProps,因为DrawerRow被包裹在TouchableHighlight中:
// Friends/app/components/DrawerRow/index.js
import React, { Component } from 'react';
import {
Text,
View
} from 'react-native';
import styles from './styles';
export default class DrawerRow extends Component {
setNativeProps (props) {
this._root.setNativeProps(props)
}
渲染路由的名称:
render () {
return (
<View
style={ styles.container }
ref={ component => this._root = component }
{ ...this.props }>
<Text style={ styles.rowTitle }>
{ this.props.routeName }
</Text>
</View>
)
}
}
这里是我为DrawerRow创建的样式:
// Friends/app/components/DrawerRow/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
height: 40,
padding: 10
},
rowTitle: {
fontSize: 20,
textAlign: 'left'
}
})
export default styles;
将抽屉与朋友集成
接下来,我们将修改根index.android.js文件,使其执行以下操作:
-
渲染一个包裹着
Icon.ToolbarAndroid和Navigator的DrawerLayoutAndroid组件。 -
导入并设置
DrawerLayoutAndroid的renderNavigationView为创建的Drawer组件。 -
创建一个回调以打开
DrawerLayoutAndroid。 -
编写一个名为
_navigateTo的回调,用于导航到给定的索引。将其作为属性传递给LoginPage。 -
使用
Navigator中的renderScene回调导入并渲染App、LoginPage、PostView和WebViewComponent组件:
// Friends/index.android.js
import React, { Component } from 'react';
import {
AppRegistry,
DrawerLayoutAndroid,
Navigator,
StyleSheet,
View
} from 'react-native';
import App from './app/App';
import Drawer from './app/components/Drawer';
import LoginPage from './app/components/LoginPage';
import PostView from './app/components/PostView';
import WebViewComponent from './app/components/WebViewComponent';
import Icon from 'react-native-vector-icons/MaterialIcons';
让我们导入所有必要的依赖项,包括 React Native SDK 组件/API、Navigator渲染的每个自定义组件,以及来自react-native-vector-icons的 Material 图标包。
export default class Friends extends Component {
constructor (props) {
super (props);
this.state = {
visibleRoutes: [
{ title: 'My Feed', index: 0 },
{ title: 'Log Out ', index: 1 }
]
}
}
建立要传递给Drawer组件的可见路由数组。
render() {
const routes = [
{ title: 'My Feed', index: 0 },
{ title: 'Sign In/Log Out', index: 1 },
{ title: 'Post Details', index: 2 },
{ title: 'Web View', index: 3 }
];
return (
<View style={styles.container}>
<DrawerLayoutAndroid
drawerLockMode={ 'unlocked' }
ref={ 'drawer' }
renderNavigationView={ () => this._renderDrawerLayout() }
>
渲染一个DrawerLayoutAndroid组件,其renderNavigationView属性委托给_renderDrawerLayout;给组件设置一个ref为drawer,这样我们就可以在_openDrawer中引用它。
<Icon.ToolbarAndroid
titleColor="#fafafa"
navIconName="menu"
height={ 56 }
backgroundColor="#365899"
onIconClicked={ () => this._openDrawer() }
/>
渲染Icon.ToolbarAndroid组件以包含汉堡菜单。它的onIconClicked回调执行_openDrawer。
<Navigator
initialRoute={{ index: 0 }}
ref={ 'navigator' }
renderScene={ (routes, navigator) =>
this._renderScene(routes, navigator) }
/>
</DrawerLayoutAndroid>
</View>
);
}
渲染Navigator,将其初始路由设置为App组件的index。将renderScene委托给_renderScene方法。给navigator一个ref,这样我们就可以在_navigateTo中引用它。
_checkLoginStatus () {
this._navigateTo(0);
}
上述代码导航到App组件,这会触发它检查用户的登录状态。
_openDrawer () {
this.refs['drawer'].openDrawer();
}
_openDrawer方法在DrawerLayoutAndroid组件上调用openDrawer。
_navigateTo (index) {
this.refs['navigator'].push({
index,
passProps: {
checkLoginStatus: () => this._checkLoginStatus()
}
});
this.refs['drawer'].closeDrawer();
}
_navigateTo方法将给定的index推送到navigator。给定一个checkLoginStatus属性,该属性将被用于LoginPage组件。最后关闭drawer。
_renderDrawerLayout () {
return (
<Drawer
navigateTo={ (index) => this._navigateTo(index) }
routes={ this.state.visibleRoutes }
/>
);
}
_renderDrawerLayout方法渲染Drawer组件,将其_navigateTo方法作为属性传递,以及路由数组。
_renderScene (route, navigator) {
if (route.index === 0) {
return (
<App
title={ route.title }
navigator={ navigator }
/>
);
}
_renderScene方法负责渲染所有四个可用的路由。
if (route.index === 1) {
return (
<LoginPage
title={ route.title }
navigator={ navigator }
{ ...route.passProps }
/>
);
}
if (route.index === 2) {
return (
<PostView
title={ route.title }
navigator={ navigator }
{ ...route.passProps }
/>
);
}
if (route.index === 3) {
return (
<WebViewComponent
title={ route.title }
navigator={ route.navigator }
{ ...route.passProps }
/>
);
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('Friends', () => Friends);
创建 App.js 的 Android 版本
现在,我们应该为“朋友”创建一个特定的 Android App组件。首先,将位于Friends/app/App.js的现有App.js文件重命名为App.ios.js,并创建一个名为App.android.js的新文件。
此文件应包含与App.ios.js类似的逻辑,但应删除任何对 iOS 特定组件的引用,例如TabBarIOS。此外,任何导航事件应更新以支持Navigator逻辑。
这是我的做法:
// Friends/app/App.android.js
...
以下三个项目从导入语句中删除:NavigatorIOS, TabBarIOS, 和 LoginPage:
export default class App extends Component {
constructor (props) {
...
}
状态中的 selectedTab 属性从 constructor 中移除:
...
async _checkLoginStatus () {
...
if (result === null) {
this.props.navigator.push({
index: 1,
passProps: {
getFeed: () => this._getFeed()
}
});
return;
}
...
}
componentWillMount 和 render 方法与 iOS 版本保持一致。在 _checkLoginStatus 中的导航方法被修改为传递一个 index 而不是 LoginPage 组件:
...
_renderView () {
...
return this._renderFeedList();
}
_getFeed, _renderFeedList, 和 _renderLoginPrompt 方法也没有被修改。在 _renderView 中,我不再返回 TabBarIOS,而是返回对 _renderFeedList 的调用。
...
}
最后,_refreshFeedList 和 _responseInfoCallback 方法也没有改变。然而,由于 _setSelectedTab 是一个 TabBarIOS 特定的方法,所以它从 App.android.js 中被移除。
修改 FeedList
在 Android 上,FeedList 的样式需要根据条件进行更改,以便其 container 样式不包含 marginTop 属性。修改 FeedList 以执行以下功能:
-
从 React Native 中导入
PlatformAPI。 -
条件检查用户的平台,并根据检查结果在 iOS 设备上提供容器样式或一个新的不包含
marginTop属性的 Android 特定样式。
这里是我的 FeedList 对 Android 的修改:
// Friends/app/components/FeedList/index.js
...
import {
Platform,
...
} from 'react-native';
...
export default class FeedList extends Component {
...
render () {
...
return (
<View style={ Platform.OS === 'ios' ? styles.container :
styles.androidContainer }>
...
</View>
)
}
...
}
我导入了 Platform API 并使用三元运算符来检查用户的操作系统,根据检查结果在 FeedList 的 render 方法中将顶层 View 组件分配一个适用的样式:
// Friends/app/components/FeedList/styles.js
androidContainer: {
flex: 1
},
我将 androidContainer 样式添加到 FeedList 的 StyleSheet 中。
在 FeedListRow 中支持 Navigator
接下来,我们必须更新 FeedListRow 以执行以下操作:
-
导入
PlatformAPI -
修改
navigateToPostView以检查用户的操作系统并使用适当的语法为每个操作系统推送PostView
我创建了 propsObject 来存储分配给 passProps 的对象,这样我就不必再次重写它:
// Friends/app/components/FeedListRow/index.js
...
import {
Platform,
...
} from 'react-native';
...
export default class FeedListRow extends Component {
...
_navigateToPostView () {
const propsObject = {
createdTime: this.props.createdTime,
message: this.props.message,
postID: this.props.postID,
story: this.props.story
};
这里我们查看 iOS 的条件逻辑:
if (Platform.OS === 'ios') {
this.props.navigator.push({
component: PostView,
passProps: propsObject
});
return;
}
由于 iOS 逻辑以 return 语句结束,所以在 Android 上使用 Navigator 的 push。
this.props.navigator.push({
index: 2,
passProps: propsObject
});
}
...
}
添加 PostView 导航器和 BackAndroid 支持
现在,让我们对 PostView 组件进行以下修改:
-
导入
Platform和BackAndroidAPI -
在
componentWillMount和componentWillUnmount中添加和移除BackAndroid的监听器。 -
在组件中编写一个回调来处理 Android 上的返回按钮点击,结果调用导航器的
pop。 -
创建类似于
FeedListRow的条件逻辑来推送WebViewComponent
我在 componentWillMount 生命周期中创建了一个 BackAndroid 的事件监听器:
// Friends/app/components/PostView/index.js
...
import {
BackAndroid,
Platform,
...
} from 'react-native';
...
export default class PostView extends Component {
...
async componentWillMount () {
BackAndroid.addEventListener('hardwareButtonPress', () =>
this._backButtonPress());
...
}
同样,我在 componentWillUnmount 中移除了那个事件监听器:
componentWillUnmount () {
BackAndroid.removeEventListener('hardwareButtonPress', () =>
this._backButtonPress())
}
此方法在按下返回按钮时在 navigator 上调用 pop:
...
_backButtonPress () {
this.props.navigator.pop();
return true;
}
在 iOS 上推送 WebViewComponent 的条件逻辑如下:
...
_renderWebView (url) {
if (Platform.OS === 'ios') {
this.props.navigator.push({
component: WebViewComponent,
passProps: {
url
}
});
return;
}
相同功能的条件逻辑,但在 Android 上如下:
this.props.navigator.push({
index: 3,
passProps: {
url
}
});
}
}
摘要
恭喜!您已经成功构建了三款 React Native 应用程序,贯穿整本书的学习过程。在本章中,您学习了如何将下拉刷新交互添加到应用程序中,让您的应用用户能够通过一个众所周知的手势快速刷新数据。然后,您使用了Image组件,将远程图片渲染到您的应用程序中。接下来,您为应用程序创建了一个WebView组件,使用户能够在不离开应用进入系统浏览器的情况下查看与 Web 相关的内容。最后,您进行了必要的修改,以创建应用程序的 Android 版本。