前端组件化认识

什么是前端组件化和模块化?
这两天一直在思考这个问题,以前对这两个概念的理解很模糊。认为“模块化”是侧重于功能或者数据的封装,目的是为了解耦合;而“组件化”更关注的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 upprops 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文档,本文不做详细介绍。

需要特别注意的是:

  1. Vue默认的是单项数据流,当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
  2. 另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告
  3. 注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
    并且当props为应用类型的时候还可以为其添加验证

从内向外的“events up”

Vue中默认的虽然是单项数据流,但还是可以实现子组件向父组件传输数据的,因为有些场景中,我们不可避免的会使用到。而Vue中向上传递数据采用的和Angular中一样的思想,通过自定义事件的方式实现。大体需要以下两步步:

  1. 使用 $emit(eventName)在子组件上触发事件
  2. 使用 $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()方式

先上一张图:


$rootScope
$rootScope

上图中清晰的展示了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的一些文章,我做了少许整理,大家可以直接按照目录由浅至深的阅读原文。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 本文章是我最近在公司的一场内部分享的内容。我有个习惯就是每次分享都会先将要分享的内容写成文章。所以这个文集也是用来...
    Awey阅读 9,406评论 4 67
  • 序 今年大前端的概念一而再再而三的被提及,那么大前端时代究竟是什么呢?大前端这个词最早是因为在阿里内部有很多前端开...
    一缕殇流化隐半边冰霜阅读 11,187评论 19 92
  • 又到了写月度总结的时间啦。刚过去的一个月里发生了很多奇妙的事情,我触碰到了很多旧人旧事,和一些原本以为不会再有交集...
    茜喵阅读 586评论 10 13
  • 好久了已经 已经快要忘记了 我们最初那些青涩的对白 也许是某些种奇怪有趣的东东吧 我们逐渐由陌生走向熟悉 第一句 ...
    Ohmyeah阅读 327评论 2 0
  • 地球每天都转,世界每天都变,我们也一天天地成长起来,成长的经历有苦也有乐,有酸也有甜。人生就像一道美味佳肴,它包括...
    奎启发阅读 396评论 0 2