我们都知道 Lodash,它是一个在项目中重用无状态逻辑的库。那么,如果在 Angular 项目中我们有一个类似的工具包来重用有状态逻辑呢?
Composables 并不是一个新的概念,它是来自 Vue.js 的一个概念。我在这篇博客中使用的许多示例和想法直接来自 Vue.js Composables 文档。
在版本 16.0.0-next.0 中,Angular 团队引入了 Signals 的实现,Signals 是一种反应性的基本组件,可以在 Angular 中提供精细的反应性能力。随着这样的重大变化,还考虑到 Angular 团队在最新版本中引入的其他非常有用的功能,比如 inject 函数或 DestroyRef 的概念,不可避免地会出现新的模式。本文试图在 Angular 的上下文中探索这个模式。
在 Angular 自身中,我们已经看到了我们可以称之为“功能型服务(Functional Services)”的过渡。它始于版本 14.2.0 中功能型守卫(functional guards)和解析器(resolvers)的引入,继续于版本 15.0.0 中功能型拦截器(functional interceptors)的引入。但是什么是 Angular Composable,为什么以及如何在项目中使用它?
什么是 Angular Composable?
在 Angular 应用程序的上下文中,一个 composable 是一个使用 Signals API 封装有状态逻辑的函数。这些可组合函数可以在多个组件中重复使用,可以相互嵌套,并且可以帮助我们将组件的有状态逻辑组织成小型、灵活和简单的单元。
与我们创建 util 函数以在组件之间重用无状态逻辑的方式相同,我们创建 composable 以共享有状态逻辑。
但是让我们看看在 Angular 应用程序中如何编写一个 composable。在下面的示例中,我没有使用 Angular Signals RFC 中提议的 API。当此 API 的所有功能就位时(例如应用程序渲染生命周期钩子,基于 Signal 的查询),我们将能够以更好的方式编写这些可组合函数,并能够为它们提供更多功能。
让我们从一个非常简单的示例开始。
Mouse Tracker Example 使用 Signals 在 Angular Component 里实现 mouse tracking 功能: @Component({ standalone: true, template: ` {{ x() }} {{ y() }} `, }) export class MouseTrackerComponent implements AfterViewInit, OnDestroy { // injectables document = inject(DOCUMENT); // state encapsulated and managed by the composable x = signal(0); y = signal(0); ngAfterViewInit() { document.addEventListener('mousemove', this.update.bind(this)); } // a composable can update its managed state over time. update(event: MouseEvent) { this.x.update(() => event.pageX); this.y.update(() => event.pageY); } ngOnDestroy() { document.removeEventListener('mousemove', this.update.bind(this)); } }
可以对其重构,增加通用性:
// mouse-tracker.ts file export function useMouse() { // injectables const document = inject(DOCUMENT); // state encapsulated and managed by the composable const x = signal(0); const y = signal(0); // a composable can update its managed state over time. function update(event: MouseEvent) { x.update(() => event.pageX); y.update(() => event.pageY); } document.addEventListener('mousemove', update); // lifecycle to teardown side effects. inject(DestroyRef).onDestroy(() => document.removeEventListener('mousemove', update) ); // expose managed state as return value return { x, y }; }
其他 Component 也可以重用了:
@Component({ standalone: true, template: ` {{ mouse.x() }} {{ mouse.y() }} `, }) export class MouseTrackerComponent { mouse = useMouse(); }