首页
/ Angular `animate.enter` 与 `animate.leave`:用 CSS 声明式动画驾驭元素进离场

Angular `animate.enter` 与 `animate.leave`:用 CSS 声明式动画驾驭元素进离场

2026-09-06 12:54:08作者:庞队千Virginia

本篇指南聚焦 Angular 编译器直接支持的 animate.enteranimate.leave 两个动画 API,系统讲解如何用纯 CSS 类(过渡或关键帧)驱动元素的进入与离场动画、如何处理事件绑定与第三方动画库的集成,以及旧版动画的兼容边界和测试策略。读完后你将能在项目中落地声明式进离场动画,并理解类清理时机、元素移除顺序、MAX_ANIMATION_TIMEOUT 超时保护等底层机制。

为什么需要进离场动画

设计良好的动画不只是装饰。Angular 官方文档指出,动画能从多个方面改善应用与用户体验:

  • 没有动画时,页面元素的切换会显得生硬突兀;
  • 动效增强了用户与应用交互的反馈感知,让用户确认应用响应了他们的操作;
  • 恰当的动画能在工作流中平滑地引导用户的注意力。

animate.enteranimate.leave 是 Angular 提供的、专门用于"元素进入/离开 DOM"这一场景的 API。它们在恰当的时机为元素应用(或移除)CSS 类,或者调用你提供的函数来驱动第三方库的动画。

一个关键认知:这两个特性不是指令(directive),而是由 Angular 编译器直接支持的特殊 API。它们可以直接写在内联模板的元素上,也可以作为宿主绑定(host binding)使用。其核心实现位于 packages/core/src/render3/instructions/animation.ts,编译器会将模板中的 animate.enter / animate.leave 编译为 ɵɵanimateEnter / ɵɵanimateLeaveListener 等指令调用。

animate.enter:元素进入动画

animate.enter 用于在元素进入 DOM 时播放动画,可以用 CSS 类定义过渡(transition)或关键帧(keyframe)动画。

官方示例(源码见 enter.tsenter.htmlenter.css):

enter.ts

import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-enter',
  templateUrl: 'enter.html',
  styleUrls: ['enter.css'],
})
export class Enter {
  isShown = signal(false);

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }
}

enter.html

<button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>

@if (isShown()) {
  <div class="enter-container" animate.enter="enter-animation">
    <p>The box is entering.</p>
  </div>
}

enter.css

.enter-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px;
  font-weight: bold;
  font-size: 20px;
}

/* 进入动画类:由 animate.enter 在元素插入 DOM 时添加 */
.enter-animation {
  animation: slide-fade 1s;
}

@keyframes slide-fade {
  from {
    opacity: 0;
    transform: translateY(20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

动画类何时被移除

动画播放完成后,Angular 会从 DOM 上移除你在 animate.enter 中指定的类。动画类只在动画进行期间存在——这是它与普通模板类绑定的本质区别:动画结束即清理,不会污染元素的静态样式。

注意:当元素同时使用了多个关键帧动画或多个过渡属性时,Angular 只有在最长的动画完成之后才会移除所有类。从源码看,这一"最长动画"判定发生在 animation.ts 中的 determineLongestAnimation 逻辑里,框架会读取计算样式中各动画的 duration 并取最大值,再结合 animationend / transitionend 事件决定类清理时机。

与 Angular 其他特性组合

animate.enter 可以与任何 Angular 特性组合使用,例如控制流(@if)和动态表达式。其取值形式支持两种:

  • 单个类名字符串(多个类用空格分隔);
  • 字符串数组。

当需要动态指定动画类时,可以使用绑定形式 [animate.enter]。官方示例(enter-binding.html)中,组件把动画类名放在信号里动态提供:

enter-binding.ts

import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-enter-binding',
  templateUrl: 'enter-binding.html',
  styleUrls: ['enter-binding.css'],
})
export class EnterBinding {
  isShown = signal(false);

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }

  // 动画类名可以是动态的
  enterClass = signal('enter-animation');
}

