使用 Typescript Mixins 组合 Angular 组件 优先选择组合而不是继承

2025-06-05

使用 Typescript Mixins 组合 Angular 组件

优先使用组合而不是继承

了解如何通过使用组合而不是继承来最大化 Angular 组件的可重用性

这是我上一篇关于使用 Angular 进行组件组合的文章的后续,其中我列出了 3 种组合 Angular 组件的方法:

  • 类继承

  • 类混合

  • 组件组成

简而言之,我最喜欢的方式是将组件分成小单元,并使用输入和输出在组件之间进行通信。为了在组件之间共享逻辑片段,我喜欢 Mixins 帮助我们避免使用类继承带来的一些陷阱。

在本文中,我想更多地关注类继承和类 Mixins 之间的关系、它们的区别以及使用 Mixins 构建组件的一些陷阱。


提示:使用**Bit ** ( Github )等工具,跨项目共享和协作 Angular 组件,从而提高代码复用率。将可复用的构建块分享到bit.dev上的合集,以便将来进行组合。

示例:Bit 集合中的共享 Angular 组件示例:Bit 集合中的共享 Angular 组件

类继承的陷阱

您可能已经知道为什么使用继承有时非常有吸引力:定义一些方法和属性一次,然后将它们用于每个公共子类:太棒了!

从表面上看,在某些情况下,这实际上是一件好事。然而,类继承也存在一些众所周知且有据可查的问题。从组件架构师的角度来看,最重要的问题如下:

  • 脆弱的基类——当基类的改变破坏了派生的子类时

  • 它鼓励在设计基类时尽早做出选择:这会使设计变得脆弱不堪

  • 它破坏了封装

事实上,你可能听过《四人帮》书中的那句传奇名言:

优先使用组合而不是继承

我发现有几种类型的组件经常使用继承:

  • 具有通用值访问器的表单字段

  • 扩展基础路由的路由组件

  • 模态框、弹出窗口等,以及常用方法(显示、隐藏等)

本文更侧重于业务逻辑,而非纯粹的视觉属性(例如禁用、动画等)。我发现组件间共享逻辑有点复杂,而且是一个容易被误解的话题,尤其是当框架本身没有提供关于该主题的官方立场时,例如与 React 的情况相反。

Typescript Mixins

Mixins 的概念非常简单:想象一下,你不再拥有一个层级结构清晰的类,而是拥有许多非常小的局部类。这些类可以灵活地组合在一起,构建更大的类。

使用 Typescript 创建 Mixins 的方式很简单:我们定义一个以类作为参数的函数,并使用作为参数传递的类来扩展新创建的类。

首先,我们定义 mixin pinMixin 和 closeMixin,它们分别定义 1 个方法:

    function pinMixin(BaseClass) {
      return class extends BaseClass {
         pin() {
            // implementation
         }
      }
    }

    function closeMixin(BaseClass) {
      return class extends BaseClass {
         close() {
           // implementation
         }
      }
    }
Enter fullscreen mode Exit fullscreen mode

我们创建一个通过合并 mixins 函数创建的 Base 类,然后扩展其实现:

    const BaseTabMixin = pinMixin(
      closeMixin(class {})
    );

    class Tab extends BaseTabMixin {}

    // Tab now can use the methods `close` and `pin`
Enter fullscreen mode Exit fullscreen mode

场景:社交媒体聚合器应用程序

举个例子,我想构建一个社交媒体聚合器应用程序的原型,其中包含来自主要社交媒体服务的帖子信息。

这是我多年前作为初级开发人员遇到的一个特殊例子:Babel 发布了,ES6 类是一个新奇的东西,直到它们不再流行。

当时还是个小伙子的我,有点天真地开始创建基类,不断扩展代码,这真是令人兴奋。看看这些基类让我分享了多少代码!一开始,你可能很难意识到这一点:需求还没有完全完善,而且众所周知,新的细节会不断涌现。

