1.开篇

先说说为什么要写这篇文章吧:不知从什么时候开始,大家相信前端摩尔定律:“每18个月,前端难度会增加一倍”。我并不完全认可这个数字的可靠性,但是这句话的本意我还是非常肯定的。

是的,前端越来越简单了,但也越来越复杂了—简单到你可以用一个Githubstarter搭建一个框架,集成所有的全家桶,涵盖单元测试和功能测试,包括部署以及发布,甚至你开发时使用的UI库都让你写不了几行css;可又复杂到如此多的框架和库层出不穷,你还没来得及学会官网的doc呢,就已经有新的替代品了,那就更别提静下心去学习其中的源码或推敲原理了,跟不上脚步强行搬砖自然略显疲惫。

正是前端飞速的发展使得前端看似简单,但若想深入却实属不易。顺便提一句,去年6月底,ES8已经发布了,没错,你没看错,是不感觉学不动了(开玩笑了,其实也没更新啥,不会再有ES5->ES6这种跨度了)。

所以,我近期觉得使用的框架有些多了,得静下心来沉淀沉淀—为什么要说写组件化思想呢?因为我觉得它是伴随着前端发展的一个不可或缺的设计细想,目前几大流行框架也都非常好的实现了组件化,比如ReactVueReact之前用得算是比较多了,所以本篇我决定以Vue作为基础,去谈一谈前端模块化,组件化,可维护化的设计细想。

2.什么是组件化

组件化并不是前端所特有的,一些其他的语言或者桌面程序等,有具有组件化的先例。确切的说,只要有UI层的展示,就必定有可以组件化的地方。简单来说,组件就是将一段UI样式和其对应的功能作为独立的整体去看待,无论这个整体放在哪里去使用,它都具有一样的功能和样式,从而实现复用,这种整体化的细想就是组件化。不难看出,组件化设计就是为了增加复用性,灵活性,提高系统设计,从而提高开发效率。

3.组件化的演变

如果你对JS的理解还停留在jQuery的话(jQuery本身是一个非常优秀的库),那么请跳过此文(开个玩笑)。在那个时候,大部分的前端开发应该都是十分过程式的开发:操作DOM,发起ajax请求,刷新数据,局部更新页面。这样的动作反反复复,甚至在同一个项目里同样的流程也许还要重复,其实jQuery本身也有有自己模块化的设计,有时我们也会用到类似jQuery UI等不错的库来减少工作量,但请注意,这里我只认为它是模块化的。

频繁操作DOM,过程式的开发方式的确不怎么样。这时开始流行MV*,比如MVC,前端开始学习后端的思想,讲业务逻辑,UI,功能,可以按照不同的文件去划分,结构清晰,设计明了,开发起来也不错。在这个基础上,又有了更加不错的MVVM框架,它的出现,更加简化了前端的操作,并且将前端的UI赋予了真实意义:你所看到的任何UI,应该都对应其相应的ViewModel,即你看到的view就是真实的数据,并且实现了双向绑定,只要UI改变,UI所对应的数据也改变,反之亦然。这的确很方便,但大部分的MVVM框架,并没有实现组件化,或者说没有很好的实现组件化,因为MVVM最大的问题就是:

  • 1.执行效率,只要数据改变,它下面所有监测数据上绑定的UI一般都会去更新,效率很低,如果你操作频繁,很可能调了几十万遍(有可能层次太深或者监测了太多的数据变化)。

  • 2.由于MVVM一般需要严格的ViewModel的作用域,因此大部分情况不支持多次绑定,或者只允许绑定一个根节点做为顶层DOM渲染,这就给组件化带来了困难(不能独立的去绑定部分UI)。

而后,在此基础上,一些新的前端框架“取其精华,去其糟粕”,开始大力推广前端组件化的开发方式,从这一点来说,ReactVue是类似的。

但从框架本身来说,ReactVue是完全不同的,前者是单向数据流管理设计的先驱,如果非让我做一个不恰当的比较的话,我觉得React+Redux是将MVC做到了极致(action->request, reducer->controller);而后者则是后起之秀,既吸取了React的数据流管理方式(Vue本身也可以用类似React去开发,但难度比较大而已,不是很Vue)的设计理念,也实现了MVVM的双向绑定和数据监控(这应该是Vue的核心了),所以Vue是比较灵活的,可以按需扩展,它才敢称自己是渐进式框架。

PS1: 并非讨论孰好孰坏,两大框架我都很喜欢。

PS2: 上面有提到模块化,个人觉得如果更广义的来讲,模块化和组件化并不在一个维度上,模块化往往是代码的设计和项目结构的设计;但很多时候在狭义的场景中,比如一个很通用的功能,也完全能够将其组件化或模块化,这两者此时十分相似,最大的区别就是组件必定是模块化的,并且往往需要实例化,也应当赋有生命周期,而模块化往往是直接引用。

4.如何实现组件化

我就以搜房网为例(最近房价居高不下,各个大佬还在吹各种牛x说房价不久后将白菜价,我顺便mark下看以后打谁的脸)进行demo分析。随手截图如下:

4.1分析页面布局

从大体上来看,可以分为顶部搜索,中间内容展示。而中间内容又分为part1,2,3三种类型。由于篇幅问题,本文只分析part1,2,3

每一个part中又可以分为header(title + link)和content(每个part不一样)

4.2初步开发

如果没有经过任何设计,也许会出现下面的代码:

