likes
comments
collection
share

Angular 函数组合式与面向对象融合-真香

作者站长头像
站长
· 阅读数 61

作为一名多年的Angular开发者,我一直面临着一个让我头疼的问题:多继承。虽然我们可以使用混合(mixins)来部分解决这个问题,但这种方法常常显得笨拙,也可以使用服务(services)来达到类似的效果,但却有许多限制。然而,直到Angular14版本引入了在函数中获取依赖的特性,这个问题终于得到了完美的解决。在本文中,我们将探讨Angular函数组合式与面向对象编程的融合,探讨它们的优势和如何让你的代码变得更加丰富和灵活。让我们一起深入研究这个真正令人满意的解决方案。

为什么不使用服务来实现?

服务的注入器与组件的注入器并不相同,所以无法满足一致性要求,需要传入更多的参数才能实现近似目标,并不是满意的解决方案

函数组合式前置条件

在我们深入探讨如何将函数组合式编程与Angular的面向对象编程相融合之前,让我们先了解一些必要的前置条件。这些前置条件将帮助我们更好地理解如何有效地利用函数组合式编程来解决多继承问题

  1. 函数中直接获取依赖

    在传统的Angular开发中,通常通过在构造函数中声明依赖并由Angular的依赖注入系统自动注入这些依赖。然而,在函数组合式编程中,我们希望能够在函数内部直接获取依赖,而不依赖于构造函数。这样做的好处是可以更灵活地组合不同的功能函数

  2. 注入器的一致性

    在Angular中,每个组件都有一个注入器(Injector)。在函数组合式编程中,我们需要确保组件注入器与函数中使用的注入器是同一个。这样做的目的是保持依赖的一致性,避免出现不同注入器之间的冲突或不一致性,从而确保代码的可预测性和可维护性。

上述的条件从Angular14开始支持了。

text组件为例