我们将了解如何为 Facebook、Twitter、Youtube 和 Reddit 等社交媒体构建帖子组件:首先,我们将使用传统的继承。

之后,我们将使用 Composition 进行重构。

使用继承构建基础帖子组件

让我们继续构建一个 BasePost 类,它与派生子类共享属性和方法。你可能已经知道,社交媒体帖子彼此非常相似,但也有细微的差别:它们都有作者、内容(可以是文本、链接或图片),并且允许一些操作,例如点赞、分享、编辑等。

我们的基类 PostComponent 将有一个输入(Post 对象),并将注入一个服务 PostService,我们将我们的操作委托给该服务。

所有社交帖子之间唯一的共同操作是删除,因此我们将其添加到基类,以便所有子类都可以继承该方法。

    class PostComponent {
      @Input() post: Post;

      constructor(protected service: PostService) {}

      delete() {
         this.post.delete(this.post.id);
      }
    }
Enter fullscreen mode Exit fullscreen mode

这就是我们可以创建的最小基类。现在,我们可以继续添加具体的操作了。

我们知道 Facebook 和 Twitter 都允许帖子被点赞,但 Twitter 和 Youtube 则不行;因此,我们创建一个名为 LikeablePost 的子类:

    class LikeablePost extends PostComponent {
        get likes() { 
          return this.post.likes;
        }

        like() {
          this.service.like(this.post.id);
        }

        unlike() {
          this.service.unlike(this.post.id);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Youtube 和 Reddit 都允许对帖子进行点赞和点踩;创建一个允许执行此类操作的子类是有意义的:

    class VoteablePost extends PostComponent {
        downvote() {
          this.service.downvote(this.post.id);
        }

        upvote() {
          this.service.upvote(this.post.id);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Facebook 和 Twitter 还有另一个相似之处:“分享”的概念是关键元数据。

    class ShareablePost extends LikeablePost {
        get shares() {
          return this.post.shares;
        }

        share() {
          this.service.share(this.post.id);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Youtube、Facebook 和 Reddit 的共同点是它们都允许编辑帖子,这与 Twitter 不同。

这是我们遇到的第一个问题:

  • 由于该方法不被所有类共享,因此将其添加到基类是错误的

  • 我们可以为所有子类实现方法编辑,但这会非常重复

我们继续实现 TwitterPostComponent

    @Component({...})
    class TwitterPostComponent extends ShareablePost {}
Enter fullscreen mode Exit fullscreen mode

让我们跳到未来,Jack 给我们带来了一个可怕的消息:我们不能再删除推文了!我们的类现在需要修改,但是等等:delete 是在基类中定义的。

  • 如果我们从基类中删除该方法,我们将破坏其他类

  • 如果我们只从 TwitterBaseComponent 中删除它,我们最终会破坏 Liskov 替换原则,这意味着 TwitterBaseComponent 和 PostComponent 应该能够互换而不会破坏任何东西

如果现在还不够清楚的话,这一切都是一个坏主意。

进入作曲

现在,我们将通过组合小类并使用 Typescript mixins 创建由许多独立的小类组成的组件来重写所有以前的内容。

让我们创建组件 TwitterPostComponent 所需的混合:likeMixin、deleteMixin 和 shareMixin。

基类

首先,我们希望 mixin 足够通用,可以应用于各种组件,并且单一依赖项是注入到组件的服务。

    export interface PostComponent {
      post: Post;
      service: PostService;
    }
Enter fullscreen mode Exit fullscreen mode

喜欢Mixin

    // like
    function likeMixin<IBasePost extends Constructor<PostComponent>>(
      Base: IBasePost
    ) {
      return class extends BasePost implements CanLike {
        get likes() {
          return this.post.likes;
        }

        like() {
          return this.service.like(this.post.id);
        }

        unlike() {
          return this.service.unlike(this.post.id);
        }
      };
    }
Enter fullscreen mode Exit fullscreen mode

删除Mixin

    function deleteMixin<IBasePost extends Constructor<PostComponent>>(
      BasePost: IBasePost
    ) {
      return class extends BasePost implements CanDelete {
        delete() {
          return this.service.delete(this.post.id);
        }
      };
    }
Enter fullscreen mode Exit fullscreen mode

分享混音

    *export function shareMixin<IBasePost extends Constructor<PostComponent>>(
      BasePost: IBasePost
    ) {
      return class extends BasePost implements CanShare {
        shares: number;

        share() {
          return this.service.share(this.post.id);
        }
      };
    }
Enter fullscreen mode Exit fullscreen mode

创建实现组件:TwitterPostComponent

一旦创建,我们就可以将它们应用到新创建的 TwitterPostComponent:

    const TwitterBase = deleteMixin(
      likeMixin(
        shareMixin(PostComponent)
      )
    );
Enter fullscreen mode Exit fullscreen mode

如果您更喜欢使用Typescript 自己的文档中描述的 applyMixins 函数,则可以执行以下操作:

    class TwitterBase extends PostComponent {}

    interface TwitterBase extends CanLike, CanDelete, CanShare {}

    applyMixins(TwitterBase, [
      shareMixin, 
      likeMixin, 
      deleteMixin
    ]);
Enter fullscreen mode Exit fullscreen mode

一旦创建了由 mixins 组成的基本组件,我们就可以扩展新组件 TwitterPostComponent:

    @Component({
      selector: 'twitter-post',
      template: `
        <div class="post">
          <div class="post-header">
            {{ post.author }}
          </div>

          <div class="post-content">
            {{ post.content }}
          </div>

          <div class="post-footer">
            <button (click)="like()">Like</button>
            <button (click)="share()">Share</button>
          </div>
        </div>
      `
    })
    export class TwitterPostComponent extends TwitterBase {}
Enter fullscreen mode Exit fullscreen mode

为了从 Tweets 组件中删除删除功能,我们不需要做太多事情 - 我们只需从我们的类中删除 deleteMixin mixin:

    const TwitterBase = likeMixin(
        shareMixin(PostComponent)
      )
    );
Enter fullscreen mode Exit fullscreen mode

使用 Mixins 的陷阱

Mixins 很棒,但它并非万无一失的工具。虽然我仍然更喜欢 Mixins 而不是多重继承,但理解使用这项技术的含义仍然很重要。

这篇React 博客文章很好地解释了为什么 Mixins 不再被视为 React 中的最佳实践:

  • Mixins 会创建隐式依赖关系:调用组件方法的 mixins、引用组件属性的 mixins 或需要 mixin 才能正常工作的组件都相互依赖

  • Mixins 一开始很小但随着时间的推移不断增长

  • Mixins 导致名称冲突

当然,由于相似性,这些也适用于与 Angular 组件一起使用的 Typescript mixin。

如何避免这些陷阱?

  • 尽量不要使用太多的 mixin;如果有太多的 mixin,也许你应该将组件拆分成几个组件,并使用带有输入和输出的组件组合来相互通信。

  • 尽量保持它们尽可能小

  • 将 mixin/component 之间的依赖关系保持在最低限度。例如,尽可能避免从 mixin 调用组件的依赖项

  • 将 mixins 技术与组件组合结合起来。结合使用小型 mixins,您可以利用这两种技术共享代码并维护健康的代码库。

资源

如果您需要任何澄清,或者您认为某些内容不清楚或错误,请发表评论!

希望你喜欢这篇文章!如果喜欢,请在MediumTwitter或我的网站上关注我,获取更多关于软件开发、前端、RxJS、Typescript 等的文章!

文章来源:https://dev.to/gc_psk/composition-angular-components-with-typescript-mixins-dn3
PREV
Symfony 4 是 PHP 框架中的新 Boss 介绍组件来自翻译包 Doctrine 注释迁移服务记录器性能总结
NEXT
开始使用 fp-ts:Eq