Svelte学习--基础篇(一)

1,202 阅读5分钟

初步了解

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

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

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

基础知识

声明式添加数据

在组件<script> 标签声明一个 name 变量,然后在标签中应用name变量:

<script>
    let name = 'world';
</script>

<h1>Hello {name}!</h1>

动态属性

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

<script>
    let imageSrc = 'image.jpg';
</script>

<img src={imageSrc} alt="Super Man">

如果变量名与属性名相同,可以省去属性名,否则会报错

<script>
    let src = 'image.jpg';
</script>

// <img src={src} alt="Super Man"> //A11y: <img> element should have an alt attribute
<img {src} alt="Super Man">

同样属性内部也可以添加变量应用

<script>
    let src = 'image.jpg';
    let name = 'You';
</script>

<img {src} alt="Super Man is { name }">

Css样式

组件的Css样式规则的作用域会被限定在当前组件中,其原理是往标签添加一个class类,类似Css Scope。

<style>
#header{
    color: green;
}
.main {
    width: 50px;
    height: 50px;
    background: yellow;
}
p{
    color: red
}
</style>

<span id="header">Header</span>
<div class="main"></div>
<p>This is a paragraph.</p>

最终css转化成:

#header.svelte-1vottuu{color:green}
.main.svelte-1vottuu{width:50px;height:50px;background:yellow}
p.svelte-1vottuu{color:red}

这样就达到作用域的效果啦。

嵌套组件

跟Vue和React一样,都会根据使用场景将应用程序拆分出不同类型的组件。组件导入在<script>标签中使用import语法导入

// Comp.svelte
<p>This is a Component.</p>
// App.svelte
<script>
    import Comp from './Comp.svelte'
</script>
<p>This is a App.</p>
<Comp />

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

HTML 标签

通常,字符串以纯文本形式插入,这意味着像 < 和 > 这样的字符没有特殊的含义。但有时需要将 HTML 直接绘制到组件中。这时候可以使用特殊标记{@html ...}实现:

<script>
let string = `This is a <strong>super man</strong>.`
</script>
<p>{@html string}</p>

在将表达式的输出插入到 DOM 之前,Svelte 不会对{@html ...}内的表达式的输出做任何清理的。换言之,如果使用此功能,则必须手动转义来自不信任源的 HTML 代码,否则会使用户面临 XSS 攻击的风险。

Reactivity 数据响应

赋值

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

<script>
let count = 0;

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

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

响应式声明 - 值

类似Vue的computed,需要通过其他组件状态计算得到。

<script>
let count = 0;
$: doubled = count * 2;
</script>

<p>{count} doubled is {doubled}</p>

响应式声明 - 语句

类型Vue的watch, 监听某个变量的变化

$: 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 的反应性是由赋值语句触发的,因此使用数组的诸如 push 和 splice 之类的方法就不会触发自动更新。

如下操作不会触发任何变化:

function addNumber() {
    numbers.push(numbers.length + 1);
}

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

function addNumber() {
    numbers.push(numbers.length + 1);
    numbers = numbers;
}
// OR
function addNumber() {
	numbers = [...numbers, numbers.length + 1];
}

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

属性

属性定义

组件通过export声明props, 也可直接给props赋值

// Child.svelte
<script>
    export let answer;
    export let name = "Tony"
</script>

// App.svelte
<script>
import Child from './Child.svelte';
</script>
<Child amswer="a mystery" name="Judy" />

属性传递

可以利用拓展符把对象属性添加到组件中

<script>
// <Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>
<Info {...pkg} />
</script>

逻辑

条件逻辑

通过把html代码放到if块中来控制渲染逻辑

