十六、展示项目清单

在本章中,您将学习如何使用项目列表。在为 Web 构建应用时,使用列表是一种常见的开发活动。使用<ul><li>元素构建列表也相对简单。在本地移动平台上尝试做类似的事情要复杂得多。

谢天谢地,React Native 提供了一个简单的条目列表界面,隐藏了所有的复杂性。我们将通过浏览一个示例来了解项目列表是如何工作的。然后,我们将介绍一些更改列表中显示的数据的控件。最后,您将看到几个从网络获取项目的示例。

渲染数据采集

让我们从一个基本的例子开始。用于呈现列表的 React 本机组件是ListView,它在 iOS 和 Android 上的工作方式相同。列表视图采用数据源属性,该属性必须是ListView.DataSource实例。别担心;在大多数情况下,它实际上只是数组的包装器。ListView组件需要这种类型的数据源的原因是,它可以执行高效的渲染。列表可能很长,频繁更新会导致性能问题。

那么,让我们现在实施一个基本列表,好吗?下面是呈现基本 100 项列表的代码:

import React from 'react'; 
import { 
  AppRegistry, 
  Text, 
  View, 
  ListView, 
} from 'react-native'; 

import styles from './styles'; 

// You always need a comparator function that's 
// used to determine whether or not a row has 
// changed. Even in simple cases like this, where 
// strict inequality is used. 
const rowHasChanged = (r1, r2) => r1 !== r2; 

const source = new ListView 
  // A data source for the list. It's eventually passed 
  // to the "<ListView>" component, and it requires a 
  // "rowHasChanged()" comparator. 
  .DataSource({ rowHasChanged }) 

  // Returns a clone of the data source with new data. 
  // The comparator function is used by the "<ListView>" 
  // component to determine what has changed. 
  .cloneWithRows( 
    new Array(100) 
      .fill(null) 
      .map((v, i) => `Item ${i}`) 
  ); 

const RenderingDataCollections = () => ( 
  <View style={styles.container}> 
    { /* Renders the list by providing a "dataSource" 
         property and a "renderRow" function which 
         renders each item in the list. */ } 
    <ListView 
      dataSource={source} 
      renderRow={ 
        i => (<Text style={styles.item}>{i}</Text>) 
      } 
    /> 
  </View> 
); 

AppRegistry.registerComponent( 
  'RenderingDataCollections', 
  () => RenderingDataCollections 
); 

让我们从source常数开始,来看看这里发生了什么。如您所见,这是使用ListView.DataSource()构造函数创建的。这里,我们给它一个rowHasChanged()函数。需要告诉数据源如何查找更改,即使是简单的相等性检查。然后,我们将实际数据传递到cloneWithRows()方法中。这实际上会导致数据源的一个新实例,而且实际上是一个令人困惑的名称,因为实际上没有克隆任何数据。您要克隆的只是您为数据源提供的选项,例如rowHasChanged()函数。DataSource实例是不可变的,我们将在下面的示例中看到如何实际更新它们。

接下来,我们呈现<ListView>组件本身。它位于<View>容器中,因为列表视图需要一个高度,才能使滚动正常工作。sourcerenderRow属性被传递给<ListView>,最终确定渲染内容。

乍一看,ListView组件似乎对我们没有太大帮助。我们得弄清楚这些东西是什么样子的?嗯,是的,ListView应该是通用的。它应该擅长处理更新,并为我们将滚动功能嵌入到列表中。以下是用于呈现列表的样式:

import { StyleSheet } from 'react-native'; 

export default StyleSheet.create({ 
  container: { 
    // Flexing from top to bottom gives the 
    // container a height, which is necessary 
    // to enable scrollable content. 
    flex: 1, 
    flexDirection: 'column', 
    paddingTop: 20, 
  }, 

  item: { 
    margin: 5, 
    padding: 5, 
    color: 'slategrey', 
    backgroundColor: 'ghostwhite', 
    textAlign: 'center', 
  }, 
}); 

这里,我们为列表中的每个项目提供一个基本样式。否则,每个项目将仅为文本,很难区分其他列表项目。容器样式通过将“弯曲方向”设置为“列”,为列表提供高度。如果没有高度,您将无法正确滚动。

让我们看看这东西现在是什么样子,好吗?

Rendering data collections

如果您在模拟器中运行此示例,您可以单击并按住屏幕上的任何位置(如手指)的鼠标按钮,然后向上或向下滚动项目。

排序过滤列表

