前言
加载动画组件是一个非常常见且重要的组件。它可以用于提升用户体验,让用户了解应用正在加载数据或执行某些操作。在本文中,我们将使用 PandaCSS 和 Vue 3 的自定义指令,来实现一个加载动画组件和指令。通过这个加载指令,我们将可以很方便地在应用中添加不同的加载动画效果。下面我们一步一步来实现这个功能。
实现加载组件
样式实现
加载组件的实现原理就是:在某个组件中使用加载组件时,加载组件会生成一个与原组件大小相同,圆角相同的遮罩元素,以绝对布局的方式挡在原本的组件前,然后内部居中展示动画效果,如下图所示:

那我们先实现一下加载组件的样式:
ts
import { RecipeVariantProps, sva } from "@/styled-system/css";
const loadingRecipe = sva({
slots: ["backdrop", "container", "load1", "load2", "load3"],
base: {
backdrop: {
position: "absolute",
top: "0px",
left: "0px",
width: "full",
height: "full",
zIndex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.25s ease",
borderRadius: "inherit",
background: "rgba(0, 0 ,0 ,0.2)",
},
container: {
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
transform: "scale(80%)",
},
},
variants: {
type: {
default: {
container: {
borderRadius: "50%",
},
load1: {
position: "absolute",
top: "0px",
bottom: "0px",
margin: "auto",
border: `3px solid token(colors.colorPalette.200)`,
borderRadius: "inherit",
borderTop: "3px solid transparent",
borderLeft: "3px solid transparent",
borderRight: "3px solid transparent",
animation: "spin 0.8s ease infinite",
},
load2: {
position: "absolute",
top: "0px",
bottom: "0px",
margin: "auto",
border: "3px dashed token(colors.colorPalette.200)",
borderRadius: "inherit",
borderTop: "3px solid transparent",
borderLeft: "3px solid transparent",
borderRight: "3px solid transparent",
animation: "spin 0.8s linear infinite",
opacity: "0.4",
},
},
},
},
});
export default loadingRecipe;
export type LoadingVariants = Exclude<
RecipeVariantProps<typeof loadingRecipe>,
undefined
>;
这里使用了 pandaCSS 中的 sva
(插槽配方)函数创建了一个组件配方,slots
声明了当前组件有哪些元素:
- backdrop:遮罩元素
- container:遮罩元素向内一层的加载容器元素
- load1,load2,load3:用于实现加载动画的三个占位元素
样式的实现有一些注意点:
container
:设置了transform: "scale(80%)"
,确保加载动画不会直接占满整个组件,防止样式贴边load1
:是通过设置一个正方形容器的某一个边框,就变成了一条直线,再加上圆角效果就变成了一条曲线,最后加上旋转的动画就实现了一个基础的加载样式load2
:与load2
实现方式一致,只是将边框样式从直线改为虚线,并且设置不同的动画执行曲线,这样就 跑的比load1
更慢一点。
动画的效果如下图:

结构实现
接着我们实现组件的基本结构:
tsx
import { PropType, Transition, computed, defineComponent, ref } from "vue";
import loadingRecipe, { LoadingVariants } from "./recipe";
import { css, cx } from "@/styled-system/css";
const ZLoading = defineComponent({
name: "ZLoading",
props: {
color: {
type: String,
default: css({ colorPalette: "gray" }),
},
type: {
type: String as PropType<LoadingVariants["type"]>,
default: "default",
},
size: {
type: String,
},
},
setup(props) {
const rootRef = ref<HTMLElement>();
const classes = loadingRecipe({ type: props.type });
const innerSize = computed(() => {
if (rootRef.value) {
const el = rootRef.value as HTMLElement;
return `${Math.min(el.offsetWidth, el.offsetHeight)}px`;
}
return 0;
});
return () => (
<Transition
enterActiveClass={css({ transition: "opacity 0.5s" })}
leaveActiveClass={css({ transition: "opacity 0.5s" })}
enterToClass={css({ opacity: 0 })}
>
<div ref={rootRef} class={classes.backdrop}>
<div
class={classes.container}
style={{
width: props.size || innerSize.value,
height: props.size || innerSize.value,
}}
>
{([1, 2, 3] as const).map((item) => (
<div key={item} class={cx(props.color, classes[`load${item}`])} />
))}
</div>
</div>
</Transition>
);
},
});
export default ZLoading;
export type ZLoadingProps = InstanceType<typeof ZLoading>["$props"];
loadingRecipe
是一个前面定义的插槽配方函数,它接受一个对象参数 { type: props.type }
,并返回一个类名对象用于生成样式类名。这个函数是根据 props 中的 type
属性来决定加载组件的样式变体。
这里有一个比较特殊的逻辑,innerSize
是一个计算属性,根据 rootRef
的值获取加载组件的宽度和高度,取其中的较小值,加载容器元素的宽度和高度使用 props.size
或计算属性 innerSize
的值。
之所以要取宽度和高度间的最小值,是为了适配宽高不等的容器。例如一个长条的按钮组件,如果还按照容器的宽高去实现加载动画,内部的曲线就会变成这样:

因此我们需要保证内部加载动画容器的始终是是一个正方形,且加载动画不要超出外层容器,这样才能实现正常的加载效果:

