关于React的一些思考

笔者最近正用React开发一个阅读web app已经到了优化和测试阶段。在这段时间里,对于React从入门到应用,再到优化,笔者的感受是React上手还是挺容易的,由于它是基于虚拟dom,性能也是棒棒哒。只是随着app功能实现的越来越复杂,components也变的越来越复杂,如果不去做架构上的思考和设计的话,整个React应用的组件将会变得臃肿而难以维护。

对于React入门,笔者是从阮一峰的《React 入门实例教程》开始的,他这里一共11个简单易懂的demo涵盖了React基本知识点。

这里笔者主要翻译介绍 Pete Hunt的这篇《Thinking in React》,感觉这篇文章对应用React的层级设计和使用原则,阐述的相当明晰透彻,以下是翻译内容:

在我看来,React是建设大型、高性能Web app的首选方案。关于这一点,大家去看看Facebook和Instagram就不会有任何怀疑了。
React的优点之一就是它让你思考构建app的方式。我将在此贴中带你体验用React构建一个’可搜索产品数据表’的整个过程。

从一个UI设计稿开始

假设现在我们已经有一个JSON API和设计好的UI稿。我们的UI设计师看起来比较粗矿,因为设计稿长这样:
UI设计稿
而JSON API返回的数据长这样:

1
2
3
4
5
6
7
8
[
{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打散成React组件层

你要做的第一件事就是在UI设计稿上画出组件(或子组件),并给它们命名。如果那个粗矿的设计就在你旁边的话,你可以去跟他谈谈,因为或许他已经给你做好了这件事——他的Photoshop图层名称或许就对应你的组件层的名称!

如果你不清楚哪些元素该成为一个React组件,想一想你平常写代码时如何决定新建一个function还是object?是同样的原理,即所谓的‘单一职责原则’(single responsibility principle),也就是说一个React组件最好只做一件事。如果这个组件最终扩展了,那么它就应该拆分为多个子组件。

如果你经常把JSON数据模型展示给用户,你会发现,只要你的数据模型建得好,那么你的UI(或者说你的组件结构)也会变的很优雅。这是因为UI和数据模型往往趋于依赖同样的信息结构。这就是说把UI分解为一个个组件往往是琐碎的工作。你只要记住一点:一个组件对应一个单一数据模块。
标记好的UI设计稿
如上图我们为这个简单的app标记了5个组件,下面我把每个组件所对应的数据模块用斜体字显示了:


  1. FilterableProductTable (橙色): 包含整个实例

  2. SearchBar (蓝色): 接收所有用户输入

  3. ProductTable (绿色): 显示和筛选以用户输入为基础的数据集合

  4. ProductCategoryRow (蓝绿色): 把每个产品类别显示为一个标题

  5. ProductRow (红色): 把每个产品显示为一行


如果你仔细看,会发现在ProductTable中,表格标题(包含”name”和”Price”)并没有作为一个单独的组件。其实这只是个人偏好的问题,两种做法都存在争议。这个例子中,我把它作为ProductTable的一部分是因为它只是渲染数据集合的一部分,这正是ProductTable的职责。但是,如果这个表头进一步扩展(比如增加排序的功能),它就当然应该单独成为一个ProductTableHeader组件。

现在我们已经在设计稿中标识了各个组件,接下来我们来组装React组件层。非常简单,当一个组件属于另一个组件时,那么它在组件层中当然就属于一个子层级:


  • FilterableProductTable

    • SearchBar

    • ProductTable

      • ProductCategoryRow

      • ProductRow





步骤2:构建一个静态React版本


现在,你已经完成了分解组件层级的工作,是时候开始实现你的app了。最简单的方式,是构建一个根据数据模型来渲染UI的静态版本,而不包含任何交互功能。最好能这样分解步骤,因为静态版本的app不需要过多的思考就能写出你的代码,而交互功能则只需要你思考怎么做,而不要写很多的代码。下面你就会明白为什么这样做。
要建设一个静态版本的app,你需要构建一些组件重复利用另一些组件并用props来传递数据。props在React中用来从父组件向子组件传递数据。如果你熟悉了state的概念,在静态版本中请不要使用statestate只用来做交互功能,也就是说数据随时会根据用户交互而改变。既然这只是一个静态版本,你当然不需要它。

你可以选择“自上而下”或者“自下而上”两种方式,也就是说你可以从组件层的高层(比如从FilterableProductTable开始)或从底层(ProductRow)开始构建组件。一般而言简单的案例适合“自上而下”的方式,而在大型项目中,则最好使用“自下而上”的方式,这样你在构建组件时可以随时测试。

完成这一步,你会建成一个能够重复使用的渲染数据模型的组件库,因为这只是个静态版本,你的组件只包含一个唯一的render()方法。在组件层的最上层(FilterableProductTable)会将数据作为一个props来使用。如果你改变一下基础数据模型,并重新调用ReactDom.render(),UI就会随着更新,如此简单,你会很轻松的看到哪儿发生了变化。React的单向数据流(或者说单向捆绑)使得一切保持模块化和高效。

如果你实现这一步有困难,请参考React官方文档

一个简短插曲:props vs state

React有两种数据模式:props和state。理解两者之间的差别至关重要;如果你还不是很清楚,请查阅React官方文档

步骤3:确定UI状态机的最小化(但完整)方案

为了使UI可以响应应用户交互,你必须能够触发基础数据模型的改变。React可以轻松的利用state来实现。

为了正确地构建你的app,首先要想好你的app里需要用到的最小化的可变状态机的配置,这里的关键是DRY:Don’t Repeat Yourself。你必须按照需要计算出最小化的组件状态机方案,举个例子,比如你正在构建一个TODO列表,只需用一个state属性保存TODO列表项数组;而不要另外再加一个state属性来保存列表的长度。当你要用到TODO列表的长度时,只需单纯的获取数组长度。

回顾一下我们这个案例中所用的每个数据,它们是:


  • 初始产品列表

  • 用户所输入的搜索关键字

  • 复选框的值

  • 筛选过的产品列表


让我们来分析一下哪一个应该作为state,你只需对它们问三个问题:

  1. 它可以通过父组件的props传递过来吗?如果是的,那么它不是state

  2. 它是随时会改变的吗?如果不是,那么它应该也不是state

  3. 它可以通过组件中的其它state或者props计算得来吗?如果是的,那么它还不是state


产品初始列表是通过props传递过来的,所以它不是state。用户所输入的搜索关键字和复选框的值应该作为state,因为它们随时会改变,并且它们不能通过其他的任何数据计算得来。最后筛选过的产品列表不是state,因为它可以通过以上三个数据计算而来。
最终,我们的state是:

  • 用户所输入的搜索关键字

  • 复选框的值

步骤4:确定用哪一个组件来包含state


很好,现在我们确定了app的最小化状态机。接下来,我们要来确定用哪一个组件来包含这些state。

请记住,React的核心就是组件层级自上而下的“单向数据流”。但一开始很难弄清到底由哪个组件来包含那些state。这对于新手来说经常是最大的挑战,那么让我们来好好分析一下:

先来明确一下你的应用中的每个state:


  • 确定每个组件都基于这个state渲染了哪些内容

  • 找到一个祖先组件(一个唯一的包含所有需要用到这个state的组件的上层组件)

  • 应该用这个共同的祖先组件或者更上层的组件来包含这个state

  • 如果你还找不到这么一个合理的祖先组件,就新建一个组件专门承载这个state,并且把它加到那个共同的祖先组件的上层。


让我们用这个思路来对照我们的案例:


  • ProductTable 需要基于state来筛选产品列表,而SearchBar则需要显示搜索文字和复选框的状态

  • 它们共同的祖先组件就是FilterableProductTable

  • 理所当然,筛选文字和复选框的值应该被包含在FilterableProductTable之中


酷毙了,我们终于得出结论:用FilterableProductTable来承载state。首先,在FilterableProductTable添加一个getInitialState()方法,它返回{filterText: ‘’, inStockOnly: false}用来初始化你的应用中的state。最后,利用props传递这些数据,用以筛选ProductTable 中的产品列表和设置SearchBar中的表单的值。

你终于可以看到你的应用是如何运行了:把filterText设成’ball’并刷新你的app,你将看到产品列表能够正确地更新。

步骤5:添加反转数据流


到现在,我们已经成功构建好了一个简单应用,它利用props和state的自上而下的数据流机制使得程序能够正确的运行。现在是时候来看看支持这种数据流的另外一个方面:在组件层级底层的表单需要去更新位于FilterableProductTable中的state。

React显式设置的这种数据流让你能够更轻易的理解你的程序运行机制,只是这要比传统的双向数据流多一些代码量。React提供一个可扩展的ReactLink(反应链)来达到和双向数据流构建一样方便的模式,但在这篇帖子中,我们主要目的是使得一切简单明了。

如果你尝试去输入文字或者选择复选框,你将发现React忽略了你的这些操作。这是我们故意的,因为我们已经把inputvalue设定为从FilterableProductTable中的state传过来值。

让我们思考一下应该怎么办。我们想要的是当用户在改变表单的值时,要让state实时的更新以映射出用户的操作。既然一个组件只能更新自己所拥有的state,FilterableProductTable将会传递一个回调函数给SearchBar,这样就可以实时的触发state的更新了。我们可以使用表单的onChange事件来监听。从FilterableProductTable传过来的回调函数将会去调用setState(),这样我们的应用就能正确更新信息了。
虽然这听上去有些复杂,其实只不过几行代码的事。并且你的程序中的数据的传递真正的是清晰明了。

到此为止,来个小结

真心希望此文能够在你思考该如何利用React构建组件和应用时有所启发。虽然它可能让你比平常多写了几行代码,但请记住,它的易读性,模块化和简易的代码将让你收获更多。这一点当你在构建大型的组件库时,你将真正体验到简易化、模块化和可复用性,最终你的代码量更少!

数组去重

js中数组用来收集和存放各类数据,实际应用中,一个数组中经常会产生许多重复值,那就有一种需求:保证数据中的每个值的唯一性。这就是所谓的“数组去重”

归纳下来数组去重大概有这么4中方案:

img与固有宽高比


在css的世界中,img元素拥有一些特殊的呈现属性,其中之一就是固有宽高比。而在一些特定应用场景中,我们可以利用padding来实现普通元素的固有宽高比。

img独特之处在于,她是行内级元素,但是却可以设置宽高。这有点类似于行内级块元素,然而没这么简单,img这货其实是“行内级置换元素”,关于置换元素,博主计划另起一篇详细探讨。

img元素默认拥有固有宽高和固有宽高比

一张图片就像美女,天然的最美,如果远看(等比缩小),还是美的,如果你拿个哈哈镜(图片拉伸)或者放大镜(图片等比放大)看,就算天生丽质也成如花。举个栗子: