Skip to content

Latest commit

 

History

History
991 lines (763 loc) · 35.8 KB

File metadata and controls

991 lines (763 loc) · 35.8 KB

二、阅读器

在本章中,我们将创建一个能够获取、处理和向用户显示多个 RSS 提要的应用。RSS 是一个 web 提要,它允许用户以标准化和计算机可读的格式访问在线内容的更新。它们通常用于新闻网站、新闻聚合器、论坛和博客,以表示更新的内容,非常适合移动世界,因为我们只需在一个应用中输入提要的 URL,就可以获得来自不同博客或报纸的所有内容。

RSS 提要阅读器将作为如何获取外部数据、存储外部数据并向用户显示外部数据的示例,但同时会给状态树增加一点复杂性;我们需要存储和管理提要、条目和帖子的列表。除此之外,我们将引入 MobX 作为一个库来管理所有这些状态模型,并根据用户的操作更新我们的视图。因此,我们将介绍操作和存储的概念,它在一些最流行的状态管理库(如 Redux 或 MobX)中广泛使用。

正如我们在上一章中所做的那样,由于我们在这两个平台上需要的 UI 模式非常相似,我们的目标是在 iOS 和 Android 上共享 100%的代码。

概述

为了更好地理解我们的 RSS 阅读器,让我们看看一旦完成它,应用将是什么样子。

iOS:

安卓:

主屏幕将显示用户已添加的提要列表。它还将在导航标题中显示一个按钮(+),以向列表中添加新提要。按下该按钮后,应用将导航到添加提要屏幕。

网间网操作系统:

安卓:

一旦添加了一个新的提要,它将显示在主屏幕上,用户只需点击它就可以打开它。

网间网操作系统:

安卓:

在此阶段,应用将检索所选提要的更新条目列表,并将其显示在列表中。在导航标题中,垃圾箱图标将允许用户从应用中删除提要。如果用户对任何条目感兴趣,她可以单击该条目以显示该条目的完整内容。

网间网操作系统:

安卓:

最后一个屏幕基本上是一个 WebView,一个默认在 URL 中打开的轻量级浏览器,其中包含所选条目的内容。用户将能够导航到子部分,并在此屏幕中与打开的网站完全交互,还可以通过点击导航标题中的后退箭头返回到提要详细信息。

本章将介绍以下主题:

  • 使用 MobX 进行状态管理
  • 从 URL 获取外部数据
  • 网络视图
  • 基本链接模块与本机资源
  • 添加图标
  • 活动指示器

设置文件夹结构

正如我们在第一章中所做的,我们需要通过 React-Native 的 CLI 初始化一个新的 React-Native 项目。这次,我们将把我们的项目命名为RSSReader

react-native init --version="0.49.3" RSSReader

对于此应用,我们总共需要四个屏幕:

  • FeedList:这是一个包含添加到应用的提要标题的列表,按添加时间排序。
  • AddFeed:这是一个简单的表单,允许用户通过发送其 URL 来添加提要。我们将在这里检索提要详细信息,最终将其添加并保存在我们的应用中,以供以后使用。
  • FeedDetail:这是一个列表,包含属于所选提要的最新条目(在安装屏幕之前检索)。
  • EntryDetail:这是一个显示所选条目内容的网络视图。

除了屏幕,我们还将包括一个actions.js文件,其中包含修改应用状态的所有用户操作。虽然我们将在后面的章节中详细介绍如何管理状态,但还需要注意的是,除了这个actions.js文件之外,我们还需要一个store.js文件来包含状态结构和修改它的方法。

最后,由于大多数 React 本机项目都很正常,我们需要一个index.js文件(已经由 React 本机的 CLI 创建)和一个main.js文件作为我们应用组件树的入口点。

所有这些文件将组织在src/src/screens/文件夹中,如下所示:

添加依赖项

对于这个项目,我们将使用几个 npm 模块来节省开发时间,并将重点放在 RSS 阅读器本身的功能方面,而不是处理自定义状态管理框架、自定义 UI 或数据处理。对于这些事项,我们将使用以下package.json文件:

{  
   "name":"rssReader",
   "version":"0.0.1",
   "private":true,
   "scripts":{  
      "start":"node node_modules/react-native/local-cli/cli.js start",
      "test":"jest"
   },
   "dependencies":{  
      "mobx":"^3.1.9",
      "mobx-react":"^4.1.8",
      "native-base":"^2.1.3",
      "react":"16.0.0-beta.5",
      "react-native": "0.49.3",
      "react-native-vector-icons":"^4.1.1",
      "react-navigation":"^1.0.0-beta.9",
      "simple-xml2json":"^1.2.3"
   },
   "devDependencies":{  
      "babel-jest":"20.0.0",
      "babel-plugin-transform-decorators-legacy":"^1.3.4",
      "babel-preset-react-native":"1.9.1",
      "babel-preset-react-native-stage-0":"^1.0.1",
      "jest":"20.0.0",
      "react-test-renderer":"16.0.0-alpha.6"
   },
   "jest":{  
      "preset":"react-native"
   }
}

如本文件所示,我们将使用以下 npm 模块和标准 React Native 模块:

  • mobx:这是我们将使用的国家管理图书馆
  • mobx-react:这是 MobX 的官方 React 绑定
  • native-base:如前一章所述,我们将使用 NativeBase 的 UI 库
  • react-native-vector-icons:NativeBase 要求该模块显示图形图标
  • react-navigation:我们将再次使用 React 原住民的社区导航库
  • simple-xml2json:一个轻量级库,用于将 XML(RSS 提要的标准格式)转换为 JSON,以便在我们的代码中轻松管理 RSS 数据

有了这个package.json文件,我们可以运行以下命令(在我们项目的根文件夹中)来完成安装:

npm install

一旦 npm 完成所有依赖项的安装,我们就可以在 iOS 模拟器中启动我们的应用:

react-native run-ios

或者在 Android emulator 中:

react-native run-android

使用矢量图标

对于这个应用,我们将使用两个图标:一个加号来添加提要,一个垃圾箱来删除它们。React Native 没有默认使用的图标列表,因此我们需要添加一个。在我们的例子中,因为我们使用native-base作为我们的 UI 库,所以使用react-native-vector-icons非常方便,因为native-base本机支持react-native-vector-icons,但它需要一个额外的配置步骤:

react-native link

有些库使用 React native 中不存在的额外本机功能。在react-native-vector-icons的情况下,我们需要包括一些存储在本机可访问的库中的向量图标。对于这些类型的任务,React Native 包括react-native link,一个脚本,用于自动链接提供的库,以准备所有本机代码和资源,以便在我们的应用中访问此库。很多库都需要这个额外的步骤,但由于 React-Native 的 CLI,这是一个非常简单的步骤,在过去需要在项目中移动文件并处理配置选项。

用 MobX 管理我们的国家

MobX 是一个库,通过透明地应用函数式反应式编程,使状态管理变得简单和可扩展。MobX 背后的原理非常简单:*任何可以从应用状态派生的东西都应该自动派生。*这一原理适用于 UI、数据序列化和服务器通信

在其网站上可以找到大量使用 MobX 的文档和示例 https://mobx.js.org/,虽然我们将在本节中做一个小的介绍,以便在本章中完全理解我们的应用代码。

商店

MobX 使用“可观察”属性的概念。我们应该声明一个包含通用应用状态的对象,它将保存并声明这些可观察属性。当我们修改其中一个属性时,MobX 将自动更新所有订阅的观察者。这是 MobX 背后的基本原理,所以让我们来看一个示例代码:

/*** src/store.js ***/

import {observable} from 'mobx';

class Store {
 @observable feeds;

 ...

 constructor() {
   this.feeds = [];
 }

 addFeed(url, feed) {
   this.feeds.push({ 
     url, 
     entry: feed.entry,
     title: feed.title,
     updated: feed.updated
   });
   this._persistFeeds();
 }

 ...

}

const store = new Store()
export default store

我们有一个属性feeds,标记为@observable,这意味着任何组件都可以订阅它,并在每次更改值时收到通知。该属性在类构造函数中初始化为空数组。

最后,我们还创建了addFeed方法,该方法将向feeds属性中推送一个新提要,因此将触发所有观察者的自动更新。为了更好地理解 MOBX 观察器,让我们看一个观察饲料列表的示例组件:

