SvelteJS 笔记(上)
Svelte 官方在线编辑和运行代码的工具:https://svelte.dev/repl/hello-world?version=3.49.0
组件 (Component)
在 Svelte 当中,一个 .svelte 文件就是一个组件。.svelte 文件中可以包含 HTML, CSS 和 JavaScript.
在组件的 HTML 里可以直接引用 JavaScript 当中定义的变量(即组件的状态)。
<script>
let name = 'world';
let src = '/tutorial/image.gif';
</script>
<h1>Hello {name}!</h1>
<img src={src} alt='image'/>
当 HTML 标签的属性名跟变量名一样的时候,还可以简写:
<img {src} alt='image'/>
在组件里可以通过 CSS 定义样式:
<p>This is a paragraph.</p>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
这里的样式只在当前组件范围内有效。
组件是可以嵌套的:
<!-- 文件 App.svelte -->
<script>
import Nested from './Nested.svelte';
</script>
<p>This is a paragraph.</p>
<Nested/>
<!-- 文件 Nested.svelte -->
<p>This is another paragraph.</p>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
有时候变量里带有 HTML 标签,默认情况下,它是以字符串形式直接将 HTML 原始内容显示在界面上的。如果你想显示渲染后的效果,可以在访问变量的时候加上 @html
.
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
响应性 (Reactivity)
组件的状态变量在变化时,会同时更新界面上所有引用到该变量的地方。
<script>
let count = 0;
function incrementCount() {
count += 1
}
</script>
<button on:click={incrementCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
可以使用 $:
来声明一个依赖于另一个变量的响应式变量:
<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>
当以上代码中的 count
变化时,doubled
会跟着变。
不仅仅是变量,任意表达式都可以被声明为响应式的:
//单行表达式
$: 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;
}
注意,Svelte 的响应性 (reactivity) 是通过赋值操作来触发的。通过数组和对象的方法来改变它们自身的属性不会触发响应性,例如:
<script>
let numbers = [1, 2, 3, 4];
function addNumber() {
numbers.push(numbers.length + 1);
}
$: sum = numbers.reduce((t, n) => t + n, 0);
</script>
<p>{numbers.join(' + ')} = {sum}</p>
<button on:click={addNumber}> Add a number </button>
上面的代码通过 Array#push()
来操作数组的内容,因此界面不会跟着变化。
修复这个问题的一个常见方法,就是将该数组重新赋值给自身:
function addNumber() {
numbers.push(numbers.length + 1);
numbers = numbers;
}
或者可以用 ES6 的展开语法 (spread syntax):
function addNumber() {
numbers = [...numbers, numbers.length + 1];
}
数组的其它方法,包括 pop()
, shift()
, splice()
等,以及对象的方法,如 Map#set()
, Set#add()
等,都同样无法触发响应性。
但是对数组、对象自身属性的赋值,又可以触发响应性。例如:
let obj = {name: 'yu'};
function changeName() {
obj.name = 'yuan';
}
又但是,将响应性的变量赋值给另一个变量时,通过第 2 个变量来改变其属性,将不会触发响应性。
let obj = {name: 'yu'};
function changeName() {
let foo = obj;
foo.name = 'yuan';
}
总之一句话:想要触发响应性,被更新的变量必须直接出现在赋值表达式的左边。
属性 (Props)
嵌套在另一个组件内部的组件,需要从外部组件获取数据时,可以使用 props(properties 的简写)。具体的方法是:在内部组件里 export 一个变量用于接收数据,再在外部组件使用内部组件的标签上加入同名属性。
<!-- 文件 App.svelte -->
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
<!-- 文件 Nested.svelte -->
<script>
export let answer;
</script>
<p>The answer is {answer}</p>
Props 可以设置默认值:
<!-- 文件 App.svelte -->
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
<Nested />
<!-- 文件 Nested.svelte -->
<script>
export let answer = 'a mystery';
</script>
<p>The answer is {answer}</p>
如果有一个对象,它的每个字段对应着组件里的每一个属性,可以直接将这个对象用解构赋值的方式传递给组件:
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg}/>
<!-- Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/ -->
另外,可以在嵌套组件内部使用 $$props
来访问传入的所有属性。这种方式在即使没有 export 变量的情况下仍然可以访问传进来的属性。不过这是 Svelte 不建议使用的方法,因为 Svelte 无法对其进行优化。
逻辑
if 条件的写法
<script>
let x = 7;
</script>
{#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}
each 遍历
<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>
<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>
keyed each block
试运行下面的代码:
<!-- 文件 App.svelte -->
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'banana' },
{ id: 3, name: 'carrot' },
{ id: 4, name: 'doughnut' },
{ id: 5, name: 'egg' },
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>Remove first thing</button>
{#each things as thing}
<Thing name={thing.name}/>
{/each}
<!-- 文件 Thing.svelte -->
<script>
const emojis = {
apple: "🍎",
banana: "🍌",
carrot: "🥕",
doughnut: "🍩",
egg: "🥚"
}
export let name;
const emoji = emojis[name];
</script>
<p><span>The emoji for { name } is { emoji }</span></p>
<style>
p { margin: 0.8em 0; }
span {
display: inline-block;
padding: 0.2em 1em 0.3em;
text-align: center;
border-radius: 0.2em;
background-color: #FFDFD3;
}
</style>
点击一次按钮,会发现 apple 那一行文字没有了,但是苹果的 emoji 仍然存在,而最后一个鸡蛋的 emoji 却消失了。这是因为 App.svelte 里的 things
在删除第一个元素之后,Svelte 认为第 1 到第 4 个元素发生了变化,就跟着修改了前面 4 个 <Thing>
的内容,但 Thing.svelte 里的 emoji 变量没有跟着变化,仍然保留着之前的值,所以看到的效果就是 banana 对应的 emoji 变成了一个苹果。同时因为最后一个 <Thing>
标签没有对应的 things
元素,就被删除了。
当然在 Thing.svelte 里可以使用 $: emoji = emojis[name]
来使 emoji 跟着 name 而变化,但这个例子要说明的问题在于:things
被删除的是第一个元素,而 <Thing>
被删除的是最后一个元素。所以在本例中,正确的解法是为每个 each block 创建一个键,告诉 Svelte 根据什么来绑定数组和 DOM 元素。
{#each things as thing (thing.id)}
<Thing name={thing.name}/>
{/each}
await blocks
Svelte 的组件还可以直接在模板上根据一个 Promise 的状态进行渲染不同的内容:
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
如果没有必要处理 Promise 的异常,可以把 catch
块省略掉。同时如果你不打算在 Promise 执行完之前显示任何内容,也可以把第一个块省略掉:
{#await promise then number}
<p>the number is {number}</p>
{/await}
事件处理
<script>
let m = { x: 0, y: 0 };
function handleMousemove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
<style>
div { width: 100%; height: 100%; }
</style>
可以把事件处理器内联 (inline event handler):
<script>
let m = { x: 0, y: 0 };
</script>
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
The mouse position is {m.x} x {m.y}
</div>
<style>
div { width: 100%; height: 100%; }
</style>
在其它框架中,可能会看到“不要使用内联事件处理“的建议,因为那会引起性能上的问题。但这个问题在 Svelte 中不存在。
另外,在事件处理器的内部,this
指向 DOM 元素本身。
<script>
function handleInput() {
console.log(this.value)
}
</script>
<input on:input={handleInput}/>
事件修饰符 (Event modifiers)
在 Svelte 中可以用事件修饰符来修饰事件处理器,比如 once
修饰符可以让某个事件只被触发一次:
<script>
function handleClick() {
alert('no more alerts')
}
</script>
<button on:click|once={handleClick}>
Click me
</button>
可用的修饰符有:
- preventDefault: 调用 event.preventDefault() 阻止控件触发事件的默认处理行为;
- stopPropagation: 调用 event.stopPropagation() 防止事件冒泡和事件捕获;
- passive: 提高了触摸/滚轮事件的滚动性能(Svelte 会在安全的地方自动添加它);
- nonpassive: 显式地设置 passive: false;
- capture: 在捕获阶段触发事件处理,而非冒泡阶段;
- once: 执行过一次之后移除该事件处理器;
- self: 仅在 event.target 是自身时触发事件处理;
- trusted: 仅在 event.isTrusted 为 true 时(例如用户主动触发时)触发事件处理
这些修饰符可以同时串联起来使用:on:click|once|capture={...}
组件事件 (Component events)
一个组件自身可以处理许多事件,例如鼠标的移动、点击、文字的输入等。如果这些事件的处理逻辑由组件自身实现,那么将代码写在组件内部即可。如果部分事件的处理逻辑需要外部来实现,那么就需要为组件编写自定义的事件,外部其它组件在使用该组件时,通过 on:自定义事件
的指令 (directive) 来处理事件。
<!-- 文件 App.svelte -->
<script>
import Inner from './Inner.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage}/>
<!-- 文件 Inner.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
注意这里的 createEventDispatcher()
必须在组件初次实例化时就被调用,不能放在诸如 setTimeout()
之类的回调函数里。
事件转发
与 DOM 事件不同,组件的自定义事件没有冒泡机制,因此当一个组件想要监听深度嵌套在其内部的组件时,需要中间的组件为事件作转发。
事件转发的实现很简单,只要在被转发的组件上添加不带值的同名指令即可:
<!-- 文件 App.svelte -->
<script>
import Outer from './Outer.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Outer on:message={handleMessage}/>
<!-- 文件 Inner.svelte 同上一个例子,保持不变 -->
<!-- 文件 Outer.svelte -->
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message/>
对于 DOM 事件的转发,也可以用相同的方法。
数据绑定
在组件内部的一个文本框里,可以通过 on:input
事件来实现数据双向绑定:
<script>
let name = 'woda';
</script>
<input value={name} on:input={(e) => name = e.target.value}>
<h1>Hello {name}!</h1>
但有个更简单的方法:将 <input>
的 value
属性改为 bind:value
指令:
<script>
let name = 'woda';
</script>
<input bind:value={name}/>
<h1>Hello {name}!</h1>
如果变量名与属性名相同的话,还可以简写为:<input bind:value/>
.
Group inputs
遇到 radio button 和 checkbox 可以使用 bind:group
指令。前者使用一个简单的变量来绑定,后者需要跟一个数组作绑定。
<script>
let scoops = 1;
let flavours = ['Mint choc chip'];
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
function join(flavours) {
if (flavours.length === 1) return flavours[0];
return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
}
</script>
<h2>Size</h2>
<label>
<input type=radio bind:group={scoops} name="scoops" value={1}>
One scoop
</label>
<label>
<input type=radio bind:group={scoops} name="scoops" value={2}>
Two scoops
</label>
<label>
<input type=radio bind:group={scoops} name="scoops" value={3}>
Three scoops
</label>
<h2>Flavours</h2>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} name="flavours" value={flavour}>
{flavour}
</label>
{/each}
{#if flavours.length === 0}
<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
<p>Can't order more flavours than scoops!</p>
{:else}
<p>
You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
of {join(flavours)}
</p>
{/if}
上面的 checkbox 也可以用 <select multiple>
替代。
Contenteditable bindings
<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>
This
每个 DOM 元素和组件都可以通过 bind:this
把自身绑定到一个变量上。但由于界面渲染完成之前,被绑定的DOM 元素或组件还不存在,所以目标变量的值会是 undefined
. 因此操作该变量的代码应该放到组件的 onMount()
回调函数内。
<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>
<script>
import { onMount } from 'svelte';
let canvas;
onMount(() => {
const ctx = canvas.getContext('2d');
//...
}
</script>
组件绑定
正如变量可以绑定到 DOM 元素上一样,变量也可以绑定到另一个组件上。
<!-- 文件 App.svelte -->
<script>
import Nested from './Nested.svelte';
let input
</script>
<input bind:value={input}/>
<Nested bind:answer={input}/>
<!-- 文件 Nested.svelte -->
<script>
export let answer = 42;
</script>
<p>The answer is {answer}</p>
如果组件绑定用得太多,会造成数据流很难跟踪,所以请有节制地使用组件绑定。
绑定组件实例
bind:this
同样可以用在组件上:
<!-- 文件 App.svelte -->
<script>
import InputField from './InputField.svelte';
let field;
</script>
<InputField bind:this={field}/>
<button on:click={() => field.focus()}>Focus field</button>
<!-- 文件 InputField.svelte -->
<script>
let input;
export function focus() {
input.focus();
}
</script>
<input bind:this={input} />
生命周期
onMount()
生命周期回调函数会在组件初次渲染之后执行,如果 onMount()
返回了一个函数,该函数会在组件销毁时执行。
为兼容服务器端渲染,最好将 fetch()
请求放在 onMount()
里执行,而不是直接写在 <script>
里边。除了 onDestroy()
回调函数之外,其它的回调函数都不会在服务端渲染期间执行,因此 fetch()
的执行被延迟到了客户端 DOM 渲染之后。
onDestroy()
在组件销毁时执行,常用于执行一些清理操作,例如 setInterval()
的清理。防止内存泄露。
import { onDestroy } from 'svelte';
let counter = 0;
const interval = setInterval(() => counter += 1, 1000);
onDestroy(() => clearInterval(interval));
为了防止忘记清理,可以自定义一个函数:
import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}
// 使用
import { onInterval } from './utils.js';
let counter = 0;
onInterval(() => counter += 1, 1000);
beforeUpdate()
和 afterUpdate()
分别在组件的 DOM 更新之前和更新之后执行。
tick()
方法可以在任何时候调用。它会返回一个 Promise, 并且该 Promise 会在 DOM 的 pending 状态变化之后执行。用一个例子来说明:
<script>
let text = `Select some text and hit the tab key to toggle uppercase`;
async function handleKeydown(event) {
if (event.key !== 'Tab') 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)
);
// 下面的代码没有效果,因为 DOM 还没有更新
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
}
</script>
<style>
textarea {
width: 100%;
height: 200px;
}
</style>
<textarea value={text} on:keydown={handleKeydown}></textarea>
运行代码之后,选中文本框中一段字符串,按键盘上的 Tab 键,可以看到选中的文本大小写被转换了。但是转换之后,光标移到了文本的末尾,并没有按照代码的意图继续保持刚刚选中的状态。这是因为执行最后两句代码的时候,DOM 还没有更新。而当 DOM 更新之后,最后两句代码已经执行完了。所以这里需要在执行最后两句代码之前,等待 DOM 更新完成,这时候就需要用到 tick()
.
import { tick } from 'svelte';
//...其它代码...
async function handleKeydown(event) {
//...其它代码...
tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
}