使用combo select完善原始select的功能,當碰到大數據量時,反應很慢,因為數據是一次性載入。 嘗試修改控制項的數據載入方案,變更為伺服器端模糊搜索,降低數據量,降低頁面響應時間。 ...
項目中使用了 combo select,為預設的select增加模糊搜索的功能,一直運行得很好。
1 碰到的問題
但最近碰到一個大數據量的select:初始化載入的數據項有2000多個。我們採用的是ajax讀取所有的option json,並由js在瀏覽器中遍歷並最終生成完整的html。當數據量變大的時候,ajax讀取數據和瀏覽器處理數據都會有比較明顯的損耗,頁面初始化時需要較長時間,降低了用戶友好度。
2 備選解決方案
大家簡單分析了一下這個問題,想到了三種可能的解決方案。
2.1 修改數據結構
目前的同級數據有2000多條,數據從邏輯上可以拆分為兩級結構。這樣,將數據拆分為兩級結構後,使用兩個聯動Select,能大大減少每個select載入的option數量。
2.2 使用redis緩存數據
因為採用分散式部署,這些數據實際上經過了多次伺服器之間的傳輸。數據量大,每一級傳輸耗時增加,導致最終的耗時難於接受。
可以在api server端利用nosql對數據進行緩存,能在一定程度上降低耗時。
2.3 修改combo select插件
從前端入手,select只顯示少量數據,當用戶輸入關鍵字進行搜索時,實時從伺服器載入。這種方式增加了調用次數,但可以大大降低數據量,縮短頁面載入的耗時。
三種方案,都能在一定程度上解決問題。我們決定先從combo select插件嘗試,如果達不到效果,再考慮redis緩存或修改數據結構的方案。
3 Combo Select代碼分析
網址 https://github.com/PebbleRoad/combo-select ,感謝提供如此優秀的一個插件。
3.1 基本用法
首先在頁面中構建一個select,並初始化option數據,然後調用腳本
$("#selectId").comboSelect(); |
其他更複雜的功能,請自行前往官網學習。
3.2 html結構
Combo Select在執行時,在原 select 外層套了一個 <div class=”combo-select”>,然後在select後面添加了三個element。
div.combo-arrow,是下拉箭頭
ul.combo-dropdown是用來顯示的下拉列表
input.combo-input 是用來輸入模糊搜索內容的輸入框
並通過修改原 select 的屬性,隱藏掉。
3.3 js數據模型
combo select初始化時,經過一系列代碼,最終構造幾個屬性:
$container : 生成一個新的div,將原來的select和新生成的ul等都放在其中。
$el : 初始的select element
$options : 所有的option 數據
$dropdown : 生成的 ul.combo-dropdown 對象
$items : 所有的options轉成 li 格式後的數據。
下圖是數據模型和html元素之間的對應關係。
3.4 插件初始化
在js插件的代碼function Plugin ( element, options )會完成插件的初始化,根據select當前的數據,完成html元素的調整,以及js數據模型的初始化。初始化流程如下
3.5 模糊查詢的邏輯
當用戶在input中輸入文字的時候,會觸發 keydown和keyup事件,在keyup事件中,對 $items中的數據依次進行匹配,設置 visible屬性,實現部分數據的展示。
在這個過程中,原始的select($el)及其所有的options($options)沒有變化,下拉列表的變化,主要是將ul.li($items)設置為可見或不可見。
4 修改為Server端實時查詢方案
整個修改方案,分別從Server API、js組件、前端調用三方面解決。
4.1 Server API 修改
Server端需要提供根據名稱進行模糊搜索的介面。不贅述,需要註意的是返回數據要設置最大條數。避免根據查詢條件返回了大量的數據,就失去瞭解決的優勢。
限制最大條數後,需要跟產品介紹清楚這個實現邏輯,如果用戶輸入的關鍵字區分度不大時,可能無法查到真正需要的數據;此時需要用戶輸入更具有區分度的關鍵字。
4.2 ComboSelect組件修改
4.2.1 修改方案
修改keyup事件時的邏輯:原來是分別設置ul.li是否可見,修改為重新載入select的所有options,並根據options重建$items,並設置為所有ul.li都是可見的。
4.2.2 為組件新增幾個參數
entity: 'entity', itemName: 'itemName', curItemField: 'curItemCode', curItemValue: '', curItemName: '', url: '', limit: 7 |
- entity: 當前處理的數據類型,這是為了適應不同api返回的json定義的差異。更好的辦法是要求所有數據類型使用相同的屬性名;變通的方案就是增加這個entity,在js上做差異化處理。這樣就減少了改造的通用性。
- itemName: 調用api時需要的用戶輸入值的參數名
- curItemField:在html中,item的input名稱
- curItemValue: 當前已選中數據的value
- curItemName: 當前已選中數據的title
- limit: 伺服器api模糊搜索返回值的分頁大小
4.2.3 修改 _filter() 方法實現伺服器端模糊查詢
修改了原組件的這個方法,判斷是否設置了伺服器端刷新的url。如果沒設置,沿用原來的邏輯;如果設置了,根據用戶輸入進行模糊查詢,並重新生成瀏覽器中被隱藏的select的所有options,並更新到$dropdown中。
if(self.settings.url != ''){ // 準備調用api需要的json數據 var self = this; var ajaxData = { "paging": true, "offset": 0, "limit": self.settings.limit }; if(self.settings.itemName != '' && needle != ''){ ajaxData[self.settings.itemName] = needle; } if(self.settings.curItemField != '' && self.settings.curItemValue != ''){ ajaxData[self.settings.curItemField] = self.settings.curItemValue; }
// 從伺服器查詢數據 $.ajax({ url : self.settings.url, type : 'post', data: ajaxData, success : function(data) { var obj = $.parseJSON(data);
// 先刪掉select原來的數據,並遍歷查詢結果生成option添加到select中 var dropdownHtml = '', k = 0, p = ''; self.$el.empty(); self.$el.append("<option value=''>請選擇</option>");
var confirmedValue; self.$dropdown.html("<li class='option-item' data-index='0' data-value=''>請選擇</li>"); for (var i = 0; i < obj.length; i++) { var itemCode; var itemName; var itemExtraCode; if(self.settings.entity == 'entity'){ itemCode = obj[i].entityCode; itemName = obj[i].entityName; itemExtraCode = obj[i].entityShortName; }else{ itemCode = obj[i].itemCode; itemName = obj[i].itemName; itemExtraCode = itemCode; }
// 生成select option var oneOption = $("<option></option>"); $(oneOption).val(itemCode); $(oneOption).html(itemName); if(itemCode == self.settings.curItemValue || itemExtraCode == self.settings.curItemValue || itemName == self.settings.curItemName){ $(oneOption).attr("selected", "selected"); self.settings.curItemValue = itemCode; confirmedValue = itemCode; } self.$el.append(oneOption);
if(confirmedValue != undefined && confirmedValue != ''){ self.$el.val(confirmedValue); }
// 生成$dropdown 中的li var oneItem = $("<li></li>"); $(oneItem).attr("class",this.disabled? self.settings.disabledClass : "option-item"); $(oneItem).attr("data-index", (i+1)); $(oneItem).attr("data-value", itemCode); $(oneItem).html(itemName);
self.$dropdown.append(oneItem); }
// 為$items 重新賦值 self.$items = self.$dropdown.children();
// 觸發後續的open方法 self.$container.trigger('comboselect:open') } }); } |
4.2.4 修改 init() 實現首次載入
代碼類似_filter()。應該要獨立出一個方法來在兩個方法中調用,沒做。
init: function () { var self = this;
if(self.settings.url != ''){ //動態刷新代碼 ... ... }else{ // 組件的原始邏輯 self._construct(); self._events(); } }, |
4.3 前端調用
4.3.1 html代碼中添加參數
使用 comboselect- 首碼,如
<select class="list-filedV" id="entityCode" name="entityCode" comboselect-entity="entity" onchange="getBranch('')"> </select> <input type='hidden' name='entityName' id='entityName'> |
4.3.2 js生成沒有預設值的combobox
在js代碼中完成初始化,代碼
//獲取數據 function getEntityData(){ $("#entityCode").comboSelect({ "itemName": "entityName", "url": contextPath+"/new/dictionary/searchEntityData.ajax", "limit": 7 }); } |
4.3.3 js生成有預設值的combobox
在編輯界面比較常見
//獲取數據_修改 function getEntityDataUp(curEntityCode, curEntityName){ $("#entityCode").comboSelect({ "itemName": "entityName", "curItemField": "includeEntityCode", "curItemValue": curEntityCode, "curItemName": curEntityName, "url": contextPath+"/new/dictionary/searchEntityData.ajax", "limit": 7 }); } |
5 降低調用伺服器頻度
註意看_keyup 的代碼,每次按鍵(不包括該函數忽略的特殊字元),每次都會刷新數據。如果是在瀏覽器內部進行數據過濾,問題還不明顯。但每次模糊查詢都通過伺服器查詢,就會帶來大量的api訪問。
5.1 修改方案
在_keyup()中,調用_delayFilter(),由它觸發前面修改後的 _filter()方法。
5.2 代碼 _delayFilter()
this.filterTimer = 0;
_delayFilter: function(search){ if(this.filterTimer > 0){ clearTimeout(this.filterTimer); this.filterTimer = 0; } var self = this; this.filterTimer = setTimeout(function(){ self._filter(search); }, 500); }, |