什么是前端组件化和模块化?
这两天一直在思考这个问题,以前对这两个概念的理解很模糊。认为“模块化”是侧重于功能或者数据的封装,目的是为了解耦合;而“组件化”更关注的UI部分,如一个页面可以分为头部、底部和内容区域等等。
这样的理解很明显是表层的简单的可能还是不正确的理解,最近反复阅读了苏宁前端“代码民工徐飞”关于组件化话的几篇文章,结合Angular1.x和Vue1.x的组件思想对组件化和模块化有了一些新的认识(原文链接在文末)。
什么是组件化
组件化的概念在后端早已存在多年,只不过近几年随着前端的发展,这个概念在前端开始被频繁提及,特别是在MV*的框架中。
前端中的“组件化”这个词,在UI这一层通常指“标签化”,也就是把大块的业务界面,拆分成若干小块,然后进行组装。
狭义的组件化一般是指标签化,也就是以自定义标签(自定义属性)为核心的机制。
广义的组件化包括对数据逻辑层业务梳理,形成不同层级的能力封装。
为什么要有组件化?
不管是前端组件化还是后端的组件化,我认为其目的无非就是为了提高开发效率和后期维护的效率。
在说的详细点,就是比如我想实现一个网站的头部,我可以把头部单独拿出来进行封装,根据不同页面或者说业务要求,可以灵活定制不同的头部(结构一致,颜色或展示等不同)。这样就可以在不同的页面进行灵活的复用,后期如果头部结构有大的变动,可以只修改该头部组件就好了。是不是这个概念很熟悉,其实你早就在这么干了,比如以前用jsp做静态页面生成的时候,就可以利用jsp的<jsp:include page="xxx.jsp"/>
指令进行引入公共组件,可能也有人理解那是模板的概念。
MV*框架中的组件
大概了解了一下组件化的概念,我们来看看Vue和Angular中是怎样运用组件的思想的。
Vue中的组件
组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
以上是Vue官网对于Vue中组件的解释,其中在徐飞的从HTML Components的衰落看Web Components的危机一文中的评论部分,Vue的作者尤雨溪(github:yyx990803)发表了对民工叔叔(我只是小菜鸟,叫叔叔没啥问题,哈哈)文章的看法,其中也提到了他对于组件的认识。
Vue中关于组件的介绍是Vue中的重点部分,从它在Vue官方文档中的篇幅就可以看出来。我认为组件中最重要的方面,到目前为止我能理解的就两部分:通讯和复用。接下来重点介绍一下Vue对于通讯的实现。
结合尤雨溪在民工叔叔文章中评论那样,组件之间的通讯可分为从内向外和从外向内两种。Vue对于这两种通信时怎样解决的呢?文档中说的很详细了,events up和props down。
从外向内的“props down”
具体来说,就是当父组件向子组件传递信息的时候,采用的是在子组件的构造对象中显示的设置props属性进行数据的传输。
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 可以用在模板内
// 同样也可以在 vm 实例中像 “this.message” 这样使用
template: '<span>{{ message }}</span>'
})
然后向他传入一个普通字符串值:
<child message="hello!"></child>
当然为了实现字面量语法或者动态语法,可以使用v-bind
在父组件上来绑定数据,详细语法请参照Vue文档,本文不做详细介绍。
需要特别注意的是:
- Vue默认的是单项数据流,当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
- 另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
- 注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
并且当props为应用类型的时候还可以为其添加验证。
从内向外的“events up”
Vue中默认的虽然是单项数据流,但还是可以实现子组件向父组件传输数据的,因为有些场景中,我们不可避免的会使用到。而Vue中向上传递数据采用的和Angular中一样的思想,通过自定义事件的方式实现。大体需要以下两步步:
- 使用 $emit(eventName)在子组件上触发事件
- 使用 $on(eventName) 在父组件上(或者祖先组件)监听事件
<div id="counter-event-example">
<p>{{ total }}</p>
//父组件监听事件
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
template: '<button v-on:click="increment">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
this.counter += 1;
this.$emit('increment');//子组件触发事件
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1;
}
}
})
中央事件总线
有时候非父子关系的组件也需要通信。在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue();
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})
在更多复杂的情况下,你应该考虑使用专门的 状态管理模式--Vuex
另外Vue还可以使用slot分发内容,具体实现和更多实现请参考Vue文档。
简单描述了一下Vue的组件思想,主要目的是从民工叔叔的组件化文章中重新认识到了组件化的思想,结合Vue框架去理解,可能更容易明白民工叔叔的见解。
Angular中的组件
严格来说,Angular1.x中并没有明确提及组件的概念,只是我们可以使用app.directive()
来实现自定义指令,我觉着这其实就是组件。
而Angular中实现组件之间进行通信的方式主要有四种方式:
基于$rootScope的全局变量和$scope作用域的继承性
基于作用域的继承性来实现组件通信仅限于作用域链上的通信,需要对Angular的controller之间的作用域关系特别熟悉,详细可以参考民工叔叔的AngularJS实例教程(二)——作用域与事件。
利用事件$on()、$emit()、$broastcase()方式
先上一张图:
上图中清晰的展示了Angular中利用事件去上传数据和广播事件的关系图,于是我们就可以利用事件做点事情了。
从作用域往上发送事件,使用scope.$emit
$scope.$emit("someEvent", {});
从作用域往下发送事件,使用scope.$broadcast
$scope.$broadcast("someEvent", {});
这两个方法的第二个参数是要随事件带出的数据。
注意,这两种方式传播事件,事件的发送方自己也会收到一份。
利用服务实现
利用myApp.factory()生成一个需要共享数据的对象,然后在controller中注入,就可以是获取到共享数据了,并进行修改了。
直接上代码:
var myApp = angular.module("myApp", []);
myApp.factory('Data', function() {
return {
name: "Ting"
}
});
myApp.controller('FirstCtrl', function($scope, Data) {
$scope.data = Data;
$scope.setName = function() {
Data.name = "Jack";
}
});
myApp.controller('SecondCtrl', function($scope, Data) {
$scope.data = Data;
$scope.setName = function() {
Data.name = "Moby";
}
});
订阅发布模式
民工叔叔在AngularJS实例教程(二)——作用域与事件一文的末尾提出了利用订阅发布模式来通信,我觉着太经典了,接收方在这里订阅消息,发布方在这里发布消息。这个过程可以用这样的图形来表示:
代码写起来也很简单,把它做成一个公共模块,就可以被各种业务方调用了:
app.factory("EventBus", function() {
var eventMap = {};
var EventBus = {
on : function(eventType, handler) {
//multiple event listener
if (!eventMap[eventType]) {
eventMap[eventType] = [];
}
eventMap[eventType].push(handler);
},
off : function(eventType, handler) {
for (var i = 0; i < eventMap[eventType].length; i++) {
if (eventMap[eventType][i] === handler) {
eventMap[eventType].splice(i, 1);
break;
}
}
},
fire : function(event) {
var eventType = event.type;
if (eventMap && eventMap[eventType]) {
for (var i = 0; i < eventMap[eventType].length; i++) {
eventMap[eventType][i](event);
}
}
}
};
return EventBus;
});
事件订阅代码:
EventBus.on("someEvent", function(event) {
// 这里处理事件
var c = event.data.a + event.data.b;
});
事件发布代码:
EventBus.fire({
type: "someEvent",
data: {
aaa: 1,
bbb: 2
}
});
注意,如果在复杂的应用中使用事件总线,需要慎重规划事件名,推荐使用业务路径,比如:"portal.menu.selectedMenuChange",以避免事件冲突。
非常经典的Angular组件之间通信的一种方式,我在解释一下,如果A组件中有值需要传递给B组件。那么在B组件Controller中通过EventBus.on()
订阅事件:
EventBus.on("someEvent", function(event) {
// 这里是B组件中对A组件传过来的值进行处理的回调函数
var c = event.data.a + event.data.b;
});
然后,在A组件Controller中将其中的值通过EventBus.fire()
发布一下:
EventBus.fire({
type: "someEvent",
data: {
aaa: 1,
bbb: 2
}
});
这样B组件就可以拿到A组件中的数据值了。
组件化再思考
简单了解了一下Vue和Angular中的组件思想,我们再来回想一下组件化的概念:
狭义的组件化一般是指标签化,也就是以自定义标签(自定义属性)为核心的机制。
广义的组件化包括对数据逻辑层业务梳理,形成不同层级的能力封装。
很明显,不管是Vue还是Angular,对组件的封装都是为了对数据逻辑业务的梳理,使得不同组件各司其职,当然这其中就包括了对HTML的组件化,CSS的组件化和JS的组件化。
对于HTML的组件化可以理解为利用各种语义化标签或者自定义标签(Vue中的组件和Angular中的自定义指令等)对结构进行封装。而对于CSS的组件化,我们现在可以利用SASS或者LESS来实现,根据不同的功能对样式进行不同的封装,我觉着现在前端的一些UI框架就是对组件化的使用,去哪儿网杜瑶的Yo框架就是一个很好的例子。
至于对于JS的组件化运用,我个人觉着就是模块化。不管是CommonJS规范、AMD规范、CMD规范还是ES2015的模块机制目的都是对JS进行模块化开发,使得不同功能的逻辑或者业务分离开,每个模块专注自己的业务逻辑,这样不仅是开发的时候工作目录一目了然,后期维护的时候也能快速定位到各个业务逻辑模块。
小结
简单的对民工叔叔的几篇文章做了一下总结,其实还没有理解到位,每篇文章读了仅仅3-4遍,每读一遍都有不一样的感受,理解也不一样。可能等过段时间再去读又会颠覆我现在的认识,不管对于错,先记录一下现在的感受,后期有了新的认识再来补充或者修改。
下面推荐民工叔叔的对于组件化和Angular的一些文章,我做了少许整理,大家可以直接按照目录由浅至深的阅读原文。