既然您已经掌握了ListView组件的基本知识,并将它们传递给DataSource实例,那么让我们向刚刚实现的列表中添加一些控件。ListView组件本身帮助您为列表控件呈现固定位置的内容。您还将看到如何操作数据源本身,最终驱动屏幕上呈现的内容。

在我们开始实现列表控件组件之前,如果我们检查一下这些组件的高级结构,使代码具有更多的上下文,可能会有所帮助。下面是我们将要实现的组件结构的示例:

Sorting and filtering lists

以下是每个组件的职责:

  • ListContainer:列表的整体容器;它遵循熟悉的 React 容器模式
  • List:一个无状态组件,将相关的状态片段传递到ListControlsListView组件中
  • ListControls:包含更改列表状态的各种控件的组件
  • ListFilter:用于过滤项目列表的控件
  • ListSort:用于更改列表排序顺序的控件
  • ListView:呈现项目的实际 ReactNative 组件

在某些情况下,像这样拆分列表的实现是过分的。然而,我认为,如果您的列表首先需要控件,那么您可能正在实现一些东西,这些东西将受益于经过深思熟虑的组件体系结构。

现在,让我们深入了解这个列表的实现,从ListContainer组件开始:

import React, { Component } from 'react'; 
import { ListView } from 'react-native'; 

import List from './List'; 

// The two comparator functions we need to pass 
// to the data source. The "rowHasChanged()" function 
// does simple strict inequality. So does 
// "sectionHeaderHasChanged()". 
const rowHasChanged = (r1, r2) => r1 !== r2; 
const sectionHeaderHasChanged = rowHasChanged; 

// Performs sorting and filtering on the given "data". 
const filterAndSort = (data, text, asc) => 
  data.filter( 
    i => 
      // Items that include the filter "text" are returned. 
      // Unless the "text" argument is an empty string, 
      // then everything is included. 
      text.length === 0 || 
      i.includes(text) 
  ).sort( 
    // Sorts either ascending or descending based on "asc". 
    asc ? 
      (a, b) => b > a ? -1 : (a === b ? 0 : 1) : 
      (a, b) => a > b ? -1 : (a === b ? 0 : 1) 
  ); 

class ListContainer extends Component { 
  constructor() { 
    super(); 

    // The initial state. The "data" is what drives 
    // the list, and it's initially filtered and sorted 
    // here. 
    this.state = { 
      data: filterAndSort( 
        new Array(100) 
          .fill(null) 
          .map((v, i) => `Item ${i}`) 
        , '', true), 
      asc: true, 
      filter: '', 
    }; 

    // The "source" is also part of the component state, 
    // but it's based on "state.data", which is why it's 
    // set here. This is the data source that's ultimately 
    // used by the "<ListView>". 
    this.state.source = new ListView 
      .DataSource({ 
        rowHasChanged, 
        sectionHeaderHasChanged, 
      }) 
      .cloneWithRows(this.state.data); 
  } 

  render() { 
    return ( 
      <List 
        source={this.state.source} 
        asc={this.state.asc} 
        onFilter={(text) => { 
          // Updates the "filter" state, the actual filter  
          // text, and the "source" of the list. The "data" 
          // state is never actually touched - 
          // "filterAndSort()" doesn't mutate anything. 
          this.setState({ 
            filter: text, 
            source: this.state.source.cloneWithRows( 
              filterAndSort( 
                this.state.data, 
                text, 
                this.state.asc 
              ) 
            ), 
          }); 
        }} 
        onSort={() => { 
          this.setState({ 
            // Updates the "asc" state in order to change  
            // the order of the list. The same principles as 
            // used in the "onFilter()" handler are applied 
            // here, only with different arguments passed to 
            // "filterAndSort()" 
            asc: !this.state.asc, 
            source: this.state.source.cloneWithRows( 
              filterAndSort( 
                this.state.data, 
                this.state.filter, 
                !this.state.asc 
              ) 
            ), 
          }); 
        }} 
      /> 
    ); 
  } 
} 

export default ListContainer; 

如果这看起来有点过分,那是因为它是。这个容器组件有很多状态需要处理。它也有一些非平凡的行为,需要让它的孩子们可以使用。因此,如果您从封装状态的角度来看,这似乎并不复杂。它的任务是用状态数据填充列表,并提供在此状态下运行的函数。

在理想情况下,这个容器的子组件应该很好而且简单,因为它们不必直接与状态接口。让我们来看看下一个组件:

