1. 开篇
先说说为什么要写这篇文章吧: 不知从什么时候开始, 大家相信前端摩尔定律: 每 18 个月, 前端难度会增加一倍我并不完全认可这个数字的可靠性, 但是这句话的本意我还是非常肯定的
是的, 前端越来越简单了, 但也越来越复杂了 --- 简单到你可以用一个 Github 的 starter 搭建一个框架, 集成所有的全家桶, 涵盖单元测试和功能测试, 包括部署以及发布, 甚至你开发时使用的 UI 库都让你写不了几行 CSS; 可又复杂到如此多的框架和库层出不穷, 你还没来得及学会官网的 doc 呢, 就已经有新的替代品了, 那就更别提静下心去学习其中的源码或推敲原理了, 跟不上脚步强行搬砖自然略显疲惫
正是前端飞速的发展使得前端看似简单, 但若想深入却实属不易顺便提一句, 去年 6 月底, ES8 已经发布了, 没错, 你没看错, 是不感觉学不动了(开玩笑了, 其实也没更新啥, 不会再有 ES5->ES6 这种跨度了)
所以, 我近期觉得使用的框架有些多了, 得静下心来沉淀沉淀 --- 为什么要说写组件化思想呢? 因为我觉得它是伴随着前端发展的一个不可或缺的设计思想, 目前几大流行框架也都非常好的实现了组件化, 比如 React,vueReact 之前用得算是比较多了, 所以本篇我决定以 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 分析随手截图如下:
demo1.png
4.1 分析页面布局
demo2.png
从大体上来看, 可以分为顶部搜索, 中间内容展示而中间内容又分为 part1,2,3 三种类型由于篇幅问题, 本文只分析 part1,2,3
每一个 part 中又可以分为 header(title + link)和 content(每个 part 不一样)
demo3.png
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(毕竟需求总是在变哪)
来源: http://www.jianshu.com/p/9445932a1daa