import React from 'react';
import { Container, Content, List, ListItem, Text } from 'native-base';
import { observer } from 'mobx-react/native';

@observer
export default class FeedsList extends React.Component {

 render() {
  const { feeds } = this.props.screenProps.store;
  return (
    <Container>
      <Content>
        <List>
          {feeds &&
            feeds.map((f, i) => (
              <ListItem key={i}>
                <Text>{f.title}</Text>
              </ListItem>
            ))}
        </List>
      </Content>
    </Container>
  );
 }
}

我们注意到的第一件事是需要用@observer装饰器标记我们的组件,以确保当我们商店中的任何@observable 属性发生变化时,它会被更新

By default, React Native's Babel configuration doesn't support the @<decorator> syntax. In order for it to work, we will need to modify our .babelrc file (found in the root of our project) and add transform-decorator-legacy as a plugin. 

另一件需要注意的事情是需要将存储作为属性在组件中接收。在这种情况下,因为我们使用的是react-navigation,所以我们将它传递到screenProps中,这是react-navigation中在<Navigator>与其子屏幕之间共享属性的标准方式

MobX 还有很多功能,但我们将把这些功能留给更复杂的应用,因为本章的目标之一是展示当我们构建小型应用时,状态管理是多么简单。

开设店铺

了解 MobX 的工作原理后,我们准备创建我们的商店:

/*** src/store.js ** */

import { observable } from 'mobx';
import { AsyncStorage } from 'react-native';

class Store {
  @observable feeds;
  @observable selectedFeed;
  @observable selectedEntry;

  constructor() {
    AsyncStorage.getItem('@feeds').then(sFeeds => {
      this.feeds = JSON.parse(sFeeds) || [];
    });
  }

  _persistFeeds() {
    AsyncStorage.setItem('@feeds', JSON.stringify(this.feeds));
  }

  addFeed(url, feed) {
    this.feeds.push({
      url,
      entry: feed.entry,
      title: feed.title,
      updated: feed.updated,
    });
    this._persistFeeds();
  }

  removeFeed(url) {
    this.feeds = this.feeds.filter(f => f.url !== url);
    this._persistFeeds();
  }

  selectFeed(feed) {
    this.selectedFeed = feed;
  }

  selectEntry(entry) {
    this.selectedEntry = entry;
  }
}

const store = new Store();
export default store;

我们已经在本章的 MobX 部分看到了该文件的基本结构。现在,我们将添加一些方法来修改提要列表,并在用户点击应用列表中的提要/条目时选择特定的提要/条目。

我们还利用AsyncStorage在每次使用addFeedremoveFeed修改提要列表时,将其持久化

定义动作

我们的应用中将有两种类型的操作:影响特定组件状态的操作和影响一般应用状态的操作。我们希望将后者存储在组件代码之外的某个地方,以便重用和轻松维护它们。MobX(以及 Redux 或 Flux)应用中的一个扩展实践是创建一个名为actions.js的文件,我们将在其中存储修改应用业务逻辑的所有操作

在我们的 RSS 阅读器中,业务逻辑围绕提要和条目,因此我们将在此文件中捕获处理这些模型的所有逻辑:

/*** actions.js ** */

import store from './store';
import xml2json from 'simple-xml2json';

export async function fetchFeed(url) {
  const response = await fetch(url);
  const xml = await response.text();
  const json = xml2json.parser(xml);
  return {
    entry:
      (json.feed && json.feed.entry) || (json.rss && 
      json.rss.channel.item),
    title:
      (json.feed && json.feed.title) || (json.rss && 
      json.rss.channel.title),
    updated: (json.feed && json.feed.updated) || null,
  };
}

export function selectFeed(feed) {
  store.selectFeed(feed);
}

export function selectEntry(entry) {
  store.selectEntry(entry);
}

export function addFeed(url, feed) {
  store.addFeed(url, feed);
}

export function removeFeed(url) {
  store.removeFeed(url);
}

