Sift Science在产品中使用React已经有差不多一年的时间。这段时间里,我们的应用从Backbone + React frankenstein的hybrid app扩充为包含一个相当庞大的React组件体系。我们需要尽可能平滑的扩展我们的UI代码,本文将介绍其中所用到的技术和最佳实践。期间也会涉及到一些一些通用的组件设计模式。
希望本文能对你维护能够一个自我构建(而不是侵入式的)的React代码库有所帮助,能在UI复杂度增长的过程中帮你节省时间,保持条理清晰,此外还会为你提供一些相关工具。
在componentDidUpdate中做更多的事
React就是将不能避免更新任务的DOM转换成声名式的DOM。它以声名的方式将不可避名的行为转换为props和state的函数,从而得到相同的效果。举个例子:
示例中我们构建一个界而查看和修改联系人. 在截图的右边部分,“contact #3” 包含未保存更改. 我们希望表单可以自动保存一旦用户浏览下一个联系人,在这里是Contact #2.
navigateToContact:function(newContactId){ if(this._hasUnsavedChanges()){ this._saveChanges(); } this.setState({ currentContactId:newContactId }); } … navigateToContact(‘contact2’)
这是一个比较稚嫩的实现. 我们需要确保“contact #2”侧边栏菜单项和左下方的“< prev contact”点击事件的处理方法都链接到navigateToContact函数,而不是直接设置currentContactId的状态。
如果我们在componentDidUpdate中使用声明式的实现,就会是这样。
componentDidUpdate:function(prevProps,prevState){ if(prevState.currentContactId!==state.currentContactId){ if(this._hasUnsavedChanges()){ this._saveChanges(); } } }
在这个版本中,在浏览新联系人同时保存上一个联系人的功能实现在组件的生命周期中。由于所有的事件处理函数都能直接调用this.setState({currentContactId:’contact2′})而不是具体使某个特定的方法,现在这样更难于打断。
确实这个例子极其简单。在所有事件处理方法都调用navigateToContact也不算太差,但组件越来越复杂时,问题就会显现。声明式的行为基于prop和state的改变而执行相应处理,会使你的组件更多的自主性和更可靠。对于需要管理许多state的组件来说这个技术尤其有用,而且令重构对于我们更加的可操作。
尽可能多的使用组件
构建一个强壮的、可维护的、和组合性好的组件库会使你的控制器组件更容易构建。在Thinking in React 教程中,作者建议把单一责任原则作为划分组件的依据。
我们的代码库的可滑动组件就是以上思想的一个例子。一个可滑动组件能且只能处理下属组件的滑入滑出动画。虽然这可能看起来像是我们在滥用单一责任原则,但实际上,可滑动组件为我们节省了不少时间,因为它能很好地处理滑动动画。可以从任何方向滑动元件,并使用边缘作为锚。只要上级组件需要,它就可以使用JS替代默认的CSS来实现变换。而且该组件兼容多种浏览器,并经过了单元测试。(因此要实施的东西也比单纯用css transition group更多)。有了这种积木,我们就不用太担心像折叠面板或者通知中心,我们的角标通知系统这些组件的动画细节。
合理划分出重用性更高的组件使我们的团队更加高效,保证了一致的外观和美感,并降低了非前端团队成员向UI添砖加瓦的壁垒。下面的几节包含构建组件时保持组合性的技巧。
状态归属问题
往上顶
React 的文档中,还有另外一个必读章节。该章节建议我们尽可能保持组件的无状态特性。如果你发现自己在子父组件中复制或者同步状态,就把这个状态完全移出子组件。让父组件管理状态并向其子组件传递prop。
就拿选择器组件,一个定制的HTML<select>标签,说事。“当前选中的选项”应该放哪儿?通常一个选择器组件用来展示某些外部数据,比如说一个数据模型的特定字段的值。如果我们在选择器内部创建一个叫“选中选项”的状态,那么当用户选择一个新的选项时,我们就不得不同时更新模型中对应的状态和这个“选中选项”。这种重复的状态 可以通过让选择器组件向父组件接受一个“选中选项”的prop,而不是维护一个自己的状态来避免。
直观上,既然组件是用来展示某个数据模型的,那么这种状态属于这个模型。选择器组件只是一个几乎没有状态的UI控件,并且数据模型是对应的后端。只所以说是几乎是因为选择器事实上可以包含一点儿状态:它当前是否已经被展开。这个状态可以让它永久定居在选择器内部吧,因为它只是UI部分的细节,而且通常父组件也鸟它。在下一节中,我们将展示“当前是否已经被展开”这个状态是怎样被传递到下一层的组件当中去,以遵守单一责任原则。
从逻辑层分离出UI细节
我们已经使用了有状态的高层次组件和无状态的较低级别的组件模式。这些无状态的组件提供了UI渲染细节、样式和标记的重用。有状态的包装组件提供了交互逻辑的重用。让组件具有复用性的这种模式已经成为我们最重要的规则。这里有一个细节是关于如何创建Select组件,以及如何重用UI代码。这段UI代码也应用在了ToolTipToggle组件中。
Select组件
Select组件与HTML里的<select>标签很相似。它需要一个选择列表作为当前的选项集合,但是这是没有状态属性的。甚至没有属性表明是否为当前展开的状态。Select组件是由具有下拉展开和折叠状态的DropdownToggle组件构成。
DropdownToggle
这个组件在点击触发时,元素和子节点将会显示一个下拉的HoverCard。Select(选择)通过一个按钮和一个向下的箭头图标触发DropdownToggle。它还可以通过可选择的选项列表作为DropdownToggle的子节点。
TooltipToggle
TooltipToggle是类似于DropdownToggle的,都是接收触发器管理HoverCard子节点的显示。不同之处是如何显示HoverCard; 他们的交互逻辑是不一样的。DropdownToggle监听单击触发元素,TooltipToggle监听鼠标悬停事件。当ESC键被按下的时候,TooltipToggle是不关闭的,而DropdownToggle是会关闭的。
HoverCard
HoverCard是显示层的明星。它增强了UI标签,风格和工具提示,以及下拉菜单的一部分事件处理。它没有状态,也不知道是否打开或关闭。如果它存在意味着打开,卸载了就是关上它。
HoverCard接受锚元素作为一个属性,这是悬浮位置的定位元素。HoverCard也有多种样式和感觉。一种样式叫工具提示(tooltip),它有黑色背景和白色文字。另一种样式叫下拉菜单(dropdown),它被Select组件调用使用,白色的背景和盒子的阴影。
HoverCard也可以定制化多种属性,例如是否显示一个三角符号(TooltipToggle开启了这个属性)。
或者通过位置属性明确HoverCard与锚点的关系(TooltipToggle 使用“top” 而 DropdownToggle 使用 “bottom”),等等这些。
HoverCard也监听外部的某些事件(例如clicks)或者ESC按键,当这些事件发生了,HoverCard通过属性回调(prop callbacks)通知父元素,由父元素决定是否关闭HoverCard。HoverCard另一个能力是侦听当前位置是否在窗口中溢出了,如果是,立刻修改当前位置。
(这个功能也可以通过属性值来关闭)
正因为HoverCard提取了所有的UI细节的代码,所以更高层的组件(例如DropdownToggle,TooltipToggle)可以只专注于状态管理和业务逻辑交互,而不必再实现一遍。Dom定位和样式代码共享于UI的hover-y中。
这只是一个例子,分离UI细节和交互逻辑。对所有组件依据这一原则,并且仔细评估哪一部分是新状态,肯定可以提高我们重用代码的能力。
什么是 Flux?
Flux 特别适合存储应用程序的状态信息,这些状态不属于任何的组件,并且状态需要持久化保存的时候。一个通常建议是不要使用this.state把什么东西都存入Flux中——不过,这也不全对。当有些被卸载了,你应该使用this.state去清空不相关的特殊状态。一个例子就是DropdownToggle组件里的isCurrentlyOpen状态。
Flux也很重,所以存储本来就应该持久化到服务器上的数据显得很不适合。我们当前使用的是全局Backbone模型缓存,用于数据获取和存储。但是我们也在试验采用REST API形式的Relay-like系统。(敬请期待更多关于这个主题)
对于所有其他的情况,我们会在代码中逐步介绍Flux。它很牛,因为不需要重写。只要你想用,在哪都可以。它很容易单元测试,容易扩展,也提供了一些很酷的特性例如在我们的核心模型里解决循环依赖问题。它也删掉了不必要的单例模型组件。
用React代替CSS
这篇文章最后一个想分享的是:明确React组件集合是你重用代码的主要工具。我们的每一个React组件都关联了一个CSS文件。部分组件甚至没有任何JS逻辑。他们只是标记和样式的绑定。
我们已经远离了类似Bootstrap 那样的全局风格的类名。你肯定还是可以使用 Bootstrap 的,而且在你的 React 组件中已经包装了Bootstrap组件,从长远来看,这将会节省你的时间。举例来说,有一个 Icon React 组件被封装进内部,标记和接受图标名称作为支撑比不得不记住什么标记和类名来使用图标要好,也变得更简单。也使得可以将功能添加进组件。
尽管我们定义了一些全局风格的元素,例如锚(anchor)和标题(heading),以及许多全局的 SCSS 变量,但是我们没有真正定义全局的 css 类。我们小心地重用 UI ,尽可能地使用React 组件,这让我们作为一个团队更有效率,因为代码更一致和更可预测。
这是关于它的!这些都是一些指导原则,它帮助我们建立一个健壮的React架构,这扩展了我们的工程团队和应用的复杂性。如果你有与这篇文章相关的想法和经验,请发表评论。
如果你想看到更多,请查看React提示和最佳实践。