注:本文为前端之巅特供稿件,如转载,请联系前端之巅。投稿请关注“前端之巅”微信公众号并发送“投稿”。
随着 SPA,前后端分离的技术架构在业界越来越流行,前端(注:本文中的前端泛指所有的用户可接触的界面,包括桌面,移动端)需要管理的内容,承担的职责也越来越多。再加上移动互联网的火爆,及其带动的 Mobile First 风潮,各大公司也开始在前端投入更多的资源。这一切,使得业界对前端开发方案的思考上多了很多,以 React 框架为代表推动的组件化开发方案就是目前业界比较认可的方案,本文将和大家一起探讨一下组件化开发方案能给我们带来什么,以及如何在 React Native 项目的运用组件化开发方案。
一、为什么要采用组件化开发方案?
在讲怎么做之前,需要先看看为什么前端要采用组件化开发方案,作为一名程序员和咨询师,我清楚地知道凡是抛开问题谈方案都是耍流氓。那么在面对随着业务规模的增加,更多的业务功能推向前端,以及随之而来的开发团队扩张时,前端开发会遇到些什么样的问题呢?
1. 前端开发面临的问题
(1)资源冗余:页面变得越来越多,页面的交互变得越来越复杂。在这种情况下,有些团队成员会根据功能写自己的 CSS、JS,这会产生大量的新的 CSS 或 JS 文件,而这些文件中可能出现大量的重复逻辑;有些团队成员则会重用别人的逻辑,但是由于逻辑拆分的粒度差异,可能会为了依赖某个 JS 中的一个函数,需要加载整个模块,或者为了使用某个 CSS 中的部分样式依赖整个 CSS 文件,这导致了大量的资源冗余。
(2)依赖关系不直观:当修改一个 JS 函数,或者某个 CSS 属性时,很多时候只能靠人力全局搜索来判断影响范围,这种做法不但慢,而且很容易出错。
(3)项目的灵活性和可维护性差:因为项目中的交叉依赖太多,当出现技术方案变化时,无法做到渐进式的、有节奏地替换掉老的代码,只能一次性替换掉所有老代码,这极大地提升了技术方案升级的成本和风险。
(4)新人进组上手难度大:新人进入项目后,需要了解整个项目的背景、技术栈等,才能或者说才敢开始工作。这在小项目中也许不是问题,但是在大型项目中,尤其是人员流动比较频繁的项目,则会对项目进度产生非常大的影响。
(5)团队协同度不高:用户流程上页面间的依赖(比方说一个页面强依赖前一个页面的工作结果),以及技术方案上的一些相互依赖(比方说某个文件只能由某个团队修改)会导致无法发挥一个团队的全部效能,部分成员会出现等待空窗期,浪费团队效率。
(6) 测试难度大:整个项目中的逻辑拆分不清晰,过多且杂乱的相互依赖都显著拉升了自动化测试的难度。
(7) 沟通反馈慢:业务的要求,UX 的设计都需要等到开发人员写完代码,整个项目编译部署后才能看到实际的效果,这个反馈周期太长,并且未来的任何一个小修改又需要重复这一整个流程。
2. 组件化开发带来的好处
组件化开发的核心是“业务的归业务,组件的归组件”。即组件是一个个独立存在的模块,它需要具备如下的特征:
- 职责单一而清晰:开发人员可以很容易了解该组件提供的能力。
- 资源高内聚: 组件资源内部高内聚,组件资源完全由自身加载控制。
- 作用域独立: 内部结构密封,不与全局或其他组件产生影响。
- 接口规范化: 组件接口有统一规范。
- 可相互组合: 组装整合成复杂组件,高阶组件等。
- 独立清晰的生命周期管理:组件的加载、渲染、更新必须有清晰的、可控的路径。
而业务就是通过组合这一堆组件完成 User Journey。下一节中,会详细描述采用组件化开发方案的团队是如何运作的。
在项目中分清楚组件和业务的关系,把系统的构建架构在组件化思想上可以:
(1) 降低整个系统的耦合度:在保持接口不变的情况下,我们可以把当前组件替换成不同的组件实现业务功能升级,比如把一个搜索框,换成一个日历组件。
(2) 提高可维护性:由于每个组件的职责单一,在系统中更容易被复用,所以对某个职责的修改只需要修改一处,就可获得系统的整体升级。独立的,小的组件代码的更易理解,维护起来也更容易。
(3) 降低上手难度:新成员只需要理解接口和职责即可开发组件代码,在不断的开发过程中再进一步理解和学习项目知识。另外,由于代码的影响范围仅限于组件内部,对项目的风险控制也非常有帮助,不会因为一次修改导致雪崩效应,影响整个团队的工作。
(4) 提升团队协同开发效率:通过对组件的拆分粒度控制来合理分配团队成员任务,让团队中每个人都能发挥所长,维护对应的组件,最大化团队开发效率。
(5) 便于自动化测试:由于组件除了接口外,完全是自治王国,甚至概念上,可以把组件当成一个函数,输入对应着输出,这让自动化测试变得简单。
(6) 更容易的自文档化:在组件之上,可以采用 Living Style Guide 的方式为项目的所有 UI 组件建立一个‘活’的文档,这个文档还可以成为业务,开发,UX 之间的沟通桥梁。这是对‘代码即文档’的另一种诠释,巧妙的解决了程序员不爱写文档的问题。
(7) 方便调试:由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题。另外,Living Style Guide 除了作为沟通工具,还可以作为调试工具,帮助开发者调试 UI 组件。
二、组件化开发方案下,团队如何运作?
前面大致讲了下组件化开发可以给项目带来的好处,接下来聊一聊采用组件化开发方案的团队是应该如何运作?
在 ThoughtWorks,我们把一个项目的生命周期分为如下几个阶段:
组件化开发方案主要关注的是在迭代开发阶段的对团队效率的提升。 它主要从以下几个方面提升了开发效率:
1. 以架构层的组件复用降低工作量
在大型应用的后端开发中,为了分工、复用和可维护性,在架构层面将应用抽象为多个相对独立的模块的思想和方法都已经非常成熟和深入人心了。但是在前端开发中,模块化的思想还是比较传统,开发者还是只有在需考虑复用时才会将某一部分做成组件,再加上当开发人员专注在不同界面开发上时,对于该界面上哪些部分可以重用缺乏关注,导致在多个界面上重复开发相同的 UI 功能,这不仅拉升了整个项目的工作量,还增加了项目后续的修改和维护成本。
在组件化开发方案下,团队在交付开始阶段就需要从架构层面对应用的 UI 进行模块化,团队会一起把需求分析阶段产生的原型中的每一个 UI 页面抽象为一颗组件树,UI 页面自己本身上也是一个组件。如下图:
通过上面的抽象之后,我们会发现大量的组件可以在多个 UI 界面上复用,而考虑到在前端项目中,构建各个 UI 界面占了 80% 以上的工作量,这样的抽象显著降低了项目的工作量,同时对后续的修改和维护也会大有裨益。
在这样的架构模式下,团队的运作方式就需要相应的发生改变:
(1) 工程化方面的支持,从目录结构的划分上对开发人员进行组件化思维的强调,区分基础组件,业务组件,页面组件的位置,职责,以及相互之间的依赖关系。
(2) 工作优先级的安排,在敏捷团队中,我们强调的是交付业务价值。而业务是由页面组件串联而成,在组件化的架构模式下,必然是先完成组件开发,再串联业务。所以在做迭代计划时,需要对团队开发组件的任务和串联业务的任务做一个清晰的优先级安排,以保证团队对业务价值的交付节奏。
2. 以组件的规范性保障项目设计的统一性
在前端开发中,因为 CSS 的灵活性,对于相同的 UI 要求(比如:布局上的靠右边框 5 个像素),就可能有上十种的 CSS 写法,开发人员的背景,经历的不同,很可能会选择不同的实现方法;甚至还有一些不成熟的项目,存在需求方直接给一个 PDF 文件的用户流程图界面,不给 PSD 的情况,所有的设计元素需要开发人员从图片中抓取,这更是会使得项目的样式写的五花八门。因为同样的 UI 设计在项目中存在多种写法,会导致很多问题,第一就是设计上可能存在不一致的情况;第二是 UI 设计发生修改时,出现需要多种修改方案的成本,甚至出现漏改某个样式导致 bug 的问题。
在组件化开发方案下,项目设计的统一性被上拉到组件层,由组件的统一性来保障。其实本来所有的业务 UI 设计就是组件为单位的,设计师不会说我要“黄色”,他们说得是我要“黄色的按钮……”。是开发者在实现过程中把 UI 设计下放到 CSS 样式上的,相比一个个,一组组的 CSS 属性,组件的整体性和可理解性都会更高。再加上组件的资源高内聚特性,在组件上对样式进行调整也会变得容易,其影响范围也更可控。
在组件化开发方案下,为了保证 UI 设计的一致性,团队的运作需要:
- 定义基础设计元素,包括色号、字体、字号等,由 UX 决定所有的基础设计元素。
- 所有具体的 UI 组件设计必须通过这些基础设计元素组合而成,如果当前的基础设计元素不能满足需求,则需要和 UX 一起讨论增加基础设计元素。
- UI 组件的验收需要 UX 参与。
3. 以组件的独立性和自治性提升团队协同效率
在前端开发时,存在一个典型的场景就是某个功能界面,距离启动界面有多个层级,按照传统开发方式,需要按照页面一页一页的开发,当前一个页面开发未完成时,无法开始下一个页面的开发,导致团队工作的并发度不够。另外,在团队中,开发人员的能力各有所长,而页面依赖降低了整个项目在任务安排上的灵活性,让我们无法按照团队成员的经验,强项来合理安排工作。这两项对团队协同度的影响最终会拉低团队的整体效率。
在组件化开发方案下,强调业务任务和组件任务的分离和协同。组件任务具有很强的独立性和自治性,即在接口定义清楚的情况下,完全可以抛开上下文进行开发。这类任务对外无任何依赖,再加上组件的职责单一性,其功能也很容易被开发者理解。所以在安排任务上,组件任务可以非常灵活。而业务任务只需关注自己依赖的组件是否已经完成,一旦完成就马上进入 Ready For Dev 状态,以最高优先级等待下一位开发人员选取。
在组件化开发方案下,为了提升团队协同效率,团队的运作需要:
(1)把业务任务和组件任务拆开,组件的归组件,业务的归业务。
(2)使用 Jira,Mingle 等团队管理工具管理好业务任务对组件任务的依赖,让团队可以容易地了解到每个业务价值的实现需要的完成的任务。
(3) Tech Lead 需要加深对团队每个成员的了解,清楚的知道他们各自的强项,作为安排任务时的参考。
(4) 业务优先原则,一旦业务任务依赖的所有组件任务完成,业务任务马上进入最高优先级,团队以交付业务价值为最高优先级。
(5)组件任务先于业务任务完成,未纳入业务流程前,团队需要 Living Style Guide 之类的工具帮助验收组件任务。
4. 以组件的 Living Style Guide 平台降低团队沟通成本
在前端开发时,经常存在这样的沟通场景:
- 开发人员和 UX 验证页面设计时,因为一些细微的差异对 UI 进行反复的小修改。
- 开发人员和业务人员验证界面流程时,因为一些特别的需求对 UI 进行反复的小修改。
- 开发人员想复用另一个组件,寻找该组件的开发人员了解该组件的设计和职责
- 开发人员和 QA 一起验证某个公用组件改动对多个界面上的影响
当这样的沟通出现在上一小节的提到的场景,即组件出现在距离启动界面有多个层级的界面时,按照传统开发方式,UX 和开发需要多次点击,有时甚至需要输入一些数据,最后才能到达想要的功能界面。没有或者无法搭建一个直观的平台满足这些需求,就会导致每一次的沟通改动就伴随着一次重复走的,很长的路径。使得团队的沟通成本激增,极大的降低了开发效率。
在组件化开发方案下, 因为组件的独立性,构建 Living Style Guide 平台变得非常简单,目前社区已经有了很多工具支持构建 Living Style Guide 平台(比如 getstorybook: https://getstorybook.io ),开发人员把组件以 Demo 的形式添加到 Living Style Guide 平台就行了,然后所有与 UI 组件的相关的沟通都以该平台为中心进行,因为开发对组件的修改会马上体现在平台上,再加上平台对组件的组织形式让所有人都可以很直接的访问到任何需要的组件,这样,UX 和业务人员有任何要求,开发人员都可以快速修改,共同在平台上验证,这种“所见即所得”的沟通方式节省去了大量的沟通成本。此外,该平台自带组件文档功能,团队成员可以从该平台上看到所有组件的 UI,接口,降低了人员变动导致的组件上下文知识缺失,同时也降低了开发者之间对于组件的沟通需求。
想要获得这些好处,团队的运作需要:
(1) 项目初期就搭建好 Living Style Guide 平台。
(2) 开发人员在完成组件之后必须添加 Demo 到平台,甚至根据该组件需要适应的场景,多添加几个 Demo。这样一眼就可以看出不同场景下,该组件的样子。
(3) UX,业务人员通过平台验收组件,甚至可以在平台通过修改组件 Props,探索性的测试在一些极端场景下组件的反应。
5. 对需求分析阶段的诉求和产品演进阶段的帮助
虽然需求分析阶段和产品演进阶段不是组件化开发关注的重点,但是组件化开发的实施效果却和这两个阶段有关系,组件化方案需要需求分析阶段能够给出清晰的 Domain 数据结构,基础设计元素和界面原型,它们是组件化开发的基础。而对于产品演进阶段,组件化开发提供的两个重要特性则大大降低了产品演进的风险:
- 低耦合的架构,让开发者清楚的知道自己的修改影响范围,降低演进风险。开发团队只需要根据新需求完成新的组件,或者替换掉已有组件就可以完成产品演进。
- Living Style Guide 的自文档能力,让你能够很容易的获得现有组件代码的信息,降低人员流动产生的上下文缺失对产品演进的风险。
三、组件化开发方案在 React Native 项目中的实施
前面已经详细讨论了为什么和如何做组件化开发方案,接下来,就以一个 React Native 项目为例,从代码级别看看组件化方案的实施。
1. 定义基础设计元素
在前面我们已经提到过,需求分析阶段需要产出基本的设计元素,在前端开发人员开始写代码之前需要把这部分基础设计元素添加到代码中。在 React Native 中,所有的 CSS 属性都被封装到了 JS 代码中,所以在 React Native 项目开发中,不再需要 LESS,SCSS 之类的动态样式语言,而且你可以使用 JS 语言的一切特性来帮助你组合样式,所以我们可以创建一个 theme.js 存放所有的基础设计元素,如果基础设计元素很多,也可以拆分位多个文件存放。
import { StyleSheet } from 'react-native'; module.exports = StyleSheet.create({ colors: {...}, fonts: {...}, layouts: {...}, borders: {...}, container: {...}, });
然后,在写具体 UI 组件的 styles, 只需要引入该文件,按照 JS 的规则复用这些样式属性即可。
2. 拆分组件树之 Component,Page,Scene
在实现业务流程前,需要对项目的原型 UI 进行分解和分类,在 React Native 项目中,我把 UI 组件分为了四种类型:
- Shared Component: 基础组件,Button,Label 之类的大部分其它组件都会用到的基础组件
- Feature Component: 业务组件,对应到某个业务流程的子组件,但其不对应路由, 他们通过各种组合形成了 Pag 组件。
- Page: 与路由对应的 Container 组件,主要功能就是组合子组件,所有 Page 组件最好名字都以 Page 结尾,便于区分。
- Scene: 应用状态和 UI 之间的连接器,严格意义上它不算 UI 组件,主要作用就是把应用的状态和 Page 组件绑定上,所有的 Scene 组件以 Scene 后缀结尾。
Component 和 Page 组件都是 Pure Component,只接收 props, 然后展示 UI,响应事件。Component 的 Props 由 Page 组件传递给它,Page 组件的 Props 则是由 Scene 组件绑定过去。下面我们就以如下的这个页面为例来看看这几类组件各自的职责范围:
(1)searchResultRowItem.js
export default function (rowData) { const {title, price_formatted, img_url, rowID, onPress} = rowData; const price = price_formatted.split(' ')[0]; return ( <TouchableHighlight onPress={() => onPress(rowID)} testID={'property-' + rowID} underlayColor='#dddddd'> <View> <View style={styles.rowContainer}> <Image style={styles.thumb} source={{ uri: img_url }}/> <View style={styles.textContainer}> <Text style={styles.price}>{price}</Text> <Text style={styles.title} numberOfLines={1}>{title}</Text> </View> </View> <View style={styles.separator }/> </View> </TouchableHighlight> );}
(2)SearchResultsPage.js
import SearchResultRowItem from '../components/searchResultRowItem';export default class SearchResultsPage extends Component { constructor(props) { super(props); const dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1.guid !== r2.guid}); this.state = { dataSource: dataSource.cloneWithRows(this.props.properties), onRowPress: this.props.rowPressed, }; } renderRow(rowProps, sectionID, rowID) { return <SearchResultRowItem {...rowProps} rowID={rowID} onPress={this.state.onRowPress} />; } render() { return ( <ListView style={atomicStyles.container} dataSource={this.state.dataSource} renderRow={this.renderRow.bind(this)} /> ); }}
(3)SearchResultsScene.js
import SearchResults from '../components/searchResultsPage';function mapStateToProps(state) { const {propertyReducer} = state; const {searchReducer:{properties}} = propertyReducer; return { properties, };}function mapDispatchToProps(dispatch) { return { rowPressed: (propertyIndex) => { dispatch(PropertyActions.selectProperty(propertyIndex)); RouterActions.PropertyDetails(); } };}module.exports = connect( mapStateToProps, mapDispatchToProps,)(SearchResults);
3.Living Style Guide
目前社区上,最好的支持 React Native 的 Living Style Guide 工具是 getstorybook,关于如何使用 getstorybook 搭建 React Native 的 Living Style Guide 平台可以参见官方文档 ( https://github.com/kadirahq/react-native-storybook ) 或者我的博客 ( http://www.jianshu.com/p/36cbd8393288 )。
搭建好 Living Style Guide 平台后,就可以看到如下的界面:
接下来的工作就是不断在往该平台添加 UI 组件的 Demo。向 storybook 中添加 Demo 非常简单,下面就是一个关于 SearchPage 的 Demo:
import React from 'react';import {storiesOf, action} from '@kadira/react-native-storybook';import SearchPage from '../../../../src/property/components/searchPage';storiesOf('Property', module) .add('SearchPage', () => ( <SearchPage request={{place_name:"London"}} isLoading={false} search={action('Search called')}/> ));
从上面的代码可以看出,只需要简单的三步就可以完成一个 UI 组件的 Demo:
(1)import 要做 Demo 的 UI 组件。
(2) storiesOf 定义了一个组件目录。
(3) add 添加 Demo。
在构建项目的 storybook 时,一些可以帮助我们更有效的开发 Demo 小 Tips:
(1)尽可能的把目录结构与源代码结构保持一致。
(2) 一个 UI 组件对应一个 Demo 文件,保持 Demo 代码的独立性和灵活性,可以为一个组件添加多个 Demo,这样一眼就可以看到多个场景下的 Demo 状态。
(3) Demo 命名以 UI 组件名加上 Demo 缀。
(4) 在组件参数复杂的场景下,可以单独提供一个 fakeData 的目录用于存放重用的 UI 组件 Props 数据。
4. 一个完整的业务开发流程
在完成了上面三个步骤后,一个完整的 React Native 业务开发流程可简单分为如下几步:
(1)使用基础设计元素构建基础组件,通过 Living Style Guide 验收。
(2)使用基础组件组合业务组件,通过 Living Style Guide 验收。
(3)使用业务组件组合 Page 组件,通过 Living Style Guide 验收。
(4)使用 Scene 把 Page 组件的和应用的状态关联起来。
(5)使用 Router 把多个 Scene 串联起来,完成业务流程。
四、总结
随着前后端分离架构成为主流,越来越多的业务逻辑被推向前端,再加上用户对于体验的更高要求,前端的复杂性在一步一步的拔高。对前端复杂性的管理就显得越来越重要了。经过前端的各种框架,工具的推动,在前端工程化实践方面我们已经迈进了很多。而组件化开发就是笔者觉得其中比较好的一个方向,因为它不仅关注了当前的项目交付,还指导了团队的运作,帮助了后期的演进,甚至在程序员最讨厌的写文档的方面也给出了一个巧妙的解法。希望对该方法感兴趣的同学一起研究,改进。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论