React式思维

在我们看来,React是用JavaScript构建大型、敏捷Web应用的首要方法。它在Facebook和Instagram上为我们提供了很好的扩展。
React中一个重要的部分就是在构建应用时的思考方式。在这篇文档中,我们将使用React构建一个可搜索的产品数据表,并带你一起经历整个思考过程。

从设计图开始

想象一下,我们已经有了一个JSON API,有个从我们设计师那拿到的设计图。它看起来是这样的:


React式思维_第1张图片

我们的JSON API返回的数据如下:

  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

步骤1:将UI分成组件层级

你想做的第一件事也许是在设计图上为每个组件(包括子组件)画上盒子,并给他们命名。如果你和一个设计师一起在做这件事,他们或许已经完成了这一步,所以直接去找他们要就好了。他们的Photoshop图层的名字或许最终成为你React组件的名字!
但如何划分组件?只需要使用决定创建新函数和对象的方法就好。该方法是单一责任原则,即一个组件理想情况下只做一件事。如果组件规模最终增长,也许就该分解成更小的子组件。
由于会经常向用户显示JSON数据模型,你会发现如果你的模型构建正确,UI(以组件结构)将会很好的映射。这是因为UI和数据模型通常遵循相同的信息结构,这意味着将UI分成组件的工作往往很简单。只需要将组件分解为代表数据模型一部分即可。

React式思维_第2张图片

在这个简答的应用中有五个组件。我们将用斜体标出每个组件对应的数据。

  1. FilterableProductTable(橙色):包含了整个例子
  2. SearchBar(蓝色):接受所有用户输入
  3. ProductTable(绿色):根据用户的输入显示和筛选数据集合
  4. ProductCategoryRow(绿松色):显示每个分类标题
  5. ProductRow(红色):显示每个product

看看ProductTable,你会发现表格头(包含"Name"和"Price"标签)并属于组件。这是喜好问题,两种方式都有争论。在这个例子中,我们将它作为ProductTable的一部分,因为它是渲染数据集合的一部分,而这是ProductTable的职责。不过,如果这个表格头变得非常复杂(比如,如果我们使其可排序),就可以单独将其作为ProductTableHeader组件。
现在我们已经在设计图中标记出组件了,让我们将他们放进层级。这很简单。设计图中,出现在另一个组件中的组件,在层级中应作为孩子:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

步骤2:在React中构建静态版本

