1.开篇
先说说为什么要写这篇文章吧:不知从什么时候开始,大家相信前端摩尔定律:“每18个月,前端难度会增加一倍”。我并不完全认可这个数字的可靠性,但是这句话的本意我还是非常肯定的。
是的,前端越来越简单了,但也越来越复杂了---简单到你可以用一个Github
的starter
搭建一个框架,集成所有的全家桶,涵盖单元测试和功能测试,包括部署以及发布,甚至你开发时使用的UI库都让你写不了几行css
;可又复杂到如此多的框架和库层出不穷,你还没来得及学会官网的doc呢,就已经有新的替代品了,那就更别提静下心去学习其中的源码或推敲原理了,跟不上脚步强行搬砖自然略显疲惫。
正是前端飞速的发展使得前端看似简单,但若想深入却实属不易。顺便提一句,去年6月底,ES8
已经发布了,没错,你没看错,是不感觉学不动了(开玩笑了,其实也没更新啥,不会再有ES5
->ES6
这种跨度了)。
所以,我近期觉得使用的框架有些多了,得静下心来沉淀沉淀---为什么要说写组件化思想呢?因为我觉得它是伴随着前端发展的一个不可或缺的设计思想,目前几大流行框架也都非常好的实现了组件化,比如React
,Vue
。React
之前用得算是比较多了,所以本篇我决定以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)。
而后,在此基础上,一些新的前端框架“取其精华,去其糟粕”,开始大力推广前端组件化的开发方式,从这一点来说,React
和Vue
是类似的。
但从框架本身来说,React
和Vue
是完全不同的,前者是单向数据流管理设计的先驱,如果非让我做一个不恰当的比较的话,我觉得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>{{ title }}</span>
<a :href="linkForMore">{{ showMore || '更多>>' }}</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
(毕竟需求总是在变哪)。