Svelte 入门

Svelte

简介

官方文档:https://www.sveltejs.cn/tutorial

Svelte 是一个构建 web 应用程序的工具。

Svelte 与诸如 React 和 Vue 等 JavaScript 框架类似,都怀揣着一颗让构建交互式用户界面变得更容易的心。

但是有一个关键的区别:Svelte 在构建/编译阶段将你的应用程序转换为理想的 JavaScript 应用,而不是在运行阶段解释应用程序的代码。这意味着你不需要为框架所消耗的性能付出成本,并且在应用程序首次加载时没有额外损失。

你可以使用 Svelte 构建整个应用程序,也可以逐步将其融合到现有的代码中。你还可以将组件作为独立的包(package)交付到任何地方,并且不会有传统框架所带来的额外开销

构建

1
2
3
npx degit sveltejs/template <my_project_name>
cd <my_project_name>
npm install

组件

在 Svelte 中,应用程序由一个或多个 组件(components) 构成。组件是一个可重用的、自包含的代码块,它将 HTML、CSS 和 JavaScript 代码封装在一起并写入 .svelte 后缀名的文件中。

1
2
3
4
5
<script>
	let name = 'world';
</script>

<h1>Hello {name.toUpperCase()}!</h1>

动态属性

就像可以使用花括号控制文本一样,也可以使用花括号控制元素属性。

1
2
3
4
5
6
<script>
	let src = 'tutorial/image.gif';
	let name = 'Rick Astley';
</script>

<img {src} alt="{name} dances.">

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/image.gif

名称和值相同的属性并不少见,比如 src={src}。Svelte 为这些情况提供了一个方便的速记法:

1
<img {src} alt="A man dances.">

CSS 样式

就像在 HTML 中一样,你可以向组件添加一个 <style> 标签。

重要的是,这些 CSS 样式规则 的作用域被限定在当前组件中。您不会意外地更改应用程序中其他地方的 <p> 元素的样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Nested.svelte
<style>
	p {
		color: purple;
		font-family: 'Comic Sans MS', cursive;
		font-size: 2em;
	}
</style>

<p>This is a paragraph.</p>

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/image-20230410000637208.png

嵌套组件

将整个应用程序放在一个组件中是不切实际的。相反,我们可以从其他文件导入组件并包含它们,就好像我们包含 HTML 元素一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
	import Nested from './Nested.svelte';
</script>

<style>
	p {
		color: purple;
		font-family: 'Comic Sans MS', cursive;
		font-size: 2em;
	}
</style>

<p>This is a paragraph.</p>
<Nested/>	

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/image-20230410001438372.png

注意,尽管 Nested.svelte 有一个 <p> 元素,但 App.svelte 中的样式是不会溢出的(也就是不会影响 Nested.svelte 中的 <p> 元素)。

还需要注意的是,组件名称 Nested首字母大写。采用此约定是为了使我们能够区分用户自定义的组件和一般的 HTML 标签。

HTML 标签

通常,字符串以纯文本形式插入,这意味着像 <> 这样的字符没有特殊的含义。

有时需要将 HTML 直接绘制到组件中

在 Svelte 中,你可以使用特殊标记 {@html ...} 实现:

1
2
3
4
5
<script>
	let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>

在将表达式的输出插入到 DOM 之前,Svelte 不会对 {@html ...} 内的表达式的输出做任何清理的。

换言之,如果使用此功能,则必须手动转义来自不信任源的 HTML 代码,否则会使用户面临 XSS 攻击的风险。

创建一个应用程序

首先,你需要将 Svelte 与构建工具集成起来。官方提供了针对 Rollupwebpack 的插件:

另外还有很多是社区维护的插件

然后,一旦您的项目设置好了,使用 Svelte 组件就很容易了。编译器将每个组件转换为常规的 JavaScript 类 - 接下来只需导入它并用 new 实例化即可:

1
2
3
4
5
6
7
8
9
import App from './App.svelte';

const app = new App({
	target: document.body,
	props: {
		// we'll learn about props later
		answer: 42
	}
});

然后,如果需要,你可以使用 组件 APIapp 进行交互。

反应性能力

Svelete 的内核是一个强大的 (反应性)reactivity 系统,能够让 DOM 与你的应用程序状态保持同步,例如,事件响应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
	let count = 0;

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

声明

