實例詳解AngularJS實作無限級聯式選單_AngularJS
多級連動選單是常見的前端組件,例如省份-城市連結、大學-學院-專業連結等等。場景雖然常見,但仔細分析起來要實現一個通用的無限分級連動選單卻不一定像想像的那麼簡單。例如,我們需要考慮子選單的載入是同步的還是非同步的?對於初始值的回填發生在前端還是後端?如果非同步加載,是否對於後端API的返回格式有嚴格的定義?是否容易實現同步、非同步共存?是否可以靈活的支援各類依賴關係?選單中是否有空值選項? ……一系列的問題都需要精心處理。
帶著這些需求搜尋了一圈,不太出乎意料,並沒有能在AngularJS的生態中找到一個很適合的插件或指令。於是只好嘗試自己實現了一個。
本文的實作是基於AngularJS,但是思路通用,熟悉其他框架類別庫的同學也可以閱讀。
首先重新梳理了一下需求,由於AngularJS的渲染發生在前端,以前在後端根據已有值獲取各級菜單的option並在模板層進行渲染的方案並不是很適合,而且和很多同學一樣,我個人並不喜歡這樣實現方式:很多時候,即使在後端完成了第一次對option選項的拉取和對初始值的回填,但由於子級菜單的加載依賴於api,前端也需要監聽onchange事件並進行ajax交互,換言之,一個簡單的二級聯動菜單竟然需要把邏輯撕裂在前、後端,這樣的方式並不值得推崇。
關於同步、非同步的載入方式,雖然大多數時候整個步驟是異步的,但是對於部分選項不多的聯動選單,也可以由一個api拉取所有數據,進行處理、快取後供子級菜單渲染使用。因此同步、非同步的渲染方式都應該支援。
至於api返回格式的問題,如果正在進行的是一個新的項目,或者後端程式設計師可以快速響應需求變動,或者前端同學本身就是全棧,這個問題可能不那麼重要;但是很多時候,我們互動的api已經被專案的其他部分所使用,出於相容性、穩定性的考慮,調整json的格式並非是一個可以輕鬆做出的決定;因此在本文中,對於子級菜單option資料的獲取將從directive本身解耦出來,由具體業務邏輯處理。
那如何實現對靈活依賴關係的支持呢?除了最常見的線性依賴以外,也應支援樹狀依賴、倒金字塔依賴甚至複雜的網狀依賴。由於這些業務場景的存在,將依賴關係硬編碼到邏輯較為複雜。經過權衡,組件間將透過事件進行通訊。
需求整理如下:
* 支援在前端完成初始值回填
* 支援子集選單選項的同步、非同步取得
* 支援選單間靈活的依賴關係(如線性依賴、樹狀依賴、倒金字塔依賴、網狀依賴)
* 支援選單空值選項(option[value=""])
* 子集選單的取得邏輯從元件本身解耦
* 事件驅動,各級選單在邏輯上相互獨立不影響
由於多級連動選單對於AngularJS中select標籤的原有行為侵入性較大,為了之後編程方便,減少潛在衝突,本文將採用
1. 首先來思考第一個問題,如何在前端進行初始值的回填
多級連動選單最明顯的特點是,上一層選單更改後,下一層選單會被(同步或非同步地)重新渲染。在回填值的過程中,我們需要逐級回填,無法在頁面載入時(或路由載入或元件載入等等)時瞬間完成該過程。尤其在AngularJS中,option的渲染過程應該發生在ngModel的渲染之前,否則即使option中有對應值,也會造成找不到匹配option的情況。
解決方案是在指令的link階段,首先保存model的初始值,並將其賦為空值(可以呼叫$setViewValue),並在渲染完成後再異步地對其賦回原值。
2. 如何解耦子選項取得的具體邏輯,並同時支援同步、非同步的方式
可以使用scope中的"="類別屬性,將一個外部函數暴露到directive的link方法中。每次在執行方法後,判斷是否為promise實例(或是否有then方法),根據判斷結果決定同步或非同步渲染。透過這樣的解耦,使用者就可以在傳入的外部函數中輕鬆地決定渲染方式了。為了讓回調函數不那麼難看,我們也可以將同步回傳也封裝為一個帶有then方法的物件。如下圖所示:
// scope.source为外部函数 var returned = scope.source ? scope.source(values) : false; !returned || (returned = returned.then ? returned : { then: (function (data) { return function (callback) { callback.call(window, data); }; })(returned) }).then(function (items) { // 对同步或异步返回的数据进行统一处理 }
3. 如何实现菜单间基于事件的通信
大体上还是通过订阅者模式实现,需要在directive上声明依赖;由于需要支持复杂的依赖关系,应该支持一个子集菜单同时有多个依赖。这样在任何一个所依赖的菜单变化时,我们都可以通过如下方式进行监听:
scope.$on('selectUpdate', function (e, data) { // data.name是变化的菜单,dependents是当前菜单所声明的依赖数组 if ($.inArray(data.name, dependents) >= 0) { onParentChange(); } }); // 并且为了方便上文提到的source函数对于变动值的调用,可以对所依赖的菜单进行遍历并保存当前值 var values = {}; if (dependents) { $.each(dependents, function (index, dependent) { values[dependent] = selects[dependent].getValue(); }); }
4. 处理两类过期问题
容易想到的是异步过期的问题:设想第一级菜单发生变化,触发对第二级菜单内容的拉取,但网速较慢,该过程需要3秒。1秒后用户再次改变第一级菜单,再次触发对第二级菜单内容的拉取,此时网速较快,1秒后数据返回,第二级菜单重新渲染;但是1秒后,第一次请求的结果返回,第二级菜单再次被渲染,但事实上第一级菜单此后已经发生过变化,内容已经过期,此次渲染是错误的。我们可以用闭包进行数据过期校验。
不容易想到的是同步过期(其实也是异步,只是未经io交互,都是缓冲时间为0的timeout函数)的问题,即由于事件队列的存在,稍不谨慎就可能出现过期,代码中会有相关注释。
5. 支持空值选项的细节问题
对于空值的支持本来觉得是一个很简单的问题,即可,但实际编码中发现,在directive的link中,由于此option的link过程并未开始,option标签被实际上移除,只剩下相关注释占位。AngularJS认为该select不含有空值选项,于是报错。解决方案是弃用ng-if,使用ng-show。这二者的关系极其微妙有意思,有兴趣的同学可以自己研究~
以上就是编码过程中遇到的主要问题,欢迎交流~
directive('multiLevelSelect', ['$parse', '$timeout', function ($parse, $timeout) { // 利用闭包,保存父级scope中的所有多级联动菜单,便于取值 var selects = {}; return { restrict: 'CA', scope: { // 用于依赖声明时指定父级标签 name: '@name', // 依赖数组,逗号分割 dependents: '@dependents', // 提供具体option值的函数,在父级change时被调用,允许同步/异步的返回结果 // 无论同步还是异步,数据应该是[{text: 'text', value: 'value'},]的结构 source: '=source', // 是否支持控制选项,如果是,空值的标签是什么 empty: '@empty', // 用于parse解析获取model值(而非viewValue值) modelName: '@ngModel' }, template: '' // 使用ng-show而非ng-if,原因上文已经提到 + '<option ng-show="empty" value="">{{empty}}</option>' // 使用朴素的ng-repeat + '<option ng-repeat="item in items" value="{{item.value}}">{{item.text}}</option>', require: 'ngModel', link: function (scope, elem, attr, model) { var dependents = scope.dependents ? scope.dependents.split(',') : false; var parentScope = scope.$parent; scope.name = scope.name || 'multi-select-' + Math.floor(Math.random() * 900000 + 100000); // 将当前菜单的getValue函数封装起来,放在闭包中的selects对象中方便调用 selects[scope.name] = { getValue: function () { return $parse(scope.modelName)(parentScope); } }; // 保存初始值,原因上文已经提到 var initValue = selects[scope.name].getValue(); var inited = !initValue; model.$setViewValue(''); // 父级标签变化时被调用的回调函数 function onParentChange() { var values = {}; // 获取所有依赖的菜单的当前值 if (dependents) { $.each(dependents, function (index, dependent) { values[dependent] = selects[dependent].getValue(); }); } // 利用闭包判断io造成的异步过期 (function (thenValues) { // 调用source函数,取新的option数据 var returned = scope.source ? scope.source(values) : false; // 利用多层闭包,将同步结果包装为有then方法的对象 !returned || (returned = returned.then ? returned : { then: (function (data) { return function (callback) { callback.call(window, data); }; })(returned) }).then(function (items) { // 防止由异步造成的过期 for (var name in thenValues) { if (thenValues[name] !== selects[name].getValue()) { return; } } scope.items = items; $timeout(function () { // 防止由同步(严格的说也是异步,注意事件队列)造成的过期 if (scope.items !== items) return; // 如果有空值,选择空值,否则选择第一个选项 if (scope.empty) { model.$setViewValue(''); } else { model.$setViewValue(scope.items[0].value); } // 判断恢复初始值的条件是否成熟 var initValueIncluded = !inited && (function () { for (var i = 0; i < scope.items.length; i++) { if (scope.items[i].value === initValue) { return true; } } return false; })(); // 恢复初始值 if (initValueIncluded) { inited = true; model.$setViewValue(initValue); } model.$render(); }); }); })(values); } // 是否有依赖,如果没有,直接触发onParentChange以还原初始值 !dependents ? onParentChange() : scope.$on('selectUpdate', function (e, data) { if ($.inArray(data.name, dependents) >= 0) { onParentChange(); } }); // 对当前值进行监听,发生变化时对其进行广播 parentScope.$watch(scope.modelName, function (newValue, oldValue) { if (newValue || '' !== oldValue || '') { scope.$root.$broadcast('selectUpdate', { // 将变动的菜单的name属性广播出去,便于依赖于它的菜单进行识别 name: scope.name }); } }); } }; }]);

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。
