BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

让React组件变得可响应

| 作者 Artemij Fedosejev 关注 0 他的粉丝 ,译者 奇舞团 关注 0 他的粉丝 发布于 2016年8月3日. 估计阅读时间: 31 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

编者按: InfoQ开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自由Artemij Fedosejev著,奇舞团译的《React 精髓》一书,介绍了如何规划React应用程序并创建可组合的React组件。

第四章 让React组件变得可响应

现在你已经知道如何创建有状态和无状态的React组件,我们可以尝试将React组件组合在一起构建更复杂的用户界面。事实上,我们现在可以开始创建第1章讨论过的Web应用程序Snapterest了。在这个过程中,我们将学习如何规划React应用程序并创建可组合的React组件。让我们开始吧。

1. 使用React解决问题

编写Web应用程序之前,要考虑Web应用要解决什么问题。尽早且尽可能清晰地定义问题是走向成功解决方案(即有用的Web应用)的最重要一步。如果在开发过程之初没有定义清楚问题,或者定义得不准确,那么此后你将不得不停下来,并重新思考该怎么做,甚至要扔掉已经完成的一些代码并重新写。这是非常低效的,作为一个专业的软件开发者,你和你的团队的时间都是非常宝贵的,因此,花点时间提前想清楚问题是非常有益处的。在本书开始的时候,我强调过使用React的好处之一是代码重用,这意味着你将在更短的时间内做更多的事。因此,在看React代码之前,让我们先讨论要解决的问题是什么,而且要记得用React来思考。

我们将创建的Snapterest是一个Web应用程序,它会实时接收来自Snapkite引擎服务器的推文,并将推文逐条显示给用户。虽然我们不知道Snapterest会在什么时候收到一条新推文,但是当新推文到达时,它至少应该显示1.5秒钟以便用户有足够的时间看到并点击它。点击推文会将它添加到一个现有的推文集合或者创建一个新的集合。最终用户能够将集合导出为一段HTML代码。

对于我们正在构建的应用,上述描述非常笼统。让我们把它分解成更小的任务,如下图:

  1. 实时地从Snapkite引擎服务器接收推文。
  2. 每条推文至少显示1.5秒。
  3. 通过用户的点击事件将推文添加到一个集合中。
  4. 显示集合中的推文。
  5. 为集合创建HTML代码并导出。
  6. 通过用户点击事件从集合中移除推文。

你可以确定哪些任务能使用React解决吗?记住React是一个用户界面库,因此,任何与用户界面或用户界面交互相关的任务都可以使用React解决。在前面的列表中,除了第一个任务外,其他的任务React都能胜任,因为第一个任务描述的是数据获取,与用户界面毫无关系。任务1将会使用其他库来解决,我们将在下一章讨论。任务2和任务4描述的内容是需要被显示的,React组件是最合适的选择。任务3和任务6描述的是用户事件,如我们在第3章所介绍的,用户事件处理可以被很好地封装在React组件中。任务5怎样使用React来解决呢?第2章我们讨论过ReactDOMServer.renderToStaticMarkup()方法能将React元素渲染成静态的HTML标记字符串,这正是我们解决任务5所需的方案。

现在我们已经为每一个任务确定了潜在的解决方案,让我们思考一下我们将要怎么把它们结合在一起来创建一个功能全面的Web应用程序。

有两种方法来创建可组合的React应用:

  • 先构建单个的React组件,然后将它们组合起来形成更高层级的React组件,自底向上来构建层级。
  • 从最顶级的React元素开始,然后实现它的子组件,自顶向下来构建层级。

从观察和理解应用架构的角度来看,第二种策略更有优势。我认为在考虑各个部分的功能如何实现之前,先了解所有组件如何组合在一起更重要。

2. 规划React应用程序

规划React应用时应遵循下面两条简单的原则:

  • 每个React组件应该代表一个用户界面元素。它应该封装最小的可复用元素。
  • 多个React组件应该组成一个独立的React组件。最终,整个用户页面应该封装成一个React组件。

参见下图,先从最上层的React组件Application开始。它将封装我们的整个React应用程序,它有两个子组件:Stream和Collection。Stream组件将负责连接到一个消息流,接收和显示最新的消息。Stream组件有两个子组件:StreamTweet和Header。StreamTweet组件将负责显示最新的消息,它由Header和Tweet组合而成。Header组件将会渲染头部,它没有子组件。Tweet组件会渲染来自推文的一张图片。注意我们已经可以复用Header组件两次了。

