首页
/ Angular 可复用动画:用 animation() 与 useAnimation() 打造可共享的动画定义

Angular 可复用动画:用 animation() 与 useAnimation() 打造可共享的动画定义

2026-09-06 13:07:32作者:谭伦延

本篇聚焦 Angular 官方文档中"可复用动画(Reusable animations)"这一主题:讲解如何用 @angular/animations 提供的 animation() 函数把一段动画定义抽取到独立 .ts 文件中并导出为常量,再通过 useAnimation() 在多个组件中复用。读完本文,你将掌握如何组织可复用动画的模块结构、如何替换运行时参数(params)、如何在 @Component 中挂载 trigger 使其生效,并能结合仓库源码理解 animation() / useAnimation() 的底层元数据结构。

重要前提:@angular/animations 包在当前仓库中已被标记为弃用(deprecated)。官方团队建议新代码使用原生 CSS 配合 animate.enteranimate.leave 来实现动画。若你正在维护既有动画代码,本文的可复用写法仍然适用;若从零开始,建议先阅读迁移指南enter/leave 动画概览

为什么需要"可复用"的动画

Angular 的动画系统构建在 CSS 能力之上,可以动画化浏览器认为可动画化的任意属性(位置、尺寸、transform、颜色、边框等)。在实际应用中,同一段"过渡动画"往往需要被多个组件共享,例如统一的开合(open/close)效果、列表项的高度变化等。如果每次都把动画写死在每个组件里,会造成大量重复代码且难以统一维护。

@angular/animations 提供了两个专门用于"复用"的函数:

函数 作用
animation() 生成一段可复用的动画定义(返回 AnimationReferenceMetadata),供别处通过 useAnimation() 调用。
useAnimation() 激活一段可复用动画(返回 AnimationAnimateRefMetadata),与 animation() 配合使用。

这套"定义一次、处处引用"的模式,就是本文要展开的核心。

第一步:用 animation() 定义并导出可复用动画

要创建可复用动画,做法是用 animation() 函数在独立的 .ts 文件中定义动画,并把这段动画定义声明为一个导出的 const 变量。仓库中的示例文件 animations.1.ts 展示了这一点:

// animations.1.ts
import {
  animation,
  style,
  animate,
  trigger,
  transition,
  useAnimation,
} from '@angular/animations';

export const transitionAnimation = animation([
  style({
    height: '{{ height }}',
    opacity: '{{ opacity }}',
    backgroundColor: '{{ backgroundColor }}',
  }),
  animate('{{ time }}'),
]);

这里有两个关键点值得注意:

  1. transitionAnimation 通过 export 变成一个可被其他模块引用的常量,这就是"可复用"的来源。
  2. {{ height }}{{ opacity }}{{ backgroundColor }}{{ time }} 这些 {{ ... }} 占位符会在运行时被替换。换句话说,动画定义本身不写死具体数值,而是留出一组"插槽",由调用方在激活时填充。

不依赖参数的可复用动画

如果你希望这段动画是"固定样式"、不需要调用方传参,可以直接把具体值写进定义。同一示例文件中还给出了一个不含占位符的版本:

export const sharedAnimation = animation([
  style({
    height: 0,
    opacity: 1,
    backgroundColor: 'red',
  }),
  animate('1s'),
]);

可以看到,animation() 的入参就是普通动画步骤数组(这里由 style()animate() 组成)。它既可以承载带占位符的"参数化"动画,也可以承载完全固定的动画,二者只是取值方式不同。

第二步:用 trigger() 导出"动画触发器"

除了导出"动画步骤",你还可以把整段 trigger(含状态与过渡)也作为常量导出,方便多处直接引用。示例文件中接着给出了一个触发器定义:

// animations.1.ts
export const triggerAnimation = trigger('openClose', [
  transition('open => closed', [
    useAnimation(transitionAnimation, {
      params: {
        height: 0,
        opacity: 1,
        backgroundColor: 'red',
        time: '1s',
      },
    }),
  ]),
]);

这里体现了完整的"参数替换"流程:

  • transition('open => closed', [...]) 声明了从 open 状态到 closed 状态的单向过渡;
  • useAnimation(transitionAnimation, { params: {...} }) 激活第一步导出的可复用动画,并在 params 对象中为每个占位符提供具体取值——height: 0opacity: 1backgroundColor: 'red'time: '1s'
  • 这些 params 的键名必须与定义里的 {{ ... }} 占位符一一对应。

