Svelte 框架介绍
传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。
与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。
减少代码量 :重复利用所掌握的编程语言:HTML、CSS、Javascript,构建的组件无需依赖模板文件。
无虚拟 DOM :Svelte 将代码编译成体积小、不依赖框架的普通 js 代码,让应用程序无论启动或者运行都变得非常迅速。
真正的反应能力 :无需复杂的状态管理库,Svelte 为 JavaScript 自身添加反应能力。
- Svelte 组件建立在 HTML 之上,然后只需添加数据即可。
- css 默认限定在组件作用域内,不再有样式冲突或者优先级的问题。还可以使用 CSS-in-JS 库。
- 通过给局部变量赋值就能触发高效、细粒度的更新。剩余的事情交给编译器处理。
- 使用一个内置于框架中的、强大的、性能卓越的动画引擎来构建漂亮的 UI。
<script>
//引入组件直接使用
import Nested from './Nested.svelte';
// 内置高性能的动画引擎
import { quintOut } from 'svelte/easing';
// 内置常用动画
import { fade, draw, fly } from 'svelte/transition';
// 引入生命周期函数
import {
onMount,
onDestroy,
beforeUpdate,
afterUpdate,
tick,
setContext,
getContext,
createEventDispatcher
} from 'svelte';
import { expand } from './custom-transitions.js';
import { inner, outer } from './shape.js';
// 直接声明变量、直接修改变量、直接在模板内使用变量
let visible = true;
let count = 0;
let canvasElement;
let pkg = {name:'zhangjinxi',age:23};
$: doubled = count * 2; //类似vue computed计算属性
$: console.log(`the count is ${count}`); // 类似watcher
$: {
console.log(`the count is ${count}`);
alert(`I SAID THE COUNT IS ${count}`);
}
$: if (count >= 10) {
alert(`count is dangerously high!`);
count = 9;
}
// 定义函数
function handleClick() {
count += 1;
}
// 声明周期函数
onMount(async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
const photos = await res.json();
});
</script>
<style>
{/* 这里直接编写css代码,页面结构有点类似Vue */}
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>使用“{doubled}”花括号包含引入的变量、函数和表达式,可以直接调用函数</p>
<p>使用@html直接渲染HTML元素而不是文本 {@html '<div></div>'}</p>
<button on:click|once={handleClick}>
绑定click事件触发器,并添加修饰符,和vue相同:
{count} {count === 1 ? 'time' : 'times'}
</button>
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
也可以内联事件 {m.x} x {m.y}
</div>
使用组件,并添加props。组件内直接通过定义这个变量的方式获取这个props。例如:let answer;
<Nested answer={42} {...pkg}/>
bind:this 类似vue中的ref,可以获取dom对象的引用
<canvas bind:this={canvasElement}></canvas>
条件判断
{#if visible}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 103 124">
<g out:fade="{{duration: 200}}" opacity=0.2>
<path
in:expand="{{duration: 400, delay: 1000, easing: quintOut}}"
style="stroke: #ff3e00; fill: #ff3e00; stroke-width: 50;"
d={outer}
/>
<path
in:draw="{{duration: 1000}}"
style="stroke:#ff3e00; stroke-width: 1.5"
d={inner}
/>
</g>
</svg>
{:else if 5 > x}
<label>
{/* on:用来绑定事件。bind:用来绑定值 bind:group绑定多个值(代表一个分组) */}
<input type="checkbox" bind:checked={visible}>
</label>
{:else}
<div class="centered" out:fly="{{y: -20, duration: 800}}">
{/* 遍历和列表渲染 字符串SVELTE解析为数组 i作为key标识*/}
{#each 'SVELTE' as char, i (i)}
<span
in:fade="{{delay: 1000 + i * 150, duration: 800}}"
>{char}</span>
{/each}
</div>
{/if}
{#await promise}
<p>...waiting 处理异步数据的需求:</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
{#await promise then value}
<p>promise 总是返回错误,则可以忽略 catch 块: {value}</p>
{/await}
支持 contenteditable="true"属性的标签,可以使用 textContent 与 innerHTML 属性的绑定:
<div contenteditable="true" bind:innerHTML={html}></div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
客户端 component API
客户端 component 使用 generate: 'dom' (或 generate 选项不指定)编译的 component 是 JavaScript 类。
const component = new Component(options);
import App from "./App.svelte";
const app = new App({
target: document.body,
props: {
answer: 42,
},
});
//以编程方式在实例上设置 prop
app.$set(props);
//返回一个函数,该函数在调用时将删除事件侦听器。
app.$on(event, callback);
//从DOM中删除component并触发所有 onDestroy 处理程序
app.$destroy();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
反应性能力
由于 Svelte 的反应性是由赋值语句触发的,因此使用数组的诸如 push 和 splice 之类的方法就不会触发自动更新。解决该问题的一种方法是添加一个多余的赋值语句:
function addNumber() {
numbers.push(numbers.length + 1);
numbers = numbers; //添加赋值语句,触发响应式
}
//赋值给数组和对象的 属性(properties) (例如,obj.foo += 1 或 array[i] = x)与对值本身进行赋值的方式相同。
numbers[numbers.length] = numbers.length + 1;
2
3
4
5
6
7
事件调度
与 DOM 事件不同, 组件事件不会 冒泡(bubble) ,如果你想要在某个深层嵌套的组件上监听事件,则中间组件必须 转发(forward) 该事件
为此,组件内必须创建一个相同事件并在外部进行分配。createEventDispatcher 必须在首次实例化组件时调用它,组件本身不支持如 setTimeout 之类的事件回调。 定义一个 dispatch 进行连接,进而把组件实例化。
<script>
import Inner from "./Inner.svelte";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
// 监听Inner组件内的message事件
function forward() {
dispatch("message", {
text: "Hello!",
});
}
</script>
<Inner on:message="{forward}" />
{/* 简写属性 on:message。 message 没有赋予特定的值得情况下意味着转发所有massage事件: */}
<Inner on:message />
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
store
import { writable,readable, derived} from 'svelte/store';
//定义不可写入的store值。传入初始值。第二个参数和writable相同
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
// 定义可以写入的store值
export const count = writable(0,function start(set){
//set更改store值的函数
//在subscriber数从0变为1(但不是从1变为2)时将调用该函数
return function stop(){
// 在subscriber数从1变为0时调用的函数
//或者当subscriber被unsubscribes时调用stop
}
});
const unsubscribe = count.subscribe(value => {
// 订阅count的变化
});
// 函数参数的返回值,设置为store的新值
count.update(n => n + 1);
// 设置count值为0
count.set(0);
// 添加$前缀在dom中使用,不能在声明此变量了
<h1>The count is {$count}</h1>
// 源于一个或多个其他 store的store,只要这些依赖项发生变更,就会执行回调
derived(a, callback: (a: any) => any)
derived(
[a, ...b],
callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any)
const doubled = derived(time, ($time,set) => $time * 2);
const summed = derived([a, b], ([$a, $b]) => $a + $b);
const delayed = derived([a, b], ([$a, $b], set) => {
setTimeout(() => set($a + $b), 1000);
});
// 只要一个对象正确的使用 subscribe ,它就是可以称之为store。因此,使用特定语法来创建自定义 stores变得非常容易。
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
运动
svelte/motion 模块导出两个函数: tweened 和 spring。用于创建 writable(可写)store,其值会在 set 和 update 之后更新,而不是立即更新。
<script>
import { tweened,spring } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
{/* Tweened(补间) store 值会在固定时间内更新,可选参数: */}
const store = tweened(0, {
duration: 400,
duration(form,to){
return milliseconds
},
easing: cubicOut,
delay:1000,
interpolate(from,to){
return t=>value
}
});
{/* spring(弹性) store通过stiffness和 damping参数逐步变更到目标值,而tweenedstore在改变一个固定时间变更其值。store在由它们现有速度决定的持续时间长短,从而实现更自然的运动效果。可选选项: */}
let coords = spring({ x: 50, y: 50 });
let size = spring(10);
let coords = spring({ x: 50, y: 50 }, {
stiffness: 0.1,//灵敏度
damping: 0.25,//惯性
precision:0.01// 控制上面两个参数的运动幅度大小
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
过渡
fade
淡入淡出 参数: delay durationblur
模糊处理 参数:delay、duration、easing、opacity、amountfly
移动 delay、duration、easing、x、y、opacityslide
滑动 delay duration easingscale
缩放 delay duration easing start opacitydraw
对 SVG 标签进行路径绘制动画 delay speed duration easingcrossfade
<script>
import { fade,fly,slide,scale,draw,blur,crossfade} from 'svelte/transition';
let visible = true;
</script>
<p transition:fade>Fades in and out</p>
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>
// transition 属性可以替换为 in 或out 属性,分别对应过渡效果的入和出,可以指定其中一个,或者指定两个
<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>
Flies in, fades out
</p>
// svelte/transition 模块含有一些内置的过渡效果,但是创建自己的过渡效果也是非常容易
function fade(node, {
delay = 0,
duration = 400
}) {
const o = +getComputedStyle(node).opacity;
return {
delay,
duration,
easing(p){
return t
},
tick(t,u){
return {}
},
css: (t,u)=> `opacity: ${t * o}`
};
}
// 过渡事件
<p
transition:fly="{{ y: 200, duration: 2000 }}"
on:introstart="{() => status = 'intro started'}"
on:outrostart="{() => status = 'outro started'}"
on:introend="{() => status = 'intro ended'}"
on:outroend="{() => status = 'outro ended'}"
>
Flies in and out
</p>
//过 局部(local)过渡
<div transition:"slide|local">
{item}
</div>
//crossfade函数创建一对称名为 send 和receive. 当一个标签被 'sent'时, 它会寻找一个被'received'的标签,并赋予一个过渡效果,反之同理。如果没有对应的接收方,过渡效果将会设置为fallback
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 600,
easing: quintOut,
css: t => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
动画
- flip 函数计算标签的开始和结束位置并在它们之间进行动画效果,并翻转 x 和 y 的值,
animate:name={params}
animation = (node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) => {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
DOMRect {
bottom: number,
height: number,
left: number,
right: number,
top: number,
width: number,
x: number,
y: number
}
// 自定义动画
<script>
import { cubicOut } from 'svelte/easing';
function whizz(node, { from, to }, params) {
const dx = from.left - to.left;
const dy = from.top - to.top;
const d = Math.sqrt(dx * dx + dy * dy);
return {
delay: 0,
duration: Math.sqrt(d) * 120,
easing: cubicOut,
tick: (t, u) =>
Object.assign(node.style, {
color: t > 0.5 ? 'Pink' : 'Blue'
});
};
}
</script>
{#each list as item, index (item)}
<div animate:whizz>{item}</div>
{/each}
import { flip } from 'svelte/animate';
// todo实现平滑切换效果:完美。。。
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip="{{duration: 200}}"
>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
easing
Easing 函数可指定根据时间变化的速率,在使用 Svelte 的内置 transition 和 animation 以及 tweened 和 spring 程序时非常有用。 svelte/easing 包含 31 个导出命名,, 一个 linear(线性)缓动使用 in, out 和 inOut 轻松生成 10 种不同的缓动函数:
Actions
pannable.js:
// action本身是个函数,接受node节点对象
export function pannable(node, ...args) {
let x;
let y;
function handleMousedown(event) {
x = event.clientX;
y = event.clientY;
// 当触发mousedown事件时,触发dom上的panstart事件
node.dispatchEvent(
new CustomEvent("panstart", {
detail: { x, y },
})
);
// 并监听mousemove和mouseup事件
window.addEventListener("mousemove", handleMousemove);
window.addEventListener("mouseup", handleMouseup);
}
function handleMousemove(event) {
const dx = event.clientX - x;
const dy = event.clientY - y;
x = event.clientX;
y = event.clientY;
node.dispatchEvent(
new CustomEvent("panmove", {
detail: { x, y, dx, dy },
})
);
}
function handleMouseup(event) {
x = event.clientX;
y = event.clientY;
node.dispatchEvent(
new CustomEvent("panend", {
detail: { x, y },
})
);
window.removeEventListener("mousemove", handleMousemove);
window.removeEventListener("mouseup", handleMouseup);
}
node.addEventListener("mousedown", handleMousedown);
return {
destroy() {
// 销毁时触发
node.removeEventListener("mousedown", handleMousedown);
},
update(newDuration) {
// 参数改变时触发该方法
},
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
app.svelte
<script>
import { spring } from "svelte/motion";
import { pannable } from "./pannable.js";
const coords = spring(
{ x: 0, y: 0 },
{
stiffness: 0.2,
damping: 0.4,
}
);
function handlePanStart() {
coords.stiffness = coords.damping = 1;
}
function handlePanMove(event) {
coords.update($coords => ({
x: $coords.x + event.detail.dx,
y: $coords.y + event.detail.dy,
}));
}
function handlePanEnd(event) {
coords.stiffness = 0.2;
coords.damping = 0.4;
coords.set({ x: 0, y: 0 });
}
</script>
<style>
.box {
--width: 100px;
--height: 100px;
position: absolute;
width: var(--width);
height: var(--height);
left: calc(50% - var(--width) / 2);
top: calc(50% - var(--height) / 2);
border-radius: 4px;
background-color: #ff3e00;
cursor: move;
}
</style>
<!-- 通过use使用action,action里可以拿到并操作dom,还可以传递参数 -->
<div
class="box"
use:pannable="{{duration:1000,delay:1000}}"
on:panstart="{handlePanStart}"
on:panmove="{handlePanMove}"
on:panend="{handlePanEnd}"
style="transform:
translate({$coords.x}px,{$coords.y}px)
rotate({$coords.x * 0.2}deg)"></div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Classes
<button
class="{current === 'foo' ? 'selected' : ''}"
class:selected="{current === 'foo'}"
class:big={big}
class:big // 简写
on:click="{() => current = 'foo'}">
foo
</button>
2
3
4
5
6
7
8
组件
像 vue 一样,支持 slot 插槽,命名插槽,插槽默认值
<!-- 定义组件 -->
<slot name="address">
<span class="missing">具名插槽address的默认内容</span>
</slot>
<slot>默认default插槽的默认内容</slot>
<!-- 获取父组件传递的comments插槽内容 -->
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments" item={var}>具名插槽传递数据给父组件</slot>
</div>
{/if}
<!-- 作用域插槽。hovering传递给父组件 -->
<slot hovering={hovering}></slot>
<!-- 使用组件。let来获取子组件传递的内容。也可以通过slot获取 -->
<ContactCard let:hovering={hovering}>
{#if hovering}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
<span slot="address">具名插槽address</span>
<span slot="comments" let:item={var}>获取具名插槽传递来的数据</span>
</ContactCard>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
上下文
// 父级组件,通过setContext暴露上下文给子级组件
import { onMount, setContext } from "svelte";
import { mapbox, key } from "./mapbox.js";
setContext(key, {
getMap: () => map,
});
// 子级组件通过getContext获取上下文传递的数据
import { getContext } from "svelte";
import { mapbox, key } from "./mapbox.js";
const { getMap } = getContext(key);
const map = getMap();
2
3
4
5
6
7
8
9
10
11
12
13
14
特殊标签
<svelte:self>
标签允许递归自己。例如:目录 <svelte:component>
和 vue 的 component 组件一样
{#if file.type === 'folder'}
<svelte:self {...file}/>
{:else}
<File {...file}/>
{/if}
<svelte:component this={selected.component}/>
//通过 <svelte:window>标签给window 对象添加事件监听:
<svelte:window on:keydown={handleKeydown} bind:innerWidth:200/>
//<svelte:body>标签允许你添加事件监听document.body
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
//<svelte:head> 允许你在页面的 <head> 标签内插入内容
<svelte:head>
<link rel="stylesheet" href="tutorial/dark-theme.css">
</svelte:head>
{/* <svelte:options>标签允许你指定编译器选项 */}
<svelte:options immutable={true}/>
<svelte:options immutable/> // 简写
// 实例之上的代码,可以控制组件的多个实例对象
<script context="module">
let current;
</script>
<script context="module">
const elements = new Set();
</script>
<script>
import { onMount } from 'svelte';
export let src;
export let title;
export let composer;
export let performer;
let audio;
let paused = true;
onMount(() => {
elements.add(audio);
return () => elements.delete(audio);
});
// 只有一个音乐组件处于播放中,会暂停其他实例播放
function stopOthers() {
elements.forEach(element => {
if (element !== audio) element.pause();
});
}
</script>
<style>
article { margin: 0 0 1em 0; max-width: 800px }
h2, p { margin: 0 0 0.3em 0; }
audio { width: 100%; margin: 0.5em 0 1em 0; }
.playing { color: #ff3e00; }
</style>
<article class:playing={!paused}>
<h2>{title}</h2>
<p><strong>{composer}</strong> / performed by {performer}</p>
<audio
bind:this={audio}
bind:paused
on:play={stopOthers}
controls
{src}
></audio>
</article>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
调试
// 调试user
{@debug user}
<h1>Hello {user.firstname}!</h1>
2
3
祝贺动画:
<script>
import { onMount } from 'svelte';
let characters = ['🥳', '🎉', '✨'];
let confetti = new Array(100).fill()
.map((_, i) => {
return {
character: characters[i % characters.length],
x: Math.random() * 100,
y: -20 - Math.random() * 100,
r: 0.1 + Math.random() * 1
};
})
.sort((a, b) => a.r - b.r);
onMount(() => {
let frame;
function loop() {
frame = requestAnimationFrame(loop);
confetti = confetti.map(emoji => {
emoji.y += 0.7 * emoji.r;
if (emoji.y > 120) emoji.y = -20;
return emoji;
});
}
loop();
return () => cancelAnimationFrame(frame);
});
</script>
<style>
:global(body) {
overflow: hidden;
}
span {
position: absolute;
font-size: 5vw;
}
</style>
{#each confetti as c}
<span style="left: {c.x}%; top: {c.y}%; transform: scale({c.r})">{c.character}</span>
{/each}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49