Svelte入门

165 阅读2分钟

前言

简单记录一下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}