Svelte从入门到精通——指令
发表于 阅读时长8分钟
目录
动作
svelte/action只是创建元素时调用的普通JavaScript函数,通过这个函数,我们能够拿到对元素本身的引用,因此我们可以通过js编程将任何行为附加到该元素。可以理解为在元素的生命周期内进行操作。
一个普通的action用法如下:
use:action={parameters}
action动作即我们上述所说的js函数,它的定义如下:
action = (node: HTMLElement, parameters: any) => {
update?: (parameters: any) => void,
destroy?: () => void
}
- node参数即元素的引用
- parameters参数即
={parameters}的值。 - 方法返回一个对象,对象里有以下属性:
update方法会在元素每次更新时都调用。destroy方法在元素销毁时调用。
action只能在正常元素中使用,无法在组件中使用:

<script>
function greet(node, params) {
console.log('hello', params);
}
</script>
<div use:greet={'app页面'}>app</div>
笔者会为大家带来三个自定义action的演示:分别是use:clickoutside、use:wave和use:space。
clickOutside
顾名思义,就是实现在绑定的DOM外事件点击监听的方法。这种功能常见于一些UI弹窗组件中,需要在点击弹窗外部时,关闭弹窗。
export const clickOutside = (node: HTMLElement, callback: () => void) => {
const handleClick = (event: MouseEvent) => {
if (!event?.target) return;
if (
node &&
!node.contains(event.target as Node) &&
!event.defaultPrevented
) {
callback();
}
};
document.addEventListener("click", handleClick, true);
return {
destroy() {
document.removeEventListener("click", handleClick, true);
},
};
};
在document中绑定监听事件,判断node.contains(event.target)如果触发事件的对象不是当前绑定的对象,则执行回调。
如果你没有使用Typescript,可以把例子中的类型定义给删除。
<script>
import { clickOutside } from './action';
const onClickOutside = () => {
console.log('点击外面');
}
</script>
<main>
<div id="box" use:clickOutside={onClickOutside}></div>
</main>
<style>
main {
width: 100px;
height: 100px;
background: #eeeeee;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
#box {
width: 30px;
height: 30px;
background: salmon;
line-height: 30px;
text-align: center;
}
</style>
当我们点击红色区域时,不会打印任何信息,点击红色区域以外,打印出信息。
wave
如果读者使用过Antd组件,那在对一些组件诸如Button组件进行点击时,会发现会泛起一层波浪效果的动效:

