使用 Typescript Mixins 组合 Angular 组件
优先使用组合而不是继承
了解如何通过使用组合而不是继承来最大化 Angular 组件的可重用性
这是我上一篇关于使用 Angular 进行组件组合的文章的后续,其中我列出了 3 种组合 Angular 组件的方法:
-
类继承
-
类混合
-
组件组成
简而言之,我最喜欢的方式是将组件分成小单元,并使用输入和输出在组件之间进行通信。为了在组件之间共享逻辑片段,我喜欢 Mixins 帮助我们避免使用类继承带来的一些陷阱。
在本文中,我想更多地关注类继承和类 Mixins 之间的关系、它们的区别以及使用 Mixins 构建组件的一些陷阱。
提示:使用**Bit ** ( Github )等工具,跨项目共享和协作 Angular 组件,从而提高代码复用率。将可复用的构建块分享到bit.dev上的合集,以便将来进行组合。
示例: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
}
}
}
我们创建一个通过合并 mixins 函数创建的 Base 类,然后扩展其实现:
const BaseTabMixin = pinMixin(
closeMixin(class {})
);
class Tab extends BaseTabMixin {}
// Tab now can use the methods `close` and `pin`
场景:社交媒体聚合器应用程序
举个例子,我想构建一个社交媒体聚合器应用程序的原型,其中包含来自主要社交媒体服务的帖子信息。
这是我多年前作为初级开发人员遇到的一个特殊例子: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);
}
}
这就是我们可以创建的最小基类。现在,我们可以继续添加具体的操作了。
我们知道 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);
}
}
Youtube 和 Reddit 都允许对帖子进行点赞和点踩;创建一个允许执行此类操作的子类是有意义的:
class VoteablePost extends PostComponent {
downvote() {
this.service.downvote(this.post.id);
}
upvote() {
this.service.upvote(this.post.id);
}
}
Facebook 和 Twitter 还有另一个相似之处:“分享”的概念是关键元数据。
class ShareablePost extends LikeablePost {
get shares() {
return this.post.shares;
}
share() {
this.service.share(this.post.id);
}
}
Youtube、Facebook 和 Reddit 的共同点是它们都允许编辑帖子,这与 Twitter 不同。
这是我们遇到的第一个问题:
-
由于该方法不被所有类共享,因此将其添加到基类是错误的
-
我们可以为所有子类实现方法编辑,但这会非常重复
我们继续实现 TwitterPostComponent
@Component({...})
class TwitterPostComponent extends ShareablePost {}
让我们跳到未来,Jack 给我们带来了一个可怕的消息:我们不能再删除推文了!我们的类现在需要修改,但是等等:delete 是在基类中定义的。
-
如果我们从基类中删除该方法,我们将破坏其他类
-
如果我们只从 TwitterBaseComponent 中删除它,我们最终会破坏 Liskov 替换原则,这意味着 TwitterBaseComponent 和 PostComponent 应该能够互换而不会破坏任何东西
如果现在还不够清楚的话,这一切都是一个坏主意。
进入作曲
现在,我们将通过组合小类并使用 Typescript mixins 创建由许多独立的小类组成的组件来重写所有以前的内容。
让我们创建组件 TwitterPostComponent 所需的混合:likeMixin、deleteMixin 和 shareMixin。
基类
首先,我们希望 mixin 足够通用,可以应用于各种组件,并且单一依赖项是注入到组件的服务。
export interface PostComponent {
post: Post;
service: PostService;
}
喜欢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);
}
};
}
删除Mixin
function deleteMixin<IBasePost extends Constructor<PostComponent>>(
BasePost: IBasePost
) {
return class extends BasePost implements CanDelete {
delete() {
return this.service.delete(this.post.id);
}
};
}
分享混音
*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);
}
};
}
创建实现组件:TwitterPostComponent
一旦创建,我们就可以将它们应用到新创建的 TwitterPostComponent:
const TwitterBase = deleteMixin(
likeMixin(
shareMixin(PostComponent)
)
);
如果您更喜欢使用Typescript 自己的文档中描述的 applyMixins 函数,则可以执行以下操作:
class TwitterBase extends PostComponent {}
interface TwitterBase extends CanLike, CanDelete, CanShare {}
applyMixins(TwitterBase, [
shareMixin,
likeMixin,
deleteMixin
]);
一旦创建了由 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 {}
为了从 Tweets 组件中删除删除功能,我们不需要做太多事情 - 我们只需从我们的类中删除 deleteMixin mixin:
const TwitterBase = likeMixin(
shareMixin(PostComponent)
)
);
使用 Mixins 的陷阱
Mixins 很棒,但它并非万无一失的工具。虽然我仍然更喜欢 Mixins 而不是多重继承,但理解使用这项技术的含义仍然很重要。
这篇React 博客文章很好地解释了为什么 Mixins 不再被视为 React 中的最佳实践:
-
Mixins 会创建隐式依赖关系:调用组件方法的 mixins、引用组件属性的 mixins 或需要 mixin 才能正常工作的组件都相互依赖
-
Mixins 一开始很小但随着时间的推移不断增长
-
Mixins 导致名称冲突
当然,由于相似性,这些也适用于与 Angular 组件一起使用的 Typescript mixin。
如何避免这些陷阱?
-
尽量不要使用太多的 mixin;如果有太多的 mixin,也许你应该将组件拆分成几个组件,并使用带有输入和输出的组件组合来相互通信。
-
尽量保持它们尽可能小
-
将 mixin/component 之间的依赖关系保持在最低限度。例如,尽可能避免从 mixin 调用组件的依赖项
-
将 mixins 技术与组件组合结合起来。结合使用小型 mixins,您可以利用这两种技术共享代码并维护健康的代码库。
资源
-
Angular Material是一个使用 mixins 的库,因此我建议你查看它们的组件,看看它们如何在各种情况下使用
如果您需要任何澄清,或者您认为某些内容不清楚或错误,请发表评论!
希望你喜欢这篇文章!如果喜欢,请在Medium、Twitter或我的网站上关注我,获取更多关于软件开发、前端、RxJS、Typescript 等的文章!
文章来源:https://dev.to/gc_psk/composition-angular-components-with-typescript-mixins-dn3