MST

星途 面试题库

面试题:Angular内置指令与自定义指令的协同设计与最佳实践

在复杂的前端业务场景下,往往需要结合Angular内置指令与自定义指令来完成功能。请阐述如何进行二者的协同设计,以达到代码的高可维护性、复用性和高效性。例如,如何设计一个自定义指令,使其能与NgModel等内置指令完美配合,实现双向数据绑定且不影响整体性能。请提供完整的设计思路、代码示例以及相关的测试策略。
17.3万 热度难度
前端开发Angular

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 理解内置指令原理:深入了解像 NgModel 这样的内置指令的工作机制。NgModel 实现双向数据绑定依赖于 ControlValueAccessor 接口,该接口定义了三个方法 writeValueregisterOnChangeregisterOnTouched,以及一个可选的 setDisabledState 方法。
  2. 自定义指令设计原则
    • 复用性:确保自定义指令具有通用的功能,可以在多个组件中复用。例如,为输入框添加特定样式或行为的指令,不应该与特定组件的业务逻辑紧密耦合。
    • 可维护性:保持指令逻辑清晰,尽量单一职责。将复杂逻辑拆分成更小的函数或服务,便于理解和修改。
    • 性能优化:避免在指令的生命周期钩子函数中执行大量计算或频繁的 DOM 操作。使用 ChangeDetectorRef 来精确控制变化检测,减少不必要的性能开销。
  3. NgModel 配合:自定义指令如果要和 NgModel 协同工作,同样需要实现 ControlValueAccessor 接口。这样可以将自定义指令融入到 Angular 的表单系统中,实现双向数据绑定。

代码示例

  1. 创建自定义指令
    import { Directive, forwardRef, ElementRef, HostListener, Input } from '@angular/core';
    import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
    
    const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputDirective),
      multi: true
    };
    
    @Directive({
      selector: '[appCustomInput]',
      providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
    })
    export class CustomInputDirective implements ControlValueAccessor {
      @Input() appCustomInput: string;
      value: any;
      onChange = (_: any) => {};
      onTouched = () => {};
    
      constructor(private el: ElementRef) {}
    
      writeValue(obj: any): void {
        this.value = obj;
        this.el.nativeElement.value = obj;
      }
    
      registerOnChange(fn: any): void {
        this.onChange = fn;
      }
    
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
    
      @HostListener('input', ['$event.target.value'])
      onInputChange(value: any) {
        this.value = value;
        this.onChange(value);
        this.onTouched();
      }
    }
    
  2. 在组件模板中使用
    <input type="text" appCustomInput [(ngModel)]="data">
    
    在组件类中定义 data 属性:
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-custom-input-demo',
      templateUrl: './custom - input - demo.component.html',
      styleUrls: ['./custom - input - demo.component.css']
    })
    export class CustomInputDemoComponent {
      data: string = '';
    }
    

测试策略

  1. 单元测试
    • 测试 writeValue 方法:验证指令是否正确更新 DOM 元素的值。
    • 测试 registerOnChangeregisterOnTouched 方法:确保回调函数被正确注册。
    • 测试 onInputChange 方法:模拟输入事件,验证 onChangeonTouched 回调是否被调用,以及值是否正确更新。
    • 使用 Angular 的 TestBedComponentFixture 来创建组件实例并测试指令。
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { CustomInputDirective } from './custom - input.directive';
    import { FormsModule } from '@angular/forms';
    import { Component } from '@angular/core';
    
    @Component({
      template: `<input type="text" appCustomInput [(ngModel)]="data">`
    })
    class TestComponent {
      data: string = '';
    }
    
    describe('CustomInputDirective', () => {
      let component: TestComponent;
      let fixture: ComponentFixture<TestComponent>;
      let inputElement: HTMLInputElement;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [FormsModule],
          declarations: [CustomInputDirective, TestComponent]
        });
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        inputElement = fixture.nativeElement.querySelector('input');
        fixture.detectChanges();
      });
    
      it('should update value on writeValue', () => {
        const customDirective = fixture.debugElement.query((de) => de.directiveInstance instanceof CustomInputDirective).injector.get(CustomInputDirective);
        customDirective.writeValue('test');
        expect(inputElement.value).toBe('test');
      });
    
      it('should call onChange on input', () => {
        const customDirective = fixture.debugElement.query((de) => de.directiveInstance instanceof CustomInputDirective).injector.get(CustomInputDirective);
        spyOn(customDirective, 'onChange');
        inputElement.value = 'new value';
        inputElement.dispatchEvent(new Event('input'));
        expect(customDirective.onChange).toHaveBeenCalledWith('new value');
      });
    });
    
  2. 集成测试
    • 测试自定义指令与 NgModel 的双向数据绑定是否正常工作。
    • 验证组件与指令集成后,数据在组件和输入框之间是否能正确同步。
    • 使用 ProtractorCypress 等端到端测试框架来模拟用户操作,检查双向数据绑定功能是否符合预期。例如,在输入框中输入值,检查组件中对应数据是否更新;修改组件数据,检查输入框的值是否同步更新。