ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ant Design ConfigProvider wave 属性详解:内置波纹机制与自定义波纹效果实现

Ant Design ConfigProvider wave 属性详解:内置波纹机制与自定义波纹效果实现 Ant Design ConfigProvider wave 属性详解内置波纹机制与自定义波纹效果实现【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/gh_mirrors/ant/ant-design波纹Wave是 Ant Design 中为 Button、Tag、Checkbox、Radio、Switch 等可点击组件提供的交互动效而ConfigProvider的wave属性是全局控制它的唯一入口既可以设置disabled: true一键关闭波纹也可以通过showEffect回调接管整个波纹渲染逻辑、实现诸如点击处扩散白点Inset或抖动Shake等完全自定义的反馈效果。读完本文你将掌握wave属性的完整 API 语义、官方示例中两种自定义效果的完整代码实现以及从源码层面理解波纹是如何被触发、定位、着色与卸载的从而能够自信地为项目定制或关闭波纹效果。1. wave 属性是什么API 定义与官方说明ConfigProvider文档的 API 表格中wave一行的定义为参数说明类型默认值版本wave设置水波纹特效{ disabled?: boolean, showEffect?: (node: HTMLElement, info: { className, token, component }) void }-5.8.0官方文档components/config-provider/index.zh-CN.md对两个字段做了如下说明disabled是否开启水波纹效果。如果需要关闭可以设置为false即wave{{ disabled: true }}关闭。showEffect自定义水波纹效果签名即ShowWaveEffect类型。该类型定义在 components/_util/wave/interface.ts 中这是理解一切细节的锚点export type ShowWaveEffect ( element: HTMLElement, info: { className: string; token: GlobalToken; component?: string; event: MouseEvent; hashId: string; }, ) void;也就是说当你传入showEffect时框架会把两个东西交给你element真正要承载波纹的 DOM 节点见第 3 节的定位逻辑info包含组件类名className、当前主题token、波纹来源组件名component取值范围为Tag | Button | Checkbox | Radio | Switch、原始鼠标事件event以及 CSS-in-JS 的hashId。component字段正是官方 Demo 文档中那句可以通过component判断来自哪个组件的出处——你的自定义效果可以只作用于按钮、或只作用于标签其余组件保持静默。2. 完整示例关闭波纹、内置波纹、Inset、Shake以下代码继承自官方演示 components/config-provider/demo/wave.tsx配合 components/config-provider/demo/wave.md 的说明完整呈现了四种形态。2.1 页面骨架用 Wrapper 演示 4 种状态import { HappyProvider } from ant-design/happy-work-theme; import { Button, ConfigProvider, Space } from antd; import type { ConfigProviderProps, GetProp } from antd; type WaveConfig GetPropConfigProviderProps, wave; // Component const Wrapper ({ name, ...wave }: WaveConfig { name: string }) ( ConfigProvider wave{wave} Button typeprimary{name}/Button /ConfigProvider ); const App () ( Space style{{ padding: 24 }} sizelarge Wrapper nameDisabled disabled / Wrapper nameDefault / Wrapper nameInset showEffect{showInsetEffect} / Wrapper nameShake showEffect{showShakeEffect} / HappyProvider Button typeprimaryHappy Work/Button /HappyProvider /Space ); export default App;四个按钮分别展示wave{{ disabled: true }}完全关闭波纹Disabled不传wave内置默认波纹Default即按钮外圈扩散一圈colorPrimary的盒阴影传入showEffect完全接管波纹逻辑Inset / Shake通过ant-design/happy-work-theme的HappyProvider叠加动态波纹注意该库是 antd 的生态包需要单独安装引入仓库源码中的示例即如此引用。2.2 Inset 效果点击处扩散白色圆点思路是动态创建一个覆盖按钮的 holder 容器再在鼠标落点创建一个透明小圆点用 CSS transition 将其放大到 200px 并淡出transitionend后移除。// Prepare effect holder const createHolder (node: HTMLElement) { const { borderWidth } getComputedStyle(node); const borderWidthNum parseInt(borderWidth, 10); const div document.createElement(div); div.style.position absolute; div.style.inset -${borderWidthNum}px; // 向外扩一个边框宽度与按钮圆角对齐 div.style.borderRadius inherit; div.style.background transparent; div.style.zIndex 999; div.style.pointerEvents none; div.style.overflow hidden; node.appendChild(div); return div; }; const createDot (holder: HTMLElement, color: string, left: number, top: number, size 0) { const dot document.createElement(div); dot.style.position absolute; dot.style.left ${left}px; dot.style.top ${top}px; dot.style.width ${size}px; dot.style.height ${size}px; dot.style.borderRadius 50%; dot.style.background color; dot.style.transform translate(-50%, -50%); dot.style.transition all 1s ease-out; holder.appendChild(dot); return dot; }; // Inset Effect const showInsetEffect: WaveConfig[showEffect] (node, { event, component }) { if (component ! Button) { return; // 只对 Button 生效这就是 component 的用途 } const holder createHolder(node); const rect holder.getBoundingClientRect(); // 鼠标落点换算为 holder 内的相对坐标 const left event.clientX - rect.left; const top event.clientY - rect.top; const dot createDot(holder, rgba(255, 255, 255, 0.65), left, top); // Motion requestAnimationFrame(() { dot.ontransitionend () { holder.remove(); // 动画结束即清理 DOM }; dot.style.width 200px; dot.style.height 200px; dot.style.opacity 0; }); };几个值得注意的工程细节createHolder先读取getComputedStyle(node).borderWidth再计算inset保证 holder 精确覆盖按钮含边框borderRadius: inherit让白点扩散时被按钮圆角裁剪不会溢出到方形直角外用requestAnimationFrame延迟一帧再修改宽高/透明度确保浏览器先提交初始尺寸transition 才能正确触发ontransitionend中移除 holder避免 DOM 泄漏。2.3 Shake 效果逐帧驱动 rotate 的抖动Shake 不使用 CSS transition而是自己用requestAnimationFrame循环按序列[0, -15, 15, -5, 5, 0]逐帧插值角度模拟物理阻尼式抖动// Shake Effect const showShakeEffect: WaveConfig[showEffect] (node, { component }) { if (component ! Button) { return; } const seq [0, -15, 15, -5, 5, 0]; const itv 10; // 每个关键帧之间的帧数 let steps 0; function loop() { cancelAnimationFrame((node as any).effectTimeout); // 连点时取消上一次动画 (node as any).effectTimeout requestAnimationFrame(() { const currentStep Math.floor(steps / itv); const current seq[currentStep]; const next seq[currentStep 1]; if (!next) { node.style.transform ; node.style.transition ; return; } // 当前帧在 current 与 next 之间线性插值 const angle current ((next - current) / itv) * (steps % itv); node.style.transform rotate(${angle}deg); node.style.transition none; steps 1; loop(); }); } loop(); };这个例子的价值在于展示了showEffect的能力边界你拿到的就是一个普通 DOM 节点任何 CSS/JS 动画transform、滤镜、Web Animations API 等都可以直接施于其上与 antd 内部实现完全解耦。3. 默认波纹如何工作从源码看触发链不传showEffect时走的是内置实现。整条链路是组件包裹Wave→ 监听捕获阶段 click →useWave按帧合并事件 → 默认showWaveEffect渲染WaveEffect动效。3.1 谁在用波纹Wave组件与五个接入方波纹的统一入口是 components/_util/wave/index.ts 中的Wave组件它接受component、disabled、children三个 props声明了WaveProps.component?: Tag | Button | Checkbox | Radio | Switch这正是showEffect回调里component字段的全部取值来源在捕获阶段addEventListener(click, onClick, true)绑定点击并做了多重过滤目标节点不可见、节点带disabled属性、类名包含disabled或-leave收起动画中时一律不播波纹通过cloneElement把 ref 合并进子组件拿到真实的HTMLElement作为波纹宿主。仓库中实际接入Wave的组件有五个例如 Button 的用法components/button/button.tsxWave componentButton disabled{innerLoading} {buttonNode} /Wave可见 loading 中的按钮也会禁用波纹disabled{innerLoading}。Tag 则是有条件接入components/tag/index.tsxisNeedWave ? Wave componentTag{tagNode}/Wave : tagNode即可关闭的 Tag 才包波纹。Switch、Checkbox、Radio 同理分别在 components/switch/index.tsx、components/checkbox/Checkbox.tsx、components/radio/radio.tsx 中包裹。3.2useWave读取 wave 配置并替换 showEffectcomponents/_util/wave/useWave.ts 是wave属性真正被消费的地方const { wave } React.useContext(ConfigContext); const showWave useEventShowWave((event) { const node nodeRef.current!; if (wave?.disabled || !node) { return; // wave.disabled true 时直接短路 } const targetNode node.querySelectorHTMLElement(.${TARGET_CLS}) || node; const { showEffect } wave || {}; // Customize wave effect (showEffect || showWaveEffect)(targetNode, { className, token, component, event, hashId }); });这段代码回答了三个问题disabled的判定位置wave?.disabled为真时整个波纹流程直接返回这就是官方示例中 Disabled 按钮毫无反应的原因波纹宿主的选择优先查找节点内部带有ant-wave-target类TARGET_CLS ant-wave-target的子元素找不到就用节点本身。这解释了为什么某些组件的波纹只出现在某一部分自定义接管点wave.showEffect存在时完全替换内置的showWaveEffect两者签名一致因此上文 Inset/Shake 的写法可以直接工作。另一个细节是showDebounceWave每次点击都会raf.cancel上一次并合并到下一帧执行Merge trigger event into one for each frame快速连点时不会叠加出多个波纹。3.3 内置效果本体WaveEffect与阴影扩散默认的showWaveEffectcomponents/_util/wave/WaveEffect.tsx做的事情是在目标节点内插入一个 holder用render()把WaveEffectReact 组件挂载进去由CSSMotion播放名为wave-motion的动画结束后unmount并移除 holder。其中几个值得了解的点先测量再播放组件挂载后延迟一帧raf执行syncPos()因为UI may change after click——点击可能导致布局变化同时用ResizeObserver持续同步尺寸颜色来自目标本身syncPos调用getTargetWaveColor(target)波纹颜色并非写死。取色逻辑在 components/_util/wave/util.ts依次检查borderTopColor、borderColor、backgroundColor只有有效波纹色才会被采用——不是白色#fff/rgb(255,255,255)等、不是透明、不是灰度色isNotGrey判断 RGB 三通道不全相等全部无效则回落到主题色colorPrimary见第 4 节样式Checkbox 的特殊规则if (component Checkbox !target.querySelector(input)?.checked) return;——未勾选的 Checkbox 不播波纹小组件加速component Checkbox || Radio且命中wave-target时会附加wave-quick类使用更短的motionDurationSlow过渡自动清理兜底motionDeadline{5000}保证即使transitionend事件丢失例如元素被移出视口5 秒后也会强制卸载 holder。3.4 波纹的视觉本质box-shadow 扩散 透明度过渡波纹的 CSS 由 components/_util/wave/style.ts 生成核心只有几行[componentCls]: { position: absolute, background: transparent, pointerEvents: none, color: var(--wave-color, ${colorPrimary}), // 波纹颜色 boxShadow: 0 0 0 0 currentcolor, opacity: 0.2, .wave-motion-appear: { transition: [ box-shadow 0.4s ${token.motionEaseOutCirc}, opacity 2s ${token.motionEaseOutCirc}, ].join(,), -active: { boxShadow: 0 0 0 6px currentcolor, // 阴影从 0 扩散到 6px opacity: 0, // 同时整体淡出 }, }, }可以推断出默认波纹的完整动画就是盒阴影从 0px 扩散到 6px 的同时透明度从 0.2 降到 0色值优先取--wave-color这个 CSS 变量由WaveEffect通过waveStyle[--wave-color] color写入即第 3.3 节测量到的目标颜色取不到时用主题colorPrimary。pointerEvents: none保证波纹层不拦截后续点击。4. 实践要点与常见问题如何全局关闭波纹在应用最外层传入ConfigProvider wave{{ disabled: true }}即可useWave会在最早期短路不影响任何其他行为。如何在 CSP 环境下使用波纹文档中特别提到部分组件为了支持波纹效果使用了动态样式若项目开启了 Content Security Policy需要通过ConfigProvider的csp属性配置 nonce见 components/config-provider/index.zh-CN.md 的 Content Security Policy 小节ConfigProvider csp{{ nonce: YourNonceCode }} ButtonMy Button/Button /ConfigProvider自定义效果中的常见陷阱忘记按component过滤。showEffect对五个接入组件都会触发如果效果只适配按钮比如依赖点击坐标务必先if (component ! Button) return;忘记清理。内置WaveEffect有motionDeadline兜底但自定义showEffect完全由你负责——参考 Inset 示例务必在transitionend或动画结束后remove()自己创建的 holder连点竞态。Shake 示例中cancelAnimationFrame((node as any).effectTimeout)就是为快速连点做的保护自定义动画涉及transform/transition直写 DOM 时建议同样处理不要依赖 React 状态写波纹。showEffect是一次性的命令式回调官方两个示例都是纯 DOM 操作这与内置实现临时render一个组件保持同样的用完即抛策略避免给组件树带来额外状态。版本前提wave属性自 5.8.0 引入使用showEffect前请确认所用版本不低于此版本component字段的联合类型约束意味着未来若新增接入波纹的组件类型会同步扩展自定义回调保持对未知component值的容错不匹配就 return即可平滑兼容。5. 小结ConfigProvider的wave属性用一个{ disabled, showEffect }的结构覆盖了波纹治理的全部场景disabled对应 useWave.ts 中的早期短路showEffect则在(showEffect || showWaveEffect)处完成对内置实现的替换。默认波纹本身是一个测量目标 → 取色 → 阴影扩散 → 自动卸载的自清理机制颜色会智能跟随按钮自身的边框/背景色这也是自定义showEffect时值得借鉴的设计。理解这套链路后无论是全局禁用波纹、实现 Inset/Shake 式的交互反馈还是像ant-design/happy-work-theme那样叠加品牌化动效都有了明确的实现基础。【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/gh_mirrors/ant/ant-design创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进