import React, { PropTypes } from 'react'; 
import { Text, ListView } from 'react-native'; 

import styles from './styles'; 
import ListControls from './ListControls'; 

// Renders the actual "<ListView>" React Native 
// component. The "renderSectionHeader" property 
// is where our controls go. The "renderRow" 
// property, as always, renders the actual item. 
const List = ({ 
  Controls, 
  source, 
  onFilter, 
  onSort, 
  asc, 
}) => ( 
  <ListView 
    enableEmptySections 
    dataSource={source} 
    renderSectionHeader={() => ( 
      <Controls 
        {...{ onFilter, onSort, asc }} 
      /> 
    )} 
    renderRow={i => ( 
      <Text style={styles.item}>{i}</Text> 
    )} 
  /> 
); 

List.propTypes = { 
  Controls: PropTypes.func.isRequired, 
  source: PropTypes.instanceOf(ListView.DataSource).isRequired, 
  onFilter: PropTypes.func.isRequired, 
  onSort: PropTypes.func.isRequired, 
  asc: PropTypes.bool.isRequired, 
}; 

// The "Controls" component is actually our own 
// "ListControls" component by default. However, 
// this can be overriden by anyone wanting to provide 
// their own control components. 
List.defaultProps = { 
  Controls: ListControls, 
}; 

export default List; 

该组件将来自ListContainer组件的状态作为属性,并呈现ListView组件。与前面的示例相比,这里的主要区别在于renderSectionHeader属性。此函数用于呈现列表的控件。此属性特别有用的是,它在可滚动列表内容之外呈现控件,确保控件始终可见。

还有一个renderHeader属性,它的作用与renderSectionHeader基本相同;然而,这个位置并不是固定的。

另外,请注意,我们将自己的ListControls组件指定为controls属性的默认值。这使得其他人很容易传入自己的列表控件。让我们看看下面的 To.T2U.组件:

import React, { PropTypes } from 'react'; 
import { View } from 'react-native'; 

import styles from './styles'; 
import ListFilter from './ListFilter'; 
import ListSort from './ListSort'; 

// Renders the "<ListFilter>" and "<ListSort>" 
// components within a "<View>". The 
// "styles.controls" style lays out the controls 
// horizontally. 
const ListControls = ({ 
  onFilter, 
  onSort, 
  asc, 
}) => ( 
  <View style={styles.controls}> 
    <ListFilter onFilter={onFilter} /> 
    <ListSort onSort={onSort} asc={asc} /> 
  </View> 
); 

ListControls.propTypes = { 
  onFilter: PropTypes.func.isRequired, 
  onSort: PropTypes.func.isRequired, 
  asc: PropTypes.bool.isRequired, 
}; 

export default ListControls; 

这可能是示例中迄今为止最简单的组件。它将ListFilterListSort控件结合在一起。因此,如果您要添加另一个列表控件,您可以将其添加到此处。现在让我们来看一下 Type T2 实现:

import React, { PropTypes } from 'react'; 
import { View, TextInput } from 'react-native'; 

import styles from './styles'; 

// Renders a "<TextInput>" component which allows the 
// user to type in their filter text. This causes 
// the "onFilter()" event handler to be called. 
// This handler comes from "ListContainer" and changes 
// the state of the list data source. 
const ListFilter = ({ onFilter }) => ( 
  <View> 
    <TextInput 
      autoFocus 
      placeholder="Search" 
      style={styles.filter} 
      onChangeText={onFilter} 
    /> 
  </View> 
); 

ListFilter.propTypes = { 
  onFilter: PropTypes.func.isRequired, 
}; 

export default ListFilter; 

filter 控件是一个简单的文本输入,它将项列表过滤为用户类型。处理这个问题的 onChange函数来自ListContainer组件。关于他的组件,最需要注意的是它是多么简单和明显。关于它的作用没有任何混淆;当用户在输入框中键入时,它调用一些函数。

ListSort组件具有与之类似的简单性:

import React, { PropTypes } from 'react'; 
import { Text } from 'react-native'; 

// The arrows to render based on the state of 
// the "asc" property. Using a Map let's us 
// stay declarative, rather than introducing 
// logic into the JSX. 
const arrows = new Map([ 
  [true, '▼'], 
  [false, '▲'], 
]); 

// Renders the arrow text. When clicked, the 
// "onSort()" function that's passed down from 
// the container. 
const ListSort = ({ onSort, asc }) => ( 
  <Text onPress={onSort}>{arrows.get(asc)}</Text> 
); 

