主題
之前寫過一篇文章分享了公司basecode查找數據后台是怎么實現的(http://www.cnblogs.com/abcwt112/p/6085335.html).今天想分享一下公司前台是怎么擴展jquery miniui組件去配合后台實現自動查找數據的.
一個使用例子
項目中多多少少會遇到要下拉一些數據(不只是下拉,可能還有選擇框等等).
這些數據來自數據庫里的某張代碼表(當然也可以是復雜的SQL查詢).這些數據寫死在前台明顯是不可能的,因為不同環境下配置的數據可能不同.但是為了每個下拉框去寫一個Controller方法,Service方法.Dao方法...好像代價又太大了,而且他們具有一定的同性.這就是Basecode的存在的一個原因.
前台實現
后台的實現請參考我另外一篇文章,這篇文章主要分享下怎么擴展jquery miniui組件(miniui是類似於jquery easyui的一個前台JS框架)來實現前台自動發送請求加載數據實現basecode的下拉.
jquery miniui不是開源的,所以自己寫組件繼承起來比較蛋疼...但是多多少少還是可以做一些簡單的擴展的.
一個最簡單的代碼表下拉的例子
實現basecode自動下拉的主要原理是利用了beforeload事件.
我們自己定義的組件繼承了原本miniui的組件,重寫了部分方法,在組件在初始化的時候額外加入了beforeload事件並觸發,這樣這個組件就會自動去后台查找數據了.
1 mini.extend(framework.component.ComboList , mini.ComboBox , { 2 3 codeType : "", //綁定代碼類型 4 5 remote : false, //是否從遠端加載數據 6 7 textField : "label", 8 9 uiCls: "mini-ext_combolist", 10 11 valueField : "value", 12 13 queryparam : {}, 14 15 /** 16 * 重定義加載數據的方法,根據codeType指定的加載數據 17 */ 18 load : function(data){ 19 if(this.codeType && this.codeType.length > 0){ 20 if(basecode[this.codeType] && !this.remote){ 21 this.setData(basecode[this.codeType]); 22 }else{ 23 this.setUrl(common.getBascodeUrl()+"/"+this.codeType); 24 } 25 } 26 }, 27 28 set : function(kv){ 29 var me = this; 30 framework.component.ComboList.superclass.set.call(this,kv); 31 me.on('beforeload',function(e){ 32 e.type = 'POST'; 33 e.dataType = 'json'; 34 e.contentType = 'application/json'; 35 e.data = mini.encode(this.queryparam); 36 }); 37 this.load({}); 38 39 }, 40 41 setCodeType: function (codeType) { 42 this.codeType = codeType; 43 }, 44 45 getCodeType : function(){ 46 return this.codeType ; 47 }, 48 getAttrs: function (el) { 49 50 var attrs = framework.component.ComboList.superclass.getAttrs.call(this, el); 51 mini._ParseString(el, attrs , ['codeType']); 52 mini._ParseString(el, attrs , ['param']); 53 var param = attrs['param']; 54 if(param){ 55 var cotrolids = param.match(common.PARAM_CMP_VALUE_REGEX); 56 if(cotrolids && cotrolids.length > 0){ 57 $.each(param.match(common.PARAM_CMP_VALUE_REGEX),function(i,item){ 58 var id = '#'+item.replace(/#/gi,''); 59 var tempobj = mini.get(id); 60 if(tempobj && tempobj.value){ 61 param = param.replace(item , tempobj.getValue()); 62 } 63 }); 64 } 65 this.queryparam = mini.decode(param) != 'undefined' ? mini.decode(param) : ""; 66 } 67 attrs['textField'] = attrs['textField'] || 'label'; 68 attrs['valueField'] = attrs['valueField'] || 'value'; 69 mini._ParseBool(el, attrs,['remote']); 70 return attrs; 71 72 } 73 74 }); 75 76 mini.regClass(framework.component.ComboList , 'ext_combolist');
完整的一個擴展的組件代碼如上面.
當組件被渲染的時候:
第一個被調用的方法是L48的getAttrs方法,這個方法的用處就是把你頁面上HTML標簽比如input里面的各種屬性解析成JS對象里面的屬性.
比如你<input codeType="ENUM_SEX">那codeType就會被解析成JS對象的一個屬性,{codeType:"ENUM_SEX"}.當然.不同框架的解析方法肯定不同,所以這里並不需要仔細研究(而且也沒有源碼).
最主要的屬性是codeType,當然param這些也有用處(param里的key和value相當於是查詢SQL where里面的條件).但是最簡單的例子里不需要用到這些額外的參數,只要知道codeType就能知道basecode里去查哪張表了(詳見另外一篇文章)
第二個被調用的方法是L28 set方法:set的作用就是把之前getAttrs取出來的JS對象的各種屬性設置到miniui組件的屬性中去.所以getAttrs返回的值就是set方法的入參.
在這里除了調用父組件的set方法完成基本的set之外,額外做了一個操作,就是最核心的beforeload事件.為自定的組件綁定了beforeload事件.
beforeload事件里主要是定義了ajax請求的一些參數.比如dataType是json等等..還有一個比較重要的就是ajax傳遞的參數data是來自組件的queryparam屬性(所以組件要傳遞的參數應該放到這個屬性里).
第三個調用的當然是L18 load方法.它是在set方法最后一行被調用的
load方法里因為我們設置了codeType又設置了remote為true,所以會走else里面的代碼.即從遠程服務器端加載數據.
當我們調用setUrl方法以后miniui組件去發起get請求查找basecode的Controller的數據.后續后台流程請參考另外一篇文章.
至此前台請求發送完畢,把返回的數據根據textField和valueField顯示到頁面上即可.
當組件查詢的值需要依賴於其他組件的時候(多級聯動)
多級聯動的情況在現實中還是比較常見的,公司本身擴展的組件也不是支持的很好,旁邊小哥也自己擴展了一個,仍然有一些BUG.然后我又在他們的基礎上修改了一下.(大部分時候開發還是自己去寫valuechange事件,而不是自動多級聯動)
多級聯動和一般的查代碼表的區別在於,多級聯動的時候你后面的下拉框的查詢的值要依賴於前面的值.
比如上面3個組件.大類A,種類B,小類C.
b能下拉出什么值依賴於a,c依賴於b.
所以我們在組件里要有個屬性記錄當前組件依賴的組件.
比如組件b的html標簽:
<input baseValue="#a組件的id#" ........>
getAttrs: function (el) { .......... this.baseValue = attrs['baseValue']; ............... }
我們用的是baseValue.
當然在getAttrs里直接set屬性是不好的,應該在set那個方法里去設置.不過這樣做也沒啥問題(反正也能用).
this.on('beforeload',function(e){
............... var tmpBaseValue = this.baseValue; ............各種加工tmpBasevalue this.queryParam = {}; this.queryParam.parent = (e.params && e.params['value']) || tmpBaseValue; if(this.queryParam.initParam != undefined){ this.queryParam.initParam = (e.params && this.queryParam.initParam) ? null : this.queryParam.initParam; } var param = {}; $.extend(true, param,this.queryParam );
................
}
然后在beforeload事件里把baseValue對應的id的miniui組件的getValue值合並到param里(中間處理邏輯被我省略了..因為不同框架處理邏輯肯定不同,這里也沒啥通用性).最后會發送給后台.這樣在查詢組件b的basecode的值的時候還會附帶上組件a的值作為查詢條件之一.
多級聯動還有個小特點就是你選了a,最好要把b的值清空.
這里是用blur事件來做的.
var id = '#'+this.baseValue.replace(/#/gi,''); var tempobj = mini.get(id); if(tempobj){ tempobj.on("blur",function(){ if(!tempobj.oldValue || (tempobj.oldValue && tempobj.getValue() != tempobj.oldValue)){ tempobj.oldValue = tempobj.getValue(); me.setData([]); me.setValue(""); me.setUrl(me.getUrl()); } }); }
根據baseValue找到依賴的組件,然后為組件綁定blur事件,當依賴組件的焦點丟失的時候(這個時候肯定是操作過組件,並且新選擇的值和原本的值不同),觸發blur事件,更新當前組件的值.
不過這里只更新了當前組件,並沒有修改依賴於當前組件的組件的值,算是有點瑕疵吧.
樹組件
樹組件是最復雜的一個組件了吧..不過我們這里用到的地方也不多,即使用到大部分時候也就CV一下別人寫好的就OK了.不過學習的話還是有些地方值得研究研究的.
樹組件復雜是因為有懶加載的情況,第一次取數據和后續取數據的SQL會有很大不同,比如第一次查數據的時候你的where條件里需要條件1,2,3....第二次加載子節點的時候你的where條件可能變成了只需要條件1加上父節點的代碼值.所以where條件變化比較大,和一般的下拉框不同.
對於后台來說(詳見另外一篇文章)就是filterSQL里依賴的參數Map不同.需要根據傳過來的filterParam或者initParam來拼不同SQL.
對於前台來說就是怎么根據不同的情況組織不同的數據(主要是initParam怎么區分是第一次加載還是后續加載,因為initParam在第一次加載和后續加載的時候會傳不同的參數,而filterParam則每次都會傳一樣的參數).
this.on('beforeload',function(e){ ................. this.queryParam.parent = (e.params && e.params['value']) || tmpBaseValue; if(this.queryParam.initParam != undefined){ this.queryParam.initParam = (e.params && this.queryParam.initParam) ? null : this.queryParam.initParam; }
................
}
忽略其他的代碼,從這4行代碼中可以看出,this.queryParam.parent取什么值是根據e.param來的.e.param到底什么時候會有值呢? 實踐中發現是當你點擊父節點加載子節點的時候才會有值(然而miniui的api中啥都沒提到.).
那么這段代碼就可以解釋為:當第一次load樹組件加載數據的時候,initParam是會有值的(自己寫在html里),而parent是沒值的,當后續懶加載點擊父節點加載子樹的時候initParam是null而parent是e.param也就是父節點的代碼值.
這樣就可以保證不同情況下樹組件可以向后台傳遞不同的值.
一個小例子
<input .... param="{'initParam':{'SCENARIO':'NONE','SWJG_DM_VALUE':['#swjg-zgsws#']} ,'filterParam':{'SCENARIO':'ASSIGNSWKS','GDSLX_DM_VALUE':${loginDTO.gdslxDm}}}"/>
第一次加載樹的時候請求的時候initParam有值
第二次加載子節點的時候請求initParam沒有值,附加了一個parent節點的值.
小問題
組件雖然好用,但是還是有一點小問題的:
1.當沒有使用filterParam,initParam的scenario的時候,html中的param是直接把key拼接到where里當做查詢條件,把value設置到JPA的Query的Parameter里的.所以查詢條件是有SQL注入的風險的.意思就是你如果構造了一個請求,傳遞的param是{1:"1"}那這樣拼接成的SQL就是select.....from...where 1=1 and 1=1. 如果使用filterParam,initParam的scenario則沒有問題,因為拼接的where條件是寫在數據庫里的.
2.拼接條件都是and連接的.如果你要復雜的條件,或者來個or連接就跪了...
小小結
雖然有一些不足的地方,但是公司封裝的這套basecode在大多數情況下還是非常好用的,而且讓我學到了不少知識:尤其是給我提供了一種自動加載數據的思路.