组件化开发

介绍

组件化什么时候在软件工程领域提出已经无从考究了,这个概念很早就被提出,近年来随着前端开发的迅速发展和工程化转变,组件化开发的思想也得到了进一步的发展和实践。我也是在前端开发中使用到Vue这个框架,接触到了组件化开发的思想。那什么是组件化呢?其实对(大)前端工程师来说很好理解,用一句话概括就是,接受外部数据输入,暴露特定事件的UI+逻辑组成的单元

对iOS工程师来说,系统提供的各种原生控件就是一种组件。例如一个输入框UITextField,它接受外部传入的初始值,接受用户输入,通过Delegate方式暴露出各种可以被关注的事件textChange。我们可以在xib/Storyboard/代码中方便的引入,用一个一个组件单元,组成我们的用户界面,同时监听我们所需的事件。对安卓工程师,也是一样的开发体验,各种原生的控件都相当的强大和方便。同时,当原生控件不能满足我们的需求时,我们也可以很方便的使用继承对原生控件进行扩展,比如添加子控件、添加自定义事件等,对于我们来说,开发手机客户端和搭积木非常相似,我们只需要选择合适的积木,按照一定的规则堆好就行了。

组件化.jpg

上图就是一个组件化开发的例子,页面分为Nav、List两个大的组件,List组件下包含了ListHeader、ListBody、ListFooter三个子组件,ListBody有包含若干ListCell组件,逻辑结构就可以表示成如下

<app>
  <Nav></Nav>
  <List>
    <ListHeader></ListHeader>
    <ListBody>
      <ListCell></ListCell>
      <ListCell></ListCell>
      ...
    </ListBody>
    <ListFooter></ListFooter>
  </List>
</app>

相信有客户端开发经验的同学看到这种结构一定非常熟悉——这不就是iOS里的Xib,Android中的Xml布局文件嘛!其实组件化的思想早已深入到了客户端开发中

Web组件化发展

而对于web前端来说,世界可能就没有那么美好了,我们没有那么多强大的控件得以使用,很多基础性的、公共的组件,浏览器都没有提供给我们,留给我们的只有

  • 简单的HTML标签
  • JS操作DOM的能力
  • CSS描述布局的功能。

在这种前提下,web前端初学者,包括我在初学的时候非常容易把代码写成意大利面条一样,所有的逻辑,比如ajax,更新DOM,更新数据等等都混杂在一起。这种写法在前端逻辑简单,界面层级不复杂的阶段是可行的,这种情况下引入框架反而会增加复杂性。

但随着前端的逐步发展以及前后端分离的开发趋势,现阶段的后端往往只提供数据,而不进行界面的构建和渲染,这样的好处就在于后端不必再给Web端提供专门的开发,而只需和手机客户端一样只提供接口和数据,让后端真正负责后台应该做的事情——处理业务逻辑、并发、分布式、缓存等等。这就要求前端处理更多的逻辑和渲染的工作,这是一个方面。另一方面,随着Google Mail Web版的横空出世,其与PC版几乎无异的流畅操作体验第一次把单页面应用(Single Page Application)带入人们的视线。于是,前端又有更多的事情要做了——路由、状态、本地存储、复杂动画。。。我们也把单页应用成为Web App,和iOS App、Android App等是平等的关系,开发的复杂程度也日趋接近。

所以,正如“人民日益增长的物质文化需求和落后的生产力之间的矛盾一样”,Web前端日益增长的需求和落后的开发标准和体验之间的矛盾,促使W3C标准的快速发展,从HTML5新增标签(也是组件的一种),到编程语言的发展ES5、ES6,以及Web的组件化标准Web Component`=,Web标准在一步一步走向完善,EmacScript更是加快到一年更新一个新版本。标准的制定是一回事,浏览器厂商的支持程度又是另一回事,目前的情况就是,虽然浏览器厂商积极的跟进Web标准,但距离主流浏览器完全实现还是有一定的距离,譬如组件化标准Web Components虽然很早提出但现在还没有定案。

正因为浏览器对组件化的支持程度不足,所以诞生了一系列的支持组件化的前端框架——Angular Directives、Ember Components、React Components、Vue.js Components等等,解决了前端开发的燃眉之急!

组件化的优势

下面以Vue组件为例,展示一下组件化开发的优势

1.可扩展

通过组件之间的合理组合搭配,可以构建出满足业务需求的新组件。
这一点很好理解,举个例子,我们有一个基础的树形组件treetree-node,现在来了一个业务需求,需要开发一个人员/机构的选择器,那么我们就可以很方便的基于我们的tree组件进行扩展,创建一个新的组件xx-selector

// xx-selector.vue
<template>
  <list :data="selectedData" /> // 已选列表
  </selected>
  <tree :data="treeData"  onSelect="onSelect">
    <template slot="treeNode">
      ...custom ui here
    </template>
  </tree> 
</template>

<script>
  import tree from ./tree.vue
  import list from ./list.vue
  module.exports = {
    data:{
      treeData:[],  
      selectedData:[]
    },
    created:function(){
      this.init()
    }
    methods:{
      onSelect:function(selected){
        this.selectedData = selected
        this.$emit('onSelectedChange',selected)
      },
      initData:function(){
        // ajax请求获取人员信息
      }
    }
  }
</script>

我们在逻辑代码部分加上诸如请求人员信息的接口、响应用户点击事件、响应全选/反选事件等等,这样就组成了一个功能完备的人员选择器组件。

2.可复用

刚才的xx-selector组件就复用到了基础的tree组件和tree-node组件。同样的道理,我们开发的xx-selector人员选择器组件,可以在任何有同样需求的地方重复使用,可以成为一个专属你们开发团队的一个基础组件,减少了开发量。

3.高内聚/低耦合

在我们使用xx-selector组件时,我们无需关心该组件内部的实现细节,我们只需要监听它暴露出来的onSelectedChanged(selectedList)事件,这个事件在选中人员发生改变时触发,把已选人员列表selectedList传递我们——我们使用选择器,所该关心就只是已选列表而已。

// my-app.vue
<template>
  <xx-selector onSelectedChanged="selectorChanged">
</template>
<script>
  import xx-selector from ./xx-selector.vue
  module.exports = {
    data:{ },
    methods:{
      selectorChanged:function(selected){
        // 处理已选
      }
    }
  }
</script>

这样就实现了

  • 组件内部高度内聚——只给外部提供功能,对外部的修改关闭
  • 组件之间低耦合——组件与组件只需要监听事件、触发事件,子组件不依赖与父组件

组件化的问题

正如《人月神话》中说到的——没有银弹,在软件工程中没有任何方法是完美的,组件化也有它的缺点,最明显的一点就是,当组件之间存在复杂和频繁的交流时,每一个组件都有可能修改系统的状态,由于多个状态分散的跨越在许多组件和交互间各个角落,大型应用复杂度也逐渐增长。

父子组件直接通信.png

通常在遇到这种情况的时候,Flux架构能较好的解决这种问题,通过单向数据流和统一的状态管理实现组件之间的依赖解耦。此时组件之间不再直接通信,而是通过修改共享的状态,去影响其他组件,间接得实现通信。这种方式在组件数量多、层次多时可以有效的降低复杂度。

Flux间接通信统一状态管理.png