class ProductCategoryRow extends React.Component {
  render() {
    return {this.props.category};
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach(function(product) {
      if (product.category !== lastCategory) {
        rows.push();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { render() { return (
); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( , document.getElementById('container') );

在CodePen上试一试

现在有了组件层级,是时候完成你的应用了。最简单的方法就是使用数据模型渲染一个没有交互的UI。最好将这两个过程分开,因为构建静态版本需要大量的编码、无需思考,而增加交互就需要大量的思考而无需大量的编码。下面我们来看下原因。

要构建渲染数据模型的静态版本应用,需要构建可重用组件,并使用props传递数据。通过props将数据从父传向子。在构建静态版本中万不可使用状态。状态仅用于交互性,状态是随时间变化的数据。所以应用的静态版本中不需要它。

构建可以自上而下,或自下而上。也就是说,你既可以从层次中的高层组件开始构建(也就是从FilterableProductTable开始),也可以从底层开始(ProductRow)。在简单些的例子中,一般自上而下更容易一些,而大型的项目中,自下而上会更简单些,也更容易书写测试。

在此步结束时,你将拥有一个用于呈现数据模型的可重用组件库。因为这是应用的静态版本,所以组件只有render()方法。层次上层的组件(FilterableProductTable)将数据模型作为prop。如果你对底层数据模型进行更改,并再次调用ReactDOM.render(),UI将会得到更新。因为没发生什么复杂的事情,很容易就能看到UI何如更新,哪里发生了改变。React的单向数据流(也叫单向绑定)让一切模块化、快速化。
如果你在执行这步时需要帮助,请参阅React文档。

简短的插曲:Props和状态

在React中有两种“模型”数据:props和状态。了解两者之间的区别很重要;如果你不清楚有什么区别,请浏览官方React文档。

步骤3: 确定UI状态的最小表示(但完整)

为了让你的UI可交互,你需要能够触发对底层数据模型的变更。React通过状态,让这变的很简单。

为了正确的构建应用,首先需要想想应用需要的最小可变状态集合。关键就在于DRT(Don't Repeat Yourself)。找出应用所需状态的绝对最小表示,并计算其他所有的需求。比如:如果在构建一个TODO列表,只需要保留TODO项数组;不用单独保存一个状态变量来表示数量。如果你要渲染TODO的数量,简单的获取TODO项数组的长度即可。

想想示例应用中所有的数据。有:

  • 初始的产品列表
  • 用户输入的搜索文本
  • 复选框的值
  • 筛选后的产品列表

让我们一个个看过去,并找出哪个是状态。对每个数据提出三个问题即可:

  1. 是否由父组件通过props传入的?如果是的话,他可能不是状态。
  2. 是否保持不变?如果是的话,他可能不是状态。
  3. 是否可通过组件中其他的状态或props计算得到?如果是的话,他不是状态。

初始的产品列表作为props传入,所以他不是状态。搜索文本和复选框看起来是状态,因为他们会发生变化,且不能由其他值计算得到。最后一个,筛选后的产品列表不是状态,因为它可以通过组合初始产品列表,搜索列表以及复选框的值计算得到。

因此,我们的状态有:

  • 用户输入的搜索文本
  • 复选框的值

步骤4:确定State的存放点

class ProductCategoryRow extends React.Component {
  render() {
    return ({this.props.category});
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        return;
      }
      if (product.category !== lastCategory) {
        rows.push();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; } render() { return (
); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( , document.getElementById('container') );

在CodePen上试一试

既然我们已经确定了应用状态的最小集合。接下来我们应该找出哪个组件拥有这个状态。

记住:React就是组件层次中的单向数据流。搞清楚组件拥有什么状态,对于新手来说是最具挑战性的部分,可以按以下步骤来做:

遍历应用中的每个状态:

  • 确认出需要该状态进行渲染的所有组件
  • 找出公共所有者组件(层次中一个在所有需要该状态组件上方的组件)
  • 公共组件或层次更靠上的组件应该拥有该状态
  • 如果不存在这样一个组件,则创建一个只用于保存改状态的新组件,然后将其插入公共所有者组件的层次结构上方。

在我们的应用中执行一遍该策略:

  • ProductTable需要根据状态筛选产品列表,SearchBar需要显示状态:搜索文本和复选值。
  • FilterableProductTable是公共所有者组件。
  • 筛选文本和复选值应该放在FilterableProductTable

因此我们决定将状态放在FilterableProductTable中。首先在FilterableProductTableconstructor中添加一个实例属性this.state = {filterText: '', inStockOnly: false}来表示应用的初始状态。然后将filterTextinStockOnly作为prop传递给ProductTableSearchBar。最后使用这些props来筛选ProductTable中的行,设置SearchBar中表单项的值。

你可以将筛选文本设置为"ball",并刷新,来看看应用会有怎样的表现。你将看到数据表格得到了正确的更新。

步骤5: 添加反向数据流

class ProductCategoryRow extends React.Component {
  render() {
    return ({this.props.category});
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    console.log(this.props.inStockOnly)
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        return;
      }
      if (product.category !== lastCategory) {
        rows.push();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { constructor(props) { super(props); this.handleFilterTextInputChange = this.handleFilterTextInputChange.bind(this); this.handleInStockInputChange = this.handleInStockInputChange.bind(this); } handleFilterTextInputChange(e) { this.props.onFilterTextInput(e.target.value); } handleInStockInputChange(e) { this.props.onInStockInput(e.target.checked); } render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; this.handleFilterTextInput = this.handleFilterTextInput.bind(this); this.handleInStockInput = this.handleInStockInput.bind(this); } handleFilterTextInput(filterText) { this.setState({ filterText: filterText }); } handleInStockInput(inStockOnly) { this.setState({ inStockOnly: inStockOnly }) } render() { return (
); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( , document.getElementById('container') );

在CodePen上试一试

到目前为止,我们已经构建了一个应用程序,渲染正确且pros和状态在层次中自上而下。现在是时候支持数据以另一种方式流动了:深层次中的表单组件需要更新FilterableProductTable中的状态。

React通过明确的数据流,使得我们很容易了解程序的工作原理,但它比起传统的双向数据绑定需要更多的编码。

在当前版本的例子中,如果你输入或点击复选框,将看到React会忽略你的输入。这是故意的,因为我们已经将inputvalueprop设置始终等于由FilterableProductTable传入的state了。

我们希望当用户更改表单时,状态都会根据用户的输入得到更新。由于组件只能更新自己的状态,FilterableProductTabel将更新状态的回调函数传给SearchBar。我们可以使用onChange事件来监听input。FilterableProductTable传入的回调函数将调用setState(),来更新应用。

虽然这听起来很复杂,但它只需要几行代码。而且整个应用中的数据流通也非常明确。

就这样吧

希望这些足够让你了解如何使用React构建组件和应用。相比之前,这可能需要更多的编码,但记住代码更多的是读大于写,而读这种模块化、明确的代码是非常容易的。当开始构建大型组件库的时候,你将感激这种显式和模块化,并通过代码重用,你代码的行数将开始缩小。:-)

你可能感兴趣的:(React式思维)