很多JS开发人员可能都会熟悉JQuery,这是一个非常流行成熟的、免费的、开源JS框架,但是使用过ExtJS的人就少多了。近半年都是用ExtJS,然后大概看了一下JQuery,有了自己的一些新的体会。可以在稍后的博客中分享一下个人对二者的看法。在这里我们仍然侧重于讲解ExtJS,尤其是它比JQuery强大的地方——Component体系。
所有的JS框架似乎起码要包括如下几部分:跨浏览器,操作Dom元素,提供面向对象的编程机制,好用的辅助方法,如prototype,jquery,extjs。如果仅仅包含这样的功能其代码量比较小,加载速度自然快。JQuery的核心就是加载很快,但是ExtJs加载始终要慢得多。为什么?因为除了这些核心内容之外,ExtJs还提供了一套完整的Component体系,其中包括各种常用的Component,如Text, Date, ComboBox等,还有很多Container以实现布局要求,如HBox, VBox,Border,Column等layout。正是这些东西导致ExtJS的膨胀,但也是这些东西使得ExtJS异常的强大和易用。这里我们仅仅简单分析一下Ext Component的实现。以后我们可以再分析一下ExtJs的Layout,Plugin和Event等很多有意思的东西。很重要的一点是,要想对这些东西有稍微深刻的理解必须要自己翻阅代码。面试过一些人,他们对于自己天天在用的东西没有一点点的好奇心,丝毫不想掀开盖子看一下里面到底是什么东西,仅仅是用他们来解决一些应用上的问题,这一类人我通常觉得他们不是合格的程序员,至少不是我想要的程序员。下面言归正传。
简单的Component结构可以参考ExtJS的官方文档http://docs.sencha.com/ext-js/4-0/#!/guide/components,它简单介绍了如何使用Component、Component的生命周期以及何时可以加入自己的逻辑——通过事件或者template method。可以看到ExtJS更加推荐用template method的方式加入自己的逻辑,至少这样不需要通过事件的方式再绕一圈才调用事件handler,而是可以直接像调用函数一样调用template method,如AbstractComponent的onRender方法就是template method中的一种。
知道这些基本的东西之后,然后再看看真的一个简单ComboBox在Dom树中的样子如下:
<div id="combobox-1994" class="x-field x-form-item x-box-item x-field-default x-form-dirty" style="width: 60px; margin-left: 0px; margin-bottom: 0px; margin-right: 0px; margin-top: 0px; left: 0px; top: 0px; "><div class="x-form-item-body x-form-trigger-wrap-focus" id="combobox-1994-bodyEl" role="presentation" style="width: 60px; "><div class="x-hide-display x-form-data-hidden" role="presentation" id="ext-gen1941"><input type="hidden" name="ext-gen1940" value=">"></div><input id="ext-gen1940" type="text" size="20" class="x-form-field x-form-text x-form-focus" autocomplete="off" aria-invalid="false" data-errorqtip="" style="-webkit-user-select: text; width: 43px; " role="textbox" aria-describedby="combobox-1994-errorEl" aria-required="false"><div id="combobox-1994-triggerWrap" class="x-form-trigger-wrap" role="presentation" style="width: 17px; "><div class="x-trigger-index-0 x-form-trigger x-form-arrow-trigger x-form-trigger-last x-unselectable" role="button" id="ext-gen1942" style="-webkit-user-select: none; "></div><div class="x-clear" role="presentation"></div></div></div><div id="combobox-1994-errorEl" class="x-form-error-msg" style="display:none"></div><div class="x-clear" role="presentation"><!-- --></div></div>
这就让人困惑了。简单的一个下拉框怎么会有这么多乱七八糟的东西呢,为什么不是简单的一个select标签呢?因为select标签的功能太简单了,远远不如ExtJS中的ComboBox,你看官方文档就会发现它是多么的强大了,包括自动提示,分页,查找等等现在很常见但是select不容易实现的功能。要提供这么多功能,自然就要好好地封装一层了。但是有一点要记住,不管怎么封装,最终还是基本的html元素显示在页面,不外乎就是像上面的代码,其html结构复杂一些而已。
ExtJS如何通过html来实现自己的Component呢?这也是本文的重点讨论。首先看看AbstractComponent里面的onRender方法
onRender : function(container, position) {
var me = this,
el = me.el,
styles = me.initStyles(),
renderTpl, renderData, i;
创建el
position = me.getInsertPosition(position);
if (!el) {
if (position) {
el = Ext.DomHelper.insertBefore(position, me.getElConfig(), true);
}
else {
el = Ext.DomHelper.append(container, me.getElConfig(), true);
}
}
else if (me.allowDomMove !== false) {
if (position) {
container.dom.insertBefore(el.dom, position);
} else {
container.dom.appendChild(el.dom);
}
}
.......
me.el = el;
me.initFrame();
///生成Component内部html元素
renderTpl = me.initRenderTpl();
if (renderTpl) {
renderData = me.initRenderData();
renderTpl.append(me.getTargetEl(), renderData);
}
//获取Component内部重要的子元素
me.applyRenderSelectors();
me.rendered = true;
}
这段代码有三个地方值得注意。
(1)创建el容器元素。这个元素实际上是component的根元素,通常默认是用div(这个可以在函数getElConfig中找到)。也就是说通常ExtJs里面的component都是在div里面。
(2)根据renderTpl创建其它必要元素以生成Component,默认是获取renderTpl属性值。然后根据renderTpl这个XTemplate以及传入的renderData生成html元素,这些元素通常都塞到el里面(me.getTargetEl)。例如如下代码
Ext.create('Ext.Component', {
renderTo: Ext.getBody(),
renderTpl: [
'<h1 class="title">{title}</h1>',
'<p>{desc}</p>'
],
renderData: {
title: "Error",
desc: "Something went wrong"
},
renderSelectors: {
titleEl: 'h1.title',
descEl: 'p'
},
listeners: {
afterrender: function(cmp){
// After rendering the component will have a titleEl and descEl properties
cmp.titleEl.setStyle({color: "red"});
}
}
});
这段代码最后生成的htm元素就如下。要想知道XTemplate和数据之间的绑定关系,得看看ExtJS的template是如何应用的。
<div>
<h1 class="title">Error</h1>
<p>Something went wrong</p>
</div>
(3) 在生成所有元素之后,会调用me.applyRenderSelectors以获取组成Component的一些重要html元素,如上面例子中renderSelectors里面的titleEl和descEl。其中titleEl实际上是一个selector path,即class 那么为title的h1。而这个元素也会作为comp的一个属性保存下面,接下来在afterrender事件中,会设置它的color为red。为什么要获取这样的元素呢?因为很多时候我们需要对这些重要的元素进行操作,大部分这些操作就是对component的操作,如ComboBox的trigger,也就是那个带下拉箭头的小按钮,我们需要监听它的click事件,以知道用户想要查看下拉选项。
除了可以用renderSelectors这种方法获取重要的元素外,还有一种更加简单的方法,如下面一段从ExtJS的AbstractComponent中拷贝出来的代码
/**
* @cfg {Object[]} childEls
* An array describing the child elements of the Component. Each member of the array
* is an object with these properties:
*
* - `name` - The property name on the Component for the child element.
* - `itemId` - The id to combine with the Component's id that is the id of the child element.
* - `id` - The id of the child element.
*
* If the array member is a string, it is equivalent to `{ name: m, itemId: m }`.
*
* For example, a Component which renders a title and body text:
*
* Ext.create('Ext.Component', {
* renderTo: Ext.getBody(),
* renderTpl: [
* '<h1 id="{id}-title">{title}</h1>',
* '<p>{msg}</p>',
* ],
* renderData: {
* title: "Error",
* msg: "Something went wrong"
* },
* childEls: ["title"],
* listeners: {
* afterrender: function(cmp){
* // After rendering the component will have a title property
* cmp.title.setStyle({color: "red"});
* }
* }
* });
*
* A more flexible, but somewhat slower, approach is {@link #renderSelectors}.
*/
对于ExtJs里面的Field会显得稍微复杂一点,但是原理是一样的。所有Field是继承自Base,Base的内部元素是有fieldSubTpl来控制的,数据由getSubTplData函数提供的。具体的实现可以从Baes.initRenderTpl和initRenderData看出来:
initRenderTpl: function() {
var me = this;
if (!me.hasOwnProperty('renderTpl')) {
me.renderTpl = me.getTpl('labelableRenderTpl');
}
return me.callParent();
}
initRenderData: function() {
return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
}
而labelableRenderTpl实际上是定义在Base所mixin的Labelable里面
labelableRenderTpl: [
'<tpl if="!hideLabel && !(!fieldLabel && hideEmptyLabel)">',
'<label id="{id}-labelEl"<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"',
'<tpl if="labelStyle"> style="{labelStyle}"</tpl>>',
'<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>',
'</label>',
'</tpl>',
'<div class="{baseBodyCls} {fieldBodyCls}" id="{id}-bodyEl" role="presentation">{subTplMarkup}</div>',
'<div id="{id}-errorEl" class="{errorMsgCls}" style="display:none"></div>',
'<div class="{clearCls}" role="presentation"><!-- --></div>',
{
compiled: true,
disableFormats: true
}
]
从initRenderData知道要获取Labelable.getLabelableRenderData(),而这个方法恰好调用Base.getSubTplMarkup来填充上面template中的subTplMarkup
getSubTplMarkup: function() {
return this.getTpl('fieldSubTpl').apply(this.getSubTplData());
}
这样所有的东西也就串起来了。之所以这么做,是因为增加label的任务全交给Labelable去做了,具体Field才去负责真正控件内部的事情。这种设计还是合理的。
所有的JS框架似乎起码要包括如下几部分:跨浏览器,操作Dom元素,提供面向对象的编程机制,好用的辅助方法,如prototype,jquery,extjs。如果仅仅包含这样的功能其代码量比较小,加载速度自然快。JQuery的核心就是加载很快,但是ExtJs加载始终要慢得多。为什么?因为除了这些核心内容之外,ExtJs还提供了一套完整的Component体系,其中包括各种常用的Component,如Text, Date, ComboBox等,还有很多Container以实现布局要求,如HBox, VBox,Border,Column等layout。正是这些东西导致ExtJS的膨胀,但也是这些东西使得ExtJS异常的强大和易用。这里我们仅仅简单分析一下Ext Component的实现。以后我们可以再分析一下ExtJs的Layout,Plugin和Event等很多有意思的东西。很重要的一点是,要想对这些东西有稍微深刻的理解必须要自己翻阅代码。面试过一些人,他们对于自己天天在用的东西没有一点点的好奇心,丝毫不想掀开盖子看一下里面到底是什么东西,仅仅是用他们来解决一些应用上的问题,这一类人我通常觉得他们不是合格的程序员,至少不是我想要的程序员。下面言归正传。
简单的Component结构可以参考ExtJS的官方文档http://docs.sencha.com/ext-js/4-0/#!/guide/components,它简单介绍了如何使用Component、Component的生命周期以及何时可以加入自己的逻辑——通过事件或者template method。可以看到ExtJS更加推荐用template method的方式加入自己的逻辑,至少这样不需要通过事件的方式再绕一圈才调用事件handler,而是可以直接像调用函数一样调用template method,如AbstractComponent的onRender方法就是template method中的一种。
知道这些基本的东西之后,然后再看看真的一个简单ComboBox在Dom树中的样子如下:
<div id="combobox-1994" class="x-field x-form-item x-box-item x-field-default x-form-dirty" style="width: 60px; margin-left: 0px; margin-bottom: 0px; margin-right: 0px; margin-top: 0px; left: 0px; top: 0px; "><div class="x-form-item-body x-form-trigger-wrap-focus" id="combobox-1994-bodyEl" role="presentation" style="width: 60px; "><div class="x-hide-display x-form-data-hidden" role="presentation" id="ext-gen1941"><input type="hidden" name="ext-gen1940" value=">"></div><input id="ext-gen1940" type="text" size="20" class="x-form-field x-form-text x-form-focus" autocomplete="off" aria-invalid="false" data-errorqtip="" style="-webkit-user-select: text; width: 43px; " role="textbox" aria-describedby="combobox-1994-errorEl" aria-required="false"><div id="combobox-1994-triggerWrap" class="x-form-trigger-wrap" role="presentation" style="width: 17px; "><div class="x-trigger-index-0 x-form-trigger x-form-arrow-trigger x-form-trigger-last x-unselectable" role="button" id="ext-gen1942" style="-webkit-user-select: none; "></div><div class="x-clear" role="presentation"></div></div></div><div id="combobox-1994-errorEl" class="x-form-error-msg" style="display:none"></div><div class="x-clear" role="presentation"><!-- --></div></div>
这就让人困惑了。简单的一个下拉框怎么会有这么多乱七八糟的东西呢,为什么不是简单的一个select标签呢?因为select标签的功能太简单了,远远不如ExtJS中的ComboBox,你看官方文档就会发现它是多么的强大了,包括自动提示,分页,查找等等现在很常见但是select不容易实现的功能。要提供这么多功能,自然就要好好地封装一层了。但是有一点要记住,不管怎么封装,最终还是基本的html元素显示在页面,不外乎就是像上面的代码,其html结构复杂一些而已。
ExtJS如何通过html来实现自己的Component呢?这也是本文的重点讨论。首先看看AbstractComponent里面的onRender方法
onRender : function(container, position) {
var me = this,
el = me.el,
styles = me.initStyles(),
renderTpl, renderData, i;
创建el
position = me.getInsertPosition(position);
if (!el) {
if (position) {
el = Ext.DomHelper.insertBefore(position, me.getElConfig(), true);
}
else {
el = Ext.DomHelper.append(container, me.getElConfig(), true);
}
}
else if (me.allowDomMove !== false) {
if (position) {
container.dom.insertBefore(el.dom, position);
} else {
container.dom.appendChild(el.dom);
}
}
.......
me.el = el;
me.initFrame();
///生成Component内部html元素
renderTpl = me.initRenderTpl();
if (renderTpl) {
renderData = me.initRenderData();
renderTpl.append(me.getTargetEl(), renderData);
}
//获取Component内部重要的子元素
me.applyRenderSelectors();
me.rendered = true;
}
这段代码有三个地方值得注意。
(1)创建el容器元素。这个元素实际上是component的根元素,通常默认是用div(这个可以在函数getElConfig中找到)。也就是说通常ExtJs里面的component都是在div里面。
(2)根据renderTpl创建其它必要元素以生成Component,默认是获取renderTpl属性值。然后根据renderTpl这个XTemplate以及传入的renderData生成html元素,这些元素通常都塞到el里面(me.getTargetEl)。例如如下代码
Ext.create('Ext.Component', {
renderTo: Ext.getBody(),
renderTpl: [
'<h1 class="title">{title}</h1>',
'<p>{desc}</p>'
],
renderData: {
title: "Error",
desc: "Something went wrong"
},
renderSelectors: {
titleEl: 'h1.title',
descEl: 'p'
},
listeners: {
afterrender: function(cmp){
// After rendering the component will have a titleEl and descEl properties
cmp.titleEl.setStyle({color: "red"});
}
}
});
这段代码最后生成的htm元素就如下。要想知道XTemplate和数据之间的绑定关系,得看看ExtJS的template是如何应用的。
<div>
<h1 class="title">Error</h1>
<p>Something went wrong</p>
</div>
(3) 在生成所有元素之后,会调用me.applyRenderSelectors以获取组成Component的一些重要html元素,如上面例子中renderSelectors里面的titleEl和descEl。其中titleEl实际上是一个selector path,即class 那么为title的h1。而这个元素也会作为comp的一个属性保存下面,接下来在afterrender事件中,会设置它的color为red。为什么要获取这样的元素呢?因为很多时候我们需要对这些重要的元素进行操作,大部分这些操作就是对component的操作,如ComboBox的trigger,也就是那个带下拉箭头的小按钮,我们需要监听它的click事件,以知道用户想要查看下拉选项。
除了可以用renderSelectors这种方法获取重要的元素外,还有一种更加简单的方法,如下面一段从ExtJS的AbstractComponent中拷贝出来的代码
/**
* @cfg {Object[]} childEls
* An array describing the child elements of the Component. Each member of the array
* is an object with these properties:
*
* - `name` - The property name on the Component for the child element.
* - `itemId` - The id to combine with the Component's id that is the id of the child element.
* - `id` - The id of the child element.
*
* If the array member is a string, it is equivalent to `{ name: m, itemId: m }`.
*
* For example, a Component which renders a title and body text:
*
* Ext.create('Ext.Component', {
* renderTo: Ext.getBody(),
* renderTpl: [
* '<h1 id="{id}-title">{title}</h1>',
* '<p>{msg}</p>',
* ],
* renderData: {
* title: "Error",
* msg: "Something went wrong"
* },
* childEls: ["title"],
* listeners: {
* afterrender: function(cmp){
* // After rendering the component will have a title property
* cmp.title.setStyle({color: "red"});
* }
* }
* });
*
* A more flexible, but somewhat slower, approach is {@link #renderSelectors}.
*/
对于ExtJs里面的Field会显得稍微复杂一点,但是原理是一样的。所有Field是继承自Base,Base的内部元素是有fieldSubTpl来控制的,数据由getSubTplData函数提供的。具体的实现可以从Baes.initRenderTpl和initRenderData看出来:
initRenderTpl: function() {
var me = this;
if (!me.hasOwnProperty('renderTpl')) {
me.renderTpl = me.getTpl('labelableRenderTpl');
}
return me.callParent();
}
initRenderData: function() {
return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
}
而labelableRenderTpl实际上是定义在Base所mixin的Labelable里面
labelableRenderTpl: [
'<tpl if="!hideLabel && !(!fieldLabel && hideEmptyLabel)">',
'<label id="{id}-labelEl"<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"',
'<tpl if="labelStyle"> style="{labelStyle}"</tpl>>',
'<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>',
'</label>',
'</tpl>',
'<div class="{baseBodyCls} {fieldBodyCls}" id="{id}-bodyEl" role="presentation">{subTplMarkup}</div>',
'<div id="{id}-errorEl" class="{errorMsgCls}" style="display:none"></div>',
'<div class="{clearCls}" role="presentation"><!-- --></div>',
{
compiled: true,
disableFormats: true
}
]
从initRenderData知道要获取Labelable.getLabelableRenderData(),而这个方法恰好调用Base.getSubTplMarkup来填充上面template中的subTplMarkup
getSubTplMarkup: function() {
return this.getTpl('fieldSubTpl').apply(this.getSubTplData());
}
这样所有的东西也就串起来了。之所以这么做,是因为增加label的任务全交给Labelable去做了,具体Field才去负责真正控件内部的事情。这种设计还是合理的。
![](http://www.google.com/uds/css/small-logo.png)