加载组件的外层还使用 <Transition>
组件包裹加载组件,实现加载动画的淡入淡出效果。
指令实现
Vue 中自定义指令的使用场景有很多,通常用于处理与 DOM 直接交互的逻辑。加载动画可以是一个很好的使用自定义指令的场景。当你想要在某个元素或组件上添加加载动画时,通过自定义指令可以将相关逻辑封装起来,使其具备可重用性,并可以在整个应用中进行统一的使用和管理。
具体的使用方式大家直接看官方文档:Vue3 自定义指令,这里直接展示我的实现:
tsx
import { Directive, DirectiveBinding, render } from "vue";
import ZLoading, { ZLoadingProps } from "./loading";
const domId = "LOADING_CONTAINER";
const mountLoading = (el: HTMLElement, binding: DirectiveBinding) => {
const loadingDom = document.createElement("div");
loadingDom.id = domId;
loadingDom.style.borderRadius = "inherit";
render(<ZLoading type={binding.arg as ZLoadingProps["type"]} />, loadingDom);
el.appendChild(loadingDom);
};
const unmountLoading = (el: HTMLElement) => {
const dom = el.querySelector(`#${domId}`);
if (dom) {
el.removeChild(dom);
}
};
const ZLoadingDirective: Directive<HTMLElement, boolean> = {
mounted(el, binding) {
// eslint-disable-next-line no-param-reassign
el.style.position = "relative";
if (binding.value !== false) {
mountLoading(el, binding);
}
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
if (binding.value !== false) {
mountLoading(el, binding);
} else {
unmountLoading(el);
}
}
},
unmounted(el) {
unmountLoading(el);
},
};
export default ZLoadingDirective;
-
mountLoading
函数用于将加载组件渲染到指定的元素中。它会创建一个容器元素loadingDom
,设置其id
为domId
和样式,然后使用render
方法将ZLoading
组件渲染到loadingDom
容器中。最后,将其添加到指定的元素el
中。 -
unmountLoading
函数用于从指定的元素中移除加载组件。它会查找指定的容器元素,并从父元素el
中移除该容器。 -
ZLoadingDirective
是一个自定义指令对象,包含了mounted
、updated
和unmounted
三个生命周期钩子函数的实现。- 在
mounted
钩子函数中,首先设置元素的position
为相对定位,然后根据绑定的值判断是否需要添加加载动画,如果需要则调用mountLoading
函数添加加载动画。 - 在
updated
钩子函数中,根据绑定的值的变化判断是否需要添加或移除加载动画。如果绑定值不为false
,则调用mountLoading
函数添加加载动画;如果绑定值变为false
,则调用unmountLoading
函数移除加载动画。 - 之所以使用
binding.value !== false
的判断方式,是为了方便指令使用时可以直接v-loading
展示动画,只有手动声明v-loading={false}
才隐藏会隐藏动画。 - 在
unmounted
钩子函数中,调用unmountLoading
函数移除加载动画。
- 在
接下来我们就可以在其他组件中使用这个自定义指令了,以按钮组件为例:
html
<button v-loading:default={props.loading}>
{slots?.default?.()}
</button>
default
代表了加载动画的样式,props.loading
代表是否需要展示加载动画。
实现更多的加载动画效果
前面我们将默认的动画效果设置为 default
,这意味着我们可以添加更多的变体,实现不同的加载动画效果:
tsx
const loadingRecipe = sva({
slots: ["backdrop", "container", "load1", "load2", "load3"],
//...
variants: {
type: {
// 默认圆圈加载变体
default: {
container: {
borderRadius: "50%",
},
load1: {
position: "absolute",
top: "0px",
bottom: "0px",
margin: "auto",
border: `3px solid token(colors.colorPalette.200)`,
borderRadius: "inherit",
borderTop: "3px solid transparent",
borderLeft: "3px solid transparent",
borderRight: "3px solid transparent",
animation: "spin 0.8s ease infinite",
},
load2: {
position: "absolute",
top: "0px",
bottom: "0px",
margin: "auto",
border: "3px dashed token(colors.colorPalette.200)",
borderRadius: "inherit",
borderTop: "3px solid transparent",
borderLeft: "3px solid transparent",
borderRight: "3px solid transparent",
animation: "spin 0.8s linear infinite",
opacity: "0.4",
},
},
// 圆角加载变体
corner: {
load1: {
width: "80%",
height: "80%",
background: "transparent",
position: "absolute",
animation: "corner 1s ease infinite",
border: `3px solid token(colors.colorPalette.200)`,
borderRadius: "50%",
},
},
// 跳跃圆点加载变体
point: {
container: {
flexDirection: "row",
},
load1: {
width: "8px",
height: "8px",
background: " token(colors.colorPalette.200)",
borderRadius: "50%",
margin: "3px",
animation: "point 0.75s ease infinite",
},
load2: {
width: "8px",
height: "8px",
background: " token(colors.colorPalette.200)",
borderRadius: "50%",
margin: "3px",
animation: "point 0.75s ease infinite 0.25s",
},
load3: {
width: "8px",
height: "8px",
background: "token(colors.colorPalette.200)",
borderRadius: "50%",
margin: "3px",
animation: "point 0.75s ease infinite 0.5s",
},
},
// 方块加载变体
square: {
load1: {
position: "absolute",
width: "60%",
height: "60%",
animation: "rotateSquare 3s ease infinite",
background: "token(colors.colorPalette.200)",
},
},
},
},
});
这里补充了三种加载样式变体,然后我们将加载动画的类型修改为变量,在 JSX/TSX
中可以这么写:
tsx:
v-loading={[props.loading, props.loadingType]}
效果相当于 vue 中:
tsx:
v-loading:[props.loadingType]="props.loading"
最终实现的效果如下图:

总结
看到这里,相信大家应该学会如何使用 PandaCSS 和 Vue 3 的自定义指令来创建和使用加载动画组件了,本篇文章中的所有代码可以在这里看到:github.com/oil-oil/zax... , 后续的更新也都在这个仓库中, 快去点个 star 🌟
如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!