由于操作会修改常规应用状态,因此需要访问应用商店。让我们分别来看一下每一个动作:

  • fetchFeed:当用户想要向 RSS 阅读器添加提要时,他需要传递 URL,以便应用可以下载该提要的详细信息(提要标题、最新条目列表以及上次更新的时间)。此操作负责从提供的 URL 检索此数据(格式化为 XML 文档),并将该数据转换为应用标准格式的 JSON 对象。从提供的 URL 获取数据将由fetch执行,这是 React Native 中的内置库,用于向任何 URL 发出 HTTP 请求。由于fetch支持承诺,我们将使用 async/await 来处理异步行为并简化代码。一旦检索到包含提要数据的 XML 文档,我们将使用simple-xml2json将该数据转换为 JSON 对象,这是一个非常轻量级的库,用于满足此类需求。最后,该操作返回一个 JSON 对象,其中只包含我们在应用中真正需要的数据(标题、条目和上次更新时间)。
  • selectFeed:一旦用户向阅读器添加了一个或多个提要,她应该能够选择其中一个,以获取该提要的最新条目列表。此操作仅将特定提要的详细信息保存在存储中,因此任何有兴趣显示该提要相关数据的屏幕(即,FeedDetail屏幕)都可以使用此操作。
  • selectEntry:与selectFeed类似,用户应该能够在提要中选择一个条目,以获取该特定条目的详细信息。在这种情况下,显示该数据的屏幕将是EntryDetail,我们将在后面的部分中看到。
  • addFeed:此操作需要两个参数:提要的 URL 和提要的详细信息。这些参数将用于将提要存储在保存的提要列表中,以便在我们的应用中全局可用。在这个应用中,我们决定使用 URL 作为存储提要详细信息的键,因为它是任何 RSS 提要的唯一属性。
  • removeFeed:用户还可以决定他们不再需要 RSS 阅读器中的特定提要,因此我们需要一个操作将该提要从提要列表中删除。此操作只需要将提要的 URL 作为参数传递,因为我们使用 URL 作为唯一标识提要的 ID 来存储提要。

React Native 中的网络

大多数移动应用需要从外部 URL 获取和更新数据。有几个 npm 模块,可在 React Native 中用于通信和下载远程资源,如 Axios 或 SuperAgent。如果您熟悉特定的 HTTP 库,您可以在 React Native 项目中使用它(只要不依赖于任何特定于浏览器的 API),尽管安全且熟练的选项是使用 React Native 中的内置网络库Fetch

FetchXMLHttpRequest非常相似,因此任何需要从浏览器执行 AJAX 请求的 web 开发人员都会感到熟悉。除此之外,Fetch还支持 promises 和 ES2017 异步/等待语法

FetchAPI 的完整文档可在 Mozilla 开发者网络网站上找到 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

By default, iOS will block any request that's not encrypted using SSL. If you need to fetch from a cleartext URL (one that begins with http instead of https), you will first need to add an App Transport Security (ATS) exception. If you know ahead of time what domains you will need access to, it is more secure to add exceptions just for those domains; if the domains are not known until runtime, you can disable ATS completely. Note, however, that from January 2017, Apple's App Store review will require reasonable justification for disabling ATS. See Apple's documentation for more information.

创建我们应用的入口点

所有 React 原生应用都有一个条目文件:index.js,我们将把组件树的根委托给我们的src/main.js文件:

/*** index.js ***/

import { AppRegistry } from 'react-native';
import App from './src/main';
AppRegistry.registerComponent('rssReader', () => App);

我们还将向操作系统注册我们的应用。

现在,让我们看一看 PosiT0A.file,了解如何设置导航并启动组件的树:

/** * src/main.js ***/

import React from 'react';
import { StackNavigator } from 'react-navigation';

import FeedsList from './screens/FeedsList.js';
import FeedDetail from './screens/FeedDetail.js';
import EntryDetail from './screens/EntryDetail.js';
import AddFeed from './screens/AddFeed.js';

import store from './store';

const Navigator = StackNavigator({
  FeedsList: { screen: FeedsList },
  FeedDetail: { screen: FeedDetail },
  EntryDetail: { screen: EntryDetail },
  AddFeed: { screen: AddFeed },
});

export default class App extends React.Component {
  constructor() {
    super();
  }

  render() {
    return <Navigator screenProps={{ store }} />;
  }
}

