1、序言
EXT JS4序列教程主要讲解WEB开发中一些常用的组件,例如Tree,Grid,Combobox,form等,EXT JS4的出现为广大程序员带来了福音,我们可以用较少的代码,实现很炫丽的效果,我在很多项目的架构中都使用EXT JS作为核心的WEB框架,配合jQuery框架,大家很容易实现一个用户体验很不错的软件系统(我们称之为高大上,哈哈哈)。EXT JS自推出以来,其性能就饱受开发的砰击,在EXT JS4以前的版本,性能确实不是很好,不过比起jQuery Easy UI,那还是要好很多的,从EXT JS4.2以后的版本开始,性能还是很不错的,代码也比较精简,结构清晰,纯面像对象的语法,BUG也较EXT JS4.1少了很多,相对比较稳定,EXT JS4推出了MVC模式的设计风格,使得代码结构更加清晰,可读性更好,非常类似于使用JAVA SWING和C# WinForm开发,但如果没有接触过AJAX框架的程序员,第一次使用EXT JS4会碰到各种各样的问题,本教程教从零开始讲解EXT JS4,从客户端到服务器都有完整的代码,服务端使用SSH框架,用注解方式进行开发,抛弃了繁锁的配置文件(我本人相当讨厌配置文件,在我设计的架构中,配置文件几乎为零)。关于源码,由于Google无法访问(IT业的一大悲剧),大家可以到CSDN上下载。
本文从实际应用出发,讲解与WEB系统开发息息相关的实例,EXT JS功能很丰富,由有时间的原因,我不会所有的功能都讲到(我都是利用业余时间写教程,目前在一家公司担任高级架构师,工作很忙,我写教程主要是在互联网上和大家一起分享自己的开发经验),大家按照本套系列教程来逐步开发代码,可以实现一个功能比较完整的WEB系统。本教程后端使用的架构为Struts2+Hibernate4+Spring4,后续我将会逐一介绍SSH架构的搭建。关于ASP.NET的教程,会在后续推出。
作者:山人
1、 异步accordion和Tree菜单
好了,各位观众,前面我们讲了layout布局中的border布局,本章我要介绍一个另大家兴奋的东西,那就是异步accordion和Tree菜单,这类菜单在实际的项目中经常会用到,accordion菜单作为功能模块菜单,Tree菜单作为功能点菜单,由其是规模较大的项目,应用较为普遍。我在网上搜索发现类似的例子有很多,但是很少有异步加载Tree的例子,很多都是一次性加载,这不仅会消耗多余的资源,造成服务端和客户端的查询、显示效率下降,而且不利于权限控制,例如Spring-acegi安全框架。如果是政府类的安全性和保密性要求较高的应用系统,是不适用的,前段时间闹的沸沸扬扬的香港占中事件,国外黑客宣布要入侵中国的电子政务系统,以支持香港占中,这对我国的电子政务系统的安全性提出了挑战,一个很小的漏网都可能会成为黑客入侵的目标,造成较大的损失。所以,菜单的异步加载和权限控制是很有必要的,因为只有异常加载菜单才能较好的与安全框架集成,如果把权限控制放在客户端脚本里,黑客就可以通过修改脚本执行顺序、跨站脚本攻击等技术攻击我们的系统,得到系统管理员的权限。说了这么多题外话,我们先来看一下效果:
怎么样,高大上吧?其实高大上就是这么来的。当然,很多按钮位置错乱、节点错乱、半天不响应的UI也是这么来的,我们称之为“土肥圆”,不知道你有没有见过,反正我是见过,最让人佩服的是,这样的系统还被客户使劲的夸,我们只能彻底拜服在这位项目经理的脚下。由此可知,做好一个系统,不是你的功能做的有多强就可以的,关键还是要维护好客户关系的。以上界面具体实现步骤如下:
第一步:老姜一块,我们需要在JSP中引入Ext JS4的类库,这一步是必须的。
<%@ page pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <%-- 样式文件,加载全部样式 --%> <link rel="stylesheet" type="text/css" href="<%=path%>/javascript/extjs-4.1.0/resources/css/ext-all.css" /> <%-- ext js 文件 --%> <script type="text/javascript" src="<%=path%>/javascript/extjs-4.1.0/ext-all.js"></script> <script type="text/javascript" src="<%=path%>/javascript/extjs-4.1.0/ext-lang-zh_CN.js"></script>
注意,大家可以把EXT JS类库定义在一个JSP中,当其他页面要引用的时候,可以使用JSP的包含动作将类库引入,这样也符合代码重用的目的,同时要把ext-lang-zh_CN.js这个文件引进来,这个是Ext JS的语言文件,用来汉化EXT JS。
接下来讲解几个EXT JS的函数,第一个函数EXT的AJAX请求函数,这个函数用来发送异步请求,向服务器请求数据,服务器收到请求后将数据返回给客户端,由EXT JS进行处理,函数如下:
Ext.Ajax.request({….})
第二个函数,哦,对了,这里我们不能称之为函数,应该称之为模型(Model),在JAVA和C#里,我们经常要定义Model,用来定义和存储对象的数据,EXT JS的模型和各种服务端编程语言是一样的,只是写法不一样,EXT JS的模型用来存储JSON格式的数据,Model存储在store里面,store就像一个数据库,里面可以有表,而Model的角色就是表,定义一个模型是这样的。
Ext.define('Menu', { extend : 'Ext.data.Model', fields : [ { name : 'id', type : 'string' } 此处略去N个字
第二步:定义一个MODEL
//定义一个菜单的MODEL,用来保存对应的JSON数据 Ext.define('Menu', { extend : 'Ext.data.Model', fields : [ { name : 'id', type : 'string' }, { name : 'text', type : 'string' }, { name : 'url', type : 'string' } ] });
第三步:定义一个TreePanel
/** * 生成treepanel */ var buildTree = function(json) { //创建一颗树了 return Ext.create('Ext.tree.Panel', { useArrows : true, rootVisible : false, border : false, store : Ext.create('Ext.data.TreeStore', { model : 'Menu', //使用AJAX动态装载Tree proxy: { type: 'ajax', url: ctx+'/queryChildMenu' }, root : { expanded : true, children : json } }), listeners : { //节点单击事件 'itemclick' : function(view, record, item,index, e) { var id = record.get('id'); var text = record.get('text'); var url = record.get('url'); var leaf = record.get('leaf'); if (leaf) { centerPanel.loadPage(url,'menu' + id, text); } }, //单击节点展开之前的事件 'beforeitemclick':function(view, record, item,index, e) { return; }, scope : this } }); };
第四步:创建AJAX请求,从服务器中获取菜单数据
/** * 创建AJAX请求,从服务器请求菜单数据生成 accordion和Tree 菜单 */ Ext.Ajax.request({ url : ctx+"/queryMenu", success : function(response) { var json = Ext.JSON.decode(response.responseText); var rows=json["rows"]; for(var index in rows) { var panel = Ext.create('Ext.panel.Panel', { title : rows[index].text, layout : 'fit' }); var menus=rows[index].menus; if(menus) { panel.add(buildTree(menus)); } leftPanel.add(panel); } }, failure : function(request) { Ext.MessageBox.show( { title : '操作提示', msg : "连接服务器失败", buttons : Ext.MessageBox.OK, icon : Ext.MessageBox.ERROR }); }, method : 'post' });
客户端的完整代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <jsp:include page="include/Ext4Lib.jsp"></jsp:include> <title>功能菜单</title> <script type="text/javascript"> Ext.onReady(function() { var ctx="${pageContext.request.contextPath}"; /** *定义右侧面版 */ Ext.define('mainTabPanel', { extend: 'Ext.tab.Panel', //重写页面加载方法,在该方法中,定义一个iframe,用来装载JSP页面 loadPage:function(url,id,title,icon,reload){ var tab = this.getComponent(id); debugger; if(tab){ this.setActiveTab(tab); }else { var p = this.add(new Ext.panel.Panel({ id:id, title:title, closable:true, icon:icon, html:'<iframe src="' + url + '"width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>' })); this.setActiveTab(p); } } }); /** *创建顶部面板 */ var topPanel = Ext.create('Ext.panel.Panel', { region : 'north', height : 55 }); /** *定义顶左侧面板 */ var leftPanel = Ext.create('Ext.panel.Panel', { region : 'west', title : '导航栏', width : 230, layout : 'accordion', split:true, collapsible : true//是否可以折叠收缩 }); /** *创建中间面板 */ var centerPanel = Ext.create('mainTabPanel', { region : 'center', layout : 'fit', tabWidth : 120, items : [{ title : '首页' }] }); /** * 创建AJAX请求,从服务器请求菜单数据生成 accordion和Tree 菜单 */ Ext.Ajax.request({ url : ctx+"/queryMenu", success : function(response) { var json = Ext.JSON.decode(response.responseText); var rows=json["rows"]; for(var index in rows) { var panel = Ext.create('Ext.panel.Panel', { title : rows[index].text, layout : 'fit' }); var menus=rows[index].menus; if(menus) { //为accordion添加树菜单 panel.add(buildTree(menus)); } leftPanel.add(panel); } }, failure : function(request) { Ext.MessageBox.show( { title : '操作提示', msg : "连接服务器失败", buttons : Ext.MessageBox.OK, icon : Ext.MessageBox.ERROR }); }, method : 'post' }); //定义一个菜单的MODEL,用来保存对应的JSON数据 Ext.define('Menu', { extend : 'Ext.data.Model', fields : [ { name : 'id', type : 'string' }, { name : 'text', type : 'string' }, { name : 'url', type : 'string' } ] }); /** * 生成treepanel */ var buildTree = function(json) { //创建一颗树了 return Ext.create('Ext.tree.Panel', { useArrows : true, rootVisible : false, border : false, store : Ext.create('Ext.data.TreeStore', { model : 'Menu', //使用AJAX动态装载Tree proxy: { type: 'ajax', url: ctx+'/queryChildMenu' }, root : { expanded : true, children : json } }), listeners : { //节点单击事件 'itemclick' : function(view, record, item,index, e) { var id = record.get('id'); var text = record.get('text'); var url = record.get('url'); var leaf = record.get('leaf'); if (leaf) { centerPanel.loadPage(url,'menu' + id, text); } }, //单击节点展开之前的事件 'beforeitemclick':function(view, record, item,index, e) { return; }, scope : this } }); }; /** * 创建视图 */ Ext.create('Ext.container.Viewport', { layout : 'border', renderTo : Ext.getBody(), items : [ topPanel, leftPanel, centerPanel ] }); }); </script> </head> <body></body> </html>
第五步:服务端代码
我们在Struts2的Action中添加两个方法,第一个方法用来加载accordion菜单和Tree的根菜单所需的数据
@Action(value="queryMenu",results = { @Result(type = "json",params={"root","pageBean"})}) public String queryMenu() { menuPanels=menuPanelService.queryMenuPanel(); this.pageBean.setRows(menuPanels); this.pageBean.setTotal(menuPanels.size());//不使用分页 return "success"; }
第二个方法用来查询Tree菜单下的子菜单
/** * 查询子菜单事件 * @return */ @Action(value="queryChildMenu",results = { @Result(type = "json",params={"root","menus"})}) public String queryChildMenu() { menus=menuPanelService.queryChildMenu(node); return "success"; }
完整的Action代码
package com.mcs.user.action; import java.util.List; import javax.annotation.Resource; import org.apache.struts2.convention.annotation.Action; import org.apache.struts2.convention.annotation.Result; import com.mcs.core.action.BaseAction; import com.mcs.user.pojo.Menu; import com.mcs.user.pojo.MenuPanel; import com.mcs.user.service.MenuPanelService; public class UserAction extends BaseAction<MenuPanel>{ @Resource private MenuPanelService menuPanelService; private boolean ignoreHierarchy=false; private List<MenuPanel> menuPanels; private List<Menu> menus; private String node; @Action(value="queryUser",results={@Result(location="main.jsp")}) public String queryUser(){ return "success"; } private String userName; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName;// } @Action(value="queryMenu",results = { @Result(type = "json",params={"root","pageBean"})}) public String queryMenu() { menuPanels=menuPanelService.queryMenuPanel(); this.pageBean.setRows(menuPanels); this.pageBean.setTotal(menuPanels.size());//不使用分页 return "success"; } /** * 查询子菜单事件 * @return */ @Action(value="queryChildMenu",results = { @Result(type = "json",params={"root","menus"})}) public String queryChildMenu() { menus=menuPanelService.queryChildMenu(node); return "success"; } public List<MenuPanel> getMenuPanels() { return menuPanels; } public void setMenuPanels(List<MenuPanel> menuPanels) { this.menuPanels = menuPanels; } public boolean isIgnoreHierarchy() { return ignoreHierarchy; } public void setIgnoreHierarchy(boolean ignoreHierarchy) { this.ignoreHierarchy = ignoreHierarchy; } public String getNode() { return node; } public void setNode(String node) { this.node = node; } public List<Menu> getMenus() { return menus; } public void setMenus(List<Menu> menus) { this.menus = menus; } }
第五步:编写Server类
public List<MenuPanel> queryMenuPanel() { return menuPanelDao.queryMenuPanel(); } @Override public List<Menu> queryChildMenu(String id) { // TODO Auto-generated method stub return menuPanelDao.queryChildMenu(id); }
第六步:编写DAO类
/** * 查询MenuPanl,作为accordion菜单和Tree菜单的根节点 */ @Override public List<MenuPanel> queryMenuPanel() { // TODO Auto-generated method stub String hql="from MenuPanel"; Query query=super.getSession().createQuery(hql); return query.list(); } /** * 使用本地查询,查询父节点等于当前节点的菜单 */ @Override public List<Menu> queryChildMenu(String id) { // TODO Auto-generated method stub String hql="select id,text,url,leaf from menu where parentId=?"; SQLQuery query=super.getSession().createSQLQuery(hql); query.setString(0, id); List<Menu> nodes=new ArrayList<Menu>(); List<Object[]> list=query.list(); for (Object[] object : list) { Menu menu=new Menu(); menu.setId(object[0]==null?null:object[0].toString()); menu.setText(object[1]==null?null:object[1].toString()); menu.setUrl(object[2]==null?null:object[2].toString()); menu.setLeaf(object[3]==null?false:Boolean.parseBoolean(object[3].toString())); nodes.add(menu); } return nodes; }
第七步:POJO类
package com.mcs.user.pojo; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.OneToMany; import javax.persistence.Table; import com.mcs.pojo.base.GenericObject; /** * 这个是MenuPanel对应的是accordion菜单 * @author lishengbo * */ @Entity @Table(name = "MENUPANEL") public class MenuPanel extends GenericObject { private String text; private List<Menu> menus=new ArrayList<Menu>(); public MenuPanel() { } public MenuPanel(String text) { this.text = text; } public MenuPanel(long id,String text) { this.text = text; super.setId(id+""); } public String getText() { return text; } public void setText(String text) { this.text = text; } /** * 一对多关联Menu菜单,作为Tree中的根节点,这里使用立即加载和MenuPanel一起加载到客户端,注意,一定要使用立即加载 * @return */ @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER,mappedBy="menuPanel") public List<Menu> getMenus() { return menus; } public void setMenus(List<Menu> menus) { this.menus = menus; } } package com.mcs.user.pojo; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.Table; import com.mcs.pojo.base.GenericObject; @Entity @Table(name="MENU") public class Menu extends GenericObject{ private String text; private String url; private boolean leaf=true;//默认是叶子节点 private MenuPanel menuPanel; private List<Menu> children; private Menu menu; public Menu() { } public Menu(String text, String url) { super(); this.text = text; this.url = url; } public Menu(long id,String text, String url,MenuPanel menuPanel) { super(); super.setId(id+""); this.text = text; this.url = url; this.menuPanel=menuPanel; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @ManyToOne(cascade={CascadeType.PERSIST,CascadeType.REMOVE}, targetEntity=MenuPanel.class) @JoinColumn(name="menuPanelId",referencedColumnName="id",insertable=true,updatable=true) public MenuPanel getMenuPanel() { return menuPanel; } public void setMenuPanel(MenuPanel menuPanel) { this.menuPanel = menuPanel; } @Column(length=1000) public boolean isLeaf() { return leaf; } public void setLeaf(boolean leaf) { this.leaf = leaf; } @OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="menu") public List<Menu> getChildren() { return children; } public void setChildren(List<Menu> children) { this.children = children; } @ManyToOne(cascade={CascadeType.PERSIST,CascadeType.MERGE}, targetEntity=Menu.class) @JoinColumn(name="parentId",referencedColumnName="id",insertable=true,updatable=true) public Menu getMenu() { return menu; } public void setMenu(Menu menu) { this.menu = menu; } }
第九步:数据库中的数据展示
Menu表中的数据
MenuPanel表中的数据