我们的React组件的层次图

Collection组件负责显示收集控件和推文列表。它有两个子组件:CollectionControls和tweetlist。前者又有两个子组件:Collection RenameForm组件将渲染一个表单,用来重命名集合;CollectionExportForm组件将渲染一个表单,用来将集合导出到一个叫作CodePen的服务,这是一个HTML、CSS和JavaScript的演示网站,可以在http://codepen.io上了解更多关于Codepen的信息。你可能已经注意到,我们将在CollectionFenameForm和CollectionControls组件中复用Header和Button组件。TweetList组件将渲染一个推文列表。每一条推文将被渲染成一个Tweet组件。在Collection组件中,我们将再次复用Header组件。事实上,我们总共要复用5次Header组件。这对我们来说能省很大事。正如我们在前一章中讨论的,我们应该尽可能保持更多组件是无状态的。因此,总共11个组件中只有下面5个组件存储状态:

  • Application
  • CollectionControls
  • CollectionRenameForm
  • Stream
  • StreamTweet

有了规划之后,我们开始实现吧。

3. 创建一个React组件容器

让我们首先编辑应用程序的JavaScript主文件,使用下面的代码片段替换~/snapterest/source/app.js文件内容:

var React = require('react');    
var ReactDOM = require('react-dom');    
var Application = require('./components/Application.react');
ReactDOM.render(<Application />, 
document.getElementById('react-   application'));

这个文件仅有四行代码,实现的是:将document.getElementById('react- application') 作为组件的部署目标,并将Application组件渲染到DOM中。Web应用程序的整个用户界面都将被封装在这个组件中。

接下来,切换到~/snapterest/source/components/目录并创建Applica tion.react.js文件。我们约定所有React组件的文件名都以react.js结尾,这样我们就可以很轻意地分辨React与非React文件。

让我们看一下Application.react.js文件的内容:

var React = require('react');     
var Stream = require('./Stream.react');     
var Collection = require('./Collection.react');     

var Application = React.createClass({       

  getInitialState: function() {         
    return {           
      collectionTweets: {}         
    };       
  },         
addTweetToCollection: function(tweet) {           
  var collectionTweets = this.state.collectionTweets;           
  collectionTweets[tweet.id] = tweet;           

  this.setState({             
    collectionTweets: collectionTweets           
  });         
},         

removeTweetFromCollection: function(tweet) {           
  var collectionTweets = this.state.collectionTweets;           

  delete collectionTweets[tweet.id];           

  this.setState({             
    collectionTweets: collectionTweets           
  });         
},         

removeAllTweetsFromCollection: function() {             
  var collectionTweets = this.state.collectionTweets; 

  delete collectionTweets[tweet.id];

  this.setState({             
    collectionTweets: {}           
  });         
},

removeAllTweetsFromCollection: function () { 
  this.setState({ 
    collectionTweets: {} 
  }); 
},

render: function() {           
  return (             
    <div className="container-fluid">                 
      <div className="row">               
        <div className="col-md-4 text-center">                      

        <Stream onAddTweetToCollection={this.addTweetToCollection} />         
        </div>    
        <div className="col-md-8">                  

          <Collection                
             tweets={this.state.collectionTweets}    
             onRemoveTweetFromCollection={this.
removeTweetFromCollection}
             onRemoveAllTweetsFromCollection={this.
 removeAllTweetsFromCollection}/> 

        </div>            
       </div>         

    </div>           
   );       
  }     
});     

module.exports = Application;

这个组件的代码比app.js多了很多,但是这些代码可以很容易地分为三个逻辑部分:

  • 引入依赖模块
  • 定义React组件
  • 作为模块导出这个React组件

大多数React组件中都可以看到这样的逻辑分割,因为包装成CommonJS模块才能使用Browserify引入它们。事实上,这个源文件的第一部分和第三部分的写法都是CommonJS规定的,与React无关。使用这种模块规范的目的是将应用程序分解成模块以便复用。因为React组件和CommonJS模块都可以封装代码并使代码更灵活,所以它们在一起自然可以很好地工作。将最终的用户界面逻辑封装在一个CommonJS模块形式的React组件中,其他模块就可以复用这个被封装好的React组件了。

