Web Component:过去、现在和未来

编者按:作者高凯从 JS 的中古时代的 jQuery 说起,到 Backbone,再到 Augular,最后到现代的时髦的 React,为我们梳理了 Web Component 从概念到标准化的历程

1. 什么是 web component

援引 MDN 上的解释:

Web Components consists of several separate technologies. You can think of Web Components as reusable user interface widgets that are created using open Web technology. They are part of the browser and so they do not need external libraries like jQuery or Dojo. An existing Web Component can be used without writing code, simply by adding an import statement to an HTML page. Web Components use new or still-developing standard browser capabilities.

简单的讲,Web Component 就是把组件封装成 html 标签的形式,并且在使用时不需要写额外的 js 代码。

2. 从前是怎么做的?

Jquery 时代

假设需要一段代码把几个色块染色。在 jq 时代,我们会从这样的代码开始:

	$('.div1')
	.html('red light')
	  .css({
    	background: 'red'
	  });
	$('.div2')
	  .html('yellow light')
	  .css({
    	background: 'yellow'
	  });
	$('.div3')
	  .html('green light')
	  .css({
	    background: 'green'
	  });

完全过程式的代码。在稍微复杂的情况下,可以进行抽象,封装成 Light 类:

    function Light (el, color) {
      this.el = el;
      this.color = color;
    }
    Light.prototype = {
      render: function () {
        this.el.css({
          background: this.color
        })
      }
    };

    var redLight = new Light($('.div1'), 'red');
    var yellowLight = new Light($('.div2'), 'yellow');
    var greenLight = new Light($('.div3'), 'green');

    var ins = [redLight, yellowLight, greenLight];

    ins.forEach(function (item) {
      item.render();
    });

面对对象之后,我们可以复用代码了。并且在维护性上也得到了些微的提升。我们可以继承这个类,加上更多的接口和展现形态:

    function Light (el, color) {
      this.el = el;
      this.color = color;
    }
    Light.prototype = {
      render: function () {
        this.el.css({
          background: this.color
        })
      }
    };

    function CircleLight () {
      Light.apply(this, arguments);
    }
    CircleLight.prototype = Object.create(Light.prototype);
    $.extend(CircleLight.prototype, {
      render: function () {
        Light.prototype.render.apply(this, arguments);
        this.el.css({
          width: 50,
          height: 50,
          borderRadius: '50%'
        });
      }
    });

    var ins = [];
    $('.div1,.div2,.div3').each(function () {
      var elem = $(this);
      var klass = Light;
      if (elem.hasClass('circle')) {
        klass = CircleLight;
      }

      ins.push( new klass(elem, elem.data('color')) );
    });

    ins.forEach(function (item) {
      item.render();
    });

CircleLight 类继承自 Light。重写了 render 接口,加上圆形的效果。在实例渲染时调用并不需要区别两种类,只调用 render 即可,这样在扩展基类时会变得非常方便。

在此之上再引入 mvc 的概念,前端发明了 Backbone 框架。

Backbone 时代

还是刚才的例子,但是这次把数据和渲染层分开,于是就有了:

     var LightModel = Backbone.Model.extend({
      defaults: {
        color: 'red'
      }
    });

    var LightCollection = Backbone.Collection.extend({
      model: LightModel
    });

    var tpl = '<div style="background: <%= color %>;border-radius:50%;height:50px;width:50px">my color is <%= color %></div>';

    var LightView = Backbone.View.extend({
      template: _.template(tpl),
      render: function() {
        var data = this.model.toJSON();
        this.$el.html(this.template(data));
        return this.$el;
      }
    });

    var LightListView = Backbone.View.extend({
      render: function () {
        var self = this;
        this.$el.empty();
        this.collection.each(function(item){
          var light = new LightView({
            model: item
          });
          self.$el.append(light.render());
        });
      }
    });

    var data = [{color: 'red'}, {color: 'yellow'}, {color: 'green'}, {}];

    var lightList = new LightListView({
      el: $('.container'),
      collection: new LightCollection(data)
    });

    lightList.render();

好吧,为了可维护性,代码其实变复杂了。但是在大型项目中这是值得的。将数据层和视图层隔离开,方便在出问题时,找到错误。当遇到页面展现上的重构,也不会影响到数据层代码。

唯一的缺点就是累。如果要在 Backbone 里做局部刷新就更麻烦了,抽象更多的 view 或者在 render 中操作局部 dom。而麻烦和累是程序的原罪。于是又过了几年,我们有了 angular。

angular 时代

    var app = angular.module('lightApp', []);

    app.controller('LightCtrl', function ($scope) {
      $scope.lights = ['red', 'yellow', 'green'];

      $scope.add = function () {
        $scope.lights.push('blue');
      };
    });

    angular.bootstrap($('#ng-container')[0], ['lightApp']);
    
   	// html
   	<div ng-controller="LightCtrl">
   		<button ng-click="add()">add</button>
   		<div class="container">container<div ng-repeat="light in lights track by $index" style="background: {{light}}">{{light}}</div>
   		</div>
   	</div>

angular 把部分渲染逻辑丢到了 html 里,因为大部分时候不需要在 js 里操作 dom,通过数据的双向绑定,把渲染的工作完全交给了 html 里的模板。这样就把渲染和数据处理的逻辑隔离开了。看起来相当简洁。

更高层的抽象

angular 中提供了 directive 来实现自定义标签,可以实现不那么标准的 web compoents。

    var app = angular.module('lightApp', []);

    app.controller('LightCtrl', function($scope) {
      $scope.lights = ['red', 'yellow', 'green'];

      $scope.add = function() {
        $scope.lights.push('blue');
      };
    }).directive('light', function() {
      return {
        // A for atrrbute, C for Class, E for Element
        restrict: 'E',
        scope: {
          color: '@'
        },
        template: '<div ng-click="lightUp()" style="background: {{bg}}">my color is {{color}}, click me</div>',
        link: function(scope, elem) {
          scope.lightUp = function() {
            scope.bg = scope.color;
          }
        }
      };
    });

    angular.bootstrap($('#ng-container')[0], ['lightApp']);
    
    //html
    
    <div ng-controller="LightCtrl">
      <light color="red"></light>
      <light color="green"></light>
      <light color="yellow"></light>
    </div>

在 js 中定义标签,再到 html 中使用,js 中不需要做初始化工作。假设在一套成熟的框架中,业务页面只需要编写 html 即可像搭积木一样完成。

更好的抽象 react

angular 的双向数据绑定实际上影响了作为 web components 时的封装。在单向即可完成工作的时候,引入双向绑定其实无形中增加了复杂度。在出现问题时经常会显得莫名奇妙,数据在哪里发生的变化?原因是什么?双倍的数据,双倍的复杂度,四倍的麻烦。

    var Light = React.createClass({
      render: function () {
        var color = this.props.color;
        var styleObj = {backgroundColor: color};
        return <div style={styleObj}>{color}</div>;
      }
    });

    React.render((
      <div>
        <Light color='red' />
        <Light color='yellow' />
        <Light color='green' />
      </div>
    ), document.getElementById('main'));

这个问题在 react 实现 web components 却会变得异常清晰。数据的初始化由 props 承载,数据的变化在 state 中进行,再在 render 时进行数据组合。

在 web components 标准化还未完成时,我们可以依靠 angular、react 这些现代框架实现。即用 js 定义标签行为,在 html 里使用标签,尽量减少组件之间的代码耦合。组件在 web 中是一个合适的抽象程度。