新的标准是,应用将在不可靠的网络连接下无缝运行。如果您的移动应用无法处理暂时的网络问题,那么您的用户只需使用不同的应用即可。当没有网络时,您必须在设备上本地保存数据。或者你的应用甚至不需要网络访问,在这种情况下,你仍然需要在本地存储数据。
在本章中,您将学习如何使用 React Native 做三件事。首先,您将学习如何检测网络连接的状态。其次,您将学习如何在本地存储数据。最后,您将学习如何同步由于网络问题而存储的本地数据,一旦这些数据重新联机。
如果您的代码试图在断开连接时通过网络发出请求,例如使用fetch()
,则会发生错误。您可能已经为这些场景准备好了错误处理代码,因为服务器可能会返回其他类型的错误。但是,在连接出现问题的情况下,您可能希望在用户尝试发出网络请求之前检测此问题。
主动检测网络状态有两个潜在原因。您可能会向用户显示一条友好消息,说明由于网络已断开连接,用户无法执行任何操作。然后,您将阻止用户执行任何网络请求,直到您检测到它重新联机。早期网络状态检测的另一个可能好处是,当网络再次连接时,您可以准备脱机执行操作并同步应用状态。
让我们来看一些使用NetInfo
实用程序处理网络状态变化的代码:
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View,
NetInfo,
} from 'react-native';
import { fromJS } from 'immutable';
import styles from './styles';
// Maps the state returned from "NetInfo" to
// a string that we want to display in the UI.
const connectedMap = {
none: 'Disconnected',
unknown: 'Disconnected',
wifi: 'Connected',
cell: 'Connected',
mobile: 'Connected',
};
class NetworkState extends Component {
// The "connected" state is a simple
// string that stores the state of the
// network.
state = {
data: fromJS({
connected: '',
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
// When the network state changes, use the
// "connectedMap" to find the string to display.
onNetworkChange = (info) => {
this.data = this.data.set(
'connected',
connectedMap[info]
);
}
// When the component is mounted, we add a listener
// that changes the "connected" state when the
// network state changes.
componentWillMount() {
NetInfo.addEventListener(
'change',
this.onNetworkChange
);
}
// Make sure the listener is removed...
componentWillUnmount() {
NetInfo.removeEventListener(
'change',
this.onNetworkChange
);
}
// Simply renders the "connected" state as
// it changes.
render() {
return (
<View style={styles.container}>
<Text>{this.data.get('connected')}</Text>
</View>
);
}
}
AppRegistry.registerComponent(
'NetworkState',
() => NetworkState
);
该组件将根据connectedMap
中的字符串值呈现网络状态。NetInfo
对象的change
事件将导致connected
状态改变。例如,当您第一次运行此应用时,屏幕可能如下所示:
然后,如果关闭主机上的网络,模拟设备上的网络状态也将发生变化,从而导致应用的状态发生变化:
在构建这个示例时,我遇到了一些问题,当网络状态更改时,如何使更改事件始终触发。这是 iOS 和 Android 设备模拟器的一个问题。因此,如果您正在编写依赖于网络状态检测的代码,可能的话,您可能希望在物理设备上测试它。
现在,让我们将注意力转移到在 React 本机应用中存储数据。AsyncStorage
API 在 iOS 和 Android 平台上的工作原理相同。您可以将此 API 用于最初不需要任何网络连接的应用,或者存储网络可用后最终使用 API 端点同步的数据。
让我们看一些代码,这些代码允许用户输入一个键和一个值,然后存储它们:
import React, { Component } from 'react';
import {
AppRegistry,
Text,
TextInput,
View,
ListView,
AsyncStorage,
} from 'react-native';
import { fromJS } from 'immutable';
import styles from './styles';
import Button from './Button';
class StoringData extends Component {
// The initial state of this component
// consists of the current "key" and "value"
// that the user is entering. It also has
// a "source" for the list view to display
// everything that's been stored.
state = {
data: fromJS({
key: null,
value: null,
source: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
}),
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
// Uses "AsyncStorage.setItem()" to store
// the current "key" and "value" states.
// When this completes, we can delete
// "key" and "value" and reload the item list.
setItem = () =>
AsyncStorage
.setItem(
this.data.get('key'),
this.data.get('value')
)
.then(() => {
this.data = this.data
.delete('key')
.delete('value');
})
.then(() => this.loadItems())
// Uses "AsyncStorage.clear()" to empty any stored
// values. Then, it loads the empty list of
// items to clear the item list on the screen.
clearItems = () =>
AsyncStorage
.clear()
.then(() => this.loadItems())
// This method is async because awaits on the
// data store keys and values, which are two
// dependent async calls.
async loadItems() {
const keys = await AsyncStorage.getAllKeys();
const values = await AsyncStorage.multiGet(keys);
this.data = this.data
.update(
'source',
source => source.cloneWithRows(values)
);
}
// Load any existing items that have
// already been stored when the app starts.
componentWillMount() {
this.loadItems();
}
render() {
// The methods that we need...
const {
setItem,
clearItems,
} = this;
// The state that we need...
const {
source,
key,
value,
} = this.data.toJS();
return (
<View style={styles.container}>
<Text>Key:</Text>
<TextInput
style={styles.input}
value={key}
onChangeText={(v) => {
this.data = this.data.set('key', v);
}}
/>
<Text>Value:</Text>
<TextInput
style={styles.input}
value={value}
onChangeText={(v) => {
this.data = this.data.set('value', v);
}}
/>
<View style={styles.controls}>
<Button
label="Add"
onPress={setItem}
/>
<Button
label="Clear"
onPress={clearItems}
/>
</View>
<View style={styles.list}>
<ListView
enableEmptySections
dataSource={source}
renderRow={([k, v]) => (
<Text>{v} ({k})</Text>
)}
/>
</View>
</View>
);
}
}
AppRegistry.registerComponent(
'StoringData',
() => StoringData
);
在我们了解这段代码的作用之前,让我们先看看屏幕,因为它将为我提供大部分解释:
如您所见,有两个输入字段和两个按钮。这些字段允许用户输入新的键和值。添加按钮允许用户在其设备上本地存储该键值对,而清除按钮则清除先前存储的任何现有项。
AsyncStorage
API 在 iOS 和 Android 上的工作原理相同。在引擎盖下,AsyncStorage
的工作原理非常不同,这取决于它在哪个平台上运行。React Native 能够在两个平台上公开相同的存储 API 的原因在于它的简单性—它只是简单的键值对。任何比这更复杂的事情都留给应用开发人员。
在本例中,我们在AsyncStorage
附近创建的抽象非常简单。想法是简单地设置和获取项目。然而,即使像这样简单的事情也需要一些抽象。例如,我们在这里实现的setItem()
方法将对AsyncStorage
进行异步调用,并在完成后更新items
状态。加载项更为复杂,因为我们需要将键和值作为两个单独的异步操作来获取。
因此,您可能想知道,为什么需要为简单的存储调用提供所有异步性?主要原因是保持 UI 的响应性。如果在将数据写入磁盘时需要进行挂起的屏幕重绘,则通过阻止这些重绘来防止这些重绘的发生将导致次优的用户体验。
到目前为止,在本章中,您已经了解了如何检测网络连接的状态,以及如何在本地应用中本地存储数据。现在是时候将这两个概念结合起来,实现一个能够检测网络中断并继续运行的应用了。
其基本思想是仅在我们确定设备处于联机状态时才发出网络请求。如果我们知道它不是,我们可以在本地存储状态中的任何更改。然后,当我们重新联机时,我们可以将这些存储的更改与远程 API 同步。
让我们实现一个简化的 React 本机应用来实现这一点。第一步是实现位于 React 组件和存储数据的网络调用之间的抽象。我们将此模块称为store.js
:
import {
NetInfo,
AsyncStorage,
} from 'react-native';
import { Map as ImmutableMap } from 'immutable';
// Mock data that would otherwise come from a real
// networked API endpoint.
const fakeNetworkData = {
first: false,
second: false,
third: false,
};
// We'll assume that the device isn't "connected"
// by default.
let connected = false;
// There's nothing to sync yet...
const unsynced = [];
// Sets the given "key" and "value". The idea
// is that application that uses this function
// shouldn't care if the network is connected
// or not.
export const set = (key, value) =>
// The returned promise resolves to true
// if the network is connected, false otherwise.
new Promise((resolve, reject) => {
if (connected) {
// We're online - make the proper request (or fake
// it in this case) and resolve the promise.
fakeNetworkData[key] = value;
resolve(true);
} else {
// We're offline - save the item using "AsyncStorage"
// and add the key to "unsynced" so that we remember
// to sync it when we're back online.
AsyncStorage
.setItem(key, value.toString())
.then(
() => {
unsynced.push(key);
resolve(false);
},
err => reject(err)
);
}
});
// Gets the given key/value. The idea is that the application
// shouldn't care whether or not there is a network connection.
// If we're offline and the item hasn't been synced, read it
// from local storage.
export const get = key =>
new Promise((resolve, reject) => {
if (connected) {
// We're online. Resolve the requested data.
resolve(
key ?
fakeNetworkData[key] :
fakeNetworkData
);
} else if (key) {
// We've offline and they're asking for a specific key.
// We need to look it up using "AsyncStorage".
AsyncStorage
.getItem(key)
.then(
item => resolve(item),
err => reject(err)
);
} else {
// We're offline and they're asking for all values.
// So we grab all keys, then all values, then we
// resolve a plain JS object.
AsyncStorage
.getAllKeys()
.then(
keys => AsyncStorage
.multiGet(keys)
.then(
items => resolve(ImmutableMap(items).toJS()),
err => reject(err)
),
err => reject(err)
);
}
});
// Check the network state when the module first
// loads so that we have an accurate value for "connected".
NetInfo.isConnected
.fetch()
.then(
(isConnected) => { connected = isConnected; },
() => { connected = false; }
);
// Register a handler for when the state of the network changes.
NetInfo.addEventListener(
'change',
(info) => {
// Update the "connected" state...
connected = [
'wifi',
'unknown',
].includes(info.toLowerCase());
// If we're online and there's unsynced values,
// load them from the store, and call "set()"
// on each of them.
if (connected && unsynced.length) {
AsyncStorage
.multiGet(unsynced)
.then((items) => {
items.forEach(([key, val]) => set(key, val));
unsynced.length = 0;
});
}
}
);
此模块导出两个功能-set()
和get()
。毫不奇怪,他们的工作是分别设置和获取数据。由于这只是演示如何在本地存储和网络端点之间进行同步,因此此模块仅使用fakeNetworkData
对象模拟实际网络。
让我们先看一下set()
函数。如您所见,它是一个异步函数,总是返回解析为布尔值的承诺。如果这是真的,那就意味着我们在线了,通过网络的呼叫成功了。如果为 false,则表示我们处于脱机状态,AsyncStorage
用于保存数据。
get()
功能也采用相同的方法。它返回一个承诺,该承诺解析一个指示网络状态的布尔值。如果提供了键参数,则会查找该键的值。否则,将从网络或AsyncStorage
返回所有值。
除了这两个功能外,这个模块还做两件事。使用NetInfo.fetch()
设置connected
状态。然后,它添加了一个侦听器,用于网络状态的更改。这就是我们脱机时本地保存的项目在再次连接网络时与网络同步的方式。
好的,现在让我们检查一下使用这些函数的主应用:
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View,
Switch,
NetInfo,
} from 'react-native';
import { fromJS } from 'immutable';
import styles from './styles';
import { set, get } from './store';
// Used to provide consistent boolean values
// for actual booleans and their string representations.
const boolMap = {
true: true,
false: false,
};
class SynchronizingData extends Component {
// The message state is used to indicate that
// the user has gone offline. The other state
// items are things that the user wants to change
// and sync.
state = {
data: fromJS({
message: null,
first: false,
second: false,
third: false,
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
// Generates a handler function bound to a given key.
save = key => (value) => {
// Calls "set()" and depending on the resolved value,
// sets the user message.
set(key, value)
.then(
(connected) => {
this.data = this.data
.set(
'message',
connected ? null : 'Saved Offline'
)
.set(key, value);
},
(err) => {
this.data = this.data.set('message', err);
}
);
}
componentWillMount() {
// We have to call "NetInfo.fetch()" before
// calling "get()" to ensure that the
// connection state is accurate. This will
// get the initial state of each item.
NetInfo.fetch().then(() =>
get()
.then(
(items) => {
this.data = this.data.merge(items);
},
(err) => {
this.data = this.data.set('message', err);
}
)
);
}
render() {
// Bound methods...
const { save } = this;
// State...
const {
message,
first,
second,
third,
} = this.data.toJS();
return (
<View style={styles.container}>
<Text>{message}</Text>
<View>
<Text>First</Text>
<Switch
value={boolMap[first.toString()]}
onValueChange={save('first')}
/>
</View>
<View>
<Text>Second</Text>
<Switch
value={boolMap[second.toString()]}
onValueChange={save('second')}
/>
</View>
<View>
<Text>Third</Text>
<Switch
value={boolMap[third.toString()]}
onValueChange={save('third')}
/>
</View>
</View>
);
}
}
AppRegistry.registerComponent(
'SynchronizingData',
() => SynchronizingData
);
如您所见,我们在这里所做的只是保存三个复选框的状态,这很容易,除非您为用户提供在线和离线模式之间的无缝转换。谢天谢地,我们在另一个模块中实现的set()
和get()
抽象,对应用功能隐藏了大部分细节。
但是,您会注意到,在尝试加载任何项目之前,我们需要检查此模块中的网络状态。如果我们不这样做,get()
函数将假定我们处于脱机状态,即使连接正常。以下是该应用的外观:
请注意,在您更改 UI 中的某些内容之前,您实际上不会看到脱机保存的消息。
本章介绍如何在 React 本机应用中脱机存储数据。您希望在本地存储数据的主要原因是当设备脱机且您的应用无法与远程 API 通信时。但是,并非所有应用都需要 API 调用,AsyncStorage
可以用作通用存储机制。您只需要围绕它实现适当的抽象。
您还学习了如何在 React 本机应用中检测网络状态的变化。了解设备何时脱机非常重要,这样存储层就不会在网络呼叫时进行无意义的尝试。相反,您可以让用户知道设备处于脱机状态,然后在连接可用时同步应用状态。
这就结束了本书的第二部分。您已经了解了如何为 Web 构建 React 组件,以及如何为移动平台构建 React 组件。在本书的开头,我假设 React 的美妙之处在于渲染目标的概念。React 的声明性编程接口永远不必更改。翻译 JSX 元素的底层机制在理论上是完全可替换的,我们可以将 React 渲染为任何东西。
在本书的最后一部分,我将讨论 React 应用中的状态。状态和控制它如何流经应用的策略可以决定 React 体系结构的成败。