十、使用 React Native 和 Expo 创建实时消息传递应用
在开发实时消息传递应用时,与服务器建立实时连接至关重要,因为您希望用户在消息发送后立即接收消息。在前两章中,您可能已经体验到,移动应用比 web 应用使用起来更直观。当您希望用户来回发送消息时,最好通过构建一个移动应用来实现,这将在本章中介绍。
在本章中,您将使用 React Native 和 Expo 创建与 GraphQL 服务器连接的实时移动消息传递应用。通过使用 WebSocket,您可以为 web 和移动应用创建与服务器的实时连接,并在应用和 GraphQL 服务器之间建立双向数据流。此连接还可以通过使用 OAuth 和 JWT 令牌进行身份验证,这是您在第 7 章中使用 React Native 和 GraphQL构建全栈电子商务应用时所做的。
本章将介绍以下主题:
- 使用 Apollo 的带有 React Native 的 GraphQL
- 本机中的身份验证流
- GraphQL 订阅
项目概述
在本章中,我们将使用 React Native 和 Expo 创建一个移动消息传递应用,该应用使用 GraphQL 服务器进行身份验证并发送和接收消息。通过 Apollo 创建的 WebSocket 使用 GraphQL 订阅时,可以实时接收消息。用户需要登录才能通过应用发送消息,为此应用使用 React Navigation 和 AsyncStorage 构建了一个身份验证流,以将身份验证详细信息存储在持久性存储中。
构建时间为 2 小时。
开始
我们将在本章中创建的项目建立在您可以在 GitHub 上找到的初始版本之上:https://github.com/PacktPublishing/React-Projects/tree/ch10-initial 。完整的源代码也可以在 GitHub 上找到:https://github.com/PacktPublishing/React-Projects/tree/ch10 。
您需要在移动 iOS 或 Android 设备上安装 application Expo 客户端,才能在物理设备上运行项目。或者,您可以在计算机上安装 Xcode 或 Android Studio,以便在虚拟设备上运行应用:
- 对于 iOS:有关如何设置本地机器以运行 iOS 模拟器的信息,请参见此处:https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/ 。
- 适用于 Android:有关如何设置本地机器以从 Android Studio 运行模拟器的信息,请参见此处:https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/ 。运行 emulator 时存在一个已知问题,可以通过确保您的
~/.bash_profile
或~/.bash_rc
文件中存在以下行来防止该问题:
export ANDROID_SDK=ANDROID_SDK_LOCATIONexport PATH=ANDROID_SDK_LOCATION/platform-tools:$PATH
export PATH=ANDROID_SDK_LOCATION/tools:$PATH
ANDROID_SDK_LOCATION
的值是本地机器上 Android SDK 的路径,可以通过打开 Android Studio 并进入首选项外观&行为系统设置【T10 Android SDK】找到。该路径列在说明 Android SDK 位置的框中,如下所示:/Users/myuser/Library/Android/sdk
。
This application was created using Expo SDK version 33.0.0,** and so, you need to ensure that the version of Expo you're using on your local machine is similar. As React Native and Expo are frequently updated, make sure that you're working with this version so that the patterns described in this chapter behave as expected. In case your application doesn’t start or if you encounter errors, refer to the Expo documentation to learn more about updating the Expo SDK.
检查初始项目
该项目由两部分组成:样板文件 React 本机应用和 GraphQL 服务器。React 本机应用可以在client
目录中找到,而 GraphQL 服务器可以在server
目录中找到。在本章中,您需要让应用和服务器始终运行,您只需对client
目录中的应用进行代码更改。
要开始本章,您需要在client
和server
目录中运行以下命令,以便安装所有依赖项并启动服务器和应用:
npm install && npm start
对于移动应用,此命令将在安装依赖项后启动 Expo,并使您能够从终端或浏览器启动项目。在终端中,您可以使用二维码在移动设备上打开应用,也可以在虚拟设备上打开应用
无论您是从物理或虚拟 iOS 或 Android 设备上使用打开应用,应用的外观应如下所示:
初始应用由五个屏幕组成:AuthLoading
、Conversations
、Conversation
、Login
和Settings
。Conversations
屏幕将是初始屏幕并显示加载消息,Settings
屏幕包含一个不起作用的注销按钮。目前,AuthLoading
、Conversation
和Login
屏幕尚不可见,因为您将在本章后面的部分中向这些屏幕添加路由。
client
目录中此 React 本机应用的项目结构如下所示,其中的结构类似于您在本书中之前创建的项目:
messaging
|-- client
|-- .expo
|-- assets
|-- icon.png
|-- splash.png
|-- Components
|-- // ...
|-- node_modules
|-- Screens
|-- AuthLoading.js
|-- Conversation.js
|-- Conversations.js
|-- Login.js
|-- Settings.js
|-- .watchmanconfig
|-- App.js
|-- AppContainer.js
|-- app.json
|-- babel.config.js
|-- package.json
在assets
目录中,您可以在主屏幕上找到用于应用图标的图像。在移动设备上安装此应用后,启动应用时将显示作为启动屏幕的图像。有关应用的详细信息,如名称、说明和版本,请参见app.json
,而babel.config.js
包含特定的 Babel 配置。
App.js
文件是应用的实际入口点,在这里导入并返回AppContainer.js
文件。在AppContainer
中,定义了此应用的所有路由,AppContext
将包含整个应用中应该可用的信息
此应用的所有组件都位于Screens
和Components
目录中,其中第一个目录包含由屏幕呈现的组件。这些屏幕的子组件可以在Components
目录中找到,该目录具有以下结构:
|-- Components
|-- Button
|-- Button.js
|-- Conversation
|-- ConversationActions.js
|-- ConversationItem.js
|-- Message
|-- Message.js
|-- TextInput
|-- TextInput.js
GraphQL 服务器位于:http://localhost:4000/graphql
并且是 GraphQL 游乐场可见的位置。在这个平台上,您可以查看 GraphQL 服务器的模式,并反思所有可用的查询、突变和订阅。尽管您不会对服务器进行任何代码更改,但了解模式及其工作方式很重要。
服务器有两个查询,通过使用userName
参数作为标识符来检索对话列表或单个对话。这些查询将返回Conversation
类型,其中包含id
、userName
和Message
类型的消息列表
在这个 GraphQL 服务器上,可以找到两个变体,它们要么让用户登录,要么发送消息。用户可以通过以下方式登录:
- 用户名:
test
- 密码:
test
最后,有一个订阅将检索添加到对话中的消息。此订阅将增强查询,并可在文档中发送以检索单个对话。
使用 React Native 和 Expo 创建实时消息传递应用
移动应用之所以流行,原因之一是它们通常提供实时数据,如更新和通知。使用 React Native 和 Expo,您可以创建移动应用,使用与 GraphQL 服务器同步的 WebSocket 处理实时数据。在本章中,您将向 React 本机应用添加 GraphQL,并向该应用添加额外功能,使其能够处理实时数据。
在 React Native with Apollo 中使用 GraphQL
在第 7 章中使用 React Native 和 GraphQL构建一个全栈电子商务应用,您已经为一个 web 应用建立了与 GraphQL 服务器的连接;类似地,在本章中,您将为移动应用中的数据使用 GraphQL 服务器。要在 React 本机应用中使用 GraphQL,可以使用 Apollo 使开发人员的体验更加流畅。
在美国建立阿波罗
react-apollo
包已经在 React web 应用中用于 Apollo,也可以在 React 本机移动应用中用于 Apollo。这完全符合 React 和 React Native 的口号:一次学习,到处写作。但在我们将 Apollo 添加到应用之前,重要的是要知道,在移动设备上使用 Expo 应用运行应用时,不支持本地主机请求。此项目的本地 GraphQL 服务器正在http://localhost:4000/graphql
上运行,但为了能够在 React 本机应用中使用此端点,您需要找到您机器的本地 IP 地址。
要查找本地 IP 地址,您需要根据您的操作系统执行以下操作:
- 对于 Windows:打开终端(或命令提示符)并运行此命令:
ipconfig
这将返回一个列表,如下所示,其中包含来自本地计算机的数据。在此列表中,您需要查找字段IPv4 地址:
- 对于 macOS:打开终端并运行此命令:
ipconfig getifaddr en0
运行此命令后,返回您机器的本地Ipv4 Address
,如下图:
192.168.1.107
获取本地 IP 地址后,可以使用此地址为 React 本机应用设置 Apollo 客户端。为了能够使用 Apollo 和 GraphQL,您需要使用以下命令从npm
使用npm
安装几个软件包。您需要从单独终端选项卡中的client
目录执行此操作:
cd client && npm install graphql apollo-client apollo-link-http apollo-cache-inmemory react-apollo
在App.js
文件中,您现在可以使用apollo-client
创建 GraphQL 客户端,使用apollo-link-http
建立与本地 GraphQL 服务器的连接,并使用apollo-cache-inmemory
缓存 GraphQL 请求。此外,ApolloProvider
组件将使用您创建的客户端使 GraphQL 服务器可用于嵌套在此提供程序中的所有组件。必须使用本地 IP 地址为API_URL
创建值,前缀为http://
,后缀为:4000/graphql
,指向正确的端口和端点,使其看起来像http://192.168.1.107:4000/graphql
。
为此,在App.js
中添加以下行:
import React from 'react';
import AppContainer from './AppContainer';
+ import { ApolloClient } from 'apollo-client';
+ import { InMemoryCache } from 'apollo-cache-inmemory';
+ import { HttpLink } from 'apollo-link-http';
+ import { ApolloProvider } from 'react-apollo';
+ const API_URL = 'http://192.168.1.107:4000/graphql';
+ const cache = new InMemoryCache();
+ const client = new ApolloClient({
+ link: new HttpLink({
+ uri: API_URL,
+ }),
+ cache
+ });
- const App = () => <AppContainer />;
+ const App = () => (
+ <ApolloProvider client={client}>
+ <AppContainer />
+ </ApolloProvider>
+ );
export default App;
现在,您可以发送文档,其中包含来自ApolloProvider
中嵌套的任何组件的查询和变化,但您还不能在文档中发送订阅。对订阅的支持不是现成的,需要为客户端 React 本机应用和 GraphQL 服务器之间的实时双向连接设置 WebSocket。在向应用添加身份验证后,将在本章后面部分完成此操作。
在本节的下一部分中,您将使用 Apollo 从 GraphQL 服务器获取数据,您在本节中刚刚链接到 Apollo 客户端。
使用阿波罗在反应本地
如果您查看应用,您将看到有两个选项卡;一个显示Conversations
画面,另一个显示Settings
画面。Conversations
屏幕现在显示文本Loading...
,其中应显示从 GraphQL 服务器返回的对话。显示对话的组件已经创建,可以在client/Components/Conversation
目录中找到,但仍需要创建请求对话的逻辑。
要添加 Apollo,请执行以下步骤:
- 第一步是将
Query
组件从react-apollo
导入client/Screens/Conversations.js
文件,您将使用该文件向 GraphQL 服务器发送文档。此Query
组件将使用GET_CONVERSATIONS
查询,ConversationItem
组件也必须导入:
import React from 'react';
import { FlatList, Text, View } from 'react-native';
import styled from 'styled-components/native';
+ import { Query } from 'react-apollo';
+ import { GET_CONVERSATIONS } from '../constants';
+ import ConversationItem from '../Components/Conversations/ConversationItem';
...
const Conversations = () => (
...
Conversations
屏幕现在应该使用Query
组件请求GET_CONVERSATIONS
查询。当请求尚未解决时,将显示加载消息。当对 GraphQL 服务器的请求得到解决时,一个样式化的Flatlist
将返回一个导入的ConversationItem
组件列表。样式为Flatlist
的组件已经创建,可以在该文件底部找到ConversationsList
组件:
...
const Conversations = () => (
<ConversationsWrapper>
- <ConversationsText>Loading...</ConversationsText>
+ <Query query={GET_CONVERSATIONS}>
+ {({ loading, data }) => {
+ if (loading) {
+ return <ConversationsText>Loading...</ConversationsText>
+ }
+ return (
+ <ConversationsList
+ data={data.conversations}
+ keyExtractor={item => item.userName}
+ renderItem={({ item }) => <ConversationItem item={item} /> }
+ />
+ );
+ }}
+ </Query>
</ConversationsWrapper>
);
export default Conversations;
Conversations
屏幕最初显示带查询的单据发送时的加载消息;查询返回数据后,将显示ConversationsList
组件。此组件呈现显示查询数据的ConversationItem
组件。
- 当您尝试单击任何对话时,都不会发生任何事情,只会看到一个改变不透明度的小动画。这是因为
ConversationItem
组件是一个样式为TouchableOpacity
的组件,它可以作为一个函数传递,当您点击它时会被调用。可通过Conversations
屏幕上的navigation
道具创建导航至对话的功能。该道具应作为道具传递给ConversationItem
:
...
- const Conversations = () => (
+ const Conversations = ({ navigation ) => (
<ConversationsWrapper>
<ConversationsText>Loading...</ConversationsText>
<Query query={GET_CONVERSATIONS}>
{({ loading, data }) => {
if (loading) {
return <ConversationsText>Loading...</ConversationsText>
}
return (
<ConversationsList
data={data.conversations}
keyExtractor={item => item.userName}
- renderItem={({ item }) => <ConversationItem item={item} /> }
+ renderItem={({ item }) => <ConversationItem item={item} navigation={navigation} />}
/>
);
}}
</Query>
</ConversationsWrapper>
);
export default Conversations;
- 点击
TouchableOpacity
时ConversationItem
组件现在可以导航到Conversation
屏幕;这个组件可以在client/Components/Conversation/ConversationItem.js
文件中找到,其中navigation
属性应该被解构并用于调用onPress
处理程序上的navigate
函数。此项通过navigate
功能传递,以便在Conversation
屏幕中使用此数据:
import React from 'react';
import { Platform, Text, View, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import styled from 'styled-components/native';
...
- const ConversationItem = ({ item }) => (
+ const ConversationItem = ({ item, navigation }) => (
- <ConversationItemWrapper>
+ <ConversationItemWrapper
+ onPress={() => navigation.navigate('Conversation', { item })} + >
<ThumbnailWrapper>
...
- 这将从
client/Screens/Conversation.js
文件导航到Conversation
屏幕,其中应显示完整的对话。要显示对话,可以使用刚刚传递到此屏幕的项目数据,也可以将另一个文档发送到包含查询的 GraphQL 服务器以检索对话。为了确保显示最新的数据,Query
组件可用于发送查询,以使用navigation
道具中的userName
字段检索对话。为此,您需要导入Query
组件、Query
使用的GET_CONVERSATION
查询和Message
组件来显示对话中的消息:
import React from 'react';
import { Dimensions, ScrollView, Text, FlatList, View } from 'react-native';
+ import { Query } from 'react-apollo'; import styled from 'styled-components/native';
+ import Message from '../Components/Message/Message';
+ import { GET_CONVERSATION } from '../constants';
...
const Conversation = () => (
...
- 在此之后,您可以将
Query
组件添加到Conversation
屏幕中,并让它使用从navigation
道具中检索到的GET_CONVERSATION
查询和userName
。一旦查询解析,Query
组件返回一个data
对象,其中包含一个名为messages
的字段。该值可以传递给FlatList
组件。在此组件中,您可以迭代此值并返回显示对话中所有消息的Message
组件。FlatList
已设置样式,可在文件底部找到MessagesList
:
...
- const Conversation = () => {
+ const Conversation = ({ navigation }) => {
+ const userName = navigation.getParam('userName', '');
+ return (
<ConversationWrapper>
- <ConversationBodyText>Loading...</ConversationBodyText>
+ <Query query={GET_CONVERSATION} variables={{ userName }}>
<ConversationBody> + {({ loading, data }) => {
+ if (loading) {
+ return <ConversationBodyText>Loading...</ConversationBodyText>;
+ }
+ const { messages } = data.conversation;
+ <MessagesList
+ data={messages}
+ keyExtractor={item => String(item.id)}
+ renderItem={({ item }) => (
+ <Message align={item.userName === 'me' ? 'left' : 'right'}>
+ {item.text}
+ </Message>
+ )}
+ />
+ }} </ConversationBody>+ </Query>
<ConversationActions userName={userName} />
</ConversationWrapper>
);
+ }; export default Conversation;
此时将显示此对话中收到的所有消息,并且可以使用此屏幕底部的表单将新消息添加到对话中。
根据您正在运行应用的设备,Conversation
和Conversation
屏幕在运行 iOS 的设备上的外观应如下所示:
但是,要发送消息,应该将带有变异的文档发送到 GraphQL 服务器,并且必须对用户进行身份验证。下一节将讨论如何处理此变异的身份验证,其中将添加身份验证流。
React-Native 中的身份验证
通常,移动应用的身份验证与您在 web 应用中处理身份验证的方式类似,尽管存在一些细微的差异。在移动应用上验证用户的流程如下所示:
- 用户打开您的应用
- 将显示一个加载屏幕,用于检查持久存储中的任何身份验证信息
- 如果经过验证,用户将被转发到应用的主屏幕;否则,它们将被转发到登录屏幕,用户可以在那里登录
- 每当用户注销时,将从持久存储中删除身份验证详细信息
这个流程中最大的一个警告是,移动设备不支持本地存储或会话存储,因为这些持久性存储解决方案绑定到浏览器。相反,您需要使用 React Native 的AsyncStorage
库,以便在 iOS 和 Android 上都具有持久性存储。在 iOS 上,它将使用本机代码块为您提供AsyncStorage
提供的全局持久存储,而在运行 Android 的设备上,将使用基于 RockDB 或 SQLite 的存储。
For more complex usages, it's recommended to use an abstraction layer on top of AsyncStorage
as encryption isn't supported out of the box by AsyncStorage
. Also, the use of a key-value system can give you performance issues if you want to store a lot of information for your application using AsyncStorage
. Both iOS and Android will have set limitations on the amount of storage each application can use.
使用 React 导航进行身份验证
要设置我们前面描述的身份验证流,您将再次使用 React 导航包。之前,您使用了 React Navigation 中不同类型的导航器,但没有使用SwitchNavigator
。使用此导航器类型,您一次只能显示一个屏幕,并且可以使用navigation
道具导航到其他屏幕。SwitchNavigator
应该是应用的主导航器,其他导航器,如StackNavigator
可以嵌套在其中。
向 React 本机应用添加身份验证涉及执行以下步骤:
- 使用此导航器类型的第一步是从
react-navigation
导入createSwitchNavigator
,就像您将其他导航器导入client/AppContainer.js
文件一样。另外,导入登录屏幕的屏幕组件,可在client/Screens/Login.js
找到:
import React from 'react';
import { Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import {
+ createSwitchContainer,
createAppContainer
} from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createBottomTabNavigator } from 'react-navigation-tabs';
import Conversations from './Screens/Conversations';
import Conversation from './Screens/Conversation';
import Settings from './Screens/Settings';
+ import Login from './Screens/Login';
const ConversationsStack = createStackNavigator({
...
- 您需要返回
SwitchNavigator
,而不是在该文件底部用createAppContainer
包装TabNavigator
。要创建此项,您需要使用在上一步中导入的createSwitchNavigator
。此导航器包含Login
屏幕和TabNavigator
,这是此应用的主屏幕。为了让用户在认证时只看到主屏幕,Login
屏幕需要是初始屏幕:
...
+ const SwitchNavigator = createSwitchNavigator( + { + Main: TabNavigator, + Auth: Login + }, + { + initialRouteName: 'Auth', + } + ); - export default createAppContainer(TabNavigator); + export default createAppContainer(SwitchNavigator);
现在显示在应用中的Login
屏幕只有在填写正确的身份验证详细信息后才会切换到TabNavigator
。
- 但是,此表单需要首先连接到 GraphQL 服务器以接收身份验证所需的 JWT 令牌。
Login
屏幕的组件已经有一个表单,但是提交此表单还没有调用任何函数来验证用户。因此,您需要使用react-apollo
中的Mutation
组件,并让该组件向 GraphQL 服务器发送一份包含正确变异的文档。需要添加到此组件的变异可以在constants.js
文件中找到,称为LOGIN_USER
。提交表单时,当用户按下Button
按钮时,需要调用Mutation
组件返回的loginUser
函数:
import React from 'react';
import { View, TextInput } from 'react-native';
import styled from 'styled-components/native';
+ import { Mutation } from 'react-apollo';
import Button from '../Components/Button/Button';
+ import { LOGIN_USER } from '../constants';
... const Login = () => {
const [userName, setUserName] = React.useState('');
const [password, setPassword] = React.useState('');
return (
+ <Mutation mutation={LOGIN_USER}>
+ {loginUser => (
<LoginWrapper>
<StyledTextInput
onChangeText={setUserName}
value={userName}
placeholder='Your username'
textContentType='username'
/>
<StyledTextInput
onChangeText={setPassword}
value={password}
placeholder='Your password'
textContentType='password'
/>
<Button
title='Login'
+ onPress={() => loginUser({ variables: { userName, password } })}
/>
</LoginWrapper>
+ )}
+ </Mutation>
);
};
export default Login;
两个TextInput
组件都是受控组件,使用useState
钩子控制它们的值。该突变所使用的userName
和password
常数均采用两个变量进行验证,即userName
和password
:
...
export const LOGIN_USER = gql`
mutation loginUser($userName: String!, $password: String!) {
loginUser(userName: $userName, password: $password) {
userName
token
}
}
`;
...
- 除了在文档中发送变异的
loginUser
函数外,Mutation
组件还将返回 GraphQL 服务器返回的loading
、error
和data
变量。loading
变量可用于向用户传达文档已发送到服务器,而data
和error
变量在 GraphQL 服务器响应此文档时返回:
import React from 'react';
import { View, TextInput } from 'react-native';
import styled from 'styled-components/native';
import { Mutation } from 'react-apollo';
import Button from '../Components/Button/Button';
import { LOGIN_USER } from '../constants'; ... const Login = () => {
const [userName, setUserName] = React.useState('');
const [password, setPassword] = React.useState('');
return (
<Mutation mutation={LOGIN_USER}>
- {loginUser => (
+ {(loginUser, { loading }) => ( <LoginWrapper>
<StyledTextInput
onChangeText={setUserName}
value={userName}
placeholder='Your username'
textContentType='username'
/>
<StyledTextInput
onChangeText={setPassword}
value={password}
placeholder='Your password'
textContentType='password'
/>
<Button
- title='Login'
+ title={loading ? 'Loading...' : 'Login'}
onPress={() => loginUser({ variables: { userName, password } })}
/>
</LoginWrapper>
}}
</Mutation>
);
};
export default Login;
当文档被发送到 GraphQL 服务器且尚未返回响应时,这会将表单底部按钮的文本更改为Loading...
。
- 若要在填写错误凭据时使用
error
变量显示错误消息,您不会从Mutation
组件的输出中对变量进行分解。相反,错误变量将从loginUser
函数返回的Promise
中检索。为了显示错误,您将使用error
变量提供的graphQLErrors
方法,该方法返回一个数组(因为可能存在多个错误),并从 React Native 在Alert
组件中呈现错误:
import React from 'react';
- import { View, TextInput } from 'react-native';
+ import { Alert, View, TextInput } from 'react-native';
import styled from 'styled-components/native';
import { Mutation } from 'react-apollo';
import Button from '../Components/Button/Button';
import { LOGIN_USER } from '../constants';
...
<Button
title={loading ? 'Loading...' : 'Login'}
onPress={() => {
loginUser({ variables: { userName, password } })
+ .catch(error => {
+ Alert.alert(
+ 'Error',
+ error.graphQLErrors.map(({ message }) => message)[0]
+ );
+ });
}}
/>
...
- 当使用正确的用户名和密码组合时,应该使用数据变量来存储将由 GraphQL 服务器返回的 JWT 令牌。就像从
loginUser
函数中检索到的error
变量一样,data
变量也可以从此Promise
中检索。此令牌可在data
变量上使用,并应存储在安全的地方,这可以通过AsyncStorage
库完成:
import React from 'react';
- import { Alert, View, TextInput } from 'react-native';
+ import { AsyncStorage, Alert, View, TextInput } from 'react-native';
import styled from 'styled-components/native';
import { Mutation } from 'react-apollo';
import Button from '../Components/Button/Button';
import { LOGIN_USER } from '../constants';
...
const Login = ({ navigation }) => {
...
<Button
title={loading ? 'Loading...' : 'Login'}
onPress={() => {
loginUser({ variables: { userName, password } })
+ .then(({data}) => {
+ const { token } = data.loginUser;
+ AsyncStorage.setItem('token', token);
+ })
.catch(error => {
if (error) {
Alert.alert(
'Error',
error.graphQLErrors.map(({ message }) => message)[0],
);
}
});
}}
/>
...
- 存储令牌后,应将用户重定向到主应用,该应用可在
Main
路径找到,并表示链接到TabNavigator
的屏幕。要重定向用户,您可以使用SwitchNavigator
传递给Login
组件的navigation
道具。由于与AsyncStorage
一起存储的东西应该是异步的,所以导航功能应该从AsyncStorage
返回的Promise
回调中调用:
import React from 'react';
import { AsyncStorage, Alert, View, TextInput } from 'react-native';
import styled from 'styled-components/native';
import { Mutation } from 'react-apollo';
import Button from '../Components/Button/Button';
import { LOGIN_USER } from '../constants';
...
- const Login = () => {
+ const Login = ({ navigation }) => {
...
<Button
title={loading ? 'Loading...' : 'Login'}
onPress={() => {
loginUser({ variables: { userName, password } })
.then(({data}) => {
const { token } = data.loginUser;
- AsyncStorage.setItem('token', token)
+ AsyncStorage.setItem('token', token).then(value => {
+ navigation.navigate('Main');
+ });
})
.catch(error => {
if (error) {
Alert.alert(
'Error',
error.graphQLErrors.map(({ message }) => message)[0],
);
}
});
}}
/>
...
然而,这只完成了验证流程的一部分,因为当应用首次呈现时,Login
屏幕将始终显示。这样,用户总是必须使用他们的身份验证详细信息登录,即使他们的 JWT 令牌存储在持久存储中。
要检查用户之前是否登录过,必须在SwitchNavigator
中添加第三个屏幕。此屏幕将确定用户是否在永久存储器中存储了令牌,如果有,用户将立即重定向到Main
路由。如果用户以前没有登录过,用户将被重定向到您刚刚创建的Login
屏幕:
- 这个确定持久存储器中是否存储有身份验证令牌的中间屏幕,即
AuthLoading
屏幕,应该添加到App.js
中的SwitchNavigator
中。此屏幕还应成为导航器提供服务的初始路线:
import React from 'react';
import { Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import {
createSwitchNavigator,
createAppContainer
} from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createBottomTabNavigator } from 'react-navigation-tabs';
import Conversations from './Screens/Conversations';
import Conversation from './Screens/Conversation';
import Settings from './Screens/Settings';
import Login from './Screens/Login';
+ import AuthLoading from './Screens/AuthLoading'; const ConversationsStack = createStackNavigator({
...
const SwitchNavigator = createSwitchNavigator(
{
Main: TabNavigator, Login,
+ AuthLoading,
},
{
- initialRouteName: 'Login',
+ initialRouteName: 'AuthLoading',
}
);export default createAppContainer(SwitchNavigator);
- 在此
AuthLoading
屏幕中,应从持久存储中检索身份验证令牌,然后处理到Login
或Main
屏幕的导航。这个屏幕可以在client/Screens/AuthLoading.js
文件中找到,其中只添加了一个简单的界面。可使用AsyncStorage
库中的getItem
方法检索令牌,并应从useEffect
钩子调用令牌,以便在首次加载AuthLoading
屏幕时检索令牌。从callback
和getItem
返回的Promise
中,navigation
道具的navigate
功能用于实际导航到以下任一屏幕:
import React from 'react';
- import { Text, View } from 'react-native';
+ import { AsyncStorage, Text, View } from 'react-native'; import styled from 'styled-components/native';
...
- const AuthLoading = () => (
+ const AuthLoading = ({ navigation }) => {
+ React.useEffect(() => {
+ AsyncStorage.getItem('token').then(value => {
+ navigation.navigate(value ? 'Main' : 'Auth');
+ });
+ }, [navigation]);
+ return (
<AuthLoadingWrapper>
<AuthLoadingText>Loading...</AuthLoadingText>
</AuthLoadingWrapper>
);
+ };
export default AuthLoading;
- 完成身份验证流程的最后一步是,通过从持久存储中删除令牌,增加用户注销应用的可能性。这在
client/Screens/Settings.js
文件中完成。这将呈现可在TabNavigator
中找到的Settings
屏幕。Settings
屏幕上有一个绿色按钮,您可以设置onPress
事件。
AsyncStorage
中的removeItem
方法可用于从持久存储中删除令牌并返回Promise
。在Promise
的回调中,您可以再次处理导航返回Login
屏幕,因为您不希望应用中有未经验证的用户:
import React from 'react';
- import { Text, View } from 'react-native';
+ import { AsyncStorage, Text, View } from 'react-native';
import styled from 'styled-components/native';
import Button from '../Components/Button/Button';
...
- const Settings = () => (
+ const Settings = ({ navigation }) => (
<SettingsWrapper>
- <Button title='Log out' />
+ <Button
+ title='Log out'
+ onPress={() => {
+ AsyncStorage.removeItem('token').then(() => navigation.navigate('AuthLoading'));
+ }} + />
</SettingsWrapper>
);
export default Settings;
通过添加注销功能,您已经完成了使用 GraphQL 服务器返回的 JWT 令牌的身份验证流。这可以通过在Login
屏幕上填写表格来请求。如果认证成功,用户将被重定向到Main
屏幕,通过使用Settings
屏幕上的注销按钮,用户可以注销并被引导回Login
屏幕。最终的身份验证流现在看起来像这样,这取决于您运行此应用的操作系统。以下屏幕截图摘自在 iOS 上运行的设备:
但是,为了让 GraphQL 服务器知道该用户是否经过身份验证,需要向其发送验证令牌。在本节的下一部分中,您将学习如何使用JSON Web 令牌(JWT)来实现这一点。
将身份验证详细信息发送到 GraphQL 服务器
现在存储在持久存储中的身份验证详细信息也应该添加到 Apollo 客户端,以便将它们与每个文档一起发送到 GraphQL 服务器。这可以通过使用令牌信息扩展 Apollo 客户端的设置来实现。因为令牌是 JWT,所以应该在其前面加上Bearer
:
- 您需要安装 Apollo 软件包来处理向
context
添加值的问题。setContext
方法可从apollo-link-context
软件包获得,您可以从npm
安装该软件包:
npm install apollo-link-context
apollo-link-context
包应该导入到client/App.js
文件中,在该文件中创建 Apollo 客户端。您需要为客户机分离HttpLink
对象的构造,因为该构造需要与创建的上下文相结合:
import React from 'react';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
+ import { setContext } from 'apollo-link-context';
import { HttpLink } from 'apollo-link-http';
import { ApolloProvider } from 'react-apollo';
import AppContainer from './AppContainer';
const API_URL = '..';
+ const httpLink = new HttpLink({
+ uri: API_URL,+ });
const cache = new InMemoryCache();
const client = new ApolloClient({
- link: new HttpLink({
- uri: API_URL, - }),
+ link: httpLink,
cache,
});
const App = () => (
...
- 在此之后,您可以使用
setContext()
方法扩展发送到 GraphQL 服务器的头,以便您还可以包括可以从持久性存储中检索的令牌。此方法应异步使用,因为从AsyncStorage
获取项目也是异步的。将返回的令牌必须以Bearer
作为前缀,因为 GraphQL 服务器希望 JWT 令牌采用该格式:
import React from 'react';
+ import { AsyncStorage } from 'react-native';
import AppContainer from './AppContainer';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { HttpLink } from 'apollo-link-http';
import { ApolloProvider } from 'react-apollo';
const API_URL = '...';
const httpLink = new HttpLink({
uri: API_URL,
});
+ const authLink = setContext(async (_, { headers }) => {
+ const token = await AsyncStorage.getItem('token');
+ return {
+ headers: {
+ ...headers,
+ authorization: token ? `Bearer ${token}` : '',
+ }
+ };
+ });
...
- 创建 Apollo 客户端时用于
link
字段的httpLink
现在应与authLink
组合,以便在将请求发送到 GraphQL 服务器时,从AsyncStorage
检索到的令牌将添加到头中:
...
const cache = new InMemoryCache();
const client = new ApolloClient({
- link: httpLink,
+ link: authLink.concat(httpLink),
cache
});
const App = () => (
...
现在,任何传递到 GraphQL 服务器的文档都将能够使用通过使用应用的登录表单检索到的令牌,这是在下一节中使用变异发送消息时需要的。
在 React Native with Apollo 中处理订阅
在继续将包含突变的文档发送到 GraphQL 服务器之前,我们需要设置 Apollo 以便处理订阅。为了处理订阅,需要为您的应用设置 WebSocket,从而在 GraphQL 服务器和应用之间实现实时双向连接。这样,例如,当您使用此移动应用发送或接收消息时,您将收到即时反馈。
为 GraphQL 订阅设置 Apollo 客户端
要在 React 本机应用中使用订阅,您需要向项目中添加更多包,例如,这些包可以添加 WebSocket。这些方案如下:
npm install apollo-link-ws subscriptions-transport-ws apollo-utilities
apollo-link-ws
包帮助您创建指向运行订阅的 GraphQL 服务器的链接,就像apollo-link-http
用于查询和突变一样。subscriptions-transport-ws
是运行apollo-link-ws
所需的包,而apollo-utilities
是运行apollo-link-ws
所需的包添加以使用这些包上可用的方法,以便您可以将有关订阅的请求与查询或更改的请求分开。
安装这些软件包后,您需要按照以下步骤在应用中使用订阅:
- 您可以使用
apollo-link-ws
将链接的创建添加到 GraphQL 服务器。GraphQL 服务器的 URL 应以ws://
作为前缀,而不是http://
,因为它涉及到与 WebSocket 的连接。在您的计算机上运行的 GraphQL 服务器的 URL 看起来像是ws://192.168.1.107/graphql
而不是http://192.168.1.107/graphql
,必须添加到SOCKET_URL
常量中:
import React from 'react';
import { AsyncStorage } from 'react-native';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { HttpLink } from 'apollo-link-http';
+ import { split } from 'apollo-link';
import { ApolloProvider } from 'react-apollo';
import AppContainer from './AppContainer';
const API_URL = '...';
+ const SOCKET_URL = 'ws://192.168.1.107/graphql';
...
+ const wsLink = new WebSocketLink({
+ uri: SOCKET_URL,
+ options: {
+ reconnect: true,
+ },
+ });
...
- 使用
split
和getMainDefinition
方法,可以通过从订阅中分离查询和突变来区分对 GraphQL 服务器的不同请求。这样,只有包含订阅的文档才会使用 WebSocket 发送,查询和转换将使用默认流:
import React from 'react';
import { AsyncStorage } from 'react-native';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { HttpLink } from 'apollo-link-http';
import { split } from 'apollo-link'; + import { WebSocketLink } from 'apollo-link-ws';
+ import { getMainDefinition } from 'apollo-utilities';
import { ApolloProvider } from 'react-apollo';
import AppContainer from './AppContainer';
...
+ const link = split(
+ ({ query }) => {
+ const definition = getMainDefinition(query);
+
+ return (
+ definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
+ );
+ },
+ wsLink,
+ httpLink,
+ );
const cache = new InMemoryCache();
const client = new ApolloClient({
- link: authLink.concat(httpLink),
+ link: authLink.concat(link),
cache,
});
const App = () => (
...
Apollo 的设置现在也支持订阅,您将在本节的下一部分中添加订阅,Conversations
屏幕将充满实时数据。
添加订阅以响应本机
在您的计算机上运行的本地 GraphQL 服务器支持查询和订阅,以便您可以从特定用户返回对话。如果查询将返回完整的对话,订阅将返回该对话中发送或接收的任何新消息。目前,Conversation
屏幕仅发送一份带有查询的文档,如果您点击Conversations
屏幕上显示的任何对话,该查询将返回与用户的对话。
订阅可以通过多种方式添加到应用中;使用react-apollo
中的Subscription
组件是最简单的。但是,由于您已经在使用client/Screens/Conversation.js
中的Query
组件检索对话,Query
组件也可以扩展为支持订阅:
- 将订阅添加到
Conversation
屏幕的第一步是将屏幕拆分为多个组件。您可以通过在client/Components/Conversation
目录中创建一个名为ConversationBody
的新组件来实现这一点。该文件名为ConversationBody.js
,包含以下代码:
import React from 'react';
import styled from 'styled-components/native';
import { Dimensions, ScrollView, FlatList } from 'react-native';
import Message from '../Message/Message';
const ConversationBodyWrapper = styled(ScrollView)`
width: 100%;
padding: 2%;
display: flex;
height: ${Dimensions.get('window').height * 0.6};
`;
const MessagesList = styled(FlatList)`
width: 100%;
`;
const ConversationBody = ({ userName, messages }) => {
return (
<ConversationBodyWrapper>
<MessagesList
data={messages}
keyExtractor={item => String(item.id)}
renderItem={({ item }) => (
<Message align={item.userName === 'me' ? 'left' : 'right'}>
{item.text}
</Message>
)}
/>
</ConversationBodyWrapper>
);
};
export default ConversationBody;
- 创建此新组件后,应将其导入到
client/Screens/Conversation.js
文件中的Conversation
屏幕中,以替换该文件中已存在的ContainerBody
组件。这也意味着一些导入已过时,ContainerBody
样式的组件也可以删除:
import React from 'react';
- import { Dimensions, ScrollView, Text, FlatList, View } from 'react-native';
+ import { Text, View } from 'react-native';
import { Query } from 'react-apollo';
import styled from 'styled-components/native';
- import Message from '../Components/Message/Message';
+ import ConversationBody from '../Components/Conversation/ConversationBody'; import { GET_CONVERSATION } from '../constants';
...
const Conversation = ({ navigation }) => {
const userName = navigation.getParam('userName', ''); return (
<ConversationWrapper> <Query query={GET_CONVERSATION} variables={{ userName }}>
- <ConversationBody>
{({ loading, data }) => {
if (loading) {
return <ConversationBodyText>Loading...</ConversationBodyText>;
}
const { messages } = data.conversation;
- return ( - <MessagesList
- data={messages}
- keyExtractor={item => String(item.id)}
- renderItem={({ item }) => (
- <Message align={item.userName === 'me' ? 'left' : 'right'}>
- {item.text}
- </Message>
- )}
- />
- );
- }} + return <ConversationBody messages={messages} userName={userName} /> }}
- </ConversationBody>
</Query>
<ConversationActions userName={userName} />
</ConversationWrapper>
);
};
export default Conversation;
- 现在,通过从
Query
组件获取subscribeToMore
方法,可以将检索订阅的逻辑添加到Query
组件中。该方法应传递给ConversationBody
组件,在该组件中调用该方法,从而检索对话中发送或接收的任何新消息:
...
return (
<ConversationWrapper> <Query query={GET_CONVERSATION} variables={{ userName }}>
- {({ loading, data }) => {
+ {({ subscribeToMore, loading, data }) => {
if (loading) {
return <ConversationBodyText>Loading...</ConversationBodyText>;
}
const { messages } = data.conversation;
- return <ConversationBody messages={messages} userName={userName} />
+ return (
+ <ConversationBody
+ messages={messages}
+ userName={userName}
+ subscribeToMore={subscribeToMore}
+ /> }} </Query>
<ConversationActions userName={userName} />
</ConversationWrapper>
);
};
- 在
ConversationBody
组件中,subscribeToMore
方法现在可以通过订阅来检索添加到对话中的任何新消息。使用的订阅名为MESSAGES_ADDED
,可在client/constants.js
文件中找到。以userName
为变量:
import React from 'react';
import styled from 'styled-components/native';
import { Dimensions, ScrollView, FlatList } from 'react-native';
import Message from '../Message/Message';
+ import { MESSAGE_ADDED } from '../../constants';
... - const ConversationBody = ({ userName, messages }) => {
+ const ConversationBody = ({ subscribeToMore, userName, messages }) => {
return (
<ConversationBodyWrapper>
<MessagesList
data={messages}
keyExtractor={item => String(item.id)}
renderItem={({ item }) => (
<Message align={item.userName === 'me' ? 'left' : 'right'}>
{item.text}
</Message>
)}
/>
</ConversationBodyWrapper>
);
};
export default ConversationBody;
- 导入订阅并从道具中解构
subscribeToMore
方法后,可以添加检索订阅的逻辑。只有在ConversationBody
组件首次安装时,才能通过useEffect
挂钩调用subscribeToMore
。任何新添加的消息都会导致Query
组件重新加载,这也会使ConversationBody
组件重新加载,因此无需检查useEffect
钩子中的任何更新:
... const ConversationBody = ({ subscribeToMore, userName, messages }) => {
+ React.useEffect(() => {
+ subscribeToMore({
+ document: MESSAGE_ADDED,
+ variables: { userName },
+ updateQuery: (previous, { subscriptionData }) => {
+ if (!subscriptionData.data) {
+ return previous;
+ }
+ const messageAdded = subscriptionData.data.messageAdded;
+
+ return Object.assign({}, previous, {
+ conversation: {
+ ...previous.conversation,
+ messages: [...previous.conversation.messages, messageAdded]
+ }
+ });
+ }
+ });
+ }, []);
return (
<ConversationBodyWrapper>
...
subscribeToMore
方法现在将使用MESSAGES_ADDED
订阅检查任何新消息,该订阅的结果将添加到名为previous
的对象上的Query
组件中。本地 GraphQL 服务器将每隔几秒钟返回一条新消息,因此您可以通过打开对话并等待新消息出现在该对话中来查看订阅是否正常工作。
除了查询,您还希望能够发送实时订阅。这将在本节的最后部分讨论。
对订阅使用突变
除了使用订阅在对话中接收消息外,它们还可用于显示您自己发送的消息。以前,您使用Mutation
组件上的refetchQueries
道具重新发送文档,其中包含可能受您执行的变异影响的任何查询。通过使用订阅,您不再需要重新回迁(例如,会话查询),因为订阅将获得您刚刚发送的新消息并将其添加到查询中。
在上一节中,您使用了来自react-apollo
的Query
组件将文档发送到 GraphQL 服务器,而在本节中,将使用新的 React Apollo 钩子。
The React Apollo Hooks can be used from the react-apollo
package, but if you only want to use the Hooks, you can install @apollo/react-hooks
instead by executing npm install @apollo/react-hooks
. The GraphQL components such as Query
or Mutation
are available in both the react-apollo
and @apollo/react-components
packages. Using these packages will decrease the size of your bundle as you're only importing the features you need.
本包装中的挂钩必须用于ConversationActions
部件。这在Conversation
屏幕组件中使用,该组件由输入字段和发送消息的按钮组成。当您按下此按钮时,不会发生任何事情,因为该按钮未连接到突变。让我们连接此按钮,看看订阅将如何显示您发送的消息:
useMutation
钩子应该导入client/Components/Conversation/ConversationActions.js
文件,该文件将用于将消息从输入字段发送到 GraphQL 服务器。您发送的文档中包含的突变也必须导入,称为SEND_MESSAGE
;可在client/constants.js
文件中找到:
import React from 'react';
import { Platform, Text, View } from 'react-native';
import styled from 'styled-components/native';
import { Ionicons } from '@expo/vector-icons';
+ import { useMutation } from 'react-apollo'; import TextInput from '../TextInput/TextInput';
import Button from '../Button/Button';
+ import { SEND_MESSAGE } from '../../constants';
... const ConversationActions = ({ userName }) => {
...
- 这个
useMutation
钩子现在可以用来包装TextInput
和Button
组件,钩子中的sendMessage
道具可以用来向 GraphQL 服务器发送带有消息的文档。TextInput
的值由useState
钩子创建的setMessage
函数控制,该函数可用于发送突变后清除TextInput
:
...
const ConversationActions = ({ userName }) => {
+ const [sendMessage] = useMutation(SEND_MESSAGE);
const [message, setMessage] = React.useState('');
return (
<ConversationActionsWrapper> + <>
<TextInput
width={75}
marginBottom={0}
onChangeText={setMessage}
placeholder='Your message'
value={message}
/>
<Button
width={20}
padding={10}
+ onPress={() => {
+ sendMessage({ variables: { to: userName, text: message } });
+ setMessage('');
+ }}
title={
<Ionicons
name={`${Platform.OS === 'ios' ? 'ios' : 'md'}-send`}
size={42}
color='white'
/>
}
/>
+ </>
+ </ConversationActionsWrapper>
);
};
通过在文本字段中键入一个值并随后按 send 按钮发送消息,现在将使用您刚刚发送的消息更新对话。但您可能会注意到,根据移动设备屏幕的大小,该组件正在键盘后面丢失。通过使用react-native
中的KeyboardAvoidingView
组件,可以很容易地避免这种行为。此组件将确保输入字段显示在键盘区域之外
KeyboardAvoidingView
组件可以从react-native
导入,用于替换当前正在样式化为ConversationsActionsWrapper
组件的View
组件:
import React from 'react';
- import { Platform, Text, View } from 'react-native';
+ import { Platform, Text, KeyboardAvoidingView } from 'react-native';
import styled from 'styled-components/native';
import { Ionicons } from '@expo/vector-icons';
import { useMutation } from 'react-apollo';
import TextInput from '../TextInput/TextInput';
import Button from '../Button/Button';
import { SEND_MESSAGE } from '../../constants';
- const ConversationActionsWrapper = styled(View)`
+ const ConversationActionsWrapper = styled(KeyboardAvoidingView)`
width: 100%;
background-color: #ccc;
padding: 2%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
`;
const ConversationActions = ({ userName }) => {
...
- 根据移动设备运行的平台,
KeyboardAvoidingView
组件可能仍然无法显示键盘区域外的输入字段。但是,可以使用keyboardVerticalOffset
和behavior
道具定制KeyboardAvoidingView
组件。对于 iOS 和 Android,这些道具的值应该不同;一般来说,Android 需要比 iOS 更小的偏移量。在这种情况下,iOS 必须将keyboardVerticalOffset
设置为190
,Android 必须设置为140
,两种平台的组件的behavior
必须设置为padding
:
...
const ConversationActions = ({ userName }) => {
const [sendMessage] = useMutation(SEND_MESSAGE);
const [message, setMessage] = React.useState('');
return (
- <ConversationActionsWrapper
+ <ConversationActionsWrapper
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 190 : 140}
+ behavior=;padding' + >
<Mutation mutation={SEND_MESSAGE}> ...
KeyboardAvoidingView
might not work as expected on the Android Studio emulator or on devices running Android, as there are a lot of different possible screen sizes for devices that can run the Android operating system.
当您按下输入字段时,键盘将不再隐藏在键盘后面,您应该能够键入并发送一条消息,该消息将向 GraphQL 服务器发送带有变异的文档。您的消息也将出现在先前显示的对话中
总结
在本章中,您构建了一个移动消息传递应用,可用于从 GraphQL 服务器发送和接收消息。这些消息是实时接收的,因为 GraphQL 订阅用于通过 WebSocket 接收消息。此外,还添加了移动身份验证流,这意味着用户应该登录以发送和接收消息。为此,AsyncStorage
用于将 GraphQL 服务器返回的 JWT 令牌存储在持久存储中。
您在本章中构建的项目非常具有挑战性,但您将在下一章中创建的项目将更加先进。到目前为止,您已经处理了 React 本机移动应用的大部分核心功能,但还有更多功能。下一章将探讨如何使用 React-Native 和 GraphQL 构建一个完整的堆栈应用,因为您将向社交媒体应用添加通知和更多内容。
进一步阅读
有关本章内容的更多信息,请参阅以下参考资料:
- WebSocket:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
- 阿波罗反应钩:https://www.apollographql.com/docs/react/api/react-hooks/**
版权属于:月萌API www.moonapi.com,转载请注明出处