:表單樹形選擇與懶加載避坑指南)
簡介這是一份面向ExtJS前端開發(fā)者的自定義組件源碼核心是ComboTree下拉樹插件用于解決下拉框中展示層級數(shù)據(jù)的選擇需求如組織結(jié)構(gòu)、地區(qū)分類等場景。資源包共1個文件為js腳本壓縮包約2KB體量輕巧便于直接引入項目或?qū)φ臻喿x。插件通過繼承Ext.form.field.ComboBox并重寫onRender、onSelect、loadData等方法將傳統(tǒng)下拉列表擴(kuò)展為可展開折疊的樹形結(jié)構(gòu)涉及TreeStore與TreeModel的數(shù)據(jù)組織、XTemplate節(jié)點渲染、展開折疊事件處理以及默認(rèn)展開級別、多選策略等配置項與公共API設(shè)計。已有182人學(xué)習(xí)適合希望深入理解ExtJS組件系統(tǒng)、數(shù)據(jù)綁定機(jī)制與自定義組件擴(kuò)展思路的中高級開發(fā)者參考可幫助快速掌握樹形下拉選擇的實現(xiàn)要點并復(fù)用到企業(yè)級Web應(yīng)用中。1. 從一次表單卡頓說起這個 Ext 下拉樹插件到底能干什么去年幫一個做后臺管理系統(tǒng)的朋友看問題他們有個組織架構(gòu)選擇器用的是 ExtJS 的 ComboBox 嵌套 TreePanel 的方案數(shù)據(jù)量一上來點開下拉框要等兩三秒選完節(jié)點回填還偶爾丟值。翻代碼發(fā)現(xiàn)是每次展開都重新構(gòu)建樹、沒有做節(jié)點緩存父子節(jié)點聯(lián)動也是手寫的遞歸。這類需求其實很典型表單里要選一個樹形結(jié)構(gòu)中的節(jié)點比如部門、分類、區(qū)域、菜單權(quán)限用普通下拉框表達(dá)不了層級用獨立彈窗又太重。ComboTree 就是干這個的——把 Ext 的 Tree 塞進(jìn) ComboBox 的觸發(fā)框里點一下彈出樹選完自動回填顯示值。這份「Ext 下拉樹插件_ComboTree_xz」是一份可直接嵌入 ExtJS 項目的源碼工具包適合正在維護(hù) Ext 老系統(tǒng)、又不想重寫整個前端棧的開發(fā)者。它解決的不是炫技問題而是「表單里怎么優(yōu)雅地選一個樹節(jié)點」這個具體痛點。2. 拆開 ComboTree它繼承了什么、擴(kuò)展了什么2.1 從 Ext.form.field.ComboBox 到樹形選擇器ExtJS 原生的 ComboBox 支持 store、displayField、valueField 這套配置但它的下拉列表本質(zhì)是一個 BoundList渲染的是平鋪的 item。ComboTree 的思路不是重寫一個組件而是繼承 ComboBox把它的 picker 替換成 TreePanel。這樣做的好處是表單字段的 API 完全保留——getValue、setValue、reset、isValid、markInvalid 這些方法不用重新實現(xiàn)校驗邏輯、表單提交時的取值方式也不變。換句話說你在 FormPanel 里原來怎么用 combo換成 ComboTree 之后大部分代碼不用動。關(guān)鍵改動集中在三處。第一重寫 getPicker 或者用 createPicker 鉤子返回一個 Ext.tree.Panel 實例而不是默認(rèn)的 BoundList。第二處理 picker 的顯示與隱藏時機(jī)——ComboBox 默認(rèn)在 blur 時收起但樹節(jié)點展開、勾選這些交互會觸發(fā) blur需要額外判斷焦點是否真的離開了組件。第三選中節(jié)點后要把節(jié)點的顯示文本寫回輸入框同時把節(jié)點的 id 或自定義字段作為 value 存起來這一步通常通過監(jiān)聽 tree 的 itemclick 或 checkchange 事件完成。我一般會先確認(rèn)項目里 Ext 的版本因為 4.x、5.x、6.x 在 picker 的創(chuàng)建流程和事件名上有差異。這份源碼包如果沒標(biāo)注版本最穩(wěn)妥的做法是先在隔離頁面里跑一個最小實例確認(rèn)能彈出樹、能回填值再往業(yè)務(wù)表單里嵌。2.2 源碼包的結(jié)構(gòu)與關(guān)鍵文件拿到一個插件包我習(xí)慣先看目錄再動手。典型的 ComboTree 源碼包大致是這幾類文件文件/目錄作用關(guān)注點ComboTree.js組件主文件繼承關(guān)系、picker 創(chuàng)建邏輯ComboTree.css樣式下拉樹寬度、滾動條、層級縮進(jìn)examples/示例頁最快驗證能否跑通resources/圖標(biāo)、箭頭圖路徑是否要按項目調(diào)整README 或注釋頭用法說明配置項、事件、依賴版本主文件里我會重點看幾個地方extend指向的是哪個類alias注冊的 xtype 是什么決定了你在 items 里怎么寫initComponent里對 tree 的 store 是怎么接的以及有沒有對onBlur、collapse做特殊處理。這些決定了你能不能把它和自己的數(shù)據(jù)源、表單校驗順暢對接。提示如果源碼里用了Ext.create(Ext.tree.Panel, {...})這種硬編碼而你的項目做了按需加載或用了自定義的類加載器可能需要把依賴顯式聲明到requires里否則會出現(xiàn)「樹彈不出來但也不報錯」的玄學(xué)現(xiàn)象。2.3 最小可運行示例先跑通再談集成不要一上來就往業(yè)務(wù)代碼里塞。新建一個空白 HTML引入 Ext 的 css 和 js再引入 ComboTree 的源碼寫一個最小實例Ext.onReady(function () { // 構(gòu)造一個簡單的樹形 store真實項目里通常換成 TreeStore proxy var treeStore Ext.create(Ext.data.TreeStore, { root: { expanded: true, children: [ { id: dept-1, text: 技術(shù)中心, leaf: false, children: [ { id: dept-1-1, text: 前端組, leaf: true }, { id: dept-1-2, text: 后端組, leaf: true } ]}, { id: dept-2, text: 產(chǎn)品中心, leaf: false, children: [ { id: dept-2-1, text: 設(shè)計組, leaf: true } ]} ] } }); Ext.create(Ext.form.Panel, { renderTo: Ext.getBody(), width: 400, bodyPadding: 20, items: [{ // xtype 以源碼里 alias 注冊的為準(zhǔn)常見為 combotree xtype: combotree, fieldLabel: 所屬部門, name: deptId, displayField: text, valueField: id, store: treeStore, // 只允許選葉子節(jié)點避免選到中間層級導(dǎo)致業(yè)務(wù)歧義 allowOnlyLeaf: true, editable: false, queryMode: local }] }); });這段代碼的邏輯說明treeStore用靜態(tài)數(shù)據(jù)模擬后端返回的層級結(jié)構(gòu)真實場景換成帶proxy的 TreeStore 即可。xtype是插件注冊的別名如果跑起來報「無法識別的 xtype」說明 alias 不是這個值去主文件里搜a(bǔ)lias確認(rèn)。allowOnlyLeaf是這類插件常見的配置項用來限制只能選葉子節(jié)點——很多業(yè)務(wù)里選一個中間部門是沒有意義的這個參數(shù)能省掉一層校驗。editable: false防止用戶手輸不存在的節(jié)點文本queryMode: local表示數(shù)據(jù)已在本地不需要遠(yuǎn)程過濾。參數(shù)怎么改displayField和valueField必須和你的樹節(jié)點數(shù)據(jù)字段對應(yīng)后端返回name/code就改成name/code。如果樹數(shù)據(jù)量大queryMode保持 local 但要做懶加載把leaf: false的節(jié)點配上children為空讓 TreeStore 在展開時再去請求。3. 接進(jìn)真實表單數(shù)據(jù)源、回填與校驗怎么處理3.1 異步 TreeStore 與節(jié)點懶加載靜態(tài)數(shù)據(jù)跑通之后下一步是把 store 換成異步的。Ext 的 TreeStore 支持proxy配置展開未加載的節(jié)點時自動發(fā)請求。常見做法是var asyncTreeStore Ext.create(Ext.data.TreeStore, { proxy: { type: ajax, url: /api/dept/tree, reader: { type: json, // 后端返回的根節(jié)點數(shù)組字段名按實際接口調(diào)整 rootProperty: data } }, // 根節(jié)點默認(rèn)展開觸發(fā)第一次請求 root: { id: root, text: 全部部門, expanded: true }, // 關(guān)鍵讓未加載的節(jié)點顯示為可展開點擊時才請求子節(jié)點 lazyFill: true });邏輯說明lazyFill: true是懶加載的核心它讓沒有children的節(jié)點仍然顯示展開箭頭用戶點擊時才通過 proxy 拉取子節(jié)點。rootProperty要和后端返回結(jié)構(gòu)對齊如果后端直接返回數(shù)組就寫rootProperty: 或者去掉 reader 配置。參數(shù)上url通常需要帶上父節(jié)點 idExt 默認(rèn)會把節(jié)點 id 作為參數(shù)傳過去后端按node參數(shù)接收即可。這里有個容易翻車的地方如果后端一次性返回整棵樹就不要開懶加載否則會出現(xiàn)「展開后子節(jié)點重復(fù)」或者「明明有數(shù)據(jù)卻顯示為空」。判斷標(biāo)準(zhǔn)很簡單——接口是否支持按父節(jié)點查詢。支持就懶加載不支持就一次性加載后本地展開。3.2 選中回填與表單取值選中節(jié)點后插件要把文本寫進(jìn)輸入框、把值存進(jìn)表單字段。大部分 ComboTree 實現(xiàn)會監(jiān)聽樹的itemclick事件然后調(diào)用setValue。但實際項目里經(jīng)常遇到兩個問題一是編輯態(tài)回填時只給了 value 沒給文本輸入框顯示的是 id 而不是名稱二是多選場景下值的拼接格式不對。編輯態(tài)回填的常見做法是在表單加載數(shù)據(jù)后手動根據(jù) value 去樹里找對應(yīng)節(jié)點把文本補(bǔ)上。如果樹是懶加載的節(jié)點可能還沒加載出來這時候要么讓后端在詳情接口里直接返回節(jié)點名稱要么在 setValue 之前先展開到目標(biāo)節(jié)點。我一般傾向于讓后端返回名稱前端少做一次樹遍歷穩(wěn)定得多。取值方面單選時getValue()返回節(jié)點 id多選時返回數(shù)組或逗號拼接的字符串取決于插件實現(xiàn)。提交前最好在表單的beforeaction或者提交回調(diào)里打印一次getValue()的結(jié)果確認(rèn)格式和接口約定一致。這個動作花不了兩分鐘但能省掉后端聯(lián)調(diào)時來回扯皮的時間。3.3 表單校驗與只讀態(tài)處理ComboTree 繼承了 ComboBox所以allowBlank: false、vtype這些校驗配置理論上都能用。但要注意如果用戶展開了樹但沒點任何節(jié)點就收起字段值仍然是空的校驗會正常攔截。真正容易出問題的是「值有但文本為空」的狀態(tài)——比如通過setValue只設(shè)了 id輸入框看起來是空的用戶以為沒選實際上表單提交時又帶上了值。這種不一致會讓測試同學(xué)提 bug。處理辦法是在setValue之后同步設(shè)置原始值setRawValue或者重寫setValue讓它在設(shè)值的同時去 store 里查文本。只讀態(tài)則通過readOnly: true控制但要注意只讀時下拉箭頭是否還允許點擊——有些實現(xiàn)只禁用了輸入框箭頭還能彈出樹這在「查看詳情」頁面里是不合適的需要額外配置hideTrigger或者禁用整個組件。注意如果表單里有多個 ComboTree且它們共用同一個 TreeStore 實例選中一個節(jié)點可能會影響另一個的展開狀態(tài)。穩(wěn)妥做法是每個字段獨立創(chuàng)建 store或者確認(rèn)插件內(nèi)部對 store 做了隔離。4. 避坑與排查那些讓我加班到深夜的細(xì)節(jié)4.1 下拉樹彈不出來控制臺也不報錯現(xiàn)象點擊下拉箭頭沒反應(yīng)或者樹閃一下就消失控制臺干凈得像什么都沒發(fā)生。原因最常見的是 picker 創(chuàng)建后被立即隱藏。ComboBox 的 blur 邏輯和樹的點擊事件沖突點擊樹節(jié)點時組件先觸發(fā)了 blur把 picker 收起來了。另一個可能是 z-index 被頁面其他浮層蓋住視覺上「沒彈出來」。解決在主文件里找onBlur或collapse相關(guān)的處理看有沒有對焦點做延遲判斷。如果是 z-index 問題給 picker 加一個足夠大的zIndex配置或者在 css 里提高.x-tree-panel的層級。我一般會先在瀏覽器里選中 picker 的 dom 節(jié)點看它的 display 和 z-index 實際值比盲猜快。4.2 編輯態(tài)回填只顯示 id 不顯示名稱現(xiàn)象新增沒問題編輯時輸入框里是一串?dāng)?shù)字或編碼不是節(jié)點名稱。原因setValue只設(shè)置了 valueField 對應(yīng)的值沒有同步 rawValue。樹節(jié)點在懶加載模式下可能還沒加載到目標(biāo)節(jié)點插件找不到對應(yīng)文本。解決優(yōu)先讓后端詳情接口返回節(jié)點名稱前端在表單賦值時同時調(diào)用setValue(id)和setRawValue(name)。如果只能拿到 id就在 store 的load事件里做一次查找找到后補(bǔ)設(shè) rawValue。懶加載場景下可以先用一個臨時文本占位等節(jié)點加載后再更新。4.3 多選時值格式與后端對不上現(xiàn)象前端多選了三項提交上去后端只收到一項或者收到一個帶方括號的字符串。原因不同 ComboTree 實現(xiàn)對多選值的處理不一樣有的返回數(shù)組有的返回逗號拼接字符串有的在getSubmitValue里做了額外處理。解決在表單提交前打印getValue()和getSubmitValue()兩個結(jié)果確認(rèn)實際格式。如果和接口約定不符重寫getSubmitValue做轉(zhuǎn)換。別在提交后才去抓包猜前端能打印的就在前端解決。4.4 樹節(jié)點展開后重復(fù)加載或數(shù)據(jù)錯亂現(xiàn)象同一個節(jié)點展開兩次子節(jié)點出現(xiàn)兩份或者展開 A 節(jié)點卻顯示了 B 節(jié)點的子級。原因懶加載時 proxy 的請求參數(shù)沒有正確攜帶父節(jié)點標(biāo)識或者后端緩存了錯誤的響應(yīng)。也有可能是 store 被多個組件共用加載狀態(tài)互相干擾。解決檢查請求 URL 里是否帶了node參數(shù)后端是否按這個參數(shù)過濾。store 共用問題就拆成獨立實例。如果后端返回的數(shù)據(jù)里id有重復(fù)也會導(dǎo)致樹結(jié)構(gòu)錯亂這個要在接口層排查。4.5 樣式錯位下拉樹寬度和輸入框?qū)Σ积R現(xiàn)象樹彈出來后比輸入框?qū)捄芏嗷蛘芏酀L動條位置奇怪層級縮進(jìn)看不清楚。原因picker 的寬度默認(rèn)可能取了 body 寬度或者固定值沒有跟隨組件寬度。css 文件沒引入或者被項目樣式覆蓋。解決給組件配置matchFieldWidth: true如果插件支持或者在 css 里顯式設(shè)置 picker 的min-width和max-width。層級縮進(jìn)靠的是 tree 的indent配置默認(rèn)值在不同 Ext 版本里不一樣按視覺需要調(diào)整。5. 進(jìn)階把 ComboTree 用得更穩(wěn)的幾個習(xí)慣5.1 封裝一層業(yè)務(wù)組件別在頁面里裸用直接在頁面 items 里寫xtype: combotree能跑但項目里一旦有十幾個地方用到配置就會散得到處都是。我的習(xí)慣是再包一層把常用的配置固化下來Ext.define(App.form.field.DeptSelector, { extend: Ext.form.field.ComboBox, // 實際應(yīng)繼承 ComboTree 的類 alias: widget.deptselector, // 固定業(yè)務(wù)語義只選葉子、不可編輯、必填 allowOnlyLeaf: true, editable: false, allowBlank: false, fieldLabel: 所屬部門, // 統(tǒng)一在這里創(chuàng)建獨立的 TreeStore避免共用 initComponent: function () { this.store Ext.create(Ext.data.TreeStore, { proxy: { type: ajax, url: /api/dept/tree }, root: { id: root, text: 全部部門, expanded: true }, lazyFill: true }); this.callParent(arguments); } });這樣頁面里只寫xtype: deptselector字段名、校驗、數(shù)據(jù)源都在一處維護(hù)。改接口地址或者調(diào)整交互時只動一個文件。參數(shù)上allowBlank按業(yè)務(wù)定如果部門是必填就設(shè) falsefieldLabel可以暴露成配置項讓調(diào)用方覆蓋。5.2 用最小用例做回歸別靠記憶Ext 這類老框架的插件最怕的是「上次能用這次不能用」。我的做法是在項目里留一個dev/combotree-test.html里面放三個用例靜態(tài)數(shù)據(jù)單選、異步懶加載單選、多選回填。每次升級 Ext 版本或者改動插件源碼后先跑這三個用例通過了再往業(yè)務(wù)里合。這個頁面不參與打包純粹是開發(fā)期的后悔藥。5.3 版本與依賴的確認(rèn)清單在把這份源碼包引入項目之前我會過一遍這幾個點Ext 版本是 4 還是 5/6插件的繼承鏈?zhǔn)欠衿ヅ漤椖渴欠裼昧?Sencha Cmd 或自定義加載器需不需要在requires里聲明依賴css 文件是否已引入路徑對不對示例頁里的 xtype 和配置項是否和主文件一致。這幾項確認(rèn)完基本能排除八成「跑不起來」的問題。從那以后我每次拿到這類 Ext 插件包都強(qiáng)制先跑最小用例、再確認(rèn)版本依賴、最后才動業(yè)務(wù)代碼。希望幫到你。本文還有配套的精品資源點擊獲取