ExtJS 4的MVC框架
ExtJS 4引入MVC架构,可以用来创建ExtJS应用程序,与经典的Ext.onReady回调方式很不相同。
在开发ExtJS应用时,往往习惯于把所有JavaScript代码编写在单个文件中,生产环境下的应用通常都比较复杂,可能包括了面板、表单、表格、布局、模型、存储等多种组件的组合。大量的组件代码导致JS文件急剧膨胀,难以维护,因此很有必要根据职责的不同把代码划分到不同的文件中,提高代码的可重用性。
在ExtJS 4以前的版本下,如何组织代码由开发人员自行决定,而MVC则是ExtJS 4提供的模式化选择。在ExtJS看来,MVC代表:
- Model:包含一系列字段、及其数据的集合,由Model类表示,亦可使用Store类来加载、保存数据
- View:可以是某种视觉组件,例如Grid、Tree、Form
- Controllers:包含了一些动作逻辑,例如用户点击按钮时需要执行的操作
在这里我们先创建一个老式的ExtJS应用,并在后面尝试将其迁移到MVC架构下,以阐述ExtJS MVC的用法。
该应用包含一个GRID和一个详情面板,JavaScript代码全部放在app.js中:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
Ext.onReady( function() { Ext.define( 'Book', { extend : 'Ext.data.Model', fields : [ 'id', 'title', 'pages', 'numChapters', 'topic', 'publisher', 'isbn', 'isbn13' ] } ); var store = Ext.create( 'Ext.data.Store', { model : 'Book', proxy : { type : 'ajax', url : 'data/books.json' } } ); var grid = Ext.create( 'Ext.grid.Panel', { store : store, title : 'Books', columns : [ { text : "Title", width : 120, dataIndex : 'title', sortable : true }, { text : "Pages", flex : 1, dataIndex : 'pages', sortable : true }, { text : "Topic", width : 115, dataIndex : 'topic', sortable : true }, { text : "Publisher", width : 100, dataIndex : 'publisher', sortable : true } ], viewConfig : { forceFit : true }, region : 'center' } ); var details = Ext.create( 'Ext.panel.Panel', { id : 'bookDetail', bodyPadding : 7, bodyStyle : "background: #ffffff;", html : 'Please select a book to see additional details.', height : 150, split : true, region : 'south' } ); var bookTplMarkup = [ '<b>Title:</b> {title}<br/>', '<b>Pages:</b> {pages}<br/>', '<b>No Chapters:</b> {numChapters}<br/>', '<b>Topic:</b> {topic}<br/>', '<b>Publisher:</b> {publisher}<br/>', '<b>ISBN:</b> {isbn}<br/>', '<b>ISBN 13:</b> {isbn13}<br/>' ]; var bookTpl = Ext.create( 'Ext.Template', bookTplMarkup ); grid.getSelectionModel().on( 'selectionchange', function( sm, selectedRecord ) { if ( selectedRecord.length ) { var detailPanel = Ext.getCmp( 'bookDetail' ); bookTpl.overwrite( detailPanel.body, selectedRecord[0].data ); } } ); Ext.create( 'Ext.container.Viewport', { frame : true, layout : 'border', items : [ grid, details ] } ); store.load(); } ); |
HTML代码:
1 2 3 4 5 6 7 8 9 10 11 |
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>MVC Architecture</title> <link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all.css" /> <script type="text/javascript" src="extjs/ext-all.js"></script> <script type="text/javascript" src="app.js"></script> </head> <body> </body> </html> |
在这里我们使用MVC重新实现上一段中的应用。下图描述了JavaScript代码的目录结构:
我们可以看到,所有的JavaScript代码被分配在app文件夹中,它们的职责划分如下:
- app.js:包装整个应用程序,通常我们使用单页面方式来开发MVC应用
- controller:控制器类存放目录
- model:模型类存放目录
- store:存储类存放目录
- view:视图类存放目录
- data:由于本示例不合服务器交互,该目录存放JSON数据
为了使用动态加载,以下ExtJS SDK文件需要被引入到extjs目录下:
- ext-debug.js或者ext.js:用于ExtJS自举
- resources目录:包含样式定义文件
- src目录:包含ExtJS所有组件的源代码
修改后的HTML代码如下:
1 2 3 4 5 6 7 8 9 |
<head> <title>MVC Architecture</title> <link rel="stylesheet" type="text/css" href="exts/resources/css/ext-all.css" /> <script type="text/javascript" src="extjs/ext-debug.js"></script> <script type="text/javascript" src="app.js"></script> </head> <body> </body> </html> |
app.js为MVC骨架,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
Ext.require( 'Ext.container.Viewport' ); Ext.application( { name : 'App', //可选的应用的名称,也在全局创建了变量App,会作为所有模型、视图、存储、控制器的名字空间 appFolder : 'app', //应用程序组件存放位置 controllers: ['Books'], //声明该应用使用的控制器 launch : function() //应用创建后自动执行的动作,必须覆写 { Ext.create( 'Ext.container.Viewport', { //所有组件均放置于该Viewport中 } ); } } ); |
1 2 3 4 5 6 7 8 |
//注意类名于目录结构限定的名字空间一致 Ext.define( 'App.controller.Books', { extend : 'Ext.app.Controller', init : function() { //在App的launch之前执行 } } ); |
控制器类继承自Ext.app.Controller,控制器负责与Store、View通信,并在特定事件发生时执行对应的Action。
控制器的init方法在App.launch方法之前执行,可以在这里编写所有需要在创建Viewport前执行的代码,例如:
- 实现与视图交互的逻辑,主要是相关的事件监听
- 加载数据
- 其它业务逻辑
修改上面编写的控制器为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Ext.define( 'App.controller.Books', { extend : 'Ext.app.Controller', init : function() { this.control( { //对于viewport的直接子panel,监听其render事件 'viewport > panel' : { render : this.onPanelRendered } } ); }, onPanelRendered : function() { } } ); |
使用control方法可以注册对视图组件生命周期的监听器,在这里,我们监听Viewport子面板的render事件。
模型类:
1 2 3 4 5 6 7 |
//注意类名于目录结构限定的名字空间一致 Ext.define( 'App.model.Book', { extend : 'Ext.data.Model', fields : [ 'id', 'title', 'pages', 'numChapters', 'topic', 'publisher', 'isbn', 'isbn13' ] } ); |
存储类:
1 2 3 4 5 6 7 8 |
Ext.define( 'App.store.Book', { extend : 'Ext.data.Store', model : 'App.model.Book', proxy : { type : 'ajax', url : 'data/books.json' } } ); |
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 |
Ext.define( 'App.view.book.Grid', { extend : 'Ext.grid.Panel', alias : 'widget.bookList', title : 'Books', initComponent : function() { this.store = 'Book'; this.columns = [ { text : "Title", width : 120, dataIndex : 'title', sortable : true }, { text : "Pages", flex : 1, dataIndex : 'pages', sortable : true }, { text : "Topic", width : 115, dataIndex : 'topic', sortable : true }, { text : "Publisher", width : 100, dataIndex : 'publisher', sortable : true } ]; this.viewConfig = { forceFit : true }; this.callParent( arguments ); } } ); |
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 |
Ext.define( 'App.view.book.DetailPanel', { extend : 'Ext.Panel', alias : 'widget.detailPanel', //xtype别名 bookTplMarkup : [ //作为实例变量来定义,而不是全局变量 '<b>Title:</b> {title}<br/>', '<b>Pages:</b> {pages}<br/>', '<b>No Chapters:</b> {numChapters}<br/>', '<b>Topic:</b> {topic}<br/>', '<b>Publisher:</b> {publisher}<br/>', '<b>ISBN:</b> {isbn}<br/>', '<b>ISBN 13:</b> {isbn13}<br/>' ], startingMarkup : 'Please select a book to see additional details.', bodyPadding : 7, initComponent : function() { this.tpl = Ext.create( 'Ext.Template', this.bookTplMarkup ); this.html = this.startingMarkup; this.bodyStyle = { background : '#ffffff' }; this.callParent( arguments ); }, updateDetail : function( data ) //供控制器调用,更新此视图组件 { this.tpl.overwrite( this.body, data ); } } ); |
同时,修改应用骨架、控制器类为最终版本:
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 |
Ext.require( 'Ext.container.Viewport' ); Ext.application( { name : 'App', appFolder : 'app', controllers : [ 'Books'], launch : function() { Ext.create( 'Ext.container.Viewport', { layout : 'fit', items : [ { xtype : 'panel', title : 'Books', items : [ { xtype : 'booklist' }, { xtype : 'detailPanel' } ] } ] } ); } } ); |
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 |
Ext.define( 'App.controller.Books', { extend : 'Ext.app.Controller', stores : [ 'Book' ], //不需要声明完整的名称,ExtJS会自动在当前App的名字空间、约定目录下寻找 models : [ 'Book' ], views : [ 'book.Grid', 'book.DetailPanel' ], //控制器可以控制的视图的列表 refs : [ //该配置用于外部引用控制器内的视图对象,使用Ext.ComponentQuery查询来获取视图 { //变量名,可以直接使用Getter获取 ref : 'panel', //组件选择符 selector : 'detailPanel' } ], init : function() { this.getBookStore().load(); //getBookStore是自动生成的方法,等价于this.getStore('Book').load(); this.control( { //控制ViewPort下的bookList(表格)的dataview组件 'viewport > bookList dataview' : { itemclick : this.bindGridToPanel } } ); }, bindGridToPanel : function( grid, record ) { //直接使用Getter获取详情面板,并调用其方法更新视图 this.getPanel().updateDetail( record.data ); } } ); |
声明在stores、views、models配置项中的对象,可以使用控制器类自动生成getter方法访问,这些方法包括两类:
- 其中一类是形如getName[Store|View|Model]()的方法,其中Name是配置项中声明的组件的名称。
- 另外一些带单个形参的getModel、getStore、getView方法,使用时通过参数传递组件的名称:
getter方法 | 说明 |
getModel(name) | 返回指定模型的引用,目标模型必须在models配置项中声明 |
getStore(name) | 返回指定存储的引用,目标存储必须在stores配置项中声明 |
getView(name) | 返回指定视图的引用,目标视图必须在views配置项中声明 |
- 创建Model、Store、View类(扩展已有UI组件)
- 在View类中不会执行任何操作(事件句柄),View只是提供方法,让控制器调用后改变视图
- 在Controller中列出其需要使用的Model、Store、View
- 注意Controller自动生成很多getter方法
- 为了方便的在Controller中获取View的引用,可以使用ref配置项
- 使用control函数声明针对View、Components的事件监听
- 注意Controller的init在App的launch之前执行
如果为Model声明关联(associations),则必须使用关联目标的全名:
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 |
Ext.define( 'App.model.Author', { extend : 'Ext.data.Model', fields : [ { name : 'id', type : 'int' }, { name : 'name', type : 'string' } ], hasMany : { model : 'App.model.Book', //如果仅仅写'Book'会出错 foreignKey : 'authorId', name : 'books' }, proxy : { type : 'ajax', url : 'data/authors/1.json', reader : { type : 'json', root : 'authors' } } } ); |
Sencha SDK tools是ExtJS官方提供的开发套件,它可以生成JSB3((JSBuilder file format))格式的JavaScript依赖关系,并且依此生成最小化的运行时JavaScript文件,便于部署。
使用如下的命令可以生成JSB依赖文件:
1 2 3 |
sencha create jsb -a index.html -p app.js rem 亦可指向服务器上的HTML文件 sencha create jsb -a http://localhost:8080/ext-theme/index.html -p app.jsb3 |
上述命令会生成一个app.jsb3文件,其内容如下:
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 |
{ "projectName" : "Project Name", "licenseText" : "Copyright(c) 2011 Company Name", "builds" : [ { "name" : "All Classes", "target" : "all-classes.js", "options" : { "debug" : true }, "files" : [ { "path" : "extjs/src/util/", "name" : "Observable.js" }, { "path" : "extjs/src/data/", "name" : "Association.js" }, { "path" : "extjs/src/data/", "name" : "Operation.js" } ] }, { "name" : "Application - Production", "target" : "app-all.js", "compress" : true, "files" : [ { "path" : "", "name" : "app.js" } ] } ], "resources" : [] } |
然后,执行命令:
1 |
sencha build -p app.jsb3 -d |
会生成两个文件:
- all-classes.js:包含应用程序用到的所有ExtJS类,未压缩,易于DEBUG
- app-all.js:包含应用程序用到的所有ExtJS类、app.js,压缩版本
我们可以修改HTML,引用生成的JavaScript文件:
1 2 3 4 5 6 7 8 9 10 |
<html> <head> <title>MVC Architecture</title> <link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all.css" /> <script type="text/javascript" src="extjs/ext.js"></script> <script type="text/javascript" src="app-all.js"></script> </head> <body> </body> </html> |
Leave a Reply