这样,triggerAnimation 本身就是一个自包含、可整体复用的动画单元。

第三步:在组件中导入并复用

定义好可复用变量后,即可把它们导入组件类并使用。示例组件 open-close.3.ts 展示了导入 transitionAnimation 并通过 useAnimation() 挂载到组件的过程:

// open-close.3.ts
import { Component, input } from '@angular/core';
import { transition, trigger, useAnimation, AnimationEvent } from '@angular/animations';
import { transitionAnimation } from './animations';

@Component({
  selector: 'app-open-close-reusable',
  animations: [
    trigger('openClose', [
      transition('open => closed', [
        useAnimation(transitionAnimation, {
          params: {
            height: 0,
            opacity: 1,
            backgroundColor: 'red',
            time: '1s',
          },
        }),
      ]),
    ]),
  ],
  templateUrl: 'open-close.html',
  styleUrls: ['open-close.css'],
})
export class OpenCloseBooleanComponent {
  isOpen = false;

  toggle() {
    this.isOpen = !this.isOpen;
  }

  logging = input(false);
  onAnimationEvent(event: AnimationEvent) {
    if (!this.logging()) {
      return;
    }
  }
}

这段代码的复用要点在于:

  • animations: 元数据属性位于 @Component() 装饰器内部,用于声明组件用到的触发器。正如概览文档所述,动画定义写在"控制被动画 HTML 元素的组件"的元数据里。
  • trigger('openClose', [...]) 给动画起了唯一名字 openClose,随后即可在模板里用 [@openClose]="expression" 的形式把触发器绑定到具体元素上;当表达式值变化到某个已定义状态时,动画被触发执行。
  • useAnimation(transitionAnimation, { params }) 是复用落地的关键:组件并没有重新书写动画步骤,而是"引用"了外部导出的常量,并传入本次使用的具体参数。

对比 open-close.3.tsanimations.1.ts 可以清楚看到:组件只需一行 import { transitionAnimation } from './animations',就能把定义复用到任意数量的组件中,这正是可复用动画相对"内联写法"的核心优势。

源码视角:animation() 与 useAnimation() 的底层结构

为了印证上文"占位符在运行时被替换、参数经 params 传入"的机制,可以直接查看 @angular/animations 包的源码实现。在 animation_metadata.ts 中,这两个函数分别位于第 1154 行与第 1200 行附近,其本质是生成两类动画元数据对象

// packages/animations/src/animation_metadata.ts (约 L1154)
export function animation(
  steps: AnimationMetadata | AnimationMetadata[],
  options: AnimationOptions | null = null,
): AnimationReferenceMetadata {
  return { type: AnimationMetadataType.Reference, animation: steps, options };
}
// packages/animations/src/animation_metadata.ts (约 L1200)
export function useAnimation(
  animation: AnimationReferenceMetadata,
  options: AnimationOptions | null = null,
): AnimationAnimateRefMetadata {
  return { type: AnimationMetadataType.AnimateRef, animation, options };
}

从源码结构看,可以得出几点实现层面的事实:

  • animation() 的返回值类型为 AnimationReferenceMetadata,它把"步骤数组 + 选项"打包成一个带 Reference 类型标记的对象,代表一段"可被引用的动画"。
  • useAnimation() 的返回值类型为 AnimationAnimateRefMetadata,其内部持有对前者的引用(animation 字段)以及自己的 options(即运行时 params)。AnimationOptions 允许开发者提供"额外覆盖值",这正是 {{ ... }} 占位符被替换的入口。
  • 因此"定义—激活"两步走,本质上是先生成一个可引用对象,再在激活点带上参数把它实例化进具体的 transition 序列中。这也解释了为什么占位符必须与 params 的键一一对应——参数替换发生在激活环节而非定义环节。

小结与延伸阅读

本文沿着官方文档"Reusable animations"的脉络,完整走通了可复用动画的三段式流程:用 animation() 定义并导出常量、用 useAnimation() 激活并填充 params、在组件的 animations: 元数据中通过 trigger() 挂载。配合 animation_metadata.ts 的源码,可以进一步确认这两个函数在元数据层面的引用关系,以及运行时参数替换的落点。

需要再次强调:@angular/animations 在当前仓库中已被标记弃用,官方推荐新代码使用原生 CSS 与 animate.enter / animate.leave。相关深入内容可参考仓库内的:

登录后查看全文
热门项目推荐
相关项目推荐