当组件的状态改变时,Svelte 会自动更新 DOM。通常,组件状态的某些部分需要通过 其它 部分的计算而得出(例如 fullname 就是 firstnamelastname 的合体),并在 其它 部分更改时重新计算。

对于这些,我们提供了 反应式声明(reactive declarations)。它们看起来像这样:

1
2
let count = 0;
$: doubled = count * 2;

看来其有点陌生,不过别担心。上述是 有效 (非常规)的 JavaScript 语句,Svelte 会将其解释为 “只要参考值变化了就重新运行此代码”。一旦看习惯了,你就再也戒不掉了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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>

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/image-20230410002438130.png

当然,你可以在 HTML 标签内书写 {count * 2},而不必非得使用反应式声明的语法。但是,当你需要多次引用它们时,或者你需要的值依赖于 其它 响应式声明所计算的来的值时,响应式声明就变得特别有用。

语句

我们不仅提供了声明反应式的 ,我们还可以运行反应式的 语句。例如,当 count 的值改变时就输出到日志中:

1
$: console.log(`the count is ${count}`);

你可以轻松地将一组语句组合成一个代码块:

1
2
3
4
$: {
	console.log(`the count is ${count}`);
	alert(`I SAID THE COUNT IS ${count}`);
}

你甚至可以将 $: 放在= if 代码块前面:

1
2
3
4
$: if (count >= 10) {
	alert(`count is dangerously high!`);
	count = 9;
}

更新数组和对象

由于 Svelte 的反应性是由赋值语句触发的,因此使用数组的诸如 pushsplice 之类的方法就不会触发自动更新。例如,点击按钮就不会执行任何操作。

解决该问题的一种方法是添加一个多余的赋值语句:

1
2
3
4
function addNumber() {
	numbers.push(numbers.length + 1);
	numbers = numbers;
}

但还有一个更惯用的解决方案:

1
2
3
function addNumber() {
	numbers = [...numbers, numbers.length + 1];
}

你可以使用类似的模式来替换 popshiftunshiftsplice 方法。

✔️ 赋值给数组和对象的 属性(properties) (例如,obj.foo += 1array[i] = x)与对值本身进行赋值的方式相同。

1
2
3
function addNumber() {
	numbers[numbers.length] = numbers.length + 1;
}

一个简单的经验法则是:**被更新的变量的名称必须出现在赋值语句的左侧。**例如下面这个:

1
2
const foo = obj.foo;
foo.bar = 'baz';

就不会更新对 obj.foo.bar 的引用,除非使用 obj = obj 方式。

属性

声明属性

到目前为止,我们仅处理属性的内部状态,也就是说,这些值只能在给定组件中访问。

在任何实际应用中,你都会需要将数据从一个组件传递给其子组件。

为此,我们需要声明属性(Declaring properties, generally shortened to ‘props’)。在 Svelte 中,我们使用 export 关键字来做到这一点。

1
2
3
<script>
	export let answer;
</script>

Just like $:, this may feel a little weird at first. That’s not how export normally works in JavaScript modules! Just roll with it for now — it’ll soon become second nature.

1
2
3
4
5
6
7
// Nested.svelte

<script>
	export let answer;
</script>

<p>The answer is {answer}</p>
1
2
3
4
5
6
7
// App.svelte

<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>

默认值

We can easily specify default values for props in Nested.svelte:

1
2
3
<script>
	export let answer = 'a mystery';
</script>

If we now add a second component without an answer prop, it will fall back to the default:

1
2
<Nested answer={42}/>
<Nested/> // will be a 'a mystery'

属性传递

如果你的组件含有有一个对象属性,可以利用 ... 语法将它们spread(传播)到一个组件上,而不用逐一指定:

1
<Info {...pkg}/>

相反,如果你需要引用传递到组件中的所有道具,包括未使用 export 声明的道具,可以利用 $$props 直接获取。但通常不建议这么做,因为 Svelte 难以优化,但在极少数情况下很有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Info.svelte
<script>
	export let name;
	export let version;
	export let speed;
	export let website;
</script>

<p>
	The <code>{name}</code> package is {speed} fast.
	Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
	and <a href={website}>learn more here</a>
</p>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// App.svelte
<script>
	import Info from './Info.svelte';

	const pkg = {
		name: 'svelte',
		version: 3,
		speed: 'blazing',
		website: 'https://svelte.dev'
	};