Application.react.js文件的引入逻辑使用require()函数引入了依赖模块:

var React = require('react');    
var Stream = require('./Stream.react');    
var Collection = require('./Collection.react');

这里Application组件引入了下面两个子组件:

  • Stream组件将在用户界面中渲染信息流部分。
  • Collection组件将在用户页面中渲染集合部分。

我们也需要引入React库,但这部分代码都是按照CommonJS模块规范编写的,与React本身无关。

Application.react.js文件的第二部分逻辑创建带有以下方法的ReactApp licaton组件:

  • getInitialState()
  • addTweetToCollection()
  • removeTweetFromCollection()
  • removeAllTweetsFromCollection()
  • render()

只有getInitialState()和render()方法是React API,其他方法都是这个组件封装的应用程序逻辑的一部分。讨论完这个组件的render()方法会渲染什么内容之后,我们再仔细分析每个逻辑方法:

render: function () {   
  return (     
    <div className="container-fluid">

      <div className="row">         
        <div className="col-md-4 text-center">           

          <Stream onAddTweetToCollection={this.addTweetToCollection} /> 
        </div>         
        <div className="col-md-8">        
        <Collection    
           tweets={this.state.collectionTweets}          
          onRemoveTweetFromCollection={this.
removeTweetFromCollection}
          onRemoveAllTweetsFromCollection={this.
removeAllTweetsFromCollection} /> 

        </div>
      </div>

    </div>   
  ); 
}

这段代码使用Bootstrap框架定义了网页布局。如果你不熟悉Bootstrap,我强烈推荐你访问http://getbootstrap.com上的文档。掌握了这个框架你就能用最快的速度和最简单的方法搭建用户界面原型。不过即使你不知道Bootstrap,也不影响理解后面的内容。我们将网页划分为两列:一个小的和一个大的。小的包含Stream组件,大的包含Collection组件。可以想象我们的网页被划分成两个不等的部分,它们都包含React组件。

我们这样使用Stream组件:

<Stream onAddTweetToCollection={this.addTweetToCollection} />

Stream组件有一个onAddTweetToCollection属性,Application组件将自己的addTweetToCollection()函数作为这个属性的值。addTweetTocollection()函数会添加一条推文到集合中。这是Applicaton组件中的一个自定义方法,我们可以用this关键字来引用它。

让我们看一下addTweetToCollection()做了什么:

addTweetToCollection: function (tweet) {    
  var collectionTweets = this.state.collectionTweets;    

  collectionTweets[tweet.id] = tweet;    

  this.setState({    
    collectionTweets: collectionTweets
  }); 
}


<div class="md-section-divider"></div>

这个函数引用存储在当前state中的CollectionTweets,添加一条新推文到CollectonTweets对象,并通过调用setState()函数来更新state。在Stream组件中,当addTweetToCollection()函数被调用时,一条新推文会作为参数被传入。这是一个子组件更新其父组件state的例子。

这是React的一个重要机制,它的工作过程如下。

  1. 父组件传递一个回调函数作为子组件的属性。子组件可以通过this.props变量访问这个回调函数。
  2. 每当子组件想要更新父组件的state时,它就会调用这个回调函数并传递所有必要的数据到父组件的新状态中。
  3. 父组件更新它的state,而且state更新会触发render()函数重新渲染所有必要的子组件。

这就是React中父组件与子组件的交互机制。这个机制允许子组件将应用程序状态管理委托到它的父组件,子组件只需要关心如何渲染自己就行了。了解了这个机制之后,我们还将多次使用它,因为大部分React组件要保持无状态。应该只有少量的父组件负责存储和管理应用程序的state。这个最佳实践允许我们按照以下两个不同的关注点来有序地组织React组件:

管理应用程序的state和渲染。

只关注渲染并且将应用程序的state管理委托到父组件上。

Application组件的第二个子组件Collection如下:

<Collection    
  tweets={this.state.collectionTweets}   
  onRemoveTweetFromCollection={this.removeTweetFromCollection}    
onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection}    />


<div class="md-section-divider"></div>