enter-binding.html

@if (isShown()) {
  <div class="enter-container" [animate.enter]="enterClass()">
    <p>The box is entering.</p>
  </div>
}

使用 CSS transition 时的 @starting-style 注意点

官方文档特别强调了一个容易踩坑的点:如果你选择用 CSS 过渡(transition)而不是关键帧动画来实现进入动画,那么 animate.enter 加到元素上的类表示的是过渡要到达的目标状态。元素自身的基类 CSS 是"没有动画运行时"的样式,通常与过渡的终态相近——因此你需要配合 @starting-style 来为过渡提供合适的起始状态,否则过渡会因为没有起点而直接跳到终态。

animate.leave 的官方 CSS 示例恰好示范了这个模式(见下文 leave.css):基类 .leave-container 中用 @starting-style { opacity: 0; } 声明初始透明度,让元素插入时从透明淡入。

animate.leave:元素离开动画

animate.leave 用于在元素离开 DOM 前播放动画。其工作方式与 animate.enter 相反:当控制流把元素移出模板时,Angular 不会立即删除它,而是先添加离场类、等动画结束再移除元素。

官方示例(leave.tsleave.htmlleave.css):

leave.html

<button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>

@if (isShown()) {
  <div class="leave-container" animate.leave="leaving">
    <p>Goodbye</p>
  </div>
}

leave.css

.leave-container {
  border: 1px solid #dddddd;
  margin-top: 1em;
  padding: 20px;
  font-weight: bold;
  font-size: 20px;
  opacity: 1;
  transition: opacity 200ms ease-in;

  /* 配合 transition 提供进入时的起始状态 */
  @starting-style {
    opacity: 0;
  }
}

/* 离场类:animate.leave 在移除前添加,动画结束后移除元素 */
.leaving {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 500ms ease-out,
    transform 500ms ease-out;
}

元素何时被移除

动画完成后,Angular 会自动把被动画的元素从 DOM 中删除。同样地,如果元素上有多个关键帧动画或过渡属性,Angular 会等最长的那个动画完成后才移除元素。从 animation.ts 的源码可以看到,离场动画的清理路径还会注册一个兜底定时器:当浏览器因离屏优化或快速 DOM 销毁而丢失 animationend / transitionend 事件时,框架会在最长动画时长后额外 50ms(longest.duration + 50)触发回退清理,保证元素不会永久滞留在 DOM 中。

绑定形式与动态类名

animate.leave 同样支持信号和其他绑定形式,类名可以是空格分隔的字符串或字符串数组([animate.leave]="leaveClass()"),用法与 [animate.enter] 完全对称,参见 leave-binding.html 官方示例。

元素移除顺序:父子离场动画的执行时机

animate.leave 的触发时机有其细节,官方文档明确说明了规则:

  1. animate.leave 写在正在被移除的元素本身上时,离场动画正常播放;
  2. animate.leave 写在正在被移除的元素的后代上,且两者位于同一组件模板内,则子元素的离场动画会先于父节点从 DOM 中移除而播放。

这保证了你可以放心地为子元素编写离场动画,而不用担心父节点"抢跑"消失。官方示例见 leave-parent.htmlleave-parent.ts

重要限制:子元素动画仅对同一组件模板内的元素生效。如果正在移除的元素内部包含子组件,而子组件模板内部定义了 animate.leave,这些动画不会在父节点移除前运行。若需要为子组件播放离场动画,应直接在父模板中、子组件的宿主元素上应用 animate.leave,或者在子组件内以编程方式触发动画、并延迟父节点移除直到动画完成。

事件绑定:函数回调与第三方动画库

animate.enteranimate.leave 都支持事件绑定语法 (animate.enter) / (animate.leave),允许你在组件代码中调用函数,从而接入 GSAP、anime.js 等任意 JavaScript 动画库。

官方示例(leave-event.tsleave-event.html):

leave-event.html

@if (isShown()) {
  <div class="leave-container" (animate.leave)="leavingFn($event)">
    <p>Goodbye</p>
  </div>
}

leave-event.ts

import {AnimationCallbackEvent, Component, signal} from '@angular/core';

@Component({
  selector: 'app-leave-binding',
  templateUrl: 'leave-event.html',
  styleUrls: ['leave-event.css'],
})
export class LeaveEvent {
  isShown = signal(false);

  toggle() {
    this.isShown.update((isShown) => !isShown);
  }

  leavingFn(event: AnimationCallbackEvent) {
    // Example of calling GSAP
    // gsap.to(event.target, {
    //   duration: 1,
    //   x: 100,
    //   // arrow functions are handy for concise callbacks
    //   onComplete: () => event.animationComplete()
    // });
    event.animationComplete();
  }
}

AnimationCallbackEventanimationComplete()

$event 的类型是 AnimationCallbackEvent,包含:

  • target:触发事件对应的 DOM 元素;
  • animationComplete():告知框架动画已经结束。

重要:使用 animate.leave 的事件绑定时,你必须调用 animationComplete(),否则 Angular 不会移除元素。如果你忘了调用,Angular 会在延迟 4 秒后自动替你调用。

这个 4 秒超时由注入令牌 MAX_ANIMATION_TIMEOUT(单位:毫秒)控制,默认值 4000 定义在 packages/core/src/animation/interfaces.ts

{ provide: MAX_ANIMATION_TIMEOUT, useValue: 6000 }

从源码实现(animation.ts)可以看到,runLeaveAnimationFunction 在注册函数动画时会同时启动一个 setTimeout(..., maxAnimationTimeout) 兜底定时器:一旦 event.animationComplete() 被调用,会立即执行清理并 clearTimeout;若函数长时间未回调,定时器到期后框架会强制清理并移除元素,保证 DOM 状态收敛。

另外值得注意的是:即便动画被禁用(如测试环境),ɵɵanimateLeaveListener 仍然会注册元素移除逻辑——源码注释明确说明这是为了确保正确的清理,并允许开发者在测试中处理元素移除。

与旧版 Angular 动画的兼容性

官方文档给出了清晰的兼容边界:

  • 同一组件内不能混用:legacy animations(@angular/animations 的触发器)与 animate.enter / animate.leave 不能在同一组件中共存。混用会导致 enter 类残留在元素上、或离场节点无法被移除。
  • 同一应用内可以共存:在应用的不同组件中分别使用旧版动画和新 API 是没问题的。
  • 内容投影(content projection)是唯一例外:如果你把带有 legacy animations 的组件的投影内容投影到使用了 animate.enter / animate.leave 的组件中(或相反),其效果等同于二者在同一组件中混用——这是不受支持的。

测试策略:TestBed 的动画开关

CSS 动画需要真实浏览器才能运行,且许多动画相关 API 在测试环境中不可用。因此 TestBed 默认在测试环境中禁用动画,让元素直接进/出 DOM,便于断言。

如果需要在浏览器测试(例如端到端测试)中验证动画确实播放,可以在测试配置中显式启用:

TestBed.configureTestingModule({animationsEnabled: true});

这样测试环境中的动画将表现得与生产环境一致。

注意:某些测试环境不会派发 animationstartanimationend 及其过渡事件对应物(transitionstart / transitionend),编写依赖这些事件的断言时要格外小心。

小结与延伸阅读

animate.enteranimate.leave 是 Angular 为"元素生命周期动画"提供的编译器级 API:类字符串或数组声明 CSS 动画、绑定形式动态指定类、事件绑定接入第三方库;框架负责动画类的生命周期管理与元素移除时序,并提供 MAX_ANIMATION_TIMEOUT(默认 4000ms,见 interfaces.ts)兜底保护。落地时请牢记三条边界:同一组件内不与 legacy animations 混用、跨组件投影不受支持、子组件内模板动画不会在父节点移除前触发。

更多相关主题可参考 Angular 官方文档中的"Complex Animations with CSS"与"Route transition animations"章节,以及核心实现 packages/core/src/render3/instructions/animation.ts

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