ListSort.propTypes = { 
  onSort: PropTypes.func.isRequired, 
  asc: PropTypes.bool.isRequired, 
}; 

export default ListSort; 

下面是结果列表:

Sorting and filtering lists

默认情况下,整个列表按升序呈现。当用户尚未提供任何内容时,您可以看到占位符文本搜索。让我们看看当我们输入一个过滤器并更改排序顺序时的情况:

Sorting and filtering lists

此搜索包括包含 1 的项目,并按降序对结果进行排序。请注意,您可以先更改顺序,也可以先输入过滤器。过滤器和排序顺序都是ListContainer状态的一部分。

取列表数据

通常,您会从某个 API 端点获取列表数据。在本节中,您将了解如何从 React 本机组件发出 API 请求。好消息是fetch()API 由 React Native 填充,因此移动应用中的网络代码看起来和感觉应该与 web 应用中的代码非常相似。

首先,让我们使用 fetch mock 为列表项构建一个模拟 API:

import fetchMock from 'fetch-mock'; 
import querystring from 'querystring'; 

// A mock item list... 
const items = new Array(100) 
  .fill(null) 
  .map((v, i) => `Item ${i}`); 

// The same filter and sort functionality 
// as the previous example, only it's part of the 
// API now, instead of part of the React component. 
const filterAndSort = (data, text, asc) => 
  data.filter( 
    i => 
      text.length === 0 || 
      i.includes(text) 
  ).sort( 
    asc ? 
      (a, b) => b > a ? -1 : (a === b ? 0 : 1) : 
      (a, b) => a > b ? -1 : (a === b ? 0 : 1) 
  ); 

// Defines the mock handler for the "/items" URL. 
fetchMock.mock(/\/items.*/, (url) => { 
  // Gets the "filter" and "asc" parameters. 
  const params = querystring.parse(url.split('?')[1]); 

  // Performs the sorting and filtering before 
  // responding. 
  return ({ 
    items: filterAndSort( 
      items, 
      params.filter ? params.filter : '', 
      !!+params.asc 
    ), 
  }); 
}); 

在 mockapi 端点就绪后,让我们对列表容器组件进行一些更改。您现在可以使用fetch()函数从 API 模拟加载数据,而不是使用本地数据源:

import React, { Component } from 'react'; 
import { ListView } from 'react-native'; 

// Note that we're importing mock here to enable the API. 
import './mock'; 
import List from './List'; 

const rowHasChanged = (r1, r2) => r1 !== r2; 
const sectionHeaderHasChanged = rowHasChanged; 

// Fetches items from the API using 
// the given "filter" and "asc" arguments. The 
// returned promise resolves a JavaScript object. 
const fetchItems = (filter, asc) => 
  fetch(`/items?filter=${filter}&asc=${+asc}`) 
    .then(resp => resp.json()); 

class ListContainer extends Component { 
  constructor() { 
    super(); 

    // The "source" state is empty because we need 
    // to fetch the data from the API. 
    this.state = { 
      // data: [], 
      asc: true, 
      filter: '', 
      source: new ListView 
        .DataSource({ 
          rowHasChanged, 
          sectionHeaderHasChanged, 
        }) 
        .cloneWithRows([]), 
    }; 
  } 

  // When the component is first mounted, fetch the initial 
  // items from the API, then 
  componentDidMount() { 
    fetchItems(this.state.filter, this.state.asc) 
      .then(({ items }) => { 
        this.setState({ 
          source: this.state.source.cloneWithRows(items), 
        }); 
      }); 
  } 

  render() { 
    return ( 
      <List 
        source={this.state.source} 
        asc={this.state.asc} 
        onFilter={text => { 
          // Makes an API call when the filter changes... 
          fetchItems(text, this.state.asc) 
            .then(({ items }) => 
              this.setState({ 
                filter: text, 
                source: this.state.source.cloneWithRows(items), 
              })); 
        }} 
        onSort={() => { 
          // Makes an API call when the sort order  
          // changes... 
          fetchItems(this.state.filter, !this.state.asc) 
            .then(({ items }) => 
              this.setState({ 
                asc: !this.state.asc, 
                source: this.state.source.cloneWithRows(items), 
              })); 
        }} 
      /> 
    ); 
  } 
} 

export default ListContainer; 

我认为这看起来简单多了,尽管它需要接触网络才能工作。任何修改列表状态的操作只需调用fetchItems(),并在承诺解决后设置适当的状态。

惰性列表加载

