浅析ExtJS新特性
ExtJS 6最重要的变化是,不再区分移动、桌面平台,所有设备上的应用程序都使用一个框架来开发。为了实现优化的用户体验,你仅仅需要编写少量的代码。
ExtJS与Sencha Touch的合并其实经历了很长的过程,在ExtJS 5中,框架的核心就被整理到core包中,便于两者的公用,ExtJS的可视化组件部分仍然留在Ext这个包中。现在,可视化组件部分被合并到一起,但是为了区分经典ExtJS、Touch的UI组件家族,ExtJS 6 引入了术语:工具包(toolkit)。
工具包是仅仅包含了框架视觉元素的包,这些视觉元素包括面板、按钮、表格等等。ExtJS 6包含两个工具包:
- classic:来自ExtJS的视觉元素位于此包
- modern:来自Sencha Touch的视觉元素位于此包
要设置某个应用程序使用的工具包,仅需要在app.json中设置 "toolkit": "classic", // or "modern"
在工具包下层,是通用的核心包,Ext.data、Ext.app之类的通用API都位于核心包中。基于核心包,应用程序可以共享数据管理逻辑、 ViewModel、MVC控制逻辑。
对于原Sencha Touch的开发人员,升级到ExtJS 6后可以立即获得ViewModel、ViewController等API。同时受益于ExtJS 5引入的多设备事件系统,鼠标事件可以无缝的转换为触碰事件(例如touchstart),不需要检测设备的特性。
使用ExtJS 6和Sencha Cmd 6,你可以创建通用应用程序(Universal Applications),两套Toolkit都可以被UA使用。通过UA你可以为所有设备创建单套统一的应用程序。你可以在app.json中使用builds而不是toolkits,从而设置Toolkit使用的主题:
1 2 3 4 5 6 7 8 9 10 11 |
"builds": { "classic": { "toolkit": "classic", "theme": "theme-triton" }, "modern": { "toolkit": "modern", "theme": "theme-triton" } } |
上面的例子中,我们为两套Toolkit分配统一个主题triton。基于上面的配置,命令 sencha app build 会构建两套配置到各自的目录,如果要构建单个配置,可以 sencha app build modern 。
Sencha Cmd可以为你的应用程序创建一个入口点。
ExtJS 6引入的Triton是一个扁平化的、简约风的主题。这种风格把关注点聚焦在内容,而不是圆角边框、渐变之类的装饰样式上。该主题为流行的文字图标(Font icons)提供广泛的支持。该主题中的很多图标都来自网站Font Awesome。用于图标都是矢量的,因此基于该主题的应用程序能够很好的支持缩放。
对于工具包modern,尺寸可以被灵活的控制。在智能手机上html元素会自动获得x-big样式类,这导致很多元素(例如按钮)变得更大,从而易于触摸。
这是Sencha新的编译器,用于根据scss文件来构建主题。Fashion使用类似于Sass的语言并添加了一些有用的扩展。Fashion使用JavaScript实现,并且在浏览器中运行。当和PhantomJS联用时,Sencha Cmd可以使用 app watch 和Fashion来进行(增量)主题构建。
由于Fashion基于JS,这意味着Ruby不再是必须的。
Fashion的一个最重要的特性是实时更新——在现代浏览器中运行ExtJS应用时,scss文件被载入(而不是生成的CSS),Fashion能够根据scss文件的变化而自动更新CSS,不需要重新载入页面。
基于Material的Android主题、新的iOS主题被加入。ExtJS应用程序可以具有更加Native的外观。
Promise代表一个将在未来发生的事件,很多其它JS框架、ES6语言本身都开始支持Promise,ExtJS 6 终于也内置Promise的支持了。使用Promise可以避免嵌套的回调函数,让代码更加可读。
未来版本的Promise将会与框架的更多组件来协作,ExtJS 6.0版本首先支持的是Ajax。
现在,你可以这些的发起Ajax请求:
1 2 3 4 5 |
Ext.Ajax.request({ url: 'some/url', }).then(function (response) { // 使用服务器响应 }); |
或者,让Ajax调用返回一个Promise,供其它代码使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function getAjax( url ) { return new Ext.Promise( function ( resolve, reject ) { Ext.Ajax( { url: url, success: function ( response ) { resolve( response ); }, failure: function ( response ) { reject( response ); } } ); } ); } |
对于原生支持Promise的浏览器,对Ext.Promise类型变量的引用会自动分配到Promise,并且仅仅包含原生Promise的特性。
ExtJS 4引入了Web端的MVC框架,在新ExtJS 5中,更加流行的MVVM支持被添加进来。MVVM最吸引人的特性就是数据绑定,数据绑定让你不再需要编写连接模型层 - 视图层的“胶水代码”,想想AngularJS的魅力吧,现在ExtJS也可以做到了。
组件现在支持新的bind配置项,通过此配置项可以把任何其它配置项(只要配置项有Setter)连接到一个 Ext.app.ViewModel 。使用绑定后,只要相关模型的值发生变化,组件的Setter就会被调用,不需要通过定制事件处理器来解决。
数据绑定机制支持所谓双向绑定,以保持视图 - 模型的双向同步。如果视图改变了数据项,这个更改会自动反映到模型中,而其它关联到此模型的组件的UI也会被立即更新。
在ExtJS 4的MVC模式中,控制器是允许你编写应用程序范围(application-wide,从工程角度来说,应该是模块范围)的逻辑。在ExtJS 5中,你可以使用针对一个视图的控制器 Ext.app.ViewController ,这个控制器仅仅关联到单个视图(组件)的实例。
这个新的控制器通常是更好的选择,它简化了事件监听器、组件引用的编写,你可以不再纠结于应用状态管理、内存泄漏等问题。
在一个视图组件包含复杂的容器层次时,连接某些子组件和监听器是视图控制器的强项,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var compx = { ... items: [ { items: [ { xtype: 'textfield', reference: 'fieldx' // 引用 } ] }, ... { xtype: 'button', text: 'Delete', listeners: { click: 'onClickDelete' //直白的绑定:使用视图控制器中定义的监听器函数 } } ] }; Ext.define( 'App.view.foo.FooController', { extend: 'Ext.app.ViewController', onClickDelete: function ( deleteButton ) { var someField = this.lookupReference( 'fieldx' ); // 根据引用查找 } } ); |
比起全局的控制器的组件查询(Component Query)风格的事件绑定,上例中的视图控制器进行的事件绑定更加直白。当嵌套两个视图组件时,它们的监听器、引用可以被隔离,独自由自己的视图控制器处理。
类Ext.app.ViewModel用于管理数据对象,并且在数据对象发生变化时通知其关注者。类似于ViewController,ViewModel也归属于引用它的视图组件。因为ViewModel是属于视图的,所以它可以依据容器的层次,连接到一个父容器所关联的Parent ViewModel。子组件可以从Parent ViewModel继承而得到数据。
ViewModel通常被扩展为子类以满足需要。为了简化子类的工作,除了持有数据、提供数据绑定功能以外,ViewModel提供了formulas、stores两个特性:
- 所谓Formula(公式)的从其它数据计算出数据的方法。ViewModel的配置项formulas允许你在ViewModel中封装数据依赖
- ViewModel还可以定义store,以向视图呈现一个命名的对象,视图可以绑定到此命名对象。store可以允许视图自由的定义数据结果,而不是和复杂的全局store定义纠缠在一起
下面是formulas和stores的一个简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 视图 var view = { xtype: 'grid', bind: { title: 'Summary for {fullName}', store: '{summary}' // 引用的store,{}标记用于数据绑定 }, viewModel : Ext.create('App.view.summary.SummaryModel') }; // 视图使用的ViewModel Ext.define( 'App.view.summary.SummaryModel', { extend: 'Ext.app.ViewModel', // 在视图模型中可以定义多个公式 formulas: { fullName: function ( get ) { return get( 'firstName' ) + ' ' + get( 'lastName' ); } }, // 在视图模型中可以定义多个Store stores: { summary: { source: 'allSummaries', // 链接(Chained)到一个全局的Store filters: [ { property: 'fullName', value: '{fullName}' } ] } } } ); |
在ExtJS 5中 ,通过使用改进的ViewController,你可以简化到视图组件的连接:
你也可以使用ViewModel,来自动化视图与数据之间的连接(绑定),并且提供操控数据的方法(在MVVM的世界里常称为Command):
当然,你也可以把上面两种风格结合起来使用:
合理的使用ViewController、ViewModel有助于实现关注点分离。特别是在基于事件来驱动视图的创建、使用路由的场景下。
ExtJS 5实现了路由机制,从而允许你在应用程序内实现“深链接”(deep linking)。路由机制的实现原理是将应用程序的URL映射到控制器的动作/方法。
路由执行之前,可以具有一个before回调,在此回调中可以实现数据验证、在AJAX请求完毕后继续路由,或者其它功能。
所谓响应式配置(responsiveConfig)是一组规则驱动的配置项,当规则满足时,配置项应用到组件上。下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Ext.create( { items: [ { // 必须使用此插件 plugins: 'responsive', responsiveConfig: { // 竖屏时应用以下配置项 tall: { region: 'north' }, // 横屏时应用以下配置项 wide: { region: 'west' }, // 规则也可以用表达式来指定 'width < 600': { region: 'north' }, 'width >= 600': { region: 'west' } } } ] } ); |
现在Ext.data被移动到Sencha 框架的核心包中,以便ExtJS、Sencha Touch共同使用之。
ExtJS 5简化了记录创建、ID生成,让原始数据对象(来自服务器)到记录(Record)的转换变得尽可能轻量。
现在模型(Model)的构造函数不需要一一声明原始数据对象中的字段,未声明的字段的数据不会丢失,而是默认被认为是持久化数据,对这些字段的修改会被跟踪并发回服务器保存。
ExtJS 5简化了关联的配置,在字段的声明中,可以增加一个reference配置项,定义one-to-one/one-to-many变得非常简单:
1 2 3 4 5 |
Ext.define('App.model.Comment', { fields: [ { name: 'userId', reference: 'User' } ] }); |
原先用于定义关联的各种配置项,均可以作为reference的子配置项声明,你不再需要为模型指定单独的配置项(hasMany、belongsTo)来声明关联。
ExtJS 5为 Ext.data.schema.ManyToMany 添加了支持,你可以在单方/双方模型上声明多对多关联:
1 2 3 4 5 6 7 |
Ext.define('App.model.User', { extend: 'Ext.data.Model', fields: [{ ... }], manyToMany: 'Group' }); |
同其它关联类似,多对多也是由store来呈现的。多对多的特殊之处在于,ExtJS维护其对称性:如果你在user.groups这个store中添加一个用户组,那么该用户组的users store中会自动添加user记录。
ExtJS中的store承担了两个重要、但是可能冲突的职责:
- 在数据层,作为客户端的“记录系统”,持有来自服务器的数据
- 持有Grid、DataView之类的组件的数据
先前版本ExtJS面临的一个挑战是,如果精确的管理被多个组件使用的Store,因为每个组件可能需要对其数据进行过滤、排序等操作,然后这些操作会影响所有共享此Store的其它组件。
ExtJS 5引入了链接存储(Chained store)的概念。所谓链接存储,是引用了一个“source”存储的存储。源存储定义了一系列的记录,而链接存储在其基础上应用过滤器、排序器、分组器。当源存储添加/删除/修改记录后,链接存储、关联的视图自动的反映此变更。
链接存储通常由ViewModel创建,但是你也可以手工的创建。由于多个链接存储可以共享单个源存储中的记录,你可以节省客户端内存、服务器调用。
在ExtJS 4中,保存客户端记录到服务器是一项需要小心的、手工完成的操作,你必须明白那些记录被修改。
现在, Ext.data.Session 可以用来自动化的管理数据保存操作。数据会话会跟踪那些记录需要在服务器端被更新/创建/删除,并且能够合理的排序这些DML操作,确保记录能够引用其它新创建的记录的、服务器分配的ID。
与Sencha Touch的ModelCache不同,你可以创建自己的Ext.data.Session。你可以在打开模块创建的同时创建一个新的会话,并与页面其它部分隔离。这个功能类似于数据库事务的Savepoint。
使用数据会话后,便捷带有关联的记录变得非常简单。
在ExtJS 5中,你可以创建包含不同类型结点的树。 Ext.data.reader.Reader的typeProperty配置用于指定表示结点类型的属性。Ext.data.TreeModel的childType配置项也可以用于相同目的。
数据验证是关于模型的常见任务。在ExtJS 5中,你可以扩展自己的字段类型,并在其中声明验证规则,而不是在模型的各个字段中,逐一声明验证规则。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 扩展一个自定义字段类型 Ext.define('App.fields.Email', { extend: 'Ext.data.field.String', alias: 'data.field.email', validators: 'email' }); // 使用新的字段类型 Ext.define('App.model.User', { extend: 'Ext.data.Model', fields: [{ name: 'emailAddress', type: 'email' }] }); |
现在,模型/字段可以执行验证了,你需要做的就是,将一个表单面板链接到ViewModel,并且把表单字段bind到ViewModel的字段:
1 2 3 4 5 6 7 8 9 10 11 |
var form = { // 绑定到Ext.data.Model#validators,此配置项仅对表单字段有用,但是此配置项可以继承,因此可以设置在父容器上 modelValidation: true, items: [ { xtype: 'textfield', bind: '{user.name}' }, { xtype: 'textfield', bind: '{user.emailAddress}' } ] }; |
表单字段会响应modelValidation配置,从而在值绑定的基础上,进一步进行验证绑定(到模型的验证规则)。下面是不使用modelValidation时的等价代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var form = { items: [ { xtype: 'textfield', bind: { value: '{user.name}', validation: '{user.validation.name}' } }, { xtype: 'textfield', bind: { value: '{user.emailAddress}', validation: '{user.validation.emailAddress}' } } ] }; |
新的字段Ext.form.field.Tag,可以让下拉列表的使用更加简单。它让选中项在主页面(而不是下拉列表的Pickup弹框)逐个展示为小Tag,可以很方面的移除。
这是移动平台上非常通用的按钮分组风格,即一系列联排的按钮,由Ext.button.Segmented类实现。
现在任何文本字段都可以配置以此触发器,而不仅仅是TriggerField的子类。
ExtJS 5.1引入了一个功能很全面的取色器组件
ExtJS 5.1引入了电子表格(Spreadsheet)模型,选区(Selection)可以被标记为可扩展的(extensible:true),你可以拖动选区由下角的把手,水平/垂直的扩展选区。
ExtJS 5的事件机制发生了重大的改变,此改变是从Sencha Touch引入的。现在,监听器不再直接的附加在目标DOM节点上,而是一律的,由单个的、附加在顶级DOM节点上的监听器来代理所有子代节点上的监听器。当DOM节点发起一个事件后,会一直冒泡到顶级DOM节点,然后代理监听器模拟浏览器中原生的事件传播规则,依次调用子代节点(通过ExtJS API添加的)监听器。
这一变化虽然引入额外的复杂性,但是也带来了好处,特别是它带来了手势(Gesture)的支持。
Sencha Touch首先引入了手势机制,以便实现浏览器不能原生支持的高级别事件,例如tap、rotate、swipe。
ExtJS 4.2开始支持设置Tab页签的位置——放到面板的左侧或者右侧,而不是默认的上面。在ExtJS 5中,你更可以设置图标对齐、文字旋转,甚至让页签栏合并到面板头部以节约空间。
ExtJS 5支持一种新的小器件列(Widget Column),你可以添加组件/小器件的配置项到Ext.grid.column.Widget 中,作为在单元格中渲染的组件的目标。结合bufferedrenderer插件,即使是很大的表格,渲染起这些组件也不会出现问题。
某些情况下,数据更新来得特别频繁,导致DOM需要频繁更新作出响应,这可能让浏览器失去响应。bufferedrenderer插件解决这一问题,你可以通过它设置DOM更新的速率。
使用Renderers可以很容易的定义单元格的HTML内容,新的单元格更新器(Cell Updater)则更加高效,当构成单元格的数据发生改变时,单元格更新器才会被调用。
以前作为扩展包的表格过滤器,被重新设计,并作为插件集成到框架主线(Ext.grid.filters.Filters,gridfilters)。gridfilters使用Ext.data.Store提供的过滤支持,很好的支持远程过滤并保存在组件的状态中。
除了使用renderer/updater之外,现在还通过小器件(Widget)来渲染Grid的单元格。
小器件时是一种轻量级的组件,可以最小成本的创建和绘制。小器件不具有典型Ext.Component那样的完整生命周期。
ExtJS 5自带了一些小器件,例如进度条、滑块,这两个是相应组件的轻量版本。sparklines则是一个最小化的Chart,可以很好的使用在Grid中。
ExtJS 5支持以面包屑(Breadcrumb,IDE中很常见)的风格,以Bar的风格显示一个TreeStore中的数据。Ext.toolbar.Breadcrumb实现此功能。
ExtJS 5可以很好的支持触摸屏设备,例如平板电脑。既有应用程序只需要极少的改动就可以在触屏设备上运行。
ExtJS 5引入两个新主题:Neptune Touch、Crisp。前者是Neptune主题的现代化、简约风变体,增加了某些可触控元素的尺寸,以便于触摸操作。后者类似,但是修改了色彩方案。这两个主题都是很好的自定义主题的起点。
ExtJS 4相比起以前的版本,是一个非常大的升级。ExtJS 4引入了新的类型系统、新的组件(例如图表、绘图组件),此外API也有很大的变动与增强。
在ExtJS 3中,诸如PagingToolbar、Toolbar的类被置于widgets包,并且直接在全局对象Ext上定义,而在ExtJS 4中,这些类已经被整理,并放置到toolbar包,定义在Ext.toolbar名字空间中。
在ExtJS 4中,属性“alternateClassName”说明了类在ExtJS 3中对应的名称,出于兼容性考虑,这些备用名称仍然可以使用。
ext-all-*.js可以加载整个ExtJS框架到浏览器中,而ext.js则仅加载最小化的运行时代码,并在需要ExtJS类时,进行动态加载。
依赖管理是动态加载的基础,要使用动态加载,应当在定义类时设置以下配置项:
配置项 | 说明 |
requires | 声明当前类正常工作所必须的依赖类,这些依赖必须在当前类实例化之前载入 |
uses | 声明当前类的可选依赖,这些类不一定需要在当前类实例化之前载入 |
Ext.Loader会在创建类实例时递归的检查其依赖是否已经加载,并在全部依赖加载完毕后才进行类的实例化。注意,循环依赖会导致死锁。
要启用动态加载,还需要设置:
1 2 3 4 5 6 |
Ext.Loader.setConfig( { enabled : true, paths : { 'Ext.ux' : 'http://localhost:5050/sshe-static/extjs-4.1.1/ux' } } ); |
在开发阶段动态加载非常有意义,但是应当避免在生产环节下使用动态加载,因为大量零散的请求会造成服务器压力,拖慢响应速度。
ExtJS 4的类型系统向后兼容与ExtJS 3,并且更加灵活易用。
ExtJS 4使用Ext.define、Ext.create函数来进行类的定义与实例化。这些方法比起ExtJS 3有以下改进:
- 在定义子类时,不强制要求父类已经定义。如果父类尚未定义,则ExtJS 4的类管理器自动延迟子类的创建,直到其父类被定义。这意味着,开发人员不需要手工保证类定义顺序的问题了
- Ext.define会按需、自动创建名字空间
- 简化了对父类函数的调用
下面是一个类创建、实例化的例子,以及与ExtJS 3 API的对比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/* ExtJS 4定义类的方式 */ //不需要声明名字空间 Ext.define( 'Gmem.LogonWindow', { //不需要父类已经载入 extend : 'Ext.Window', initComponent : function() { this.items = [ { xtype : 'textfield', name : 'userName', fieldLabel : 'User Name' } ], this.callParent( arguments ); } } ); /* ExtJS 4创建实例的方式 */ var win = Ext.create( 'Gmem.LogonWindow' ); win.show(); /* ExtJS 3定义类的方式 */ Ext.namespace( 'Gmem' ); //必须声明名字空间 //extend函数在ExtJS 4中不推荐使用 //在定义新类时,其父类必须已经载入 Gmem.LogonWindow = Ext.extend( Ext.Window, { initComponent : function() { Ext.apply( this, { items : [ { xtype : 'textfield', name : 'userName', fieldLabel : 'User Name' } ] } ); Gmem.LogonWindow.superclass.initComponent.apply( this, arguments ); } } ); /* ExtJS 3创建实例的方式 */ var win = new Gmem.LogonWindow(); win.show(); |
混入(Mixin)是很多动态语言具有的功能特性,可以为已定义类动态添加新的行为(或属性),混入机制属于AOP的一种。
ExtJS 4的类型系统提供混入功能(配置项:mixins),该功能与Ext.override类似,但是不会覆盖已有类的方法。下面是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Ext.define( 'Gmem.mixins.Logger', { debug : function( msg ) { console.log( msg ); } } ); Ext.define( 'Gmem.Object', { mixins : { logger : 'Gmem.mixins.Logger' } } ); obj = Ext.create( 'Gmem.Object' ); obj.debug( 'Hello World' ); |
使用ExtJS 4进行类定义时,可以提供config参数,其所有属性均会生成为类实例的属性,并且ExtJS会自动没这些属性生成4个方法,以name属性为例:
自动方法 | 说明 |
getName() | 获取name属性的当前值 |
setName() | 设置name属性的值 |
resetName() | 重置name为默认值(类定义中config提供的值) |
applyName() | 每次setName()调用之后,自动调用该方法,可以自行扩展,实现需要的逻辑 |
上面4个方法均可手工定义,在存在手工定义的情况下ExtJS不会自动生成。
ExtJS 4的任何类均可以具有静态方法、字段,并可以使用ClassName.fieldName的形式访问,不需要实例化:
1 2 3 4 5 6 7 |
Ext.define( 'StringUtils', { static : { EMPTY : '', indexOf : function( str, search ) {} } } ); |
使用this.statics()或者this.self.fieldName可以在类的内部访问静态成员,在使用继承并且覆盖父类静态属性时,这两者有微妙的差别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Ext.define( 'StringUtils', { static : { EMPTY : '', indexOf : function( str, search ) {} } } ) Ext.define( 'StringUtils2', { static : { EMPTY : 'EMPTY' }, constructor : function() { //如果调用了父类构造器 this.callParent(); }, init : function() { //则下面两个表达式的含义不同 this.statics().EMPTY == '' this.self.fieldName == 'EMPTY' } } ) |
下表列出ExtJS 4组件系统的一些重要改进:
组件 | ExtJS 4的改进 |
存储(Store) | 不再需要指定此Store能加载的数据的格式(不需要指定JsonStore或者XmlStore),ExtJS会自动判断数据格式。Store支持排序和过滤 |
读取器(Reader) | 可以从服务器读取嵌套的数据 |
模型(Model) | 该类型与ExtJS 3的Record功能类似,增加了关联(Association)、验证等功能 |
代理(Proxy) | 在ExtJS 4中,代理可用于加载、保存数据,支持配置使用的Reader、Writer。可以直接为Model配置Proxy,因而不需要使用Store |
绘图组件 | 支持绘制SVG、HTML5 Canvas或者VML驱动的矢量图 |
布局(Layout) | 引入新的布局类型,例如:DockLayout、ToolbarLayout、FieldLayout,同时增强了性能和灵活性 |
表格(Grid) | 内置了缓冲支持,显示数千条数据不会有性能问题 表格编辑的功能得到改进,基于插件机制实现对任意表格的编辑 可以扩展Ext.grid.Feature来定制特殊功能的表格 |
表单(Form) | 表单可以支持任意布局,原先的FormLayout已经废弃 引入了FieldContainer用于在表单内部管理字段的布局 表单内可以放置任意组件甚至是Grid 在表单验证方面进行了重大改进 |
Leave a Reply