接下来,我们就来实现这个效果:
export function wave(ele) {
let clickWaveTimeoutId;
let styleForPesudo;
function getAttributeName() {
return 'ant-click-animating-without-extra-node';
}
function _onClick(node, waveColor) {
const attributeName = getAttributeName();
node.removeAttribute(attributeName);
node.setAttribute(attributeName, 'true');
styleForPesudo = styleForPesudo || document.createElement('style');
if (waveColor) {
styleForPesudo.innerHTML =
`[ant-click-animating-without-extra-node]:after { border-color: ${waveColor}; }`;
if (!document.body.contains(styleForPesudo)) {
document.body.appendChild(styleForPesudo);
}
}
node.addEventListener('animationend', onTransitionEnd);
}
function bindAnimationEvent(node) {
const onClick = (e) => {
resetEffect(node);
const waveColor =
getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible
getComputedStyle(node).getPropertyValue('border-color') ||
getComputedStyle(node).getPropertyValue('background-color');
clickWaveTimeoutId = window.setTimeout(() => _onClick(node, waveColor), 0);
};
node.addEventListener('click', onClick, true);
return {
cancel: () => {
node.removeEventListener('click', onClick, true);
},
};
}
function resetEffect(node) {
const attributeName = getAttributeName();
node.removeAttribute(attributeName);
removeExtraStyleNode();
node.removeEventListener('animationend', onTransitionEnd);
}
function onTransitionEnd(e) {
if (!e || e.animationName !== 'fadeEffect') {
return;
}
resetEffect(e.target);
}
function removeExtraStyleNode() {
if (styleForPesudo) {
styleForPesudo.innerHTML = '';
}
}
bindAnimationEvent(ele);
return {
destroy() {
if (clickWaveTimeoutId) {
clearTimeout(clickWaveTimeoutId);
}
}
}
}
因为Svelte组件内的style是scoped限制的,我们将以下样式代码放到全局:
:root {
--theme-color: purple;
}
[ant-click-animating-without-extra-node="true"] {
position: relative;
}
[ant-click-animating-without-extra-node="true"]::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: block;
border-radius: inherit;
box-shadow: 0 0 0 0 var(--theme-color);
opacity: 0.2;
animation: fadeEffect 2s cubic-bezier(0.08, 0.82, 0.17, 1),
waveEffect 0.4s cubic-bezier(0.08, 0.82, 0.17, 1);
animation-fill-mode: forwards;
content: "";
pointer-events: none;
}
@keyframes waveEffect {
100% {
box-shadow: 0 0 0 var(--theme-color);
box-shadow: 0 0 0 6px var(--theme-color);
}
}
@keyframes fadeEffect {
100% {
opacity: 0;
}
}
简单讲解下实现思路:
- 当调用
use:wave进入到我们的action,执行bindAnimationEvent方法,该方法则是实现波浪动效的入口方法。 - 在bindAnimationEvent方法内,对当前绑定的元素进行点击事件的绑定。
- 当元素进行点击时,先给元素添加
ant-click-animating-without-extra-node属性,之后再给添加了ant-click-animating-without-extra-node属性的元素添加伪类。 - 当添加了伪类后,伪类的样式生效,内部有fadeEffect和waveEffect两种动画,fadeEffect用于设置透明度,waveEffect则是最关键的动画,设置了两种box-shadow。
- 动画结束后,清除
ant-click-animating-without-extra-node属性,同时移除伪类。
测试一下我们的自定义action:
<script>
import { wave } from "./action";
</script>
<button use:wave>按钮</button>
<style>
button {
width: 100px;
height: 30px;
}
</style>

这样我们在不改动原有组件逻辑的情况下,便可以通过使用use:wave来添加点击波浪的效果。因此,use非常适合用于拓展一些组件、甚至是第三方库的功能。
这种效果的实现源码,感兴趣的读者可以参考motion/other.less和wave。
space
在前面介绍《插槽》的章节中,我们了解到,在Svelte中并不能劫持$$slots来二次定义插槽的内容,那我们便可以使用指令的形式来完成这一操作。
// action.js
export function space(node: HTMLElement, spaceClasses: string) {
const wrap = () => {
node.childNodes.forEach((element) => {
if (!element) {
return;
}
if (
element.nodeType === 3 &&
(!element.nodeValue || /^\s+$/.test(element.nodeValue))
) {
return;
}
if (element.parentNode) {
const wrapItem = document.createElement("div");
wrapItem.className = spaceClasses;
element.parentNode.insertBefore(wrapItem, element);
wrapItem.appendChild(element);
}
});
};
wrap();
return {
destroy: wrap,
};
}
之后我们在Space组件中使用space的action:
<script>
// Space.svelte
import { space } from "./action"
let spaceChildClass = "ant-space-item";
let spaceClass = "ant-space";
</script>
<div use:space={spaceChildClass} class={spaceClass}>
<slot />
</div>
<style>
.ant-space {
display: flex;
}
:global(.ant-space .ant-space-item){
margin-right: 4px;
}
:global(.ant-space-item:last-child) {
margin-right: 0;
}
</style>
在App中测试一下:
<script>
import Space from './Space.svelte';
</script>
<Space>
<div>item1</div>
<h2>item2</h2>
<span>item3</span>
</Space>

其他
除了使用use来实现自定义的动作,Svelte还支持其他指令。如在介绍数据与dom引用时的bind:,在动画章节介绍的transition:、in:、out:、animate:以及在样式章节介绍的style:。
小结
本章我们学习了:
- 什么是action,如何自定义action方法;通过
use:action的方式来使用自定义action - 用具体案例演示了实际开发中action的作用