在本节中,我们将实现一种不同的列表,一种无限滚动的列表。有时候,用户实际上并不知道他们在寻找什么,所以过滤或排序是没有帮助的。想想你登录账户时看到的 Facebook 新闻提要;这是应用的主要功能,您很少寻找特定的功能。您需要通过滚动列表来查看发生了什么。

要使用ListView组件实现这一点,您需要能够在用户滚动到列表末尾时获取更多 API 数据。为了了解它是如何工作的,我们需要使用大量的 API 数据。发电机在这方面很棒!因此,让我们修改我们在上一个示例中创建的模拟,以便它只使用新数据继续响应:

import fetchMock from 'fetch-mock'; 

// Items...keep'em coming! 
function* genItems() { 
  let cnt = 0; 

  while (true) { 
    yield `Item ${cnt++}`; 
  } 
} 

const items = genItems(); 

// Grabs the next 20 items from the "items" 
// generator, and responds with the result. 
fetchMock.mock(/\/items.*/, () => { 
  const result = []; 

  for (let i = 0; i < 20; i++) { 
    result.push(items.next().value); 
  } 

  return ({ 
    items: result, 
  }); 
}); 

有了它,您现在可以在每次到达列表末尾时对新数据发出 API 请求。好吧,最终这将失败,但我只是试图向您展示在 React Native 中实现无限滚动的一般方法。以下是ListContainer组件的外观:

import React, { Component } from 'react'; 
import { ListView } from 'react-native'; 

import './mock'; 
import List from './List'; 

const rowHasChanged = (r1, r2) => r1 !== r2; 
const sectionHeaderHasChanged = rowHasChanged; 

class ListContainer extends Component { 
  constructor() { 
    super(); 

    this.state = { 
      data: [], 
      asc: true, 
      filter: '', 
      source: new ListView 
        .DataSource({ 
          rowHasChanged, 
          sectionHeaderHasChanged, 
        }) 
        .cloneWithRows([]), 
    }; 

    // This function is passed to the "onEndReached" 
    // property of the React Native "ListView" component. 
    // Instead of replacing the "source", it concatenates 
    // the new items with those that have already loaded. 
    this.fetchItems = () => 
      fetch('/items') 
        .then(resp => resp.json()) 
        .then(({ items }) => 
          this.setState({ 
            data: this.state.data.concat(items), 
            source: this.state.source.cloneWithRows( 
              this.state.data 
            ), 
          }) 
        ); 
  } 

  // Fetches the first batch of items once the 
  // component is mounted. 
  componentDidMount() { 
    this.fetchItems(); 
  } 

  render() { 
    return ( 
      <List 
        source={this.state.source} 
        fetchItems={this.fetchItems} 
      /> 
    ); 
  } 
} 

export default ListContainer; 

每次调用fetchItems()时,响应都与数据数组连接。这将成为新的列表数据源,而不是像前面的示例中那样替换它。现在让我们看一看 Oracle T1 组件,看看你如何回应列表的结尾:

import React, { PropTypes } from 'react'; 
import { Text, ListView } from 'react-native'; 

import styles from './styles'; 

// Renders a "<ListView>" component, and 
// calls "fetchItems()" and the user scrolls 
// to the end of the list. 
const List = ({ 
  source, 
  fetchItems, 
}) => ( 
  <ListView 
    enableEmptySections 
    dataSource={source} 
    renderRow={i => ( 
      <Text style={styles.item}>{i}</Text> 
    )} 
    onEndReached={fetchItems} 
  /> 
); 

List.propTypes = { 
  source: PropTypes.instanceOf(ListView.DataSource).isRequired, 
  fetchItems: PropTypes.func.isRequired, 
}; 

export default List; 

如果您运行这个示例,您将看到,当您在滚动时接近屏幕底部时,列表会不断增长。

总结

在本章中,您了解了 React Native 中的ListView组件。该组件是通用的,因为它不会对渲染的项目施加任何特定的外观。相反,列表的外观取决于您,而ListView组件有助于高效地呈现数据源。ListView组件还为其呈现的项目提供了一个可滚动区域。

您实现了一个示例,该示例利用了列表视图中的节标题。这是渲染静态内容(如列表控件)的好地方。然后,您学习了如何在 React Native 中进行网络调用;这就像在任何其他 web 应用中使用fetch()。最后,您实现了无限滚动的惰性列表,只在滚动到已渲染内容的底部后加载新项目。

在下一章中,您将学习如何显示网络呼叫等事项的进度。