Angular `animate.enter` 与 `animate.leave`:用 CSS 声明式动画驾驭元素进离场
本篇指南聚焦 Angular 编译器直接支持的 animate.enter 与 animate.leave 两个动画 API,系统讲解如何用纯 CSS 类(过渡或关键帧)驱动元素的进入与离场动画、如何处理事件绑定与第三方动画库的集成,以及旧版动画的兼容边界和测试策略。读完后你将能在项目中落地声明式进离场动画,并理解类清理时机、元素移除顺序、MAX_ANIMATION_TIMEOUT 超时保护等底层机制。
为什么需要进离场动画
设计良好的动画不只是装饰。Angular 官方文档指出,动画能从多个方面改善应用与用户体验:
- 没有动画时,页面元素的切换会显得生硬突兀;
- 动效增强了用户与应用交互的反馈感知,让用户确认应用响应了他们的操作;
- 恰当的动画能在工作流中平滑地引导用户的注意力。
animate.enter 和 animate.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.ts、enter.html、enter.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.ts、leave.html、leave.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 的触发时机有其细节,官方文档明确说明了规则:
animate.leave写在正在被移除的元素本身上时,离场动画正常播放;- 若
animate.leave写在正在被移除的元素的后代上,且两者位于同一组件模板内,则子元素的离场动画会先于父节点从 DOM 中移除而播放。
这保证了你可以放心地为子元素编写离场动画,而不用担心父节点"抢跑"消失。官方示例见 leave-parent.html 与 leave-parent.ts。
重要限制:子元素动画仅对同一组件模板内的元素生效。如果正在移除的元素内部包含子组件,而子组件模板内部定义了
animate.leave,这些动画不会在父节点移除前运行。若需要为子组件播放离场动画,应直接在父模板中、子组件的宿主元素上应用animate.leave,或者在子组件内以编程方式触发动画、并延迟父节点移除直到动画完成。
事件绑定:函数回调与第三方动画库
animate.enter 和 animate.leave 都支持事件绑定语法 (animate.enter) / (animate.leave),允许你在组件代码中调用函数,从而接入 GSAP、anime.js 等任意 JavaScript 动画库。
官方示例(leave-event.ts、leave-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();
}
}
AnimationCallbackEvent 与 animationComplete()
$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});
这样测试环境中的动画将表现得与生产环境一致。
注意:某些测试环境不会派发
animationstart、animationend及其过渡事件对应物(transitionstart/transitionend),编写依赖这些事件的断言时要格外小心。
小结与延伸阅读
animate.enter 与 animate.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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00