Mehrstufige Verknüpfungsmenüs sind gängige Frontend-Komponenten, z. B. die Verknüpfung zwischen Provinz und Stadt, die Verknüpfung zwischen Universität, Hochschule und Hauptfach usw. Auch wenn die Szenarien häufig vorkommen, ist die Implementierung eines universellen unendlichen hierarchischen Verknüpfungsmenüs bei sorgfältiger Analyse möglicherweise nicht so einfach wie gedacht. Wir müssen beispielsweise überlegen, ob das Untermenü synchron oder asynchron geladen wird. Erfolgt das Auffüllen der Anfangswerte am Frontend oder am Backend? Gibt es beim asynchronen Laden eine strikte Definition des Rückgabeformats der Backend-API? Ist es einfach, eine synchrone und asynchrone Koexistenz zu erreichen? Kann es verschiedene Abhängigkeiten flexibel unterstützen? Gibt es im Menü eine Nullwertoption? …Eine Reihe von Problemen müssen sorgfältig angegangen werden.
Nachdem ich mich mit diesen Anforderungen umgesehen hatte, überraschte es nicht, dass ich im AngularJS-Ökosystem kein sehr geeignetes Plug-in oder keine geeignete Anleitung finden konnte. Also musste ich versuchen, selbst eines umzusetzen.
Die Implementierung dieses Artikels basiert auf AngularJS, die Ideen sind jedoch allgemeiner Natur und können auch von Studenten gelesen werden, die mit anderen Framework-Bibliotheken vertraut sind.
Zuerst habe ich die Anforderungen neu organisiert, da das Rendern von AngularJS im Frontend erfolgt, die bisherige Lösung, die Optionen von Menüs auf allen Ebenen basierend auf den vorhandenen Werten im Backend abzurufen und einzugeben Die Vorlagenebene ist nicht sehr geeignet, und wie viele Studenten mag ich persönlich diese Implementierung nicht: Oftmals werden sie aufgrund des Ladens selbst dann im Backend abgeschlossen, wenn die Option zum ersten Mal gezogen und der Anfangswert wieder aufgefüllt wird Da das Untermenü von der API abhängt, muss das Frontend auch auf Änderungsereignisse warten und eine Ajax-Interaktion durchführen. Mit anderen Worten: Ein einfaches Verknüpfungsmenü der zweiten Ebene erfordert eine Aufteilung der Logik zwischen Front- und Back-End des Lobes.
Was die synchronen und asynchronen Lademethoden betrifft, so ist zwar der gesamte Schritt meist asynchron, bei einigen Verknüpfungsmenüs mit wenigen Optionen kann eine API jedoch auch alle Daten abrufen, verarbeiten, zwischenspeichern und dem Sub bereitstellen -menu Wird zum Rendern verwendet. Daher sollten sowohl synchrone als auch asynchrone Rendering-Methoden unterstützt werden.
Was das Problem des API-Rückgabeformats betrifft: Wenn Sie an einem neuen Projekt arbeiten oder die Back-End-Programmierer schnell auf Nachfrageänderungen reagieren können oder die Front-End-Studenten selbst mit dem Full-Stack vertraut sind, ist dieses Problem möglicherweise nicht der Fall Aus Gründen der Kompatibilität und Stabilität ist die Anpassung des JSON-Formats jedoch oft nicht einfach Die Daten der Untermenüoptionen werden von der Direktive selbst entkoppelt und von einer bestimmten Geschäftslogik verarbeitet.
Wie implementiert man die Unterstützung für flexible Abhängigkeiten? Neben den häufigsten linearen Abhängigkeiten sollten auch Baumabhängigkeiten, umgekehrte Pyramidenabhängigkeiten und sogar komplexe Netzwerkabhängigkeiten unterstützt werden. Aufgrund der Existenz dieser Geschäftsszenarien ist die harte Kodierung von Abhängigkeiten in die Logik komplex. Nach Kompromissen kommunizieren Komponenten über Ereignisse.
Die Anforderungen sind wie folgt zusammengefasst:
* Unterstützt das Auffüllen des Anfangswerts am Frontend
* Unterstützt die synchrone und asynchrone Erfassung von Teilmenüoptionen
* Unterstützt flexible Abhängigkeiten zwischen Menüs (z. B. lineare Abhängigkeit, Baumabhängigkeit, umgekehrte Pyramidenabhängigkeit, Netzabhängigkeit)
* Option für leere Werte im Menü unterstützen (option[value=""])
* Die Erfassungslogik des Subset-Menüs ist von der Komponente selbst entkoppelt
* Ereignisgesteuert, Menüs auf allen Ebenen sind logisch unabhängig voneinander und beeinflussen sich nicht gegenseitig
Da das mehrstufige Verknüpfungsmenü stärker in das ursprüngliche Verhalten des Select-Tags in AngularJS eingreift, wird in diesem Artikel
1. Lassen Sie uns zunächst über die erste Frage nachdenken, wie der Anfangswert am Frontend aufgefüllt werden kann
Das offensichtlichste Merkmal von mehrstufigen Verknüpfungsmenüs besteht darin, dass nach einer Änderung des Menüs der oberen Ebene das Menü der unteren Ebene neu gerendert wird (synchron oder asynchron). Beim Auffüllen von Werten müssen wir Schritt für Schritt auffüllen. Dieser Vorgang kann nicht sofort abgeschlossen werden, wenn die Seite geladen wird (oder die Route geladen wird, die Komponente geladen wird usw.). Insbesondere in AngularJS sollte der Rendervorgang der Option vor dem Rendern von ngModel erfolgen. Andernfalls wird die passende Option nicht gefunden, selbst wenn ein entsprechender Wert in der Option vorhanden ist.
Die Lösung besteht darin, zunächst den Anfangswert des Modells in der Verknüpfungsphase der Anweisung zu speichern, ihm einen Nullwert zuzuweisen (Sie können $setViewValue aufrufen) und ihn dann nach Abschluss des Renderns asynchron wieder dem ursprünglichen Wert zuzuweisen.
2. So entkoppeln Sie die spezifische Logik der Unteroptionserfassung und unterstützen sowohl synchrone als auch asynchrone Methoden
Sie können das Klassenattribut „=" im Bereich verwenden, um eine externe Funktion für die Link-Methode der Direktive verfügbar zu machen. Jedes Mal nach der Ausführung dieser Methode wird beurteilt, ob es sich um eine Promise-Instanz handelt (oder ob sie über eine Then-Methode verfügt), und basierend auf dem Beurteilungsergebnis wird über synchrones oder asynchrones Rendering entschieden. Durch eine solche Entkopplung können Benutzer die Rendering-Methode in der übergebenen externen Funktion leicht entscheiden. Um die Callback-Funktion weniger hässlich zu machen, können wir die synchrone Rückgabe auch als Objekt mit einer then-Methode kapseln. Wie unten gezeigt:
// 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 }); } }); } }; }]);