{#if user.loggedIn}
    <button on:click={toggle}>
            Log out
    </button>
{/if}

{#if !user.loggedIn}
    <button on:click={toggle}>
        Log in
    </button>
{/if}

也可以直接写成 if ... else ...

{#if user.loggedIn}
    <button on:click={toggle}>
        Log out
    </button>
{:else}
    <button on:click={toggle}>
        Log in
    </button>
{/if}

也可以直接写成 if ... else if ...

{#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

<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),类似于:

{#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 }来解构,用cat.id 和 cat.name 来代替 id 和 name

{#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块添加key值

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

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

Await 块

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

<script>
const promise = getData();

async function getData(){
    const res = await axios.get(...)
    // TODO
    return res
}
</script>
{#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>the value is {value}</p>
{/await}

事件

通过在标签添加on:event来添加事件

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

添加内联函数

在某些框架中,出于性能原因,您可能会看到避免内联事件处理程序的建议,特别是在循环内部。这个建议不适用于Svelte——无论您选择哪种形式,编译器都会正常运行。

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

事件修饰符

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

<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
<script>
    // setup code goes here
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher()

    function sayHello() {
        dispatch('message',{
            text: 'Hello123!'
        });
    }
</script>

<button on:click={sayHello}>
	Click to say hello
</button>

// 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)  该事件。

Svelte 设立了一个简写属性 on:message。 message 没有赋予特定的值得情况下意味着转发所有massage事件:

// App.svelte
<Outer on:message={handleMessage} />

// Outer svelte
<Inner on:message />

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

DOM事件转发

在子组件<button>标签添加click 事件,就可以接收到外部的handleClick()事件。

<button on:click>
    Click me
</button>

Binding

input

通过bind:value来绑定变量

<script>
    let name = 'world';
</script>

<input bind:value={name}>
<input bind:value={name}>
<h1>Hello {name}!</h1>

在input使用数字处理方法

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

复选框

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

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

输入框组绑定

<script>
	let scoops = 1;
	let flavours = ['Mint choc chip'];

	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>

<label>
	<input type=checkbox bind:group={flavours} value="Cookies and cream">
	Cookies and cream
</label>

<label>
	<input type=checkbox bind:group={flavours} value="Mint choc chip">
	Mint choc chip
</label>

<label>
	<input type=checkbox bind:group={flavours} value="Raspberry ripple">
	Raspberry ripple
</label>

文本域 textarea

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

{@html marked(value)}

选项框

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

<script>
	const datas = [
		{
			id: 1,
			value: "data1",
		},
		{
			id: 2,
			value: "data2",
		},
		{
			id: 3,
			value: "data3",
		},
	];

	let selected;
</script>

<select bind:value={selected}>
	{#each datas as data}
		<option value={data}>{data.value}</option>
	{/each}
</select>
<p>{selected.id} -- {selected.value}</p> // Error: Cannot read property 'id' of undefined

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

如果选择框含有一个名为 multiple 的属性,在这种情况下,它将会被设置为数组而不是单值。

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

<select multiple bind:value={flavours}>
    {#each menu as flavour}
	<option value={flavour}>{flavour}</option>
    {/each}
</select>

Contenteditable

支持 contenteditable="true"属性的标签,可以使用 textContent 与 innerHTML 属性的绑定:

<script>
	let html = '<p>Write some text!</p>';
</script>

<div contenteditable="true" bind:innerHTML={html}></div>

<pre>{@html html}</pre>

Each 块绑定

以TODO List为例

<script>
	let todos = [
		{ done: false, text: "1" },
		{ done: false, text: "2" },
		{ done: false, text: "3" },
	];

	function removeElement(index) {
		todos.splice(index, 1);
		todos = todos;
	}

	function addElement() {
		// todos = [...todos, {done: false, text: ''}]
		todos = todos.concat({ done: false, text: "" });
	}

	function clear() {
		todos = todos.filter((el) => !el.done);
	}
</script>

{#each todos as todo, index}
	<div class:done={todo.done}>
		<input type="checkbox" bind:checked={todo.done} />

		<input type="text" disabled={todo.done} bind:value={todo.text} />

		<button on:click={() => removeElement(index)}>删除</button>
	</div>
{/each}

<button on:click={addElement}>Add New</button>

<button on:click={clear}>Remove Complete</button>

<style>
	.done {
		opacity: 0.4;
	}
</style>

媒体标签的绑定

<audio> 和 <video> 同样支持部分的属性绑定,接下来演示其中几个属性。

<video
	poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
	src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
	on:mousemove={handleMousemove}
	on:mousedown={handleMousedown}
	bind:currentTime={time}
	bind:duration
	bind:paused
></video>

bind:duration 相当于 bind:duration={duration}

现在,当您单击视频时,它将视情况更新 timeduration 和 paused 属性的值。这意味着我们可以使用它们来创建自定义控件。

通常,在网页中, 你会将currentTime用于对 timeupdate 的事件监听并跟踪。但是这些事件很少触发,导致UI不稳定。 Svelte 使用currentTime 对 requestAnimationFrame进行查验,进而避免了此问题。

针对 <audio> 和 <video> 的 6 个readonly 属性绑定 :

  • duration (readonly) :视频的总时长,以秒为单位。
  • buffered (readonly) :数组{start, end} 的对象。
  • seekable (readonly) :同上。
  • played (readonly) :同上。
  • seeking (readonly) :布尔值。
  • ended (readonly) :布尔值。

...以及 4 个双向 绑定:

  • currentTime :视频中的当前点,以秒为单位。
  • playbackRate :播放视频的倍速, 1 为 '正常'。
  • paused :暂停。
  • volume :音量,0到1之间的值。

<video> 还有多出了具有readonly 属性videoWidthvideoHeight 属性的绑定。

尺寸绑定

每个块级标签都可以对 clientWidthclientHeightoffsetWidth 以及 offsetHeight 属性进行绑定:

<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px">{text}</span>
</div>

这些绑定是只读的,更改w 和 h 的值不会有任何效果。

对标签的尺寸更改请阅读这里。 由于涉及到额外的性能开销,因此不建议在页面中大量的使用。

使用display: inline的标签无法获得尺寸,当然包含有其他有尺寸的标签 (例如<canvas>)也不会得到正常显示。在这种情况下建议对该标签嵌套一层标签或者直接绑定它的父级。

this

this可以绑定到任何标签 (或组件) 并允许你获取对渲染标签的引用。 例如,我们对<canvas> 标签进行绑定:

<script>
	import { onMount } from 'svelte';
	let canvas;
	onMount(() => {
		const ctx = canvas.getContext('2d');
		ctx.fillStyle="#FF0000";
		ctx.fillRect(0,0,150,75);
	});
</script>
<canvas
	bind:this={canvas}
	width={32}
	height={32}
></canvas>

注意,canvas的值 undefined 会一直存在直至组件挂载完毕,因此我们给 onMount添加一个 生命周期函数来处理。

组件绑定

正如可以绑定到DOM元素的属性一样,你也可以将组件的属性绑定。例如,我们能绑定位于<Sinput>组件内的 value 属性,就如同一个表单标签一般:

// App.svelte
<script>
    import Sinput from "./Sinput.svelte"
    let value = 1;
</script>

<Sinput bind:value={value}></Sinput>
<div>
    {value}
</div>

// Sinput.svelte
<script>
    export let value;
</script>

<input type="text" bind:value={value}>

总结

感觉svelte和react语法非常像,比如赋值什么的虽然react使用state方法添加响应值而svelte则是通过赋值的方式触发响应,不过svelte没有批处理,这样如果程序大时就会降低性能。接下来就是Runtime部分,虽然Svelte说没有Runtime,但是其中包含有生命周期等属性,这些都是属于Runtime的部分。