前言
简单记录一下Svelte的学习。所有的代码均来自于官网学习指南。
数据渲染
Vue是基于template的,Svelte则可以说是基于html,把它当作html文件写即可。
<script>
let name = 'world';
</script>
<h1>Hello world!</h1>
<script>
let src = 'tutorial/image.gif';
</script>
<img src={src} alt="A man dances.">
<img {src} alt="A man dances.">
样式
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is a paragraph.</p>
引入组件
Svelte也遵循组件化的理念。
<script>
import Nested from './Nested.svelte';
</script>
<p>This is a paragraph.</p>
<Nested/>
转译
对标v-html
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
入口
App挂载入口
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
// we'll learn about props later
answer: 42
}
});
响应式
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
计算属性和监听
对标Vue中的computed、watch,React中的watchEffect。
<script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>
甚至可以这么写。
$: console.log(`the count is ${count}`);
$: {
console.log(`the count is ${count}`);
alert(`I SAID THE COUNT IS ${count}`);
}
$: if (count >= 10) {
alert(`count is dangerously high!`);
count = 9;
}
数组和对象的响应式
push、pop等数组的方法无法触发响应式,对象的方法也是如此。
触发响应式的必要条件是赋值,也就是更新引用地址。
function addNumber() {
numbers = [...numbers, numbers.length + 1];
}
// 不会更新对obj.foo.bar的引用
const obj = { foo: {} }
const foo = obj.foo;
foo.bar = 'baz';
组件属性
组件传入的属性值定义和传入方式
<!-- 子组件 -->
<script>
export let answer;
</script>
<!-- 父组件 -->
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
赋初始值
<script>
export let answer = 12;
</script>
类似于React和Vue的组件属性传值方式也是支持的。
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg} />
条件渲染
对标v-if
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
也可以写成
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
elseif的语法
{#if x > 10}
<p>{x} is greater than 10</p>
{:else if 5 > x}
<p>{x} is less than 5</p>
{:else}
<p>{x} is between 5 and 10</p>
{/if}
循环渲染
对标v-for
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<h1>The Famous Cats of YouTube</h1>
<!-- 可以不取index,也可以对cat进行解构 -->
<ul>
{#each cats as cat, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{i + 1}: {cat.name}
</a></li>
{/each}
</ul>
关于key
涉及循环就必定涉及key,Vue和React当中的key是用于辅助diff算法的,部分情况下不算特别强制。
Svelte则不同,必须设定一个key保证DOM更新符合预期。
{#each things as thing (thing.id)}
<Thing current={thing.color}/>
{/each}
异步渲染
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
如果并不关心错误提示DOM。
{#await promise then value}
<p>the value is {value}</p>
{/await}
事件监听
需要以on:指令开头。
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
也可以在内部定义handler。
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
The mouse position is {m.x} x {m.y}
</div>
这里的引号不是必要的,但是它在部分情况下有助于语法高亮。
修饰符
和Vue还有React中的修饰符作用一致。
使用方式
<script>
function handleClick() {
alert('no more alerts')
}
</script>
<button on:click|once={handleClick}>
Click me
</button>
<!-- 也可以一次性用多个修饰符 -->
<button on:click|once|capture={handleClick}>
Click me
</button>
修饰符列表
- preventDefault 阻止默认提交
- stopPropagation 阻止冒泡
- passive 优化touch和wheel事件的滚动表现
- capture 捕获阶段触发
- once 仅运行一次事件
- self event.target是其本身时执行
组件事件
方式与Vue类似,但是Svelte里叫dispatch而不是emit
<!-- 子组件 -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
<!-- 父组件 -->
<script>
import Inner from './Inner.svelte';
function handleMessage(event) {
console.log(event.detail)
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage}/>
事件转发
Svelte特色,可以简单理解为逆向的透传。Svelte本身并不支持组件事件的冒泡,所以父组件如果要触发孙子组件的事件,就需要子组件进行转发,Svelte提供了语法糖。
<!-- 父组件 -->
<script>
import Outer from './Outer.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Outer on:message={handleMessage}/>
<!-- 孙子组件 Inner -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
<!-- 子组件 Outer -->
<script>
import Inner from './Inner.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function forward(event) {
dispatch('message', event.detail);
}
</script>
<Inner on:message={forward}/>
<!-- 子组件Outer 语法糖 -->
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message/>
值得一提的是这种语法糖同样适用于DOM事件,达到React当中forwardRef类似的效果,可以将DOM节点的方法暴露给父组件。
<!-- 子组件 FancyButton -->
<button on:click>
Click me
</button>
<!-- 父组件 -->
<script>
import FancyButton from './FancyButton.svelte';
function handleClick() {
alert('clicked');
}
</script>
<FancyButton on:click={handleClick}/>
双向绑定
和Vue一致
<script>
let name = 'world';
</script>
<input bind:value={name}>
<h1>Hello {name}!</h1>
和Vue一样,默认传递值的类型都是字符串,如果使用了双向绑定语法,那么传递值就可以是其他类型。
<script>
let yes = false;
</script>
<input type=checkbox bind:checked={yes}>
input组绑定
radio绑定
input标签type为radio时,绑定值会被赋值为当前value属性值。
<script>
let scoops = 1;
</script>
<h2>Size</h2>
<label>
<input type=radio bind:group={scoops} value={1}>
One scoop
</label>
<label>
<input type=radio bind:group={scoops} value={2}>
Two scoops
</label>
<label>
<input type=radio bind:group={scoops} value={3}>
Three scoops
</label>
checkbox绑定
input标签type为checkbox时,绑定值会被赋值为数组,并且在数组中添加删除当前input的value属性值。
<script>
// 尝试下来这里赋值成数组或者字符串都行,不影响
let flavours = 'Mint choc chip';
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
</script>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} value={flavour}>
{flavour}
</label>
{/each}
语法糖
绑定中如果变量名和属性名相同,则可省略
<textarea bind:value={value}></textarea>
<!-- 可省略为以下形式 -->
<textarea bind:value></textarea>
select绑定
<select bind:value={selected}>
{#each questions as question}
<option value={question}>
{question.text}
</option>
{/each}
</select>
multiple的select标签
<select multiple bind:value={flavours}>
{#each menu as flavour}
<option value={flavour}>
{flavour}
</option>
{/each}
</select>
尺寸绑定
每个块级标签都可以对clientWidth、clientHeight、offsetWidth以及offsetHeight属性进行绑定。这些属性都是只读的,不支持手动更改。
<script>
let w;
let h;
let size = 42;
let text = 'edit me';
</script>
<style>
input { display: block; }
div { display: inline-block; }
span { word-break: break-all; }
</style>
<input type=range bind:value={size}>
<input bind:value={text}>
<p>size: {w}px x {h}px</p>
<div>
<div bind:clientWidth={w} bind:clientHeight={h}>
<span style="font-size: {size}px">{text}</span>
</div>
</div>
this绑定
对标Vue和React当中的ref,用于获取DOM元素。
<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>
组件属性绑定
组件属性属性绑定方面不同于Vue和React。直接绑定然后子组件当中改变属性值即可,这就有点趋向于双向数据流。Vue和React当中组件接收的属性都是只读的,哪怕是使用了Vue当中的v-model也需要一个update事件去更新
<!-- 父组件 -->
<Keypad bind:value={pin} on:submit={handleSubmit}/>
<!-- 子组件 -->
<script>
import { createEventDispatcher } from 'svelte';
export let value = '';
const dispatch = createEventDispatcher();
// 对props直接更改
const select = num => () => value += num;
const clear = () => value = '';
const submit = () => dispatch('submit');
</script>
<style>
.keypad {
display: grid;
grid-template-columns: repeat(3, 5em);
grid-template-rows: repeat(4, 3em);
grid-gap: 0.5em
}
button {
margin: 0
}
</style>
<div class="keypad">
<button on:click={select(1)}>1</button>
<button disabled={!value} on:click={clear}>clear</button>
<button on:click={select(0)}>0</button>
<button disabled={!value} on:click={submit}>submit</button>
</div>
特性:contenteditable
可以让一个div标签变为特殊的input元素。
<script>
let html = '<p>Write some text!</p>';
</script>
<div contenteditable="true" bind:innerHTML={html}></div>
<pre>{html}</pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
生命周期
和其他主流框架一样,可以将通用的生命周期逻辑抽离,在组件外定义。
import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}
onMount
最常用的是onMount这个生命周期节点。
Svelte的onMount结合了Vue的onMounted和React的useEffect,onMount的回调函数的返回值也像React一样可以接收一个函数在destroy的时候调用,用于抹除某些状态。
官方推荐在挂载完成的时候进行请求而不是在顶层script当中。因为在服务端渲染的情况下,生命周期函数并不会运行(onDestroy除外),这样就可以避免额外请求数据。
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
photos = await res.json();
});
</script>
onDestroy
onMount中提到的抹除状态也可以在onDestroy生命周期函数中执行。
<script>
import { onDestroy } from 'svelte';
let seconds = 0;
const interval = setInterval(() => seconds += 1, 1000);
onDestroy(() => clearInterval(interval));
</script>
beforeUpdate和afterUpdate
DOM更新前执行beforeUpdate,更新后执行afterUpdate。
let div;
let autoscroll;
beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
});
tick
和Vue的nextTick功能类似,这个生命周期节点源于Svelte的DOM更新机制。和Vue还有React相同,Svelte追求高效也会将DOM的更新延迟到下一次microTask队列的执行,如果你需要对更新后的DOM进行操作,那么tick函数是必须的。
async function handleKeydown(event) {
if (event.which !== 9) return;
event.preventDefault();
const { selectionStart, selectionEnd, value } = this;
const selection = value.slice(selectionStart, selectionEnd);
const replacement = /[a-z]/.test(selection)
? selection.toUpperCase()
: selection.toLowerCase();
text = (
value.slice(0, selectionStart) +
replacement +
value.slice(selectionEnd)
);
await tick()
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
}
集成状态管理
Svelte自带状态管理,不需要像React一样借助Redux。使用也相当方便。
更新可写状态
可写状态是可以双向绑定的。
// store.js
import { writable } from 'svelte/store';
export const count = writable(0);
<!-- 组件使用 -->
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
function reset() {
count.set(0)
}
</script>
<button on:click={decrement}>
-
</button>
<button on:click={reset}>
reset
</button>
订阅更新
需要订阅全局状态的更新只需要调用subscribe方法即可。
值得注意的是,这个方法返回的是一个取消订阅的方法,请在组件销毁的时候调用该方法,避免内存泄漏。
<script>
import { count } from './stores.js';
let count_value;
const unsubscribe = count.subscribe(value => {
count_value = value;
});
onDestroy(unsubscribe);
</script>
<div>{count_value}</div>
这么做看起来有些冗余,Svelte提供了语法糖解决这个问题。
<!-- 等效于上方代码 -->
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
正是因此,Svelte当中变量名应避免以$开头。
只读状态
使用readable可定义一个只读的状态。第一个参数接收初始值,可以为null或undefined;第二个则是一个回调函数,当状态被订阅时触发,这个回调函数会传入一个set函数用于更新状态,返回一个stop函数,作用同unsubscribe。
import { readable } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
派生
可以理解为基于已有状态得到新状态。
// 在上一代码块基础上
const start = new Date();
export const elapsed = derived(
time,
$time => Math.round(($time - start) / 1000)
);
解构并自定义状态及其更新
import { writable } from 'svelte/store';
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const count = createCount();
动画
相当于使用js的形式控制页面动画,Svelte内置了很多js动画方法供开发者使用,相当强大。值得一提的是,最后渲染到页面上还是CSS动画,所以性能不会受到影响。
基本使用
<script>
import { writable } from 'svelte/store';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
<style>
progress {
display: block;
width: 100%;
}
</style>
<progress value={$progress}></progress>
<button on:click="{() => progress.set(0)}">
0%
</button>
<button on:click="{() => progress.set(0.25)}">
25%
</button>
<button on:click="{() => progress.set(0.5)}">
50%
</button>
<button on:click="{() => progress.set(0.75)}">
75%
</button>
<button on:click="{() => progress.set(1)}">
100%
</button>
通过标签属性设置动画
也支持属性类型的动画设置。
<script>
import { fly } from 'svelte/transition';
import { fade } from 'svelte/transition';
let visible = true;
</script>
{#if visible}
<p transition:fade>
Fades in and out
</p>
<p transition:fly="{{ y: 200, duration: 2000 }}">
Fades in and out
</p>
<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>
Flies in, fades out
</p>
{/if}
支持高度自定义,形式类似CSS in JS,篇幅有限,有机会展开描述。
动画过渡监听
每一个动画过渡阶段的监听
<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>
为标签添加类
主要使用方法语法同Vue一致,有特殊语法糖
<button
class="{selected ? 'selected' : ''}"
on:click="{() => current = 'foo'}"
>foo</button>
<!-- 等效 -->
<button
class:selected="{selected}"
on:click="{() => current = 'foo'}"
>foo</button>
<!-- 变量名称和类名相同时可省略 -->
<button
class:selected
on:click="{() => current = 'foo'}"
>foo</button>
插槽
使用方法同Vue
具名插槽
<!-- ContactCard.svelte -->
<article class="contact-card">
<h2>
<slot name="name">
<span class="missing">Unknown name</span>
</slot>
</h2>
<div class="address">
<slot name="address">
<span class="missing">Unknown address</span>
</slot>
</div>
<div class="email">
<slot name="email">
<span class="missing">Unknown email</span>
</slot>
</div>
</article>
<!-- 父组件 -->
<ContactCard>
<span slot="name">
P. Sherman
</span>
<span slot="address">
42 Wallaby Way<br>
Sydney
</span>
</ContactCard>
也可以通过$$slot来判断当前插槽是否被使用。
<!-- 子组件 -->
<article class:has-discussion={$$slots.comments}>
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
</article>
插槽属性
<!-- 子组件 -->
<script>
let hovering;
function enter() {
hovering = true;
}
function leave() {
hovering = false;
}
</script>
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot hovering={hovering}></slot>
</div>
<!-- 父组件 -->
<script>
import Hoverable from './Hoverable.svelte';
</script>
<style>
div {
padding: 1em;
margin: 0 0 1em 0;
background-color: #eee;
}
.active {
background-color: #ff3e00;
color: white;
}
</style>
<Hoverable let:hovering={hovering}>
<div class:active={hovering}>
{#if hovering}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>
context透传
方式同React
内置标签
<svelte:self>
允许当前组件在DOM当中添加自身
<!-- Folder.svelte -->
{#if expanded}
<ul>
{#each files as file}
<li>
{#if file.type === 'folder'}
<svelte:self {...file}/>
<!-- 等价于<Folder {...file}/> -->
{:else}
<File {...file}/>
{/if}
</li>
{/each}
</ul>
{/if}
<svelte:component>
和Vue内置的<component>一样
{#if selected.color === 'red'}
<RedThing/>
{:else if selected.color === 'green'}
<GreenThing/>
{:else if selected.color === 'blue'}
<BlueThing/>
{/if}
<!-- 等价于 -->
<svelte:component this={selected.component}/>
<svelte:window>
其实就是把BOM当中抽象化的window通过标签具象化,可以在window上添加事件监听,绑定一些属性。
<svelte:window on:keydown={handleKeydown}/>
<svelte:window bind:scrollY={y}/>
<svelte:body>
作用类似<svelte:window>
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
<svelte:head>
作用类似<svelte:window>
<svelte:head>
<link rel="stylesheet" href="tutorial/dark-theme.css">
</svelte:head>
<svelte:options>
编译器针对当前组件的编译选项
共享代码块
试想一下,一个页面当中有多个相同组件,而你只希望这个组件当中的一段代码只被运行一次,那么过去需要写在它的父组件当中,Svelte给出了另一个方案.
<script context="module">
let current;
</script>
使用module导出一个变量、函数
如果没有context="module"这个属性,那么export出去的变量将变为当前组件的属性,而有了这个属性,那么这个变量将可以被别的模块引入。
<script context="module">
export let current;
</script>
特殊的debug方式
<!-- 空字符串是为了方式console.log返回的undefined被渲染到DOM上去 -->
{(console.log(user), '')}
<!-- 等效于 -->
{@debug user}