原文
Thinking in React - Peter Hunt
概述
在我看来,使用React是用JavaScript快速开发大型Web应用的最佳方式。我们在Facebook和Instagram中大规模应用React的效果很好。
React的众多优点之一就是能让你重新认识你开发的应用时的过程,通过本文,我会一步一步向你展示用React创建一个可检索的产品数据列表的思路。
从一个原型设计开始
假设我们已经有一个JSON API和一个设计师做的原型设计。我们的设计师很明显不是很优秀因为我们的原型图长这个样子。
我们的JSON API会返回类似如下格式的数据:
1 | [ |
第一步:把UI打散成组件层级
你需要做的第一件事就是在原型图中围着每一个组件(和子组件)画矩形框并为组件命名。如果你是和设计师一起工作,他们可能已经把这一步做完了,直接去找他们商量就好。他们的Photoshop层名可能最后就是你的React组件名。
但是你怎么确定组件的划分呢?答案和你是如何决定是否需要创建一个函数或者对象是一样的。其中的技巧之一就是单一职责原则(single responsibility principle),即一个组件理想情况下应该只做一件事。如果组件的规模一直增长,它应该被分解成更小的子组件。
介于你需要经常展现JSON数据模型给用户,所以你会发现如果模型建造正确的话,它会和UI会匹配得非常好。那是因为UI和数据模型倾向于黏着于相同的信息构造,也就是说把你把UI分解成组件的工作通常是微不足道的。将其分解成一堆能准确代表你数据模型中某部分的组件就好。
这里在我们简单的应用中我们有五个组件。我已经将每个组件代表的数据用斜体标注了出来。
- FilterableProductTable (橙色框):表示整个示例;
- SearchBar (蓝色框):获取所有用户输入;
- ProductTable (绿色框):根据用户输入展示和筛选数据集;
- ProductCategoryRow (青绿色框):展示每个类型头;
- ProductRow (红色框):展示某行数据中的每个产品。
如果你注意到了ProductTable,你会发现表头(包含”Name”和”Price”标签)并不能算独立的组件。这只是一种偏好,不同的做法(这里只指是否把表头算作一个组件)各有争议。在本例中,我将其作为ProductTable,是因为他是数据集渲染的一部分,而数据集渲染是ProductTable的职责。不管怎样,如果这个表头逐渐变得很复杂(i.e. 如果我们需要添加排序功能),那把ProductTableHeader设计成单独的组件就肯定是有意义的了。
现在我们已经在我们的原型中鉴别出了所有组件,下面让我们把它们排进一个层级结构中。这一步很简单。原型中,在另一个组件中出现的组件应该作为子层出现在层级结构中:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
第二步:用React先建造一个静态版本
现在你有组件层级结构了,是时候实现你的应用了。最简单的方法是先写一个实现你的数据模型且渲染UI但却不交互的版本。这里最好是把交互和渲染UI的实现各自分离开,由于创建一个静态版本需要敲很多代码而不太需要想太多,而添加交互需要考虑更多逻辑而不太需要敲代码。之后我们会看到为什么这么说。
为了建造一个能渲染你的数据模型的应用的静态版本,你需要建造可复用其他组件且能通过props传递数据的组件。props是从父层传递数据到子层的一种方法。如果你很熟悉state,也千万别在写静态版本的时候用state。保留状态(state)只用于交互,也就是当数据一直在变化时才使用。由于这里只是一个静态版本,就别用状态了。
你可以从上向下创建应用,也可以从下向上创建应用。即你可以从在层级结构中更高的组件开始创建(i.e.从FilterableProductTable开始),也可以从更低的组件开始(ProductRow)。一般在层级结构更单一的实例中,从上向下创建更简单,而在比较大的项目中,从下向上,创建和测试都更简单。
在这一步的结尾,你会得到一个能渲染你的数据模型的可复用的组件库。由于这只是一个静态版本,这里的组件只有render()方法。层级结构顶层的组件(FilterableProductTable)会把你的数据模型当成一个属性。如果你对你底层的数据模型做了改变,并再次调用ReactDOM.render(),UI也会被更新。这里并没有什么地方特别难理解,很容易就能看出你的UI是怎样更新的以及知道在哪里做变化。React的单向数据流(也成为单向绑定)使一切都部件化并且变得快速。
如果你在这一步需要帮助的话你只需要查看React文档即可。
稍微暂停一下:props vs state
在React中有两种『模型』数据:props和state。理解这两者的区别很关键;如果你不是很明白二者的区别的话,去看看React官方文档。
第三步:确定UI状态的最小(但完整)表述
为了让你的UI能交互,你需要让你底层的数据模型能触发改变。React使用state解决了这个问题。
为了正确地创建你的应用,你首先需要考虑你的应用所需的可变状态的最小集合。这里的关键在于DRY:Don’t Repeat Yourself(不创建重复组件)。想清楚你的应用所需的最小的表述以及其他实际需求。例如,如果你在创建一个任务列表组件,就紧紧围绕需要做的任务项去创建;别为任务数单独保存一个分离的状态变量。相反,当你需要渲染一个任务列表的任务数(总量)时,单纯地获取任务项数组的长度即可。
仔细想想我们这个示例应用中的所有数据。我们有:
- 产品的原始列表
- 用户输入的检索关键字
- 复选框的值
- 筛选过的产品列表
让我们每一个都过一遍,搞清楚哪一个是状态。这个过程就是对于每一类数据问以下三个问题:
- 当前数据是否是父层通过属性(props)传递过来的?如果是,这类数据应该不是状态。
- 当前数据是否时刻变动?如果不是,这类数据应该不是状态。
- 当前数据是否可以通过对组件中的其他状态或者属性进行计算而得到?如果是,这类数据不是状态。
产品的原始列表通过属性传入,所以它不是状态。检索关键字和复选框的值处于变动中,并且无法通过计算得到,所以它应该是是状态。最后,筛选过的产品列表也不是状态,因为它可以通过结合原始产品列表,检索关键字,以及复选框的值来计算得出。
所以最终我们的状态有:
- 用户输入的检索关键字
- 复选框的值
第四步:确定你的状态存放的位置
好了,目前我们已经确定了应用状态的最小集合。下一步,我们需要确定哪个控件拥有这些状态。
记住:React专注于层级结构自顶向下的单向数据流。我们可能不会立刻清楚哪个控件应该拥有哪个状态。这对于初学者而言是最难理解的地方,所以跟随下属几个步骤,弄清楚这个问题。
对于你的应用中的每个状态:
- 确定每个组件根据目标状态来渲染一些东西。
- 找顶层组件(原文中为common owner component,这里译为顶层组件,在层级结构中所有需要这个状态的组件的最顶层组件)。
- 不是顶层组件拥有目标状态,就是层级结构中更高级别的其他组件拥有目标状态。
- 如果你找不到一个有意义的组件来拥有目标状态,创建一个新的组件,该组件单独用来持有目标状态,把它添加到层级结构中某个比顶层组件更高层的地方。
下面让我们把这些策略应用到我们的示例中:
- ProductTable需要基于状态来过滤产品列表,SearchBar需要展示检索关键字和复选框状态。
- 顶层组件是FilterableProductTable。
- 就组件定义而言,把检索关键字和复选框值放在FilterableProductTable中是合乎常理的。
很好,所以我们已经决定了我们的状态应该放在FilterableProductTable。首先,给FilterableProductTable添加一个返回{filterText: ‘’, inStockOnly: false}的getInitialState()方法,用于反映你的应用的初始状态。然后,把filterText和inStockOnly作为属性传递给ProductTable和SearchBar。最后,用这些属性来筛选ProductTable和SearchBar中表单域的值的集合。
你可以来看看应用将会怎样运作:设置filterText为”ball”,然后刷新你的应用。你会看到数据正确地刷新了。
第五步:添加反向数据流
目前为止,我们已经创建一个拥有自顶向下的状态和属性的数据流向机能,且能正确渲染的应用。现在是时候支持另一个方向的数据流了:在FilterableProductTable层级结构深处的表单组件需要更新状态。
React保证了数据流的清晰,这使你能更容易地理解你的程序是怎样工作的,但是相比传统的双向数据绑定,它需要你多敲一些代码。React提供一个名为ReactLink的扩展,来让这个模式能像双向绑定一样方便。但在本文中,我们还是让一切都保持清晰。
如果你在当前版本的示例中输入检索关键字或者勾选复选框,你会发现React忽略了你的输入(原文意为在处理输入值时我们并不关心各种input,而是只关心组件的状态,所以说『忽略了你的输入』)。这是有意为之的,由于我们已经设定了检索关键字的值和从FilterableProductTable中传入状态恒等。
让我们想一下我们希望发生什么。我们希望确定无论何时,当用户改变了表单,我们就更新状态为用户输入。由于组件应该更新自己所拥有的状态,FilterableProductTable会传一个回调函数给SearchBar,只要状态应该被更新,该回调函数就应该触发执行。我们可以用input的onChange事件来通知它。然后由FilterableProductTable传递回调函数会执行setState()函数,应用就能更新了。
虽然这听起来有点复杂,但这里的代码量真的很少。而你的数据是如何在应用中是怎样流动的,一目了然。
总结
理想的话,本文应该教会了怎样用React创建组件和应用的理念。相比以前,你可能需要多敲一些代码,但请记住读的代码永远比写的代码多,而读这种多组件式的清晰的代码真的非常容易。一旦你开始创建大型的组件库,你会很欣赏这种多组件话和明晰化的代码,而有了代码重用,你代码的行数就会变少。:)