1、use 指令
动作(Action),其本质是元素上的生命周期函数。它们可用于譬如以下几个方面:
- 与第三方库对接
- 延迟加载图片
- 工具提示(tooltips)
- 添加自定义事件处理程序
我们模拟先做一个第三方库,要使第三方库适配 Svelte 的 Action 十分简单,实际上,Action 只是一个普通的函数,它接收一个参数,就是当前元素的 DOM 节点对象。
我们自制的“第三方”库的功能假设是一个让任意元素支持移动的 JS 库,它的框架大概是这样:
movable.js
export function movable(node) {
// ... 第三方库的代码
return {
destroy() {
// ... destroy 函数会在元素被清除时调用,
// 我们可以在此做一些清理工作
}
}
}
这个函数返回一个包含了 destroy
方法的对象,destroy
函数在元素被移除时自动被调用。
让任意元素支持移动,我们需要监听一些事件:
movable.js
export default function movable(node) {
let moving = false
let x, y, left, top
function handleMove(e) {
if (moving) {
node.style.left = (e.clientX - x + left) + 'px'
node.style.top = (e.clientY - y + top) + 'px'
}
}
function startMove(e) {
moving = true
x = e.clientX
y = e.clientY
left = parseInt(node.style.left) || 0
top = parseInt(node.style.top) || 0
}
function endMove() { moving = false }
window.addEventListener('mousemove', handleMove)
window.addEventListener('mouseup', endMove)
node.addEventListener('mousedown', startMove)
return {
destroy() {
window.removeEventListener('mousemove', handleMove)
window.removeEventListener('mouseup', endMove)
node.removeEventListener('mousedown', startMove)
}
}
}
完成了“第三方”组件后,我们现在来实现主程序,并通过 use
指令,将 movable
应用到指定的元素中:
App.svelte
<script>
import movable from './movable'
</script>
<style>
div {
position: absolute;
width: 100px;
height: 100px;
background-color: blue;
text-align: center;
line-height: 100px;
color: #fff;
user-select: none;
cursor: move;
}
</style>
<!-- 通过 use 指令将 movable 应用到元素 div -->
<div use:movable>Box</div>
此时,尝试拖动 div
,可以看到 div
已经支持拖动。
下一步,我们要思考的问题是:div 能否监听得到它是什么时候开始移动?什么时候结束移动的?在这些时刻使用端可能有自己的事情要做。
这就需要 movable 支持发送这些事件供外部使用,我们计划设计3个事件,分别是:movestart
、moving
和 moveend
,顾名思义,它们分别代表移动开始、移动中和移动结束。
先在 UI 上监听这些事件:
<script>
import movable from './movable'
</script>
<style>
div {
position: absolute;
width: 100px;
height: 100px;
background-color: blue;
text-align: center;
line-height: 100px;
color: #fff;
user-select: none;
cursor: move;
}
</style>
<div
use:movable
on:movestart={e => console.log(`start => x: ${e.detail.x}, y: ${e.detail.y}`)}
on:moving={e => console.log(`moving => x: ${e.detail.x}, y: ${e.detail.y}`)}
on:moveend={() => console.log('move end...')}
>Box</div>
然后转到 movable 去实现这些事件的发送:
export default function movable(node) {
let moving = false
let x, y, left, top
// Note: 发送自定义事件
function fire(type, option) {
node.dispatchEvent(new CustomEvent(type, option))
}
function handleMove(e) {
if (moving) {
node.style.left = (e.clientX - x + left) + 'px'
node.style.top = (e.clientY - y + top) + 'px'
// Note: 发送 moving
fire('moving', {
detail: { x: e.clientX - x + left, y: e.clientY - y + top }
})
}
}
function startMove(e) {
moving = true
x = e.clientX
y = e.clientY
left = parseInt(node.style.left) || 0
top = parseInt(node.style.top) || 0
// Note: 发送 movestart
fire('movestart', { detail: {x:left, y:top}})
}
function endMove() {
moving = false
// Note: 发送 moveend
fire('moveend')
}
window.addEventListener('mousemove', handleMove)
window.addEventListener('mouseup', endMove)
node.addEventListener('mousedown', startMove)
return {
destroy() {
window.removeEventListener('mousemove', handleMove)
window.removeEventListener('mouseup', endMove)
node.removeEventListener('mousedown', startMove)
}
}
}
这个实现方案仅用于演示,真实场景还需考虑触摸事件。
2、附加参数
正如 transition
过渡效果和 animate
动画一样,动作
(Action)也支持附带参数,动作函数会与它所在的元素一起被调用。
我们现在设计了一个无聊透顶的游戏,叫 长按按钮
游戏,界面上有一个按钮,长按指定时间显示“恭喜你按够了 x 秒”之类的傻瓜提示,先来写 UI:
App.svelte
<script>
import { longpress } from './longpress.js';
let pressed = false;
let duration = 8;
</script>
<label>
<input type=range bind:value={duration} max={8} step={1}>
{duration}s
</label>
<button
use:longpress
on:longpress={() => pressed = true}
on:mouseenter={() => pressed = false}
>按住我别放</button>
{#if pressed}
<p>恭喜你,长按了 {duration} 秒</p>
{/if}
在这里我们使用longpress
这个动作,每当用户按下按钮并持续一段给定的时间(通过 rang 组件提供)的话,该动作就会触发一个具有相同名称的时间。
现在有一个问题,我们如何将 pressed
告知 longpress
这个 action
呢?
答案自然就是通过动作参数。
事不宜迟,我们开始编写longpress.js
文件:
longpress.js
export function longpress(node, duration) {
let timer;
const handleMousedown = () => {
timer = setTimeout(() => {
node.dispatchEvent(
new CustomEvent('longpress')
);
}, duration * 1000);
};
const handleMouseup = () => clearTimeout(timer);
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('mouseup', handleMouseup);
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', handleMouseup);
}
};
}
longpress
借助计时器来控制时间,如果按够了 duration
秒,就展示恭喜信息。
回到App.svelte
,我们可以传递一个具体的duration
值给动作了:
<button
use:longpress={duration}
...
>按住我别放</button>
如果你调整了持续时间的滑块,例如从5秒调整到2秒,你会发现还是需要5秒。
要修复这个问题,我们可以在longpress.js
中添加一个update
方法,每当参数有变,立即调用此方法:
longpress.js
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', handleMouseup);
},
update(newDuration) {
duration = newDuration;
}
};
如果你需要将多个参数同时传递给一个动作,则需要将它们组合成一个对象,比如:
use:longpress={
{duration, spiciness}}