前面几篇文档,我们基本实现了一个静态的extjs页面,本篇开始,实现左侧导航树与右侧内容的联动,也就是点击导航菜单,加载对应模块页面和业务逻辑,实现js文件的按需加载。
业务需求是这样的:
左侧的treelist,当点击某个节点的时候,系统根据tree数据里配置的模块信息,加载这个模块,并且把模块对应的主页面显示在中间区域的tabpanel里。
改造主控制器:app/luter/controller/MainController.js
监听导航树的node点击事件,进行后续处理:
'navlist': {
'itemclick':function(el, record, opt){
//可以通过如下方式获取点击节点的数据。
var nodeData = record.node.data;
}
}
完整的代码如下:
Ext.define('luter.controller.MainController', {
extend: 'Ext.app.Controller',
views: ['main.ViewPort'],
stores: ['NavTreeStore'],
init: function (application) {
var me = this;
this.control({
'viewport': {//监听viewport的初始化事件,可以做点其他事情在这里,如有必要,记得viewport定义里的alias么?
'beforerender': function () {
console.log('viewport begin render at:' + new Date());
},
'afterrender': function () {
console.log('viewport render finished at:' + new Date());
},
},
'syscontentpanel': {
'afterrender': function (view) {
console.log('syscontentpanel rendered at:' + new Date());
}
},
'navlist': {
'itemclick': function (el, record, opt) {
var nodeData = record.node.data;//当前点击节点的数据
var tabpanel = Ext.getCmp('systabpanel');//中间tabpanel
var tabcount = tabpanel.items.getCount();//当前tabpanel已经打开几个tab了。
var maxTabCount = 5;//最大打开的tab个数
if (tabcount && tabcount > 5) {
showFailMesg({
title: '为了更好的使用,最多允许打开5个页面',
msg: '您打开的页面过多,请关掉一些!'
});
return false;
}
if (nodeData.leaf) {//是打开新模块,否则是展开树节点
var moduleID = nodeData.module_id;//找到控制器ID,定义在tree的数据里modole_id
if (!moduleID || moduleID == '') {
showFailMesg({
title: '创建模块失败.',
msg: '模块加载错误,模块id为空,创建模块失败'
});
return false;
}
console.log('to add module with id:' + moduleID);
//开始加载控制器
try {
//尝试加载这个控制器,这个过程就是按需ajax加载js文件的过程。
//如果这个模块被加载过,则不会重复加载。
var module = luterapp.getController(moduleID);
} catch (error) {
showFailMesg({
msg: '根据模块ID:' + moduleID + '创建模块失败。' +
'
可能的原因 :
1、该模块当前没有实现.' +
'
2、模块文件名称与模块名称不一致,请检查' +
'Error: ' + error + ''
});
return false;
} finally {
}
//判断模块是否加载下来,因为是ajax加载,所以还是判断一下比较好
if (!module) {
showFailMesg({
msg: 'B:load module fail,the module object is null.' +
'
maybe :the module is Not available now.'
});
return false;
}
//加载到之后,默认去获取控制器里views:[]数组里的第一个作为主视图
var viewName = module.views[0];
console.log('will create a tab with view ,id:' + viewName);
var view = module.getView(viewName);
console.log('get the view el:' + view);
if (!view) {
showFailMesg({
msg: 'Sorry ,to get the module view fail...'
});
return false;
}
//判断一下这个视图是不是已经加载到tabpanel里去了
var tabid = me.getTabId(moduleID);
console.log('will create a tab with id:' + tabid);
var notab = tabpanel.getComponent(tabid);
var viewEL = view.create();
if (!viewEL) {
showFailMesg({
msg: 'Sorry ,to get the module viewEL fail...'
});
return false;
}
if (!notab && null == notab) {//不存在新建
//不管是啥,都放到一个panel里面。
notab = tabpanel.add(Ext.create('Ext.panel.Panel', {
tooltip: nodeData.text + ':' + nodeData.qtip,
id: tabid, // tab的唯一id
title: nodeData.text, // tab的标题
layout: 'fit', // 填充布局,它不会让load进来的东西改变大小
border: false, // 无边框
closable: true, // 有关闭选项卡按钮
iconCls: nodeData.iconCls,
listeners: {
// 侦听tab页被激活里触发的动作
scope: this,
destroy: function () {
console.log("tab :" + tabid + ",has been destroyed")
}
},
items: [view.create()]
}));
//新建之后focus
tabpanel.setActiveTab(notab);
}
else {//如果这个tab已经存在了,则focus到这个tab
tabpanel.setActiveTab(notab);
}
} else {
//如果leaf =false,则说明这不是一个最底层节点,是目录,展开。
console.log('tree node expand')
}
}
}
});
},
//这个方法从tab id里分离出控制器名称
getTabId: function (mid) {
var winid = mid;
var c = winid.split('.');
winid = c.pop();
return winid + '-tab';
}
});
左侧菜单树对应的测试数据:app/testdata/menu.json
一般情况下,这个菜单数据是保存在后端的,通过权限判断加载用户可访问的资源形成树结构返回给前端使用。leaf标明了这是一个目录还是一个模块,module_id对应的是控制器的路径。 比如下面这个测试数据中。 "leaf": true, "module_id": "sys.UserController", 在app.js中,我们配置了appFolder:‘app/luter’, leaf标明了这是一个控制器模块,点击后会去触发控制器加载动作。 所以这个模块的实际路径(也就是js文件)就是:${appFolder}/controller/${module_id}.js 即:app/luter/controller/sys.UserController.js
[
{
"id": "111",
"text": "系统管理",
"href": null,
"leaf": false,
"iconCls": "fa fa-home",
"module_id": "no sign",
"qtip": "这个地方显示鼠标悬停提示",
"children": [
{
"id": "11111",
"text": "用户管理",
"href": null,
"leaf": true,
"iconCls": "fa fa-user",
"module_id": "sys.UserController",
"qtip": "系统用户管理",
"children": []
}
]
}
]
导航菜单与tabpanel 联动完成,下面弄个控制器实验一下效果,以新建一个系统管理分类下的用户管理模块功能为例:
系统管理部分模块放在sys目录下,so:
- 控制器:app/luter/controller/sys/UserController.js
- 模型:app/luter/model/UserModel.js
- Store:app/luter/store/UserStore.js
- 视图主入口:app/luter/model/view/sys/user/User.js
- 列表视图:app/luter/model/view/sys/user/UserList.js
- ......
用户管理控制器 :app/luter/controller/sys/UserController.js
Ext.define('luter.controller.sys.UserController', {
extend: 'Ext.app.Controller',
stores: ['UserStore'], //用户store
views: ['sys.user.User'], //主view ,tab里会加载第一个视图。
init: function () {
this.control({
'userlistview': {
'beforerender': function (view) {
console.log("beforerender list...... ");
},
'afterrender': function (view) {
console.log("afterrender list...... ");
// this.getUserStoreStore().load();//如果UserStore里没设置autoLoad: true,就可以在这里加载用户数据
}
}
});
}
});
用户模型Model:app/luter/model/UserModel.js
Ext.define('luter.model.UserModel', {
extend: 'Ext.data.Model',
fields: [
{name: 'id', type: 'string'},
{name: 'username', type: 'string'},
{name: 'gender', type: 'string'},
{name: 'real_name', type: 'string'}
]
});
用户Store:app/luter/store/UserStore.js
Ext.define('luter.store.UserStore', {
extend: 'Ext.data.Store',
autoLoad: true,//自动加载数据
model: 'luter.model.UserModel',//使用的模型
pageSize: 15,//每页数据多少
proxy: {
type: 'ajax',//ajax获取数据
actionMethods: {
create: 'POST',
read: 'POST',
update: 'POST',
destroy: 'POST'
},
api: {
read: 'app/testdata/user.json'//从这个地方获取数据,当然,这里用测试数据
},
reader: {//返回数据解析器
type: 'json',
root: 'root',//用户列表数据在这个字段下
successProperty: 'success',//成功与失败的标志位是这个字段
totalProperty: 'total'//记录总数在这个字段
},
listeners: {
exception: function (proxy, response, operation, eOpts) {
DealAjaxResponse(response);//监听ajax异常提示错误
}
}
},
remoteSort: true,//服务器端排序
sortOnLoad: true,//加载就排序
sorters: {//拿ID排序
property: 'id',
direction: 'DESC'
}
});
用户管理模块主视图:app/luter/model/view/sys/user/User.js
Ext.define('luter.view.sys.user.User', {
extend: 'Ext.panel.Panel',
alias: 'widget.userview',
layout: 'fit',
requires: ['luter.view.sys.user.UserList'],//引入用户列表模块
border: false,
initComponent: function () {
var me = this;
me.items = [{
xtype: 'userlistview',
layout: 'fit'
}]
me.callParent(arguments);
}
});
用户列表视图:app/luter/model/view/sys/user/UserList.js
Ext.define('luter.view.sys.user.UserList', {
extend: 'Ext.grid.Panel',
alias: 'widget.userlistview',//其他地方就可以这么用:xtype:‘userlistview’
requires: [],
store: 'UserStore',//用到的store
itemId: 'userGrid',//自己的itemid
columnLines: true,//是否显示表格线
viewConfig: {
emptyText: '暂无数据'//store没数据的时候显示这个
},
initComponent: function () {
var me = this;
me.columns = [{
xtype: 'rownumberer',
text: '序号',
width: 60
}, {
header: "操作",
xtype: "actioncolumn",
width: 60,
sortable: false,
items: [{
text: "删除",
iconCls: 'icon-delete',
tooltip: "删除这条记录",
handler: function (grid, rowIndex, colIndex) {
var record = grid.getStore().getAt(rowIndex);
if (!record) {
toast({
msg: '请选中一条要删除的记录'
})
} else {
showConfirmMesg({
message: '确定删除这条记录?',
fn: function (btn) {
if (btn === 'yes') {
Ext.Ajax.request({
url: 'sys/user/delete',
method: 'POST',
params: {
id: record.get('id')
},
success: function (response, options) {
DealAjaxResponse(response);
Ext.data.StoreManager.lookup('User').load();
},
failure: function (response, options) {
DealAjaxResponse(response);
}
});
} else {
return false;
}
}
})
}
}
}]
}, {
header: baseConfig.model.user.id,
dataIndex: 'id',
hidden: false,
flex: 1
},
{
header: baseConfig.model.user.username,
dataIndex: 'username',
flex: 1
},
{
header: baseConfig.model.user.real_name,
dataIndex: 'real_name',
flex: 1
}
]
me.bbar = Ext.create('Ext.PagingToolbar', {
store: me.store,
displayInfo: true,
displayMsg: '当前数据 {0} - {1} 总数: {2}',
emptyMsg: "没数据显示",
plugins: [new Ext.create('luter.ux.grid.PagingToolbarResizer', {
options: [5, 10, 15, 20, 25, 50, 100]
})]
})
me.dockedItems = [{
xtype: 'toolbar',
items: [{
text: '添加',
iconCls: baseConfig.appicon.add,
tooltip: '添加',
handler: function () {
var win = Ext.create('luter.view.sys.user.UserAdd');
win.loadView();
win.show();
}
}]
}]
me.listeners = {
'itemdblclick': function (table, record, html, row, event, opt) {
if (record) {
var id = record.get('id');
var view = Ext.create('luter.view.sys.user.UserEdit', {title: '编辑数据'});
view.loadView();
loadFormDataFromDb(view, 'sys/user/view?id=' + id);
} else {
showFailMesg({
msg: '加载信息失败,请确认。'
})
}
}
}
me.plugins = []
me.callParent(arguments);
}
});
//这里的baseConfig定义在公共配置文件config.js中,如下:
公共配置参数定义文件:app/luter/config.js
别忘记在app.html中app.js之前引入这个文件。
/**
* icon_prefix font字体前缀定义
* baseConfig 全局配置
*/
var icon_prefix = " fa blue-color ", baseConfig = {
/**
* 全局常量定义
*/
cons: {
noimage: 'app/resource/images/noimage.jpg',
/**
* 静态服务器的地址
*/
static_server: ''
},
/**
* 渲染器,对Boolean类型的表格列的显示内容进行渲染
*/
renders: {
trueText: '',
falseText: '',
cancel: ''
},
/**
* 图标定义
*/
appicon: {
home: icon_prefix + 'fa-home',
add: icon_prefix + "fa-plus",
update: icon_prefix + "fa-edit",
trash: icon_prefix + "fa-trash",
delete: icon_prefix + "fa-remove red-color",
set_wallpaper: icon_prefix + "fa-image",
setting: icon_prefix + "fa-gears",
desktop: icon_prefix + "fa-desktop",
pailie: icon_prefix + "fa-cubes",
logout: icon_prefix + "fa-power-off",
avatar: icon_prefix + "fa-photo",
key: icon_prefix + "fa-key",
user: icon_prefix + "fa-user",
refresh: icon_prefix + "fa-refresh blue-color",
close: icon_prefix + "fa-close",
male: icon_prefix + 'fa-male',
female: icon_prefix + 'fa-female',
role: icon_prefix + 'fa-users',
user_add: icon_prefix + "fa-user-plus",
undo: icon_prefix + 'fa-undo',
search: icon_prefix + 'fa-search',
reset: icon_prefix + 'fa-retweet',
yes: icon_prefix + 'fa-check green-color',
no: icon_prefix + 'fa-close red-color',
list_ol: icon_prefix + ' fa-list-ol',
list_alt: icon_prefix + ' fa-list-alt',
ban: icon_prefix + "fa-ban",
log: icon_prefix + "fa-tty",
printer: icon_prefix + "fa-print",
fax: icon_prefix + "fa-fax",
download: icon_prefix + "fa-cloud-download",
upload: icon_prefix + "fa-cloud-upload",
comment: icon_prefix + " fa-commenting-o",
credit: icon_prefix + "fa fa-gift"
},
/**
* 模型定义
*/
model: {
/**
* 系统用户模型
*/
user: {
id: 'ID',
username: '用户名',
real_name: '真实姓名'
}
}
};
最后,附上用户列表的测试数据(当然,瞎编的......):app/testdata/user.json
{
"total": 33,
"root": [
{
"id": "aaa",
"username": "user",
"real_name": "用户"
},
{
"id": "ccc",
"username": "user",
"real_name": "用户"
},
{
"id": "ddd",
"username": "user",
"real_name": "用户"
},
{
"id": "eee",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
},
{
"id": "fff",
"username": "user",
"real_name": "用户"
}
],
"success": true
}
如上,没问题的话,刷新页面,应该能看到如下所示:
上图中,一些Extjs默认的样式经过了hack。不是默认样式。
最终,整个项目的目录结构如下:
判断是不是动态按需加载?
1、打开chrome的开发控制台,切换到network面板的js下。
2、刷新页面
3、重复点击左侧用户管理,查看JS加载情况。正常情况下同一个模块的js只加载一次。