</script>

<Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>
// 等价于
<Info {...pkg}/>

逻辑

if

HTML doesn’t have a way of expressing logic, like conditionals and loops. Svelte does.

To conditionally render some markup, we wrap it in an if block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<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}

else

Since the two conditions — if user.loggedIn and if !user.loggedIn — are mutually exclusive(互斥的), we can simplify this component slightly by using an else block:

1
2
3
4
5
6
7
8
9
{#if user.loggedIn}
	<button on:click={toggle}>
		Log out
	</button>
{:else}
	<button on:click={toggle}>
		Log in
	</button>
{/if}
  • A # character always indicates a block opening tag.

  • A / character always indicates a block closing tag.

  • A : character, as in {:else}, indicates a block continuation tag.

Don’t worry — you’ve already learned almost all the syntax Svelte adds to HTML.

else if

将多个条件链接在一起请使用else if

1
2
3
4
5
6
7
{#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

如果你遇到需要遍历的数据列表,请使用 each 块:

1
2
3
4
5
6
7
<ul>
	{#each cats as cat}
		<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
			{cat.name}
		</a></li>
	{/each}
</ul>

表达式 cats 是一个数组

遇到数组或类似于数组的对象 (即具有 length 属性),你都可以通过 each [...iterable] 遍历迭代该对象。

你也可以将 index 作为第二个参数 (key),类似于:

1
2
3
4
5
{#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}

如果你希望代码更加健壮,你可以使用 each cats as { id, name } 来解构,用 idname 来代替 cat.idcat.name

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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 { id, name }, i}
		<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
			{i + 1}: {name}
		</a></li>
	{/each}
</ul>

为 each 块添加 key 值

具体示例见 https://www.sveltejs.cn/tutorial/keyed-each-blocks

一般来说,当你修改 each 块中的值时,它将会在尾端进行添加或删除条目,并更新所有变化, 这可能不是你想要的效果。

为此,我们为 each 块指定一个唯一标识符,作为 key 值:

1
2
3
{#each things as thing (thing.id)}
	<Thing current={thing.color}/>
{/each}

(thing.id) 告诉 Svelte 什么地方需要改变。

你可以将任何对象用作 key 来使用,就像 Svelte 用 Map 在内部作为 key 一样,换句话说,你可以用 (thing) 来代替 (thing.id) 作为 key 值。但是,使用字符串或者数字作为 key 值通常更安全,因为这能确保它的唯一性,例如,使用来自 API 服务器的新数据进行更新时。

Await

很多 Web 应用程序都可能在某个时候有需要处理异步数据的需求。使用 Svelte 在标签中使用 await 处理 promises 数据亦是十分容易:

1
2
3
4
5
6
7
{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}

promise 总是获取最新的信息,这使得你无需关心 rece 状态

如果你的知道你的 promise 返回错误,则可以忽略 catch 块。如果在请求完成之前不想程序执行任何操作,也可以忽略第一个块。

1
2
3
{#await promise then value}
	<p>the value is {value}</p>
{/await}

事件

DOM events

As we’ve briefly seen already, you can listen to any event on an element with the on: directive:

1
2
3
<div on:mousemove={handleMousemove}>
	The mouse position is {m.x} x {m.y}
</div>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<script>
	let m = { x: 0, y: 0 };

	function handleMousemove(event) {
		m.x = event.clientX;
		m.y = event.clientY;
	}
</script>

<style>
	div { width: 100%; height: 100%; }
</style>

<div on:mousemove={handleMousemove}>
	The mouse position is {m.x} x {m.y}
</div>

在此列举一些常用的 DOM Events

Inline handlers

You can also declare event handlers inline:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
	let m = { x: 0, y: 0 };
</script>

<style>
	div { width: 100%; height: 100%; }
</style>

<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
	The mouse position is {m.x} x {m.y}
</div>

The quote marks are optional, but they’re helpful for syntax highlighting in some environments.

In some frameworks you may see recommendations to avoid inline event handlers for performance reasons, particularly inside loops. That advice doesn’t apply to Svelte — the compiler will always do the right thing, whichever form you choose.

事件修饰符

DOM 事件处理程序具有额外的 修饰符(modifiers) 。 例如,带 once 修饰符的事件处理程序只运用一次:

1
2
3
4
5
6
7
8
9
<script>
	function handleClick() {
		alert('no more alerts')
	}
</script>

<button on:click|once={handleClick}>
	Click me
</button>

所有修饰符列表:

  • preventDefault:调用 event.preventDefault() ,在运行处理程序之前调用。比如,对客户端表单处理有用。
  • stopPropagation:调用 event.stopPropagation(), 防止事件影响到下一个元素。
  • passive:优化了对 touch/wheel 事件的滚动表现 (Svelte 会在合适的地方自动添加滚动条)。
  • capture:在 capture 阶段而不是 bubbling 阶段触发事件处理程序 (MDN docs)
  • once:运行一次事件处理程序后将其删除。
  • self:仅当 event.target 是其本身时才执行。

你可以将修饰符组合在一起使用,例如:on:click|once|capture={...}

组件事件

组件也可以调度事件,为此,组件内必须创建一个相同事件并在外部进行分配。 更改 Inner.svelte

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 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>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// App.svelte
<script>
	import Inner from './Inner.svelte';

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Inner on:message={handleMessage}/>

createEventDispatcher 必须在首次实例化组件时调用它,—组件本身不支持如 setTimeout 之类的事件回调。 定义一个 dispatch 进行连接,进而把组件实例化。

事件转发

与 DOM 事件不同, 组件事件不会 冒泡(bubble) ,如果你想要在某个深层嵌套的组件上监听事件,则中间组件必须 转发(forward) 该事件。

这种情况,我们有类似的 App.svelteInner.svelte 在上一章节,但是现在多出一个 Outer.svelte 来包含 <Inner/>

解决这个问题的方法之一是添加 createEventDispatcherOuter.svelte中,监听其 message 事件,并为它创建一个转发程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Outer.svelte
<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}/>

但这样书写似乎有些臃肿,因此 Svelte 设立了一个简写属性 on:messagemessage 没有赋予特定的值得情况下意味着转发所有massage事件:

1
2
3
4
5
<script>
	import Inner from './Inner.svelte';
</script>

<Inner on:message/>

DOM 事件转发

事件转发也可以应用到 DOM 事件。

我们希望 <FancyButton> 组件内部接收外部的 handleClick(),为此,我们只需要为 FancyButton.svelte 内的 <button> 标签添加 click 事件即可:

1
2
3
<button on:click>
	Click me
</button>

绑定

Text inputs

(自上而下的数据流)As a general rule, data flow in Svelte is top down — a parent component can set props on a child component, and a component can set attributes on an element, but not the other way around.

Sometimes it’s useful to break that rule. Take the case of the <input> element in this component — we could add an on:input event handler that sets the value of name to event.target.value, but it’s a bit… boilerplatey(样板化). It gets even worse with other form elements, as we’ll see.

Instead, we can use the bind:value directive:

1
<input bind:value={name}>

This means that not only will changes to the value of name update the input value, but changes to the input value will update name.

Numeric inputs

In the DOM, everything is a string. That’s unhelpful when you’re dealing with numeric inputs — type="number" and type="range" — as it means you have to remember to coerce(强制) input.value before using it.

With bind:value, Svelte takes care of it for you:

1
2
<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<script>
	let a = 1;
	let b = 2;
</script>

<label>
	<input type=number value={a} min=0 max=10>
	<input type=range value={a} min=0 max=10>
</label>

<label>
	<input type=number value={b} min=0 max=10>
	<input type=range value={b} min=0 max=10>
</label>

<p>{a} + {b} = {a + b}</p>

复选框

我们不仅可以使用 input.value,也可以将复选状态绑定 input.checked 将复选框的状态绑定:

1
<input type=checkbox bind:checked={yes}>

输入框组绑定

如果你需要绑定更多值,则可以使用 bind:groupvalue 属性放在一起使用。 在 bind:group 中,同一组的单选框值是互斥的,同一组的复选框会形成一个数组。

添加 bind:group 到每一个选择框:

1
<input type=radio bind:group={scoops} value={1}>

在这种情况下,我们可以给复选框标签添加一个 each 块来简化代码。 首先添加一个menu变量到 <script>标签中:

1
2
3
4
5
let menu = [
	'Cookies and cream',
	'Mint choc chip',
	'Raspberry ripple'
];

接下来继续替换:

1
2
3
4
5
6
7
8
<h2>Flavours</h2>

{#each menu as flavour}
	<label>
		<input type=checkbox bind:group={flavours} value={flavour}>
		{flavour}
	</label>
{/each}

现在,我们可以轻易的拓展我们的“ice cream menu”。

例:

 1
 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
<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} 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>

<h2>Flavours</h2>

{#each menu as flavour}
	<label>
		<input type=checkbox bind:group={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}

文本域绑定

同样,<textarea> 标签在 Svelte 也可使用 bind:value 进行绑定:

1
<textarea bind:value={value}></textarea>

在这种情况下,如果值与变量名相同,我们也可以使用简写形式:

1
<textarea bind:value></textarea>

这种写法适用于所有绑定,而不仅仅是 <textarea>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
	import marked from 'marked';
	let value = `Some words are *italic*, some are **bold**`;
</script>

<style>
	textarea { width: 100%; height: 200px; }
</style>

<textarea bind:value></textarea>

{@html marked(value)}

选择框绑定

我们还可以利用 bind:value<select> 标签进行绑定

 1
 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
<script>
	let questions = [
		{ id: 1, text: `Where did you go to school?` },
		{ id: 2, text: `What is your mother's name?` },
		{ id: 3, text: `What is another personal fact that an attacker could easily find with Google?` }
	];

	let selected;

	let answer = '';
</script>

<style>
	input { display: block; width: 500px; max-width: 100%; }
</style>


<select bind:value={selected} on:change="{() => answer = ''}">
    {#each questions as question}
        <option value={question}>
            {question.text}
        </option>
    {/each}
</select>


<p>selected question {selected ? selected.id : '[waiting...]'}</p>

即使 <option> 中的值是对象而非字符串, Svelte 对它进行绑定也不会有任何困难。

由于我们没有 selected 设置为初始值,因此绑定会自动将其 (列表中的第一个) 设置为默认值。 但也要注意,在绑定的目标未初始化前,selected 仍然是未定义的,因此我们应该谨慎的使用诸如selected.id中的内容。

生命周期

onMounted

Every component has a lifecycle that starts when it is created, and ends when it is destroyed. There are a handful of functions that allow you to run code at key moments during that lifecycle.

The one you’ll use most frequently is onMount, which runs after the component is first rendered to the DOM. We briefly encountered it earlier when we needed to interact with a <canvas> element after it had been rendered.

We’ll add an onMount handler that loads some data over the network:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<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>

It’s recommended to put the fetch in onMount rather than at the top level of the <script> because of server-side rendering (SSR).

With the exception of onDestroy, lifecycle functions don’t run during SSR, which means we can avoid fetching data that should be loaded lazily once the component has been mounted in the DOM.

Lifecycle functions must be called while the component is initialising so that the callback is bound to the component instance — not (say) in a setTimeout.

If the onMount callback returns a function, that function will be called when the component is destroyed.

例:

 1
 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
<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>

<style>
	.photos {
		width: 100%;
		display: grid;
		grid-template-columns: repeat(5, 1fr);
		grid-gap: 8px;
	}

	figure, img {
		width: 100%;
		margin: 0;
	}
</style>

<h1>Photo album</h1>

<div class="photos">
	{#each photos as photo}
		<figure>
			<img src={photo.thumbnailUrl} alt={photo.title}>
			<figcaption>{photo.title}</figcaption>
		</figure>
	{:else}
		<!-- this block renders when photos.length === 0 -->
		<p>loading...</p>
	{/each}
</div>

onDestroy

To run code when your component is destroyed, use onDestroy.

For example, we can add a setInterval function when our component initialises, and clean it up when it’s no longer relevant. Doing so prevents memory leaks.

1
2
3
4
5
6
7
8
<script>
	import { onDestroy } from 'svelte';

	let seconds = 0;
	const interval = setInterval(() => seconds += 1, 1000); // 每1000毫秒

	onDestroy(() => clearInterval(interval));
</script>

While it’s important to call lifecycle functions during the component’s initialisation, it doesn’t matter where you call them from. So if we wanted, we could abstract the interval logic into a helper function in utils.js

1
2
3
4
5
6
7
8
9
import { onDestroy } from 'svelte';

export function onInterval(callback, milliseconds) {
	const interval = setInterval(callback, milliseconds);

	onDestroy(() => {
		clearInterval(interval);
	});
}

…and import it into our component:

1
2
3
4
5
6
<script>
	import { onInterval } from './utils.js';

	let seconds = 0;
	onInterval(() => seconds += 1, 1000);
</script>

beforeUpdate 和 afterUpdate

顾名思义,beforeUpdate 函数实现在 DOM 渲染完成前执行。afterUpdate 函数则相反,它会运行在你的异步数据加载完成后。

总之,它们对于一些需要以状态驱动的地方很有用, 例如渲染标签的滚动位置。

这个 Eliza 聊天机器人窗口体验不太好,一旦消息超过窗口高度,你必须手动滚动窗口才能查看最新消息,让我们来改进它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let div;
let autoscroll;

beforeUpdate(() => {
	autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});

afterUpdate(() => {
	if (autoscroll) div.scrollTo(0, div.scrollHeight);
});

请注意,beforeUpdate 函数需要在组件挂载前运行,所以我们需要先将 div 与组件标签绑定,并判断 div 是否已被正常渲染

例:

  1
  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
<script>
	import Eliza from 'elizabot';
	import { beforeUpdate, afterUpdate } from 'svelte';

	let div;
	let autoscroll;

	beforeUpdate(() => {
        // determine whether we should auto-scroll
		// once the DOM is updated...
		autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
	});

	afterUpdate(() => {
        // ...the DOM is now in sync with the data
		if (autoscroll) div.scrollTo(0, div.scrollHeight);
	});

	const eliza = new Eliza();

	let comments = [
		{ author: 'eliza', text: eliza.getInitial() }
	];

	function handleKeydown(event) {
		if (event.which === 13) {
			const text = event.target.value;
			if (!text) return;

			comments = comments.concat({
				author: 'user',
				text
			});

			event.target.value = '';

			const reply = eliza.transform(text);

			setTimeout(() => {
				comments = comments.concat({
					author: 'eliza',
					text: '...',
					placeholder: true
				});

				setTimeout(() => {
					comments = comments.filter(comment => !comment.placeholder).concat({
						author: 'eliza',
						text: reply
					});
				}, 500 + Math.random() * 500);
			}, 200 + Math.random() * 200);
		}
	}
</script>

<style>
	.chat {
		display: flex;
		flex-direction: column;
		height: 100%;
		max-width: 320px;
	}

	.scrollable {
		flex: 1 1 auto;
		border-top: 1px solid #eee;
		margin: 0 0 0.5em 0;
		overflow-y: auto;
	}

	article {
		margin: 0.5em 0;
	}

	.user {
		text-align: right;
	}

	span {
		padding: 0.5em 1em;
		display: inline-block;
	}

	.eliza span {
		background-color: #eee;
		border-radius: 1em 1em 1em 0;
	}

	.user span {
		background-color: #0074D9;
		color: white;
		border-radius: 1em 1em 0 1em;
		word-break: break-all;
	}
</style>

<div class="chat">
	<h1>Eliza</h1>

	<div class="scrollable" bind:this={div}>
		{#each comments as comment}
			<article class={comment.author}>
				<span>{comment.text}</span>
			</article>
		{/each}
	</div>

	<input on:keydown={handleKeydown}>
</div>

tick

tick 函数不同于其他生命周期函数,因为你可以随时调用它,而不用等待组件首次初始化。它返回一个带有 resolve 方法的 Promise,每当组件 pending 状态 变化便会立即体现到 DOM 中 (除非没有 pending 状态 变化)

在 Svelte 中每当组件状态失效时,DOM 不会立即更新。 反而 Svelte 会等待下一个 microtask 以查看是否还有其他变化的状态或组件需要应用更新。这样做避免了浏览器做无用功,使之更高效。

这点在本示例中有所体现。选择文本,然后按 “Tab” 键。 因为 <textarea> 标签的值已发生变化,浏览器会将选中区域取消选中并将光标置于文本末尾,这显然不是我们想要的,我们可以借助 tick 函数来解决此问题:

1
import { tick } from 'svelte';

然后在 this.selectionStartthis.selectionEnd 设置结束前立即运行handleKeydown

1
2
3
await tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;

例:

 1
 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
<script>
	import { tick } from 'svelte';

	let text = `Select some text and hit the tab key to toggle uppercase`;

	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;
	}
</script>

<style>
	textarea {
		width: 100%;
		height: 200px;
	}
</style>

<textarea value={text} on:keydown={handleKeydown}></textarea>

Stores

stores 用于跨组件之间的状态共享。

当期望脱离组件的层级(父-子)关系且能够在任意位置都能访问某个状态(变量)时,状态管理仍然是非常有用的一个特性。

Store 写法只需要写在一个 js 文件中,然后通过 svelte/store 中提供的 writable 方法来向公共仓库中注册一个值作为一个仓库元素,之后在组件内可以通过 subscribe 来监听仓库元素的变化(理解上来说本质上是一个发布订阅的模式),通过 set 和 update 来发布仓库内某一个值的变化。

  • Set:直接将仓库内的某个数指定为某个值
  • Update:接收一个仓库当前值的参数的回调函数,将执行结果作为要更新仓库参数的值

可写状态 Writable stores

并非所有的状态都属于在组件层次的结构内。某些时候,有些状态需要被多个毫不相干的组件或普通的 JavaScript 模块访问。

在 Svelte 中,我们通过 store 来实现。

Svelte 将状态划分为两种,一种可读可写,一种只读,都用可读可写(可以读取,也可以修改)虽然省事,不过允许或者说强制状态是只读的,可以防止状态被意外修改。

要创建一个可读且可写的状态十分简单,例如我们要创建一个数字型的可写状态 count,Svelte 提供 writable 函数来创建:

1
2
3
4
// stores.js
import { writable } from 'svelte/store';

export const count = writable(0);

我们可以将创建状态的代码,放到单独的文件 store.js 中,以便其他需要用到 count 状态的组件可以引入使用。

所谓 store(也即状态),只不过是具有 subscribe 方法的对象,它允许当 store 的值改变时自动通知对此感兴趣的相关组件或程序。在 App.svelte 中,count 便是一个 store,我们在 count.subscribe 的回调中设置 count_value 的值。

1
2
3
4
5
let count_value;

const unsubscribe = count.subscribe(value => {
count_value = value;
});

除了 subscribe 方法外,它还具有 setupdate 方法

1
2
3
4
5
6
7
function increment() {
  count.update(n => n + 1);
}

function reset() {
  count.set(0);
}

✔️ 当我们需要知道 count 当前值的时候,应该使用 update,它会将当前值传递到回调函数供你使用;如果无需知道,则使用 set

例子:$count += 1 就相当于 count.update(n => n + 1) 或者 count.set($count + 1)

自动订阅(Auto-subscriptions)

上一个例子中,程序虽然可以这么写,不过存在一个不易察觉的错误:unsubscribe 函数没有机会被调用。如果该组件会被多次实例化和销毁,这将导致 内存泄露

解决之道,应该使用 onDestroy 这个生命周期 Hook。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script>
  import { onDestroy } from 'svelte';
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Reset from './Reset.svelte';

  let count_value;

  const unsubscribe = count.subscribe(value => {
    count_value = value;
  });

  onDestroy(unsubscribe);
</script>

<h1>count 当前的值是:{count_value}</h1>

不过事情又开始变得有点呆板重复了。

特别是当你的组件 subscribe 了很多的 store 的时候。Svelte 给出一个绝佳的替代方案,你可以在 store 名称前面加上 $ 前缀来引用这个 store 的值:

1
2
3
4
5
6
7
8
<script>
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Reset from './Reset.svelte';
</script>

<h1>count 当前的值是:{$count}</h1>

自动订阅仅适用于在组件的顶层范围声明(或者导入的JS文件中)的 store 变量。

在标记中使用 $count 不会有任何限制,你也可以在 <script> 的任何位置使用它,例如在事件处理程序或者响应式声明中。

Svelte 假定所有以 $ 开头的任何标识符都表示引用某个 store 值,而 $ 实际上是一个保留字符,Svelte 会禁止你使用 $ 作为你声明的变量的前缀。

只读状态(Readable stores)

并非所有 store 允许所有人可写的。例如,你可能有一个 store 表示鼠标位置或者用户地理位置,允许 ‘外部’ 来修改这个值是没有意义的。对于这种情况,我们可以用只读 store。

本节我们要制作一个数字钟,显示当前的时间,先看看主程序的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// App.svelte
<script>
  import { time } from './stores.js';

  const formatter = new Intl.DateTimeFormat('zh-CN', {
    hour12: false,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
</script>

<h1>The time is {formatter.format($time)}</h1>