这个组件有如下一些属性。

  • tweets:引用当前推文集合。
  • onRemoveTweetFromCollection:这个函数从集合中删除特定的推文
  • onRemoveAllTweetFromCollection:这个函数从集合中删除所有的推文。

Collection组件的属性仅仅关注下面两点:

  • 如何访问应用程序的state。
  • 如何改变应用程序的state。

显然,onRemoveTweetFromCollection和onRemoveAllTweetsFromCollection函数允许Collection组件改变Application组件的state。另一方面,tweets属性把Application组件的state传递给Collection组件,使Collection组件获得访问state的只读权限。

你能觉察到在Application和Collection组件之间的数据的单向流动吗?以下是它的工作过程:

  1. 使用Application组件的getInitalState()方法初始化collection Tweets数据。
  2. collectionTweets数据作为tweets属性传递给Collection组件。
  3. Collection组件调用removeTweetFromCollection和removeAllTweet FromCollection,更新Application中的collectionTweets数据,然后再次开始循环。

注意,Collection组件不能直接改变Application组件的state。Collection组件有通过this.props对象访问state的只读权限,并仅可以通过调用父组件传递的回调函数来更新父组件的state。在Collection组件中,这些回调函数是this.props.onRemoveTweetFromCollection和this.props.onRemoveAllTweetFrom Collection。

在React组件层次中,这种数据流动的简单思维模型有助于增加组件的数量,而不增加用户页面的复杂性。比如,它可以有10个层级的React组件嵌套,如下图所示。

    在这个层次结构中,如果组件G要改变根组件A的state,其所用的方法与组件B、组件F或者其他任何组件使用的方法完全相同。然而,在React中,你不应该将数据直接从组件A传递到组件G。相反,你首先可以把它传递给组件B,然后给组件C,然后给组件D,依此类推,直至组件G。组件B到组件F必须携带一些transit属性,这些属性实际上只对组件G有用。这看起来可能是在浪费时间,但是这个设计使我们更容易调试应用程序,并可以推理出它是如何工作的。想优化应用程序的架构总是有办法的。Flux就是一种优化方案,本书后面会讨论它。

    最后,再看一下改变Application的state的两个方法:

    removeTweetFromCollection: function (tweet) {    
      var collectionTweets = this.state.collectionTweets;  
    
      delete collectionTweets[tweet.id];   
    
      this.setState({  
        collectionTweets: collectionTweets
      });
    }
    
    
    <div class="md-section-divider"></div>

    removeTweetFromCollection()方法从存储在Application组件的推文集合中移除一条推文,它需要从组件的state中得到当前的collectionTweet对象,然后根据给定的ID从state对象中删除一条推文,并使用一个新的collectionTweets对象来更新组件的state。

    此外removeAllTweetsFromCollection()方法会从组件state中移除所有推文:

    removeAllTweetsFromCollection: function () {    
      this.setState({    
        collectionTweets: {}    
      }); 
    }

    这些方法都是在子组件Collection中调用的,因为该组件没有其他方法可以改变Application组件的state。

    4. 小结

    在这一章中,我们学习了如何使用React解决问题。我们首先把问题分解成一些小问题,并讨论了如何使用React解决它们。然后,创建一些需要实现的React组件。最后,我们创建了第一个可组合的React组件,并学习了如何让父组件与它的子组件交互。在下一章,我们将实现我们的子组件并学习React生命周期相关的方法。

    书籍介绍

    《React 精髓》面向初中级前端开发者,从头到尾、由浅入深地介绍了使用React 实现组件化Web 应用的完整流程。作者从React 元素、React 组件等基本的概念讲起,循序渐进地讨论了组件状态和生命周期,为开发完整的React 应用打下了基础。与第三方JavaScript 框架集成,以及对React 组件进行单元测试,都是开发React 应用的重要内容,《React 精髓》也有详细讲解。最后,为进一步提升React 应用的灵活性,作者还以实例展示了如何引入Flux 架构,让读者的开发技能更上一层楼。

    评价本文

    专业度
    风格

    您好,朋友!

    您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

    获得来自InfoQ的更多体验。

    告诉我们您的想法

    允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

    当有人回复此评论时请E-mail通知我
    社区评论

    允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

    当有人回复此评论时请E-mail通知我

    允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

    当有人回复此评论时请E-mail通知我

    讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT