☰
Freelens UI 动画体系解析:@freelensapp/animate 组件原理、API 与扩展指南
2026/10/12 1:27:48 网站建设 项目流程
  • 云原生
  • 开发工具
  • 运维

【免费下载链接】freelens

Free IDE for Kubernetes

项目地址:https://gitcode.com/gh_mirrors/fr/freelens
点击查看免费下载

导读

@freelensapp/animate是 Freelens(Free IDE for Kubernetes)UI 组件库packages/ui-components中负责进出场动画的独立子包,提供AnimateReact 组件、预置动画名称以及一套基于依赖注入(DI)的可扩展配置(默认动画时长、requestAnimationFrame封装等)。本文以该包的官方 README 为骨架,结合仓库内的组件实现(animate.tsx)、样式定义(animate.scss)和 Freelens 核心应用中的真实使用场景(Dialog、Drawer、Menu),系统讲解其安装方式、完整 API、三种内置动画的实现原理、自定义动画的扩展路径,以及它在 Freelens 渲染进程中的 DI 注册方式,帮助你在自己的 Freelens 扩展或基于该组件库的应用中正确使用并深度定制动画。

一、包概览与定位

@freelensapp/animate是 Freelens 仓库中packages/ui-components/animate目录对应的 npm 包,其package.json中描述为"Highly extendable animate in the Freelens"(Freelens 中高度可扩展的动画组件)。包内结构非常精简,核心文件如下:

文件作用
src/animate.tsxAnimate组件的核心实现
src/animate.scss预置动画的 SCSS 定义
src/feature.tsanimateFeature特性对象,用于 DI 注册
src/register-injectables.ts显式注册包内三个 injectable
src/default-enter-duration.injectable.ts默认进入动画时长(100ms)
src/default-leave-duration.injectable.ts默认离开动画时长(100ms)
src/request-animation-frame.injectable.ts对requestAnimationFrame的 DI 封装
index.ts包的公共导出入口

该包与 Freelens 其他 UI 组件(button、icon、tooltip、notifications等)同属packages/ui-components工作区,遵循docs/styling.md中约定的共享组件规范:全局 PascalCase 类名 + 原生 SCSS +var(--...)主题变量,不使用 Tailwind 或 CSS Modules(因为类名如.Animate是扩展可覆盖的公共 API)。

二、安装与导入

根据 packages/ui-components/animate/README.md,该包通过 npm 安装:

npm install @freelensapp/animate

在 Freelens 的 pnpm workspace 中,它作为workspace:^依赖被@freelensapp/feature-core、@freelensapp/utilities、@ogre-tools/injectable、@ogre-tools/injectable-react、mobx、mobx-react和react支撑(见 package.json)。

导入方式(与 README 示例一致,均从包根导出):

// 特性对象,用于注册到 DI 容器 import { animateFeature } from "@freelensapp/animate"; // 可注入的 requestAnimationFrame 封装(与 animateFeature 可同时导入) import { animateFeature, requestAnimationFrameInjectable } from "@freelensapp/animate"; // 动画组件本体 import { Animate } from "@freelensapp/animate"; // 类型:动画名称 import type { AnimateName } from "@freelensapp/animate"; // 类型:requestAnimationFrame 回调签名 import type { RequestAnimationFrame } from "@freelensapp/animate";

注意 index.ts 还额外导出了defaultEnterDurationForAnimatedInjectable和defaultLeaveDurationForAnimatedInjectable,并对外暴露了./styles子路径入口("./styles": "./src/animate.scss"),方便按需引入动画样式。

三、Animate 组件:完整 API

Animate是一个“包装”组件:它接收恰好一个子元素,并在子元素上追加动画相关的 className 与 CSS 变量。完整接口定义在 src/animate.tsx:

export type AnimateName = "opacity" | "slide-right" | "opacity-scale" | string; export interface AnimateProps { name?: AnimateName; // 预置动画名(对应 CSS 类) enter?: boolean; // 是否处于进入状态 onEnter?: () => void; // 进入动画开始时回调 onLeave?: () => void; // 离开动画开始时回调 enterDuration?: number; // 进入动画时长(ms) leaveDuration?: number; // 离开动画时长(ms) children?: StrictReactNode; }

3.1 属性详解与默认值

结合NonInjectedAnimate的解构逻辑(animate.tsx),各属性的默认行为如下:

属性默认值说明
entertrue为true时执行进入动画并渲染子元素;切换为false时先播放离开动画,再在leaveDuration后卸载子元素
name"opacity"使用的预置动画名,最终会作为额外 className 追加到子元素上
enterDurationdefaultEnterDuration(DI 注入,默认100ms)进入动画时长,以 CSS 变量--enter-duration形式写入子元素 style
leaveDurationdefaultLeaveDuration(DI 注入,默认100ms)离开动画时长,以 CSS 变量--leave-duration形式写入子元素 style
onEnternoop(空操作)进入动画开始时触发
onLeavenoop(空操作)离开动画开始时触发
children—必须是唯一的子元素(React.Children.only强校验),否则抛错

默认时长来自两个独立 injectable:default-enter-duration.injectable.ts 与 default-leave-duration.injectable.ts,两者都instantiate: () => 100。这正是“可扩展”的关键:你可以通过 override 这两个 injectable 来全局改变所有 Animate 动画的默认时长,无需修改任何组件代码。

3.2 最小用法示例

import { Animate } from "@freelensapp/animate"; // 最简单的进入动画(默认 opacity,100ms) <Animate> <div>Hello, Kubernetes</div> </Animate> // 指定动画名与时长 <Animate name="slide-right" enter={open} enterDuration={250} leaveDuration={200}> <aside className="my-panel">...</aside> </Animate> // 监听动画生命周期 <Animate name="opacity-scale" enter={this.isOpen} onEnter={() => console.log("动画进入开始")} onLeave={() => console.log("动画离开开始")} > {content} </Animate>

四、三种预置动画:SCSS 实现剖析

Animate的视觉表现完全由 animate.scss 驱动。根类.Animate定义了公共行为(&:empty { display: none; },即无内容时不渲染占位),然后按name追加.opacity/.slide-right/.opacity-scale类,配合&.enter与&.leave状态类完成进出场过渡。

4.1 opacity:淡入淡出

@mixin animate-opacity($enterDuration: var(--enter-duration), $leaveDuration: var(--leave-duration)) { opacity: 0; &.enter { transition-property: opacity; transition-duration: $enterDuration; opacity: 1; } &.leave { transition-duration: $leaveDuration; transition-timing-function: ease-out; opacity: 0; } }

进入时元素从opacity: 0过渡到1,离开时以ease-out缓动回到0。这是默认动画,也是开销最小的一种。

4.2 slide-right:从右侧滑入

@mixin animate-slide-right($enterDuration: var(--enter-duration), $leaveDuration: var(--leave-duration)) { transform: translateX(100%); will-change: transform; &.enter { transform: translateX(0); transition: transform $enterDuration; transition-timing-function: ease-in-out; } &.leave { transform: translateX(100%); transition: transform $leaveDuration; } }

初始位置在容器右侧之外(translateX(100%)),进入时平移到translateX(0),离开时滑回右侧。will-change: transform提示浏览器对该元素做合成层优化,避免动画期间反复重排。该动画常被 Freelens 的侧滑抽屉(Drawer)使用。

4.3 opacity-scale:淡入 + 缩放

@mixin animate-opacity-scale($enterDuration: var(--enter-duration), $leaveDuration: var(--leave-duration)) { opacity: 0; &.enter { transition: opacity $enterDuration; opacity: 1; } &.leave { will-change: opacity, transform; opacity: 0; transform: scale(1.25); transition: transform $leaveDuration ease-in, opacity $leaveDuration ease-out; } }

进入时仅淡入,离开时同时淡出并放大到scale(1.25),配合ease-in/ease-out产生一种“消散”效果。这是 Freelens 对话框(Dialog)使用的动画。

4.4 时长如何生效:CSS 变量桥接

三个 mixin 的默认参数都取var(--enter-duration)/var(--leave-duration),而这两个 CSS 变量正是Animate组件在渲染时通过内联 style 写入的:

const cssVarsForAnimation = { "--enter-duration": `${enterDuration}ms`, "--leave-duration": `${leaveDuration}ms`, } as React.CSSProperties;

(见 animate.tsx)。因此props 中的enterDuration/leaveDuration会被注入子元素的 style,再由 SCSS 中的var()消费,实现“属性驱动时长、样式保持解耦”的桥接设计。传入的数值以毫秒为单位并自动拼接ms后缀。

五、Animate 的运作原理:状态机 + cloneElement

从 animate.tsx 可以看到整个组件是一个轻量状态机:

  1. 三个状态:isVisible(是否挂载子元素)、showClassNameEnter(是否追加.enter)、showClassNameLeave(是否追加.leave)。
  2. 进入流程:enter变为true时,先设置isVisible = true让子元素挂载(此时 class 尚未带.enter,元素处于opacity: 0等初始态);然后在下一帧(通过注入的requestAnimationFrame)追加.enter类并调用onEnter。之所以延迟一帧,是为了让浏览器先完成初始样式排版,CSS transition 才能真正触发。
  3. 离开流程:enter变为false且元素当前可见时,追加.leave类并调用onLeave;同时启动window.setTimeout(leaveDuration)定时器,时长结束后清空全部状态并卸载子元素,且返回清理函数在组件卸载时clearTimeout防泄漏。
  4. 渲染:使用React.cloneElement(contentElem, { className, style })将合并后的类名与动画 CSS 变量回传给唯一的子元素,而不改变子元素的 children(children: contentElem.props.children),保持子树引用不变。

一个值得注意的实现细节是requestAnimationFrame没有直接调用全局requestAnimationFrame,而是通过 injectable 注入——request-animation-frame.injectable.ts 中甚至专门注释了“不能简化为=> requestAnimationFrame,否则会抛出 Illegal Invocation 错误”,因此包了一层箭头函数。这既是可测试性的体现(测试中可注入假实现),也避免了浏览器环境下的调用上下文问题。

六、可扩展性:自定义动画与 DI 覆盖

README 中专门有 "Extendability"(可扩展性)一节。虽然该节未展开细节,但结合源码可以梳理出三条明确的扩展路径:

6.1 自定义动画名:利用AnimateName的 string 放宽

AnimateName = "opacity" | "slide-right" | "opacity-scale" | string,最后一项string意味着任何自定义字符串都能作为name传入。组件会把它直接拼进 className:

const classNames = cssNames("Animate", name, contentElem.props.className, { enter: showClassNameEnter, leave: showClassNameLeave, });

于是你只需在自己的全局样式表(遵循docs/styling.md的公共 API 约定,使用非 scoped 的全局 SCSS)中按同样模式编写规则:

.Animate { &.my-custom-fade { @include animate-opacity; // 复用包内 mixin(需要引入 animate.scss) } // 或完全手写 &.rotate-in { opacity: 0; transform: rotate(-6deg); &.enter { opacity: 1; transform: rotate(0); transition: all var(--enter-duration) ease-out; } &.leave { opacity: 0; transform: rotate(6deg); transition: all var(--leave-duration) ease-in; } } }
<Animate name="rotate-in" enter={open}> <div>自定义旋转动画</div> </Animate>

三个 mixin(animate-opacity、animate-slide-right、animate-opacity-scale)的参数化设计(默认读 CSS 变量、可显式传时长)就是为这类复用准备的。

6.2 全局默认时长:override injectable

由于Animate的默认时长来自 DI 注入,你可以在注册animateFeature后对以下 injectable 做 override:

  • defaultEnterDurationForAnimatedInjectable
  • defaultLeaveDurationForAnimatedInjectable
  • requestAnimationFrameInjectable

例如在测试环境中用假的时间控制函数替换requestAnimationFrame,或在应用层面统一把默认时长从 100ms 改为 200ms,即可全局生效。

6.3 特性注册:animateFeature

Animate本体通过withInjectables(来自@ogre-tools/injectable-react)把三个依赖注入到内部实现(animate.tsx):

export const Animate = withInjectables<Dependencies, AnimateProps>(NonInjectedAnimate, { getProps: (di, props) => ({ ...props, requestAnimationFrame: di.inject(requestAnimationFrameInjectable), defaultEnterDuration: di.inject(defaultEnterDurationForAnimatedInjectable), defaultLeaveDuration: di.inject(defaultLeaveDurationForAnimatedInjectable), }), });

而 feature.ts 按照@freelensapp/feature-core的getFeature契约(见 feature-core/src/feature.ts,要求提供id与register(di)回调)定义了animateFeature,其register委托给 register-injectables.ts 显式注册上述三个 injectable(每个注册都有 try/catch 忽略重复注册)。在 Freelens 渲染进程的测试容器中,可以看到它与其他 feature 一起注册:

import { animateFeature, requestAnimationFrameInjectable } from "@freelensapp/animate"; import { registerFeature } from "@freelensapp/feature-core"; registerFeature( di, messagingFeature, routingFeature, loggerFeature, animateFeature, // <-- 这里 clusterSidebarFeature, randomFeature, kubeApiSpecificsFeature, notificationsFeature, );

(见 renderer/getDiForUnitTesting.tsx)

七、Freelens 核心应用中的真实使用场景

Animate不是孤立组件,Freelens 渲染进程的多个核心 UI 组件都在使用它:

7.1 Dialog(对话框):opacity-scale

renderer/components/dialog/dialog.tsx 中,当animated为真时,对话框内容被包装进Animate:

if (animated) { dialog = ( <Animate enter={this.isOpen} name="opacity-scale"> {dialog} </Animate> ); } else if (!this.isOpen) { return null; } return createPortal(dialog, document.body);

注意这里Animate包装的是即将通过createPortal挂到document.body的对话框节点,进出场动画与 Portal 结合使用。

7.2 Drawer(侧滑抽屉):自定义动画名

renderer/components/drawer/drawer.tsx 中,抽屉面板被包进<Animate name={animation} enter={open}>。animation是AnimateName类型的 prop,也就是说Drawer 的动画名本身对外开放,调用方可以传入"slide-right"或其他任意自定义值:

const drawer = ( <Animate name={animation} enter={open}> <div className={cssNames("Drawer", className, position)} style={{ "--size": drawerSize } as React.CSSProperties} ...> ... </div> </Animate> );

这与侧滑抽屉的slide-right动画语义天然契合。

7.3 Menu(右键菜单):默认 opacity

renderer/components/menu/menu.tsx 中,菜单在animated时使用默认动画(name缺省即opacity):

if (animated) { menu = <Animate enter={this.isOpen}>{menu}</Animate>; }

文件中的注释还揭示了一个实践细节:由于 React 18 下 Portal 菜单通过<Animate>挂载会晚一个渲染周期,bindRef里在元素真正挂载后需要重新执行一次refreshPosition(),否则菜单会停留在屏幕外的默认位置(见 menu.tsx)。这说明使用Animate时若需要测量布局,要注意元素挂载是延迟一帧的。

7.4 测试场景:挂载延迟的显式处理

在 scale/dialog.test.tsx、scale-dialog/dialog.test.tsx 等测试中都有一句相同注释:对话框刚挂载时<Animate />渲染null。这是测试编写时需要注意的时序问题——需要等待动画进入完成后再断言内容。

八、样式入口与使用注意点

  • 样式引入:组件文件顶部有副作用导入import "./animate.scss";同时package.json暴露"./styles": "./src/animate.scss"子路径,供需要直接引入动画样式(包括复用三个 mixin)的使用方使用。
  • 必须唯一子元素:React.Children.only(children)强制children为单个元素,传入多个子节点会抛错;children的类型为StrictReactNode(@freelensapp/utilities中定义,见 isReactNode.ts),涵盖 ReactElement、字符串、数字、可迭代片段、Portal、布尔与 null。
  • 样式规范约束:按照 docs/styling.md 的约定,作为共享组件,Animate的.Animate类名是扩展可覆盖的公共 API,扩展开发者可以直接在自己的全局样式中针对.Animate追加自定义动画规则。
  • 时序语义:进入动画从“下一帧”开始,离开动画在leaveDuration后卸载;若希望在动画结束后做清理或测量,应使用onEnter/onLeave回调,而不是依赖元素同步挂载。

九、总结

@freelensapp/animate用不到 200 行代码(组件 + 样式 + 三个 injectable)提供了一个“小而美”的动画基础设施:Animate组件负责状态机与生命周期,animate.scss负责视觉表现,CSS 变量桥接时长,DI 负责默认值与可测试性,AnimateName的字符串放宽与全局类名契约负责可扩展性。无论是直接使用三种预置动画,还是通过自定义类名、override injectable 深度定制,都可以在不侵入核心代码的前提下完成——这正是 README 中 "Extendability"(可扩展性)一节的落地体现。若要在 Freelens 扩展中复用它,只需安装@freelensapp/animate、注册animateFeature,然后像 Dialog、Drawer、Menu 那样将需要动画的子树包进<Animate>即可。

  • 云原生
  • 开发工具
  • 运维

【免费下载链接】freelens

Free IDE for Kubernetes

项目地址:https://gitcode.com/gh_mirrors/fr/freelens
点击查看免费下载
上一篇:终极gitingest CI/CD指南:自动化测试与部署全景攻略
下一篇:OWASP dependency-check 数据目录缓存实战:用 GitHub Actions 加速 Maven 漏洞扫描

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询