1. 程式人生 > >Ext JS4序列教程之二 :非同步載入accordion和Tree選單

Ext JS4序列教程之二 :非同步載入accordion和Tree選單

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 SWINGC# 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的函式,第一個函式EXTAJAX請求函式,這個函式用來發送非同步請求,向伺服器請求資料,伺服器收到請求後將資料返回給客戶端,由EXT JS進行處理,函式如下:

  Ext.Ajax.request({….})

第二個函式,哦,對了,這裡我們不能稱之為函式,應該稱之為模型(Model),在JAVAC#裡,我們經常要定義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>

第五步:服務端程式碼

我們在Struts2Action中新增兩個方法,第一個方法用來載入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表中的資料