>  기사  >  웹 프론트엔드  >  무한레벨 연계를 구현한 AngularJS의 상세 예시 menu_AngularJS

무한레벨 연계를 구현한 AngularJS의 상세 예시 menu_AngularJS

WBOY
WBOY원래의
2016-05-16 15:19:461232검색

다단계 연계 메뉴는 도-시 연계, 대학-전문대학-전공 연계 등 공통 프론트엔드 구성요소입니다. 시나리오는 일반적이지만 신중하게 분석하면 범용 무한 계층 연결 메뉴를 구현하는 것이 상상만큼 간단하지 않을 수 있습니다. 예를 들어, 하위 메뉴가 동기적으로 로드되는지 아니면 비동기적으로 로드되는지 고려해야 합니다. 초기값의 백필은 프런트 엔드에서 발생합니까, 아니면 백엔드에서 발생합니까? 비동기식으로 로드되는 경우 백엔드 API의 반환 형식에 대한 엄격한 정의가 있습니까? 동기식 및 비동기식 공존을 달성하는 것이 쉬운가요? 다양한 종속성을 유연하게 지원할 수 있나요? 메뉴에 null 값 옵션이 있나요? …일련의 문제를 신중하게 처리해야 합니다.

이러한 요구 사항을 검색한 결과 당연히 AngularJS 생태계에서 매우 적합한 플러그인이나 지침을 찾을 수 없었습니다. 그래서 직접 구현해 보아야 했습니다.

이 기사의 구현은 AngularJS를 기반으로 하지만 아이디어는 일반적이며 다른 프레임워크 라이브러리에 익숙한 학생도 읽을 수 있습니다.

우선 AngularJS의 렌더링이 프런트엔드에서 이루어지기 때문에 기존 솔루션은 백엔드의 기존 값을 기반으로 모든 레벨의 메뉴 옵션을 얻어서 렌더링하는 방식으로 구성했습니다. 템플릿 레이어는 그다지 적합하지 않으며 많은 학생들처럼 개인적으로 이 구현을 좋아하지 않습니다. 옵션의 첫 번째 풀과 초기 값의 백필이 백엔드에서 완료되더라도 로딩이 하위 메뉴의 구성은 API에 따라 달라지며, 프런트엔드는 onchange 이벤트를 수신하고 Ajax 상호작용을 수행해야 합니다. 즉, 간단한 2단계 링크 메뉴에서는 프런트엔드와 백엔드 간에 로직을 분할해야 합니다. 칭찬의.

동기식 및 비동기식 로딩 ​​방법과 관련하여 대부분의 경우 전체 단계가 비동기식이지만 옵션이 거의 없는 일부 연결 메뉴의 경우 API는 모든 데이터를 가져와서 처리하고 캐시하여 하위에 제공할 수도 있습니다. -menu 렌더링에 사용됩니다. 따라서 동기식 렌더링 방식과 비동기식 렌더링 방식이 모두 지원되어야 합니다.

API 반환 형식 문제는 새로운 프로젝트를 진행 중이거나, 백엔드 프로그래머가 수요 변화에 빠르게 대응할 수 있거나, 프론트엔드 학생 자체가 풀스택인 경우 이 문제가 발생하지 않을 수 있습니다. 매우 중요하지만 우리가 상호 작용하는 API는 프로젝트의 다른 부분에서 사용되었습니다. 호환성과 안정성을 위해 json 형식을 조정하는 것은 쉬운 결정이 아닙니다. 하위 메뉴 옵션 데이터는 지시문 자체에서 분리되어 특정 비즈니스 로직에 의해 처리됩니다.

유연한 종속성에 대한 지원을 구현하는 방법은 무엇입니까? 가장 일반적인 선형 종속성 외에도 트리 종속성, 역피라미드 종속성 및 복잡한 네트워크 종속성도 지원해야 합니다. 이러한 비즈니스 시나리오가 존재하기 때문에 논리에 대한 종속성을 하드코딩하는 것은 복잡합니다. 절충 후 구성 요소는 이벤트를 통해 통신합니다.

요구사항을 요약하면 다음과 같습니다.

* 프런트엔드에서 초기값 채우기 지원
* 하위 메뉴 옵션의 동기 및 비동기 획득 지원
* 메뉴 간의 유연한 종속성 지원(선형 종속성, 트리 종속성, 역피라미드 종속성, 메시 종속성 등)
* 메뉴 빈 값 옵션 지원(option[value=""])
* 하위 세트 메뉴의 획득 로직은 구성 요소 자체에서 분리됩니다
* 이벤트 중심, 모든 레벨의 메뉴는 논리적으로 서로 독립적이며 서로 영향을 미치지 않습니다

다중 레벨 연결 메뉴는 AngularJS에서 선택 태그의 원래 동작을 더 방해하므로 후속 프로그래밍을 용이하게 하고 잠재적인 충돌을 줄이기 위해 이 기사에서는 항목에서 de376f663cd17357aa43c3dde0998300{{item.text}}7b7962f53b61b9abe321e770490cee51의 순진한 방법입니다.

1. 먼저 첫 번째 질문인 프런트엔드에서 초기값을 어떻게 백필할지 생각해 보겠습니다

다단계 연계 메뉴의 가장 큰 특징은 상위 메뉴가 변경된 후 하위 메뉴가 다시 렌더링(동기 또는 비동기)된다는 점입니다. 값을 백필하는 과정에서 단계별로 백필을 해야 하는데, 이 프로세스는 페이지가 로드될 때(또는 경로 로드, 컴포넌트 로드 등) 즉시 완료될 수 없습니다. 특히 AngularJS에서는 ngModel을 렌더링하기 전에 option의 렌더링 프로세스가 이루어져야 합니다. 그렇지 않으면 option에 해당 값이 있어도 일치하는 옵션을 찾을 수 없습니다.
해결 방법은 먼저 명령의 링크 단계에서 모델의 초기 값을 저장하고 이를 null 값($setViewValue 호출 가능)에 할당한 다음 렌더링이 완료된 후 비동기식으로 원래 값에 다시 할당하는 것입니다.

2. 하위 옵션 획득의 특정 로직을 분리하고 동기 및 비동기 방식을 모두 지원하는 방법

범위에서 "=" 클래스 속성을 사용하여 지시어의 링크 메서드에 외부 함수를 노출할 수 있습니다. 이 메서드를 실행할 때마다 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. 支持空值选项的细节问题

对于空值的支持本来觉得是一个很简单的问题,accd74be04ce4d5f8e4a628a199fee5e{{empty}}4afa15d3069109ac30911f04c56f3338即可,但实际编码中发现,在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 &#63; 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 &#63; scope.source(values) : false;
// 利用多层闭包,将同步结果包装为有then方法的对象
!returned || (returned = returned.then &#63; 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 &#63; 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
});
}
});
}
};
}]);

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.