动作Actions #
一、Actions 概述 #
Actions 是一种增强 DOM 元素的方式,当元素挂载时执行函数,销毁时执行清理。
text
┌─────────────────────────────────────────────────────────────┐
│ Actions 工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <div use:action={options}> │ │
│ │ Content │ │
│ │ </div> │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ function action(node, options) { │ │
│ │ // node: DOM 元素 │ │
│ │ // options: 传递的参数 │ │
│ │ │ │
│ │ // 初始化逻辑 │ │
│ │ │ │
│ │ return { │ │
│ │ update(options) { }, // 参数更新时调用 │ │
│ │ destroy() { } // 元素销毁时调用 │ │
│ │ }; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
二、基本用法 #
2.1 简单 Action #
svelte
<script>
function focus(node) {
node.focus();
}
</script>
<input use:focus placeholder="Auto-focused input" />
2.2 带参数的 Action #
svelte
<script>
function clickOutside(node, callback) {
function handleClick(event) {
if (!node.contains(event.target)) {
callback?.();
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
let showModal = true;
</script>
{#if showModal}
<div class="modal" use:clickOutside={() => showModal = false}>
<p>Click outside to close</p>
</div>
{/if}
2.3 完整 Action 结构 #
svelte
<script>
function tooltip(node, { text = '', position = 'top' }) {
let tooltipEl;
function create() {
tooltipEl = document.createElement('div');
tooltipEl.textContent = text;
tooltipEl.className = `tooltip tooltip-${position}`;
document.body.appendChild(tooltipEl);
}
function position() {
const rect = node.getBoundingClientRect();
tooltipEl.style.left = rect.left + rect.width / 2 + 'px';
tooltipEl.style.top = rect.top - tooltipEl.offsetHeight - 10 + 'px';
}
function show() {
tooltipEl.style.opacity = '1';
position();
}
function hide() {
tooltipEl.style.opacity = '0';
}
create();
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update({ text: newText }) {
tooltipEl.textContent = newText;
},
destroy() {
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
tooltipEl.remove();
}
};
}
</script>
<button use:tooltip={{ text: 'Click me!', position: 'top' }}>
Hover me
</button>
三、常用 Actions #
3.1 点击外部 #
svelte
<script>
function clickOutside(node, handler) {
const onClick = (event) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
handler();
}
};
document.addEventListener('click', onClick, true);
return {
destroy() {
document.removeEventListener('click', onClick, true);
}
};
}
</script>
<div use:clickOutside={() => console.log('Clicked outside')}>
Click outside this div
</div>
3.2 长按 #
svelte
<script>
function longpress(node, { duration = 500, onLongpress }) {
let timer;
function handleMouseDown() {
timer = setTimeout(() => {
onLongpress?.();
}, duration);
}
function handleMouseUp() {
clearTimeout(timer);
}
node.addEventListener('mousedown', handleMouseDown);
node.addEventListener('mouseup', handleMouseUp);
node.addEventListener('mouseleave', handleMouseUp);
return {
update({ duration: newDuration }) {
duration = newDuration;
},
destroy() {
node.removeEventListener('mousedown', handleMouseDown);
node.removeEventListener('mouseup', handleMouseUp);
node.removeEventListener('mouseleave', handleMouseUp);
}
};
}
</script>
<button use:longpress={{ duration: 1000, onLongpress: () => alert('Long pressed!') }}>
Hold me
</button>
3.3 懒加载 #
svelte
<script>
function lazyload(node, { onLoad, threshold = 0.1 }) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onLoad?.();
observer.unobserve(node);
}
});
},
{ threshold }
);
observer.observe(node);
return {
destroy() {
observer.unobserve(node);
}
};
}
let loaded = false;
</script>
<div use:lazyload={{ onLoad: () => loaded = true }}>
{#if loaded}
<img src="image.jpg" alt="Lazy loaded" />
{:else}
<div class="placeholder">Loading...</div>
{/if}
</div>
3.4 自动调整大小 #
svelte
<script>
function autosize(node) {
function resize() {
node.style.height = 'auto';
node.style.height = node.scrollHeight + 'px';
}
node.addEventListener('input', resize);
resize();
return {
destroy() {
node.removeEventListener('input', resize);
}
};
}
</script>
<textarea use:autosize placeholder="Auto-resizing textarea"></textarea>
3.5 拖拽 #
svelte
<script>
function draggable(node, { bounds = null, onDragStart, onDrag, onDragEnd }) {
let isDragging = false;
let startX, startY;
let initialX, initialY;
function handleMouseDown(event) {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
const rect = node.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
node.style.position = 'fixed';
node.style.zIndex = '1000';
onDragStart?.({ x: initialX, y: initialY });
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
function handleMouseMove(event) {
if (!isDragging) return;
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
let newX = initialX + deltaX;
let newY = initialY + deltaY;
if (bounds) {
newX = Math.max(bounds.left, Math.min(newX, bounds.right - node.offsetWidth));
newY = Math.max(bounds.top, Math.min(newY, bounds.bottom - node.offsetHeight));
}
node.style.left = newX + 'px';
node.style.top = newY + 'px';
onDrag?.({ x: newX, y: newY });
}
function handleMouseUp() {
isDragging = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
onDragEnd?.();
}
node.addEventListener('mousedown', handleMouseDown);
return {
destroy() {
node.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
};
}
</script>
<div
use:draggable={{
bounds: { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight },
onDragStart: () => console.log('Drag started'),
onDrag: (pos) => console.log(pos),
onDragEnd: () => console.log('Drag ended')
}}
>
Drag me
</div>
四、Actions 与 TypeScript #
4.1 类型定义 #
typescript
import type { Action } from 'svelte/action';
interface TooltipOptions {
text: string;
position?: 'top' | 'bottom' | 'left' | 'right';
}
const tooltip: Action<HTMLElement, TooltipOptions> = (node, options) => {
// Implementation
return {
update(newOptions) {
// Update logic
},
destroy() {
// Cleanup
}
};
};
4.2 完整类型示例 #
svelte
<script lang="ts">
import type { Action } from 'svelte/action';
interface ClickOutsideOptions {
handler: () => void;
enabled?: boolean;
}
const clickOutside: Action<HTMLElement, ClickOutsideOptions> = (
node,
{ handler, enabled = true }
) => {
function onClick(event: MouseEvent) {
if (enabled && !node.contains(event.target as Node)) {
handler();
}
}
document.addEventListener('click', onClick, true);
return {
update({ enabled: newEnabled }) {
enabled = newEnabled;
},
destroy() {
document.removeEventListener('click', onClick, true);
}
};
};
</script>
五、Actions 库 #
5.1 svelte-actions 库 #
svelte
<script>
import { clickOutside, longpress, lazyload } from 'svelte-actions';
</script>
<div use:clickOutside={() => console.log('outside')}>
Click outside
</div>
<button use:longpress={{ duration: 500 }} on:longpress={handleLongpress}>
Long press
</button>
<img use:lazyload data-src="image.jpg" />
5.2 自定义 Actions 库 #
javascript
export function clickOutside(node, handler) {
const onClick = (event) => {
if (!node.contains(event.target)) {
handler();
}
};
document.addEventListener('click', onClick, true);
return {
destroy() {
document.removeEventListener('click', onClick, true);
}
};
}
export function longpress(node, { duration = 500, handler }) {
let timer;
const onmousedown = () => {
timer = setTimeout(handler, duration);
};
const onmouseup = () => {
clearTimeout(timer);
};
node.addEventListener('mousedown', onmousedown);
node.addEventListener('mouseup', onmouseup);
node.addEventListener('mouseleave', onmouseup);
return {
destroy() {
node.removeEventListener('mousedown', onmousedown);
node.removeEventListener('mouseup', onmouseup);
node.removeEventListener('mouseleave', onmouseup);
}
};
}
export function lazyload(node, { threshold = 0.1, onLoad }) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
onLoad?.();
observer.unobserve(node);
}
},
{ threshold }
);
observer.observe(node);
return {
destroy() {
observer.unobserve(node);
}
};
}
六、Actions 最佳实践 #
6.1 命名规范 #
svelte
<script>
function useClickOutside(node, handler) {
// ...
}
function useLongpress(node, options) {
// ...
}
</script>
<div use:useClickOutside={handler}>
<!-- ... -->
</div>
6.2 参数验证 #
svelte
<script>
function tooltip(node, options) {
if (typeof options === 'string') {
options = { text: options };
}
const { text = '', position = 'top' } = options;
// ...
}
</script>
<button use:tooltip="Simple tooltip">Hover</button>
<button use:tooltip={{ text: 'Advanced', position: 'bottom' }}>Hover</button>
6.3 派发事件 #
svelte
<script>
function longpress(node, { duration = 500 }) {
let timer;
function handleMouseDown() {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
}
function handleMouseUp() {
clearTimeout(timer);
}
node.addEventListener('mousedown', handleMouseDown);
node.addEventListener('mouseup', handleMouseUp);
node.addEventListener('mouseleave', handleMouseUp);
return {
destroy() {
node.removeEventListener('mousedown', handleMouseDown);
node.removeEventListener('mouseup', handleMouseUp);
node.removeEventListener('mouseleave', handleMouseUp);
}
};
}
</script>
<button use:longpress={{ duration: 1000 }} on:longpress={() => alert('Long pressed!')}>
Hold me
</button>
七、完整示例 #
7.1 可调整大小 #
svelte
<script>
function resizable(node, { minWidth = 100, minHeight = 100, maxWidth, maxHeight }) {
let startX, startY, startWidth, startHeight;
const handle = document.createElement('div');
handle.className = 'resize-handle';
handle.style.cssText = `
position: absolute;
right: 0;
bottom: 0;
width: 10px;
height: 10px;
background: #ccc;
cursor: se-resize;
`;
node.style.position = 'relative';
node.appendChild(handle);
function handleMouseDown(event) {
startX = event.clientX;
startY = event.clientY;
startWidth = node.offsetWidth;
startHeight = node.offsetHeight;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
function handleMouseMove(event) {
let newWidth = startWidth + event.clientX - startX;
let newHeight = startHeight + event.clientY - startY;
newWidth = Math.max(minWidth, maxWidth ? Math.min(newWidth, maxWidth) : newWidth);
newHeight = Math.max(minHeight, maxHeight ? Math.min(newHeight, maxHeight) : newHeight);
node.style.width = newWidth + 'px';
node.style.height = newHeight + 'px';
}
function handleMouseUp() {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
handle.addEventListener('mousedown', handleMouseDown);
return {
destroy() {
handle.removeEventListener('mousedown', handleMouseDown);
handle.remove();
}
};
}
</script>
<div
use:resizable={{ minWidth: 100, minHeight: 100, maxWidth: 500, maxHeight: 500 }}
style="width: 200px; height: 150px; border: 1px solid #ccc;"
>
Resize me
</div>
八、总结 #
| 方法 | 说明 |
|---|---|
use:action |
应用 Action |
use:action={options} |
带参数的 Action |
update() |
参数更新时调用 |
destroy() |
元素销毁时调用 |
Actions 要点:
- Action 是增强 DOM 元素的函数
- 接收 node 和 options 参数
- 返回 update 和 destroy 方法
- 适合封装可复用的 DOM 操作
- 可以派发自定义事件
- 与 TypeScript 配合获得类型安全
最后更新:2026-03-28