选择以文本组件为例,是因为它足够简单,在总结这一章会给出源码链接,源码中会有很多其他组件使用同样的思想来开发。

  1. 需求分析

    文本组件需要自定义文字颜色,禁用状态,文本强调等。

    从这些需求中可以看出,我们需要操作宿主元素,因此需要封装一个useHostDom工具函数来实现这一功能。另外,组件库中的很多组件可能需要自定义颜色类型,例如主色、次要色、警告色等,这些颜色类型是通用的,因此需要封装一个通用的函数来处理它们。

  2. 实现useHostDom工具函数

    useHostDom工具函数的主要作用是使组件能够操作宿主元素,代码如下:

    export interface HostDom {
      readonly renderer: Renderer2;
      readonly elementRef: ElementRef<HTMLElement>;
      readonly element: Signal<HTMLElement>;
    
      setHostStyle<K extends keyof CSSStyleDeclaration>(style: K, value: SafaAny): void;
    
      setHostStyles(css: Partial<CSSStyleDeclaration>): void;
    
      removeStyle<K extends keyof CSSStyleDeclaration>(style: K): void;
    
      getHostStyle<K extends keyof CSSStyleDeclaration>(style: K): string;
    
      addClass(name: string): void;
    
      removeClass(name: string): void;
    
      removePrefixClass(prefix: string): void;
    
      removeSuffixClass(suffix: string): void;
    
      hasClass(name: string): boolean;
    }
    
    export function useHostDom(): HostDom {
      const renderer = inject(Renderer2);
      const elementRef: ElementRef<HTMLElement> = inject(ElementRef);
      const element = computed(() => elementRef.nativeElement);
    
      function setHostStyle<K extends keyof CSSStyleDeclaration>(style: K, value: SafaAny): void {
        renderer.setStyle(element(), toHyphenCase(style as string), value);
      }
    
      function getHostStyle<K extends keyof CSSStyleDeclaration>(style: K): string {
        return (element()?.style[style] as string) ?? '';
      }
    
      function setHostStyles(css: Partial<CSSStyleDeclaration>): void {
        Object.entries(css).forEach(([style, value]) => setHostStyle(style as SafaAny, value));
      }
    
      function removeStyle<K extends keyof CSSStyleDeclaration>(style: K): void {
        renderer.removeStyle(element(), toHyphenCase(style as string));
      }
    
      function addClass(name: string): void {
        !hasClass(name) && renderer.addClass(element(), name);
      }
    
      function removeClass(name: string): void {
        renderer.removeClass(element(), name);
      }
    
      function removePrefixClass(prefix: string): void {
        element()?.classList.forEach(item => {
          if (!item.startsWith(prefix)) {
            return;
          }
    
          renderer.removeClass(element(), item);
        });
      }
    
      function removeSuffixClass(suffix: string): void {
        element()?.classList.forEach(item => {
          if (!item.endsWith(suffix)) {
            return;
          }
    
          renderer.removeClass(element(), item);
        });
      }
    
      function hasClass(name: string): boolean {
        return element()?.classList.contains(name);
      }
    
      return {
        setHostStyle,
        setHostStyles,
        removeStyle,
        getHostStyle,
        addClass,
        hasClass,
        removeClass,
        removePrefixClass,
        removeSuffixClass,
        renderer,
        elementRef,
        element
      };
    }
    
  3. 实现颜色类型函数

    为了支持不同的颜色类型,我们可以实现一个通用的函数,该函数接受prefix作为参数,用于自定义classname。

    export interface TypeInput {
      type?: 'secondary' | 'primary' | 'success' | 'warning' | 'danger';
    }
    
    export function withTypeClassInput<T extends TypeInput>(this: T, prefix: string): void {
      // useHostDom 在上文已经实现了 通过函数组合 可以轻松集成进来
      const { addClass, removeClass } = useHostDom();
    
      const nxType = signal(this.type);
      const computedClassName = computed(() => `yk-${prefix}-${nxType()}`);
    
      effect(() => {
        const className = computedClassName();
    
        if (!className) {
          removeClass(className);
          return;
        }
    
        addClass(className);
      });
    
      onChanges.call(
        this,
        watchInputs(['type'], (key, change) => {
          key === 'type' && nxType.set(change.currentValue);
        })
      );
    }
    
  4. 通过函数组合来实现组件完整功能

    最后,我们通过函数组合的方式来实现完整的文本组件功能,至此我们通过函数组合式来达到了多继承的效果,代码如下:

    interface TextUiInputs {
      disabled: boolean;
      strong: boolean;
      mark: boolean;
      underline: boolean;
      del: boolean;
    }
    
    function withTextUiInputs<T extends TextUiInputs>(this: T): void {
      const { setHostStyles } = useHostDom();
    
      const nxDisabled = signal(this.disabled);
      const nxStrong = signal(this.strong);
      const nxMark = signal(this.mark);
      const nxUnderline = signal(this.underline);
      const nxDel = signal(this.del);
    
      const computedStyle = computed(() => {
        const disabled = nxDisabled();
        const strong = nxStrong();
        const mark = nxMark();
        const underline = nxUnderline();
        const del = nxDel();
    
        const style: Partial<CSSStyleDeclaration> = {
          ...(disabled && { color: `rgba(${getCssVar('color', ['info-rgb'])}, .32)`, cursor: 'not-allowed' }),
          ...(underline && { textDecoration: 'underline' }),
          ...(strong && { fontWeight: 'bold' }),
          ...(mark && { backgroundColor: '#ffd61c7a' }),
          ...(del && { textDecoration: 'line-through' })
        };
    
        return style;
      });
    
      effect(() => setHostStyles(computedStyle()));
    
      onChanges.call(
        this,
        watchInputs(['disabled', 'strong', 'mark', 'underline', 'del'], (key, { currentValue }) => {
          key === 'disabled' && nxDisabled.set(currentValue);
          key === 'strong' && nxStrong.set(currentValue);
          key === 'mark' && nxMark.set(currentValue);
          key === 'underline' && nxUnderline.set(currentValue);
          key === 'del' && nxDel.set(currentValue);
        })
      );
    }
    
    @Component({
      selector: 'nx-text, [nx-text]',
      standalone: true,
      imports: [CommonModule],
      template: ` <ng-content></ng-content> `,
      styles: [],
      encapsulation: ViewEncapsulation.None,
      changeDetection: ChangeDetectionStrategy.OnPush,
      host: {
        class: 'yk-typography yk-typography-text'
      }
    })
    export class NxTextComponent implements TypeInput, TextUiInputs {
      @Input() type?: TypeInput['type'];
      @Input({ transform: booleanAttribute }) disabled = false;
      @Input({ transform: booleanAttribute }) strong = false;
      @Input({ transform: booleanAttribute }) mark = false;
      @Input({ transform: booleanAttribute }) underline = false;
      @Input({ transform: booleanAttribute }) del = false;
    
      constructor() {
        withTypeClassInput.call(this, 'typography-text');
        withTextUiInputs.call(this);
      }
      
      /* 其他逻辑 */
    }
    

    效果如下:

    Angular 函数组合式与面向对象融合-真香

总结

本文介绍了如何将函数组合式编程与Angular的面向对象编程相融合,以解决多继承问题并开发灵活的组件。我们以Text组件为例,演示了如何使用函数组合的思想来构建具有自定义样式组件。

完整示例&更多组件请参考源码