用上帝视角来看待组件的设计模式
此文已由作者黄锴授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
组件设计,从简单来看,就是如何提高编码效率,提高代码的复用率的方法,从高级来看,这是一门程序设计的艺术
最近看了redux作者——Dan Abramov写的《Presentational and Container Components》 感觉受益匪浅,发现有很多人都在讨论组件模式,作为一个每次写需求就抓耳挠腮的思考如何组织代码结构的人来说,如何更好的在工作中使用设计和使用组件,这确实是一个值得讨论的问题。
注:本文图片部分参考于Michael Chan 做的有关React component patterns的演讲: React component patterns, 有兴趣的童鞋可以去看看。
前言
组合模式
首先,我们先从宏观的角度来聊聊组件。
现在很多人现在都在谈设计模式,设计模式。什么是设计模式?设计模式主要是指GoF(四人组)《设计模式》一书中提出了23种设计模式,这23种设计模式被广泛应用在软件工程中,代表着最佳的实践方案。而在这些模式中,专门有一个设计模式叫:组合模式(Composite pattern),根据wikipedia的解释:
组合模式的目的是将对象组合成树型结构来表示部分-整体(part-whole)的层次结构。
组合模式使得用户对单个对象和组合对象的使用具有一致性。
它有以下优点
可扩展性强:由于组件间是充分解耦的,你可以轻松的更新,替换组件
编码高效:由于把功能拆解开,你可以专注于每个子模块功能的实现。
javascript中的组合模式——组件
如果你在现在的工作中使用了框架,你会发现,这些框架基本都使用了组合模式这种设计模式。例如Vue和React的Components,它允许你编写小的组件,然后通过他们的组合构建出你的实际应用。
在学校,总被老师说,要有大局观,大局观。使用组合模式,就可以很好的锻炼你的大局观。 这里我觉得云谦前辈 说的很贴切:
要记得,接到需求的第一步永远不是写代码,而是想清楚你要做什么,以上帝模式做整体设计。
开启上帝视角
接下来,就让我们在我们的编程世界扮演一个上帝(准确来说是女挖),看看我们这个充满组件世界到底发生了什么。
组件的诞生
在框架类的语言中,组件就是我们操作的基本单位,React对组件的操作提供了很多实用的API,一个组件就是通过使用这些API,产生了强大的生命力,你可以把它看作一个功能完善的细胞或者个体,我们所有的APP都是由这一个个细胞(个体)构成的。
然后,你在日常使用中会发现,有些组件,它总是使用其中的一部分API,有些组件经常使用另一部分API。说明这些组件开始有了自己的思考,产生了差异化(你可以把它看作基因突变)。当这些差异化越来越多,我们发现,这些组件因为各自的能力偏好,逐减组成了不同的阵营:
对于组件的阵营分类,有很多的说法:
胖和瘦( Fat and Skinny,)组件
状态和无状态/纯(Stateful and Stateless/pure)组件
聪明和愚笨(Smart and Dumb)组件
容器和展示( Container and Presentational )组件……
他们本质上都差不多,这里我们选用Redux作者Dan Abramov对组件的称法,叫他们容器和展示组件,这里使用Container和Components
组件分好了阵营,就要开始制定标准,正所谓无规矩不成方圆,一个良好的组织对确定他们统一的旗号,核心价值观,才能方便后续找到更多的同类人,发展壮大。
组件的阵营
容器组件(Container)阵营
他们称自己是统治者,这里汇聚的都是精英份子,它们习惯管理和组织。作为高高在上的管理者,能让下属去做的事情自己一定不会动手。
因此他们制定了一套符合他们价值观的标准:
代表结构:我只关心事情如何运作的,而不关心它是 如何表现的。
无样式:我这里除了一些包装div,基本没有其他标签,并且从不具有任何样式
有状态:我掌握着核心数据,剩下的事情你们去做
通常由其他组件生成:我们通常代表更高等的智慧(一般使用更高阶的组件生成)
代表人物:各种Page页面,路由页面
展示组件(Components)阵营
他们通常由蓝白领组成,代表这着广大的工人阶层,什么脏活累活都是他们做,他们通常没有太多想法,只是想简单的做好自己的工作,少背锅就好。
在这里,每个组件关心的事情很少,他们的志向只是做好本质工作,因此他们制定了一套符合他们工作习惯的标准:
代表渲染:关注事物的外观。
有样式:基本上dom的渲染和样式这些脏活累活都在这里干。
强调独立(很重要): 我们彼此分工明确,不依赖于应用程序的其余部分(例如Flux操作或Store)。
不关心数据:我们不关心数据怎么产生的,不要在我们这里指定数据的加载方式或变更方式。
接受传回指令:仅通过props接收数据和回调。
弱状态:我们基本不需要有自己的状态(当我们这样做时,它是UI状态而不是数据)。
代表人物:Search,SiderBar,UserList,Pagintation
这两种阵营基本是完全独立的,由于这种良好的划分,他们在社会中能很好的相处(蓝色是展示组件,灰色是容器组件。
实际运行
好了,看他们风风火火的把阵营分了,得给他们找点事干,看这个社会分工的实际运行效果怎么样。
这里顺便提一句React的组件定义,通常有两种方式类定义和无状态函数定义
无状态函数定义:
这样的好处是它本身是独立的,没有状态的,它只会根据传入的props(可以考虑成指令)来修改自己的显示,这样的组件就具备很好的通用性,而且修改起来也方便,不用担心会影响到别的组件。
var User = ({name}) => ( <span> {name} </span>);
类定义:
如果有些内置的状态需要维护,或者需要使用生命周期,可以使用类定义的方式。
class User extends Component { constructor(props) { super(props); this.state = { name: props.name, editable: false, }; } render(){ return ... } }
容器组件——定义结构,下发指令
容器组件可以通过类定义,因为可能会使用到生命周期钩子(使用dva也可能无需生命周期钩子)。
如果使用了redux类的状态管理机制,一般就会由connect生成(Relay的createContainer(),或Flux Utils的Container.create())。
我们现在要做一个用户页面,我们把这个任务布置下去,有一个容器阵营拿到了这个页面制作指标,然后这些精英开始做组织架构工作,他们觉得完成这个工作需要造三个组件:UserSearch,UserList, UserModal。然后每个组件需要的处理的指令(数据)也写好了,放在userXXXProps里,好了,他们的任务完成,开始去找展示容器阵营的人去实现这些功能。
function Users({ users }) { // 从Redux里获取数据 const { loading, list, total, current, } = users; // 下发任务 const userSearchProps = {}; const userListProps = {}; const userModalProps = {}; // 整体结构设计 return ( <div> <UserSearch {...userSearchProps} /> <UserList {...userListProps} /> <UserModal {...userModalProps} /> </div> ); } // 指定订阅数据,这里关联了 users function mapStateToProps({ users }) { return { users }; } // 建立数据关联关系 export default connect(mapStateToProps)(Users);
展示容器——接收指令,开工
一般来说,展示容器推荐使用 无状态函数 的方式生成
一个良好的展示容器应该是彼此独立的,这也是一个良好的分工社会应该具备的,大家各司其职,因此这里通常使用无状态函数生成一个组件。在这里,通常是根据props传入的值去处理相应的事情(老板要你干嘛就干嘛)。
// 从props里获取指令(数据)const UserList = ({ total, current, loading, dataSource, }) => {const columns = [……];// 开始工作,构建组件的渲染return ( <div> <Table columns={columns} dataSource={dataSource} loading={loading} /> </div>); }
这样看下来,这种社会分工好像运行的挺顺畅的。那这就是这个社会的全貌吗?当然不是,实际的生活中,组件的区分可以更加精确更加精细。
例如有一些组件设计模式里把组件模式细分为:
Proxy component
Presentational component
Layout component
Container component
Higher-order component (HOC's)
Render callback
但是,我个人认为以上两种划分已经足够应付生活中的绝大部分情形,剩下的,可以通过两者的组合实现mixed Component。
总结
这样看下来,这种社会分工好像运行的挺顺畅的,实际上,这种分类方式也是 很多开发者经验的总结。
根据“约定优于配置(convention over configuration)”的思想,提前做一定的规范肯定是没错的,但很多人将这种分工看做了他们设计组件的教条,这样就不好了,因为凡事总有例外,也许在某些复杂的业务场景中,Container 和 Components需要混用。你需要灵活的去使用,这也是为什么Dan修改了自己文章:
I amended the article because people were taking the separation as a dogma. 90% of React users don’t ever plan to have something like a living styleguide. There is no need to make life harder for them. Even those that do, don’t need to make every component available in such tool.
适合自己的才是最好的。
何苦让生活更艰难?
最后,再说说比较好的应用开发步骤是什么?我觉得可能是:
彻底弄清楚你要做什么
功能的划分
根据功能组织目录结构
设计你的数据模型(modal)
设计你的容器,确定整体框架结构
开始依次填充展示容器
连接组件和数据模型
测试
参考
Presentational and Container Components
React.js Conf 2015 - Making your app fast with high-performance components
Guide to Using the Composite Pattern with JavaScript
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 客户端SDK测试思路
【推荐】 Docker 的优势
【推荐】 最小化局部边际的合并聚类算法(中篇)