<template>
  <div id="app">
    <div class="nav-search">...</div>
    <div class="panel">
      <div class="part1 left">
        <div>
          <span>万科城润园楼盘动态</span>
          <a href="">更多动态>></a>
        </div>
        <div>这里是每个part里面的具体内容</div>
      </div>
      <div class="part2 right">
        <div>
          <span>楼盘故事</span>
          <a href="">更多>></a>
        </div>
        <div>这里是每个part里面的具体内容</div>
      </div>
      <div class="part3">
        <div>
          <span>万科城润园户型</span>
          <a href="">二居(1)</a>
          <a href="">三居(4)</a>
          <a href="">四居(3)</a>
          <a href="">更多>></a>
        </div>
        <div>这里是每个part里面的具体内容</div>
      </div>
    </div>
  </div>
</template>

其中我省略了大部分的细节实现,实际代码量应该是这里的数倍。

这段代码有几个问题:

  • 1.part1,2,3的结构很类似,有些许重复

  • 2.实际的代码量将会很多,很难快速定位问题,维护难度较大

4.3化繁为简

首先我们可以将part1,2,3进行分离,这样就独立出来三个文件,那么结构上将会非常清晰

<template>
  <div id="app">
    <div class="nav-search">...</div>
    <div class="panel">
      <part1 />
      <part2 />
      <part3 /> 
  </div>
</template>

这有些类似将一个大函数逐步拆解成几部分的过程,不难想象part1,2,3中的代码,必然是适用性很差,确切的说只有这里能够引用。(但我看过很多项目的代码,就是这么干的,认为自己做了组件化,抽象还不错(@_@))

4.4组件抽象

仔细观察part1,2,3,正如我上面所说,它们其实是很相似的:都具有相同的外层border并附有shadow,都具有抬头和显示更多,各自内容部分暂不细说的话,这三个完全就是一模一样。

如此,我们将具有高度相似的业务数据进行抽离,实现组件的抽象。

part.vue

<template>
  <div class="part">
    <div class="hearder">
      <span></span>
      <a :href="linkForMore"></a>
    </div>
    <slot name="content" />
  </div>
</template>

我们将part内可以抽象的数据都做成了props,包括利用slot去做模版,同时showMore || '更多>>'也考虑到了part1的link名字和其他几个part不一致的情况。

这样一来app.vue就更加清晰化

<template>
  <div id="app">
    <div class="nav-search">...</div>
    <div class="panel">
      <part
        title="万科城润园楼盘动态"
        linkForMore="#1"
        showMore="更多动态>>"
      >
        <div slot="content">这里是part1里面的具体内容</div>
      </part>
      <part
        title="楼盘故事"
        linkForMore="#2"
      >
        <div slot="content">这里是part2里面的具体内容</div>
      </part>
      <part
        title="万科城润园户型"
        linkForMore="#3"
      >
        <div slot="content">这里是part3里面的具体内容</div>
      </part>
  </div>
</template>

这里有几点需要说明一下:

  • 1.三个part中部分UI差异应该在哪里定义?

比如三个part的宽度都不一样,并且part1和part2可能要需要进行浮动。

必须要记住,这种差异并不是组件本身的,<part />的设计本身应该是无浮动并且宽度占100%的,至于占谁的100%,那就取决于谁引用它,至于向左还是向右浮动,同样也取决于引用它的container需要自己去定义,在上面的代码中,app.vue就应该是<part />的container,app想要的是一个左浮动且宽度为80%的part(part1),右浮动且宽度为20%的part(part2)和一个宽度为100%的part(part3),但它们都是part,所以应该由app来设置这些差异。

记住这一点,将给你的抽象和扩展但来事半功倍的效果。

  • 2.三个part中的数据差异应该在哪里定义?

比如part3中,其他的part只有一个类似更多>>的link,但是它却有多个(一居,二居...)。

这里我推荐将这种差异体现在组件内部,设计方法也很多:

比如可以将link数组化为links;

比如可以将更多>>看作是一个default的link,而多余的部分则是用户自定义的特殊link,这两者合并组成了links。用户自定义的默认是没有的,需要引用组件时进行传入。

总之,只要有数据差异化,就应该结合组件本身和业务上下文将差异合理的消除在内部。

  • 3.注意组件内数据的命名方式

一个通用的,可扩展性高的组件,必然是有非常合理的命名的,比如观察一些组件库的命名,总会出现类似list,data,content,name,key,callback,className等名词,绝对不会出现我们系统中的类似iterationList, projectName等业务名词,这些名词和任一产品和应用都无关,它与自身抽象的组件有关,只表明组件内部的数据含义,偶尔也会代表其结构,所以只有这样,才能让用户通用。

我们在组件化时,也需要遵循这种设计原则,但库往往是想让广大开发者通用,而我们可以降低scope,做到在整个app内通用即可。所以从这个角度来说,好的组件化必然有好的BA和UX,这是大实话

5.写在最后

你也许会认为这样抽象没有太大的必要性,毕竟它只是一段静态UI(pure component),但任何的设计都是基于一定的复杂度才衍生出来的,其实大部分情况下这种设计都是需要将功能逻辑代码也纳入其中的,并不光只是UI(如antd, element-ui等),我这里举的例子也相对比较简单,并不想有太多的代码。

个人认为在一个大型前端项目中,这种组件化的抽象设计是很重要的,不仅增加了复用性提高了工作效率,从某种程度上来说也反应了程序员对业务和产品设计的理解,一旦有问题或者需要功能扩展时,你就会发现之前的设计是多么的make sense(毕竟需求总是在变哪)。