改造 Combo Select支援伺服器端模糊搜索

  • 2019 年 10 月 8 日
  • 筆記

項目中使用了 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);},