除去脚本,组件的另一个核心点是 UI,用于描述 UI 的 HTML 是一种“静态”的语言,HTML 是无法表达 逻辑 的,比如条件和循环,但 Svelte 可以。Svelte 为其增加了一些逻辑支持,譬如判断、遍历等,以增强你对 UI 的表达能力。
这些额外附加的逻辑能力似曾相识,它十分接近一些以往非常流行的 Handlebars 或者 Mustache 模板语言,例如 {
{#if ...}}
,稍有区别的是,Svelte 使用单个大括号括起:{#if ...}
。
Svelte 的编译器将会编译这些逻辑,通过将 HTML 编译成使用原生 JS 创建的形式,也即这些 HTML 最终将使用 createElement 等原生的 DOM API 来创建。
解释 HTML 模板时,Svelte 就已经明确哪些状态需要更新。
1、If 块
先来参看这些代码:
App.svelte
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
<button on:click={toggle}>Log out</button>
<button on:click={toggle}>Log in</button>
为了可以按条件渲染某些标记,我们将其放置在 if
块中:
App.svelte
<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}
当然,这仅仅是为了演示,实际上我们知道两个 button 唯一的区别是“out”和“in”两个单词之差,完全可以使用一个 ? 号表达式来替代上述繁琐的 if。
2、Else 块
由于if user.loggedIn
和if !user.loggedIn
这两个条件是互斥的,因此可以使用else
块来简化它:
App.svelte
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>Log out</button>
{:else}
<button on:click={toggle}>Log in</button>
{/if}
#
字符代表 块开始 标记。/
字符代表 块结束 标记。而{:else}
中的:
字符,则表示 块连续 标记。不用担心,至此你几乎已经学完 Svelte 为 HTML 添加的所有语法了。
3、Else-If 块
假设我们已有如下代码:
App.svelte
<script>
let x = 7;
</script>
{#if x > 10}
<p>{x} 大于 10</p>
{:else}
{#if 5 > x}
<p>{x} 小于 5</p>
{:else}
<p>{x} 在 5 和 10 之间</p>
{/if}
{/if}
可以使用else if
将多个条件 “连接” 在一起:
App.svelte
<script>
let x = 7;
</script>
{#if x > 10}
<p>{x} 大于 10</p>
{:else if 5 > x}
<p>{x} 小于 5</p>
{:else}
<p>{x} 在 5 和 10 之间</p>
{/if}
4、each 块
如果你需要遍历数据列表,请使用each
块,如下方代码,我们有一个数组需要遍历:
App.svelte
<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}
<li>
<a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a>
</li>
{/each}
</ul>
这个例子中的表达式cats
的地方,事实上可以是任意数组或类数组的对象(只要它具有length
属性)。你可以使用each [...iterable]
来遍历迭代器。
你可以将当前的 index 索引作为第二个参数,如下所示:
App.svelte
<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>
只要你愿意,还可以使用解构语法each cats as { id, name }, i
,也即是用id
、name
来代替cat.id
、cat.name
。
5、提供 key 的 each 块
默认情况下,当你修改 each
块的值时,它将该块的 结束 位置添加和删除项,并更新所有已更改的值。不过那可能不是你想要的。
示例胜千言,不过这个示例还挺复杂的,分为两个组件:
Thing.svelte
<script>
// 每当 prop 的值更改时,`current` 都会更新...
export let current;
// ...但 `initial` 初始化后就固定不变。
const initial = current;
</script>
<p>
<span style="background-color: {initial}">initial</span>
<span style="background-color: {current}">current</span>
</p>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>
App.svlete
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>Remove first thing</button>
{#each things as thing}
<Thing current={thing.color}/>
{/each}
点击几次 “Remove first thing” 按钮,需要注意的是,它从最后位置删除 <Thing>
,并为剩余的组件更新了 color
。
假设我们期望删除第一个 <Thing>
组件,其余的不受影响。
为此,我们为 each
块指定一个唯一的 ID:
{#each things as thing (thing.id)}
<Thing current={thing.color}/>
{/each}
这个 (thing.id)
告知 Svelte 如何计算变化的地方。
你可以使用任何对象作为 key,因为 Svelte 内部使用的是Map
来实现的,换而言之,你甚至可以使用(thing)
代替(thing.id)
。但是字符串或数字通常会更安全,因此这代表 ID 保持不变,且没有引用相等性问题,例如使用来自 API 服务传回来的新数据进行更新的时候。
6、await 块
大多数Web应用程序必须在某个时候处理异步数据,比如下方的代码:
App.svelte
<script>
async function getRandomNumber() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>生成随机数字</button>
<!-- 我们稍候将在此处使用 await 块 -->
<p>{promise}</p>
Svelte使您可以轻松地在标记中直接 await Promise的值:
App.svelte
<script>
async function getRandomNumber() {
const res = await fetch(`https://svelte.dev/tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>生成随机数字</button>
{#await promise}
<p>{promise}</p>
{:then number}
<!-- 返回的结果使用 :then 块连续标记,
后面提供接收Promise resolve 的变量名称
-->
<p>The number is {number}</p>
{:catch error}
<!-- 如果发生错误,在 :catch 子块中接收错误对象 -->
<p style="color: red">{error.message}</p>
{/await}
仅考虑最新的
promise
,这说明你无需顾虑 race 情况。
如果你事前就明确了 promise 不会 reject,则可省略 catch
块。如果在 promise 被 resolve 之前不想显示任何内容,甚至也可以省略第一个块:
App.svelte
<script>
async function getRandomNumber() {
const res = await fetch(`https://svelte.dev/tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>生成随机数字</button>
{#await promise then number}
<p>The number is {number}</p>
{/await}
7、key 块
现在我们有一个需求是展示一个数字,点击按钮每次加 2:
App.svelte
<script>
let num = 0
</script>
<p>{num}</p>
<button on:click={ () => num += 2 }> +2 </button>
如果我们希望数字变化时使用“淡入”的效果,在不使用 Svelte 内置的 transition
的情况下(动画相关的内容在后续章节有详尽的讲述),可能需要一个定时器配合 opacity 样式规则来做:
App.svelte
<script>
let num = 0
let opacity = 1
let duration = 300
function plus2() {
opacity = 0
setTimeout(() => {
opacity = 1
num += 2
}, duration)
}
</script>
<p style="transition: {duration}ms; opacity: {opacity};">{num}</p>
<button on:click={ plus2 }> +2 </button>
这显得繁琐了些,我们使用内置的 fade
:
App.svelte
<script>
import { fade } from 'svelte/transition'
let num = 0
</script>
<p in:fade>{num}</p>
<button on:click={ () => num += 2 }> +2 </button>
因为自带的 fade 淡入效果需要元素创建时才会展现,如你所见上述代码点击 +2 按钮并没有任何“动静”。
如何能够让 Svelte 知道:num 这个变量的值被修改(每次 +2,状态变化) 后,p 元素需要强制销毁,并重新渲染?
这就是 key 块的用途,事实上它的作用与第5节《提供 key 的 each 块》的作用是一样的,所不同的是一个应用在可迭代的对象,而 key 可以应用在任意状态。
现在我们通过加入 key 块轻松解决这个问题,现在它满足需求了:
App.svelte
<script>
import { fade } from 'svelte/transition'
let num = 0
</script>
{#key num}
<p in:fade>{num}</p>
{/key}
<button on:click={ () => num += 2 }> +2 </button>
只要 num 的值改变了,key 块内的元素都会被销毁
且重新渲染
。
总结
本章已涵盖了模板逻辑的全部内容,长篇累牍。
用得较多的通常会是 #each 块,包括使用了 key 的 each 块,使用 key 目的是计算出哪些是没有变化的元素,这样可以不需要重新创建它,以便减少 DOM 渲染的工作量。