我们将使用react-navigation作为导航器库,StackNavigator作为导航模式。将我们的每个屏幕添加到StackNavigator函数以生成我们的<Navigator>。所有这些与我们在第 1 章购物清单中使用的导航模式非常相似,但我们对其进行了改进:我们在<Navigator>screenProps属性中传递store,而不是直接传递属性和方法来修改我们应用的状态。这简化并清理了代码库,正如我们将在后面的部分中看到的,它将使我们在每次状态更改时不必通知导航。多亏了 MobX,所有这些改进都是免费的。

构建 FeedsList 屏幕

提要列表将用作此应用的主屏幕,因此让我们重点构建提要标题列表:

/** * src/screens/FeedsList.js ***/

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

export default class FeedsList extends React.Component {
  render() {
    const { feeds } = this.props.screenProps.store;
    return (
      <Container>
        <Content>
          <List>
            {feeds &&
              feeds.map((f, i) => (
              <ListItem key={i}>
              <Text>{f.title}</Text>
              </ListItem>
             ))
          </List>
        </Content>
      </Container>
    );
  }
}

该组件希望从this.props.screenProps.store接收提要列表,然后在该列表上迭代构建一个 NativeBase<List>,显示存储上每个提要的标题

现在让我们来介绍一些 MobX 魔法。由于我们希望在提要列表更改时(添加或删除提要时)重新呈现组件,因此我们必须使用@observer装饰符标记组件。MobX 将在任何更新时自动强制组件重新呈现。现在让我们看看如何将装饰器添加到组件中:

...

@observer
export default class FeedsList extends React.Component {

...

就这样。现在,当存储发生更改时,我们的组件将收到通知,并将触发重新渲染。

添加事件处理程序

让我们添加一个事件处理程序,当用户点击某个提要的标题时触发,以便该提要的条目列表显示在新屏幕上(FeedDetail

/** * src/screens/FeedsList.js ***/

...

@observer
export default class FeedsList extends React.Component {
  _handleFeedPress(feed) {
    selectFeed(feed);
    this.props.navigation.navigate('FeedDetail', { feedUrl: feed.url });
  }

  render() {
    const { feeds } = this.props.screenProps.store;
    return (
      <Container>
        <Content>
          <List>
            {feeds &&
              feeds.map((f, i) => (
              <ListItem key={i} onPress=
              {this._handleFeedPress.bind(this, f)}>
              <Text>{f.title}</Text>
              </ListItem>
             ))
            }
          </List>
        </Content>
      </Container>
    );
  }
}

...

为此,我们向名为_handleFeedPress的组件添加了一个方法,该方法将接收提要细节作为参数。调用此方法时,它将运行操作selectFeed并触发一个导航事件,将提要的 URL 作为属性传递,因此下一个屏幕(FeedDetail可以包含一个按钮,用于根据该 URL 删除提要

最后,我们将添加navigationOptions,包括导航标题的标题和添加提要的按钮:

/** * src/screens/FeedsList.js ***/

...

@observer
export default class FeedsList extends React.Component {
  static navigationOptions = props => ({
    title: 'My Feeds',
    headerRight: (
      <Button transparent onPress={() => 
      props.navigation.navigate('AddFeed')}>
        <Icon name="add" />
      </Button>
    ),
  });

...

}

按下AddFeed按钮将导航至AddFeed屏幕。通过在navigationOptions中将该按钮作为名为headerRight的属性传递,该按钮将显示在导航标题的右侧。

让我们看看这个组件的整体外观:

/*** src/screens/FeedsList.js ** */

import React from 'react';
import {
  Container,
  Content,
  List,
  ListItem,
  Text,
  Icon,
  Button,
} from 'native-base';
import { observer } from 'mobx-react/native';
import { selectFeed, removeFeed } from '../actions';

@observer
export default class FeedsList extends React.Component {
  static navigationOptions = props => ({
    title: 'My Feeds',
    headerRight: (
      <Button transparent onPress={() => 
       props.navigation.navigate('AddFeed')}>
        <Icon name="add" />
      </Button>
    ),
  });

  _handleFeedPress(feed) {
    selectFeed(feed);
    this.props.navigation.navigate('FeedDetail', { feedUrl: feed.url });
  }

  render() {
    const { feeds } = this.props.screenProps.store;
    return (
      <Container>
        <Content>
          <List>
            {feeds &&
              feeds.map((f, i) => (
              <ListItem key={i} onPress=
              {this._handleFeedPress.bind(this, f)}>
              <Text>{f.title}</Text>
              </ListItem>
             ))
          </List>
        </Content>
      </Container>
    );
  }
}

现在我们已经有了功能齐全的提要列表,让我们允许用户通过AddFeed屏幕添加一些提要。

构建 AddFeed 屏幕

该屏幕由一个基本表单组成,其中一个<Input>用于来自提要的 URL,另一个<Button>用于从提供的 URL 检索提要信息,以便稍后将提要的详细信息存储在我们的存储中

我们需要导入两个动作(addFeedfetchFeed,一旦按下Add按钮就会调用:

/*** src/screens/AddFeed.js ** */

import React from 'react';
import {
  Container,
  Content,
  Form,
  Item,
  Input,
  Button,
  Text,
} from 'native-base';
import { addFeed, fetchFeed } from '../actions';
import { Alert, ActivityIndicator } from 'react-native';

export default class AddFeed extends React.Component {
  static navigationOptions = {
    title: 'Add feed',
  };

  constructor(props) {
    super(props);
    this.state = {
      url: '',
      loading: false,
    };
  }

  _handleAddPress() {
    if (this.state.url.length > 0) {
      this.setState({ loading: true });
      fetchFeed(this.state.url)
        .then(feed => {
          addFeed(this.state.url, feed);
          this.setState({ loading: false });
          this.props.navigation.goBack();
        })
        .catch(() => {
          Alert.alert("Couldn't find any rss feed on that url");
          this.setState({ loading: false });
        });
    }
  }

  render() {
    return (
      <Container style={{ padding: 10 }}>
        <Content>
          <Form>
            <Item>
              <Input
                autoCapitalize="none"
                autoCorrect={false}
                placeholder="feed's url"
                onChangeText={url => this.setState({ url })}
              />
            </Item>
            <Button
              block
              style={{ marginTop: 20 }}
              onPress={this._handleAddPress.bind(this)}
            >
              {this.state.loading && (
                <ActivityIndicator color="white" style={{ margin: 10 }}  
                />
              )}
              <Text>Add</Text>
            </Button>
          </Form>
        </Content>
      </Container>
    );
  }
}

该组件中的大部分功能都在_handleAddPress中,因为它是处理程序,一旦按下Add按钮,就会触发处理程序。该处理程序负责四项任务:

  • 检查是否存在要从中检索数据的 URL
  • 从提供的 URL 检索提要数据(通过fetchFeed操作)
  • 将该数据保存到应用的状态(通过addFeed操作)
  • 如果在获取或保存数据时出现问题,则提醒用户。

需要注意的一件重要事情是fetchFeed动作是如何使用的。因为它是用async语法声明的,所以我们可以将其用作承诺,并将其附加到thencatch的侦听器的结果中

活动指示器

每次应用需要等待 HTTP 请求的响应时,都会显示一个微调器,这是一个很好的做法。iOS 和 Android 都有标准的活动指示器来显示这种行为,并且都可以通过 React Native 模块中的<ActivityIndicator>组件获得

显示该指示器的最简单方法是将loading标志保持在组件状态。由于此标志仅由我们的组件用于显示此<ActivityIndicator>,因此将其置于组件的状态中而不是移动到通用应用的状态是有意义的。然后,可以在render功能中使用:

{ this.state.loading && <ActivityIndicator color='white' style={{margin: 10}}/>}

此语法在 React 应用中非常常见,用于根据标志或简单条件显示或隐藏组件。它利用了 JavaScript 评估&&操作的方式:检查第一个操作数的真实性,如果真实,则返回第二个操作符;否则,它将返回第一个运算符。这种语法将代码行保存在一种非常常见的指令上,因此它将在本书中广泛使用。

构建 FeedDetail 屏幕

让我们回顾一下当用户点击FeedsList屏幕上的一个提要时发生的情况:

_handleFeedPress(feed) {
  selectFeed(feed);
  this.props.navigation.navigate('FeedDetail', { feedUrl: feed.url });
}

navigation属性上调用navigate方法以打开FeedDetail屏幕。作为一个参数,_handleFeedPress函数传递feedUrl,因此它可以检索提要数据并将其显示给用户。这是一个必要的步骤,因为我们存储中所选提要的数据可能已过时。因此,最好在向用户显示数据之前重新获取该数据,这样我们就可以确保数据 100%更新。我们也可以做一个更复杂的检查,而不是每次用户选择一个提要时都检索整个提要,但是为了保持这个应用的简单性,我们将使用给定的方法。

让我们从检索componentWillMount方法中更新的条目列表开始:

/*** src/screens/FeedDetail.js ***/

import React from 'react';
import { observer } from 'mobx-react/native';
import { fetchFeed} from '../actions';

@observer
export default class FeedDetail extends React.Component {
 ... 

 constructor (props) {
  super(props);
  this.state = {
    loading: false,
    entry: null
  }
 }

 componentWillMount() {
  this.setState({ loading: true });
  fetchFeed(this.props.screenProps.store.selectedFeed.url)
   .then((feed) => {
    this.setState({ loading: false });
    this.setState({ entry: feed.entry});
  });
 }

 ...

}

我们会将我们的组件标记为@observer,以便在每次选择的提要更改时更新它。然后,我们需要一个具有两个属性的状态:

  • loading:这是一个向用户发出信号的标志,表示我们正在获取更新提要的数据
  • entry:这是要显示给用户的条目列表

然后,在安装组件之前,我们希望开始检索更新的条目。对于这个问题,我们可以重用我们在AddFeed屏幕中使用的fetchFeed动作。当接收到馈送数据时,组件状态下的loading标志被设置为false,这将隐藏<ActivityIndicator>,并且馈送的条目列表将被设置为组件状态。现在我们有了一个条目列表,让我们看看如何将它显示给用户:

/** * src/screens/FeedDetail.js ** */

import React from 'react';
import {
  Container,
  Content,
  List,
  ListItem,
  Text,
  Button,
  Icon,
  Spinner,
} from 'native-base';
import { observer } from 'mobx-react/native';
import { fetchFeed } from '../actions';
import { ActivityIndicator } from 'react-native';

@observer
export default class FeedDetail extends React.Component {

  ...

  render() {
    const { entry } = this.state;

    return (
      <Container>
        <Content>
          {this.state.loading && <ActivityIndicator style=
          {{ margin: 20 }} />}
          <List>
            {entry &&
              entry.map((e, i) => (
                <ListItem key={i}>
                  <Text>{e.title}</Text>
                </ListItem>
              ))}
          </List>
        </Content>
      </Container>
    );
  }
}

再次使用&&语法显示<ActivityIndicator>,直到检索到数据。一旦数据可用并正确存储在组件状态中的entry属性中,我们将呈现包含所选字段的条目标题的列表项。

现在,我们将添加一个事件处理程序,当用户点击其中一个条目的标题时将触发该事件处理程序:

/** * src/screens/FeedDetail.js ** */

import React from 'react';
import {
  Container,
  Content,
  List,
  ListItem,
  Text,
  Button,
  Icon,
  Spinner,
} from 'native-base';
import { observer } from 'mobx-react/native';
import { selectEntry, fetchFeed } from '../actions';
import { ActivityIndicator } from 'react-native';

@observer
export default class FeedDetail extends React.Component {

  ...

  _handleEntryPress(entry) {
    selectEntry(entry);
    this.props.navigation.navigate('EntryDetail');
  }

  render() {
    const { entry } = this.state;

    return (
      <Container>
        <Content>
          {this.state.loading && <ActivityIndicator style=
          {{ margin: 20 }} />}
          <List>
            {entry &&
              entry.map((e, i) => (
                <ListItem
                  key={i}
                  onPress={this._handleEntryPress.bind(this, e)}
                >
                  <Text>{e.title}</Text>
                </ListItem>
              ))}
          </List>
        </Content>
      </Container>
    );
  }
}

该处理程序名为_handleEntryPress,负责两项任务:

  • 将抽头条目标记为选中
  • 导航到EntryDetail

为了完成组件,让我们通过navigationOptions方法添加导航标题:

/** * src/screens/FeedDetail.js ** */

...

@observer
export default class FeedDetail extends React.Component {
  static navigationOptions = props => ({
    title: props.screenProps.store.selectedFeed.title,
    headerRight: (
      <Button
        transparent
        onPress={() => {
          removeFeed(props.navigation.state.params.feedUrl);
          props.navigation.goBack();
        }}
      >
        <Icon name="trash" />
      </Button>
    ),
  });

  ...

}

除了添加此屏幕的标题(提要的标题),我们还希望在导航中添加一个图标,以便用户能够从应用中存储的提要列表中删除提要。为此,我们将使用native-basetrash图标。按下时,removeFeed动作将被调用,传递当前提要 URL 的 URL,因此可以将其从存储中删除,然后强制导航返回FeedList屏幕。

让我们来看看完成的组件:

/*** src/screens/FeedDetail.js ** */

import React from 'react';
import {
  Container,
  Content,
  List,
  ListItem,
  Text,
  Button,
  Icon,
  Spinner,
} from 'native-base';
import { observer } from 'mobx-react/native';
import { selectEntry, fetchFeed, removeFeed } from '../actions';
import { ActivityIndicator } from 'react-native';

@observer
export default class FeedDetail extends React.Component {
  static navigationOptions = props => ({
    title: props.screenProps.store.selectedFeed.title,
    headerRight: (
      <Button
        transparent
        onPress={() => {
          removeFeed(props.navigation.state.params.feedUrl);
          props.navigation.goBack();
        }}
      >
        <Icon name="trash" />
      </Button>
    ),
  });

  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      entry: null,
    };
  }

  componentWillMount() {
    this.setState({ loading: true });
    fetchFeed(this.props.screenProps.store.selectedFeed.url).
    then(feed => {
      this.setState({ loading: false });
      this.setState({ entry: feed.entry });
    });
  }

  _handleEntryPress(entry) {
    selectEntry(entry);
    this.props.navigation.navigate('EntryDetail');
  }

  render() {
    const { entry } = this.state;

    return (
      <Container>
        <Content>
          {this.state.loading && <ActivityIndicator style=
          {{ margin: 20 }} />}
          <List>
            {entry &&
              entry.map((e, i) => (
              <ListItem key={i} onPress=
              {this._handleEntryPress.bind(this, e)}>
              <Text>{e.title}</Text>
          </ListItem>
          ))
          </List>
        </Content>
      </Container>
    );
  }
}

现在,让我们转到最后一个屏幕:EntryDetail

构建 EntryDetail 屏幕

EntryDetail屏幕就是 WebView:一个能够在本机视图中呈现 web 内容的组件。您可以将 WebView 视为一种轻量级 web 浏览器,显示所提供 URL 的网站内容:

import React from 'react';
import { Container, Content } from 'native-base';
import { WebView } from 'react-native';

export default class EntryDetail extends React.Component {
  render() {
    const entry = this.props.screenProps.store.selectedEntry;
    return <WebView source={{ uri: entry.link.href || entry.link }} />;
  }
}

此组件中的render方法只是返回一个新的WebView组件,该组件从存储区内的所选条目加载 URL。正如我们在前面几节中处理提要的数据一样,我们需要从this.props.screenProps.store中检索selectedEntry数据。根据提要的 RSS 版本,URL 可以以两种不同的方式存储:在链接属性中或在link.href中更深一层。

总结

当复杂性开始增长时,每个应用都需要一个状态管理库。根据经验,当应用由四个以上的屏幕组成并且它们之间共享信息时,最好添加一个状态管理库。对于这个应用,我们使用了 MobX,它简单但功能强大,足以处理所有提要和条目的数据。在本章中,您学习了 MobX 的基础知识以及如何将其与react-navigation结合使用。理解 Action 和 Store 的概念很重要,因为我们将在未来的应用中使用它们,不仅是围绕 MobX 构建的应用,而且还将在 Redux 上使用

您还学习了如何从远程 URL 获取数据。这是大多数移动应用中非常常见的操作,尽管我们只介绍了它的基本用法。在接下来的章节中,我们将深入探讨FetchAPI。此外,我们还了解了如何处理和格式化获取的数据,以便在我们的应用中对其进行形式化。

最后,我们回顾了什么是 WebView,以及如何将 web 内容插入本机应用。这可以使用本地 HTML 字符串或通过 URL 远程完成,因此这是移动开发人员用来重用或访问纯 web 内容的一个非常强大的技巧。