编者按:作者高凯从JS的中古时代的jQuery说起,到Backbone,再到Augular,最后到现代的时髦的React,为我们梳理了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 代码。
假设需要一段代码把几个色块染色。在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 框架。
还是刚才的例子,但是这次把数据和渲染层分开,于是就有了:
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。
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 即可像搭积木一样完成。
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 中是一个合适的抽象程度。