编者按:InfoQ开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自由Artemij Fedosejev著,奇舞团译的《React 精髓》一书,介绍了如何规划React应用程序并创建可组合的React组件。
现在你已经知道如何创建有状态和无状态的React组件,我们可以尝试将React组件组合在一起构建更复杂的用户界面。事实上,我们现在可以开始创建第1章讨论过的Web应用程序Snapterest了。在这个过程中,我们将学习如何规划React应用程序并创建可组合的React组件。让我们开始吧。
编写Web应用程序之前,要考虑Web应用要解决什么问题。尽早且尽可能清晰地定义问题是走向成功解决方案(即有用的Web应用)的最重要一步。如果在开发过程之初没有定义清楚问题,或者定义得不准确,那么此后你将不得不停下来,并重新思考该怎么做,甚至要扔掉已经完成的一些代码并重新写。这是非常低效的,作为一个专业的软件开发者,你和你的团队的时间都是非常宝贵的,因此,花点时间提前想清楚问题是非常有益处的。在本书开始的时候,我强调过使用React的好处之一是代码重用,这意味着你将在更短的时间内做更多的事。因此,在看React代码之前,让我们先讨论要解决的问题是什么,而且要记得用React来思考。
我们将创建的Snapterest是一个Web应用程序,它会实时接收来自Snapkite引擎服务器的推文,并将推文逐条显示给用户。虽然我们不知道Snapterest会在什么时候收到一条新推文,但是当新推文到达时,它至少应该显示1.5秒钟以便用户有足够的时间看到并点击它。点击推文会将它添加到一个现有的推文集合或者创建一个新的集合。最终用户能够将集合导出为一段HTML代码。
对于我们正在构建的应用,上述描述非常笼统。让我们把它分解成更小的任务,如下图:
你可以确定哪些任务能使用React解决吗?记住React是一个用户界面库,因此,任何与用户界面或用户界面交互相关的任务都可以使用React解决。在前面的列表中,除了第一个任务外,其他的任务React都能胜任,因为第一个任务描述的是数据获取,与用户界面毫无关系。任务1将会使用其他库来解决,我们将在下一章讨论。任务2和任务4描述的内容是需要被显示的,React组件是最合适的选择。任务3和任务6描述的是用户事件,如我们在第3章所介绍的,用户事件处理可以被很好地封装在React组件中。任务5怎样使用React来解决呢?第2章我们讨论过 ReactDOMServer.renderToStaticMarkup()
方法能将React元素渲染成静态的HTML标记字符串,这正是我们解决任务5所需的方案。
现在我们已经为每一个任务确定了潜在的解决方案,让我们思考一下我们将要怎么把它们结合在一起来创建一个功能全面的Web应用程序。
有两种方法来创建可组合的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个组件存储状态:
有了规划之后,我们开始实现吧。
让我们首先编辑应用程序的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组件中都可以看到这样的逻辑分割,因为包装成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组件引入了下面两个子组件:
我们也需要引入React库,但这部分代码都是按照CommonJS模块规范编写的,与React本身无关。
Application.react.js文件的第二部分逻辑创建带有以下方法的ReactApp licaton组件:
只有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的一个重要机制,它的工作过程如下。
这就是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>
这个组件有如下一些属性。
Collection组件的属性仅仅关注下面两点:
显然,onRemoveTweetFromCollection和onRemoveAllTweetsFromCollection函数允许Collection组件改变Application组件的state。另一方面,tweets属性把Application组件的state传递给Collection组件,使Collection组件获得访问state的只读权限。
你能觉察到在Application和Collection组件之间的数据的单向流动吗?以下是它的工作过程:
注意,Collection组件不能直接改变Application组件的state。Collection组件有通过this.props对象访问state的只读权限,并仅可以通过调用父组件传递的回调函数来更新父组件的state。在Collection组件中,这些回调函数是this.props.onRemoveTweetFromCollection和this.props.onRemoveAllTweetFrom Collection。
在React组件层次中,这种数据流动的简单思维模型有助于增加组件的数量,而不增加用户页面的复杂性。比如,它可以有10个层级的React组件嵌套,如下图所示。