svelte初体验

154 阅读2分钟

目的

让构建交互式用户界面变得更容易

特点

Svelte 在 构建/编译阶段 将你的应用程序转换为理想的 JavaScript 应用,而不是在 运行阶段 解释应用程序的代码。

  1. 不需要为框架所消耗的性能付出成本,并且在应用程序首次加载时没有额外损失
  1. 可以将组件作为独立的包(package)交付到任何地方

组件

组件是一个可重用的、自包含的代码块,它将 HTML、CSS 和 JavaScript 代码封装在一起并写入 .svelte 后缀名的文件中。

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

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

速记属性 and 动态属性

<script>
	let src = 'tutorial/image.gif';
	let name = 'Rick Astley';
</script>

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

css 样式

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

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

嵌套组件

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

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


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

<Nested/>

@html

<script>
	let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

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

响应式

响应式值

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

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

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

响应式语句

还可以👇这么用,每次count 改变,都会触发打印

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

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

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

响应式块

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

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

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

或者

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

更新数组和对象

	let numbers = [1, 2, 3, 4];

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

接受单个属性 props

子组件要接受父级组件传过来的 props,必须使用 export 声明

<Child answer={42}/>
	
// Child
<script>
	export	let answer;
</script>


<p>The answer is {answer}</p>

传入多个属性 and 接受多个属性

// Parent
<script>
	import Info from './Info.svelte';

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

<Info {...pkg}/>



	
// Child
<script>
// 	export let name;
// 	export let version;
// 	export let speed;
// 	export let website;
	const { name, version, speed, website } = $$props;
</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>

逻辑表达

  • 条件 conditions
  • 循环 loop

是不是很像是模版语法,用过 ejs 或 artTemplate 的小伙伴是不是很熟悉.

只需要记住一套统一的规范:

开始:{#xxx }

中间:{:xxx}

结束:{/xxx}

if...else

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

<script>
	let x = 7;
</script>

{#if x > 10}
	<p>{x} is greater than 10</p>
{:else if x < 5 }
		<p>{x} is less than 5</p>
{:else}
		<p>{x} is between 5 and 10</p>
{/if}

Each

each 常常是和 as 关键字一起使用,参数可选 item, index.

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

<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="norefer" href="https://www.youtube.com/watch?v={cat.id}">
			{cat.name}
		</a></li>
	{/each}
</ul>

Each 唯一标示符

语法: ( xxx )

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

	let things = [
		{ id: 1, color: '#0d0887' },
		{ id: 2, color: '#6a00a8' },
		{ id: 3, color: '#b12a90' },
		{ id: 4, color: '#e16462' },
		{ id: 5, color: '#fca636' }
	];

	function handleClick() {
		things = things.slice(1);
	}
</script>

<button on:click={handleClick}>
	Remove first thing
</button>

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

promise + async ... await 直接控制视图

{#await promise}
	<p>
		loading
	</p>
{:then data}
	<p>
	{data}	
	</p>
{:catch error}
	<p>
	error:	{error}
	</p>
{/await}

事件

事件绑定方式:on:eventName

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

// 或者 使用行内定义的方式
	
<div on:mousemove={(event) => {
		m.x = event.clientX;
		m.y = event.clientY;
	}}>
	The mouse position is {m.x} x {m.y}
</div>

// 应可以选择加 引号,引号是可选的
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
	The mouse position is {m.x} x {m.y}
</div>

在某些框架中,您可能会看到一些建议:出于性能考虑,避免行内事件定义,尤其是在循环内。该建议不适用于Svelte - 编译器将始终做正确的事情,无论您选择哪种形式。

事件修饰符

语法:on:click|once|capture|preventDefaut|stopPropagation{passive|self={...}

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

示例:

<script>
	function handleClick() {
		alert('clicked')
	}
</script>

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

自组件向父级组件通信

使用 dispatch 触发事件,dispatch 由 createEventDispatcher 实例化而来

// Parent
<script>
	import Inner from './Inner.svelte';

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

<Inner on:message={handleMessage}/>

// Child
<script>
import { createEventDispatcher } from 'svelte';
	
	const dispatch = createEventDispatcher();

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

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

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

多层级事件转发

方式一:一层一层往上转发,需要每一层都实现 dispatch 的逻辑

方式二(简写):在中间层 只需要写:on:message 既可

// 最外层
<script>
	import Outer from './Outer.svelte';

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

<Outer on:message={handleMessage}/>

// 中间层
<script>
import Inner from './Inner.svelte';
</script>

<Inner on:message/> // 重点关注

// 最内层
<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 事件转发

// Parent
<script>
	import FancyButton from './FancyButton.svelte';

	function handleClick() {
		alert('clicked');
	}
</script>

<FancyButton on:click={handleClick}/>

// Child
<style>
button {
	height: 4rem;
	width: 8rem;
	background-color: #aaa;
	border-color: #f1c40f;
	color: #f1c40f;
	font-size: 1.25rem;
	background-image: linear-gradient(45deg, #f1c40f 50%, transparent 50%);
	background-position: 100%;
	background-size: 400%;
	transition: background 300ms ease-in-out;
}
button:hover {
	background-position: 0;
	color: #aaa;
}
</style>

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

button 增加 on:click 把dom 点击事件转发到外层,否则,外层事件句柄不会被触发

数据绑定

如果我们要实现 vue 中经典的的双向数据绑定应该怎么做呢

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

<input bind:value={name}>

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

是不是很简单!!!

再看下面这个例子

<script>
	let a = 1;
	let b = 2;
</script>

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

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

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

bind:value 会自动帮你把 number 数字额变量处理为 input value 接受的字符串

再来看一个 复选框的例子

<script>
	let yes = false;
</script>

<label>
	<input type=checkbox bind:checked={yes}>
	Yes! Send me regular email spam
</label>

{#if yes}
	<p>Thank you. We will bombard your inbox and sell your personal details.</p>
{:else}
	<p>You must opt in to continue. If you're not paying, you're the product.</p>
{/if}

<button disabled={!yes}>
	Subscribe
</button>

bind:checked 会自动“跟踪” yes 的值

当遇到一组单选框和一组复选框的时候,可以使用 bind:group

如果值与变量名相同,我们也可以使用简写形式:

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

// 👇

<textarea bind:value></textarea>

甚至~

设置了 contenteditable=‘true’ 的标签也可以进行绑定

<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

<script>
	import { onMount } from 'svelte';

	let canvas;

	onMount(() => {
		const ctx = canvas.getContext('2d');
		let frame;

		(function loop() {
			frame = requestAnimationFrame(loop);

			const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

			for (let p = 0; p < imageData.data.length; p += 4) {
				const i = p / 4;
				const x = i % canvas.width;
				const y = i / canvas.height >>> 0;

				const t = window.performance.now();

				const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
				const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
				const b = 128;

				imageData.data[p + 0] = r;
				imageData.data[p + 1] = g;
				imageData.data[p + 2] = b;
				imageData.data[p + 3] = 255;
			}

			ctx.putImageData(imageData, 0, 0);
		}());

		return () => {
			cancelAnimationFrame(frame);
		};
	});
</script>

<style>
	canvas {
		width: 100%;
		height: 100%;
		background-color: #666;
		-webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
		mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
	}
</style>

<canvas
  bind:this={canvas}
	width={32}
	height={32}
></canvas>

如果没有 bind:this={canvas} ,那么将无法建立 标签和 canvas 变量的关联,其实就是类似 react 中的 ref

将自组件的值 绑定 到父级组件(或者叫 父级组件跟踪子组件的值)

// Parent
<script>
	import Keypad from './Keypad.svelte';

	let pin;
	$: view = pin ? pin.replace(/\d(?!$)/g, '•') : 'enter your pin';

	function handleSubmit() {
		alert(`submitted ${pin}`);
	}
</script>

<h1 style="color: {pin ? '#333' : '#ccc'}">{view}</h1>

<Keypad bind:value={pin} on:submit={handleSubmit}/>


	
// Child
<script>
	import { createEventDispatcher } from 'svelte';

	export let value = '';

	const dispatch = createEventDispatcher();

	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 on:click={select(2)}>2</button>
	<button on:click={select(3)}>3</button>
	<button on:click={select(4)}>4</button>
	<button on:click={select(5)}>5</button>
	<button on:click={select(6)}>6</button>
	<button on:click={select(7)}>7</button>
	<button on:click={select(8)}>8</button>
	<button on:click={select(9)}>9</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>

生命周期

每个组件都有一个生命周期,该生命周期始于创建时,并在销毁时结束。有几个函数使您可以在该组件生命周期期间的关键时刻执行一些代码。

onMount

您最常使用的是 OnMount,该生命周期方法将在组件渲染到DOM之后运行。

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

由于组件也可能在服务器端渲染 (SSR),建议将请求数据的操作放在 onMount 中,而不是

如果 onMount 回调返回一个函数,则该函数将在组件被销毁时调用。这点和 react 中的 useEffect 类似。

onDestory

在组件销毁的时候被调用,比如常用来销毁一些实例或者清除一些定时器的逻辑

<script>
	import { onDestroy } from 'svelte';

	let seconds = 0;
	const interval = setInterval(() => seconds += 1, 1000);

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

onDestory 的调用时机是很灵活的,你甚至这样做:

<script>
	import { onInterval } from './utils.js';

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


// utils.js
import { onDestroy } from 'svelte';

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

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

beforeUpdate 和 afterUpdate

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

tick

tick 是一个很特别的生命周期函数,你可以随时调用它,它返回一个带有 resolve 方法的Promise,每当 该 promise 的 pending 状态变化的时候,便会立即体现到 DOM 中。

原理

等待下 一个 microtask 任务队列,以查看是否还有其他的变化状态或者组件需要更新,其实是为了性能考虑,避免频繁的 dom 操作

例子

<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)
		);

		// this has no effect, because the DOM hasn't updated yet
		await tick();
		this.selectionStart = selectionStart;
		this.selectionEnd = selectionEnd;
	}
</script>

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

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

效果如下

不使用tick:

使用tick:

Store

可写 store

writable store 有两个方法:

  • update
  • set

可以被订阅:

const unsubscribe = count.subscribe(()=>{
// do sth ...
})

subscribe 返回的是一个unsubscribe 方法,用于 onDestory 的时候进行销毁

// App.svelte
<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';

	let count_value;

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

<h1>The count is {count_value}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>


// stores.js
import { writable } from 'svelte/store';

export const count = writable(0);

// Decrement.svelte
<script>
	import { count } from './stores.js';

	function decrement() {
		// TODO decrement the count
		count.update(n => n - 1)
	}
</script>

<button on:click={decrement}>
	-
</button>


// incrementer.svelte
<script>
	import { count } from './stores.js';

	function increment() {
		// TODO increment the count
		count.update(n => n + 1);
	}
</script>

<button on:click={increment}>
	+
</button>

// reseter.svelte
<script>
	import { count } from './stores.js';

	function reset() {
		// TODO reset the count
		count.set(0);
	}
</script>

<button on:click={reset}>
	reset
</button>

上面通过回调的方式可以达到订阅的目的,但是如果我们订阅了多个,就会显得很繁琐,可以使用“自定订阅”(快捷订阅方式):$xxx

<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

你不仅仅可以在html 中使用,也可以在

只读 store

只读 store 并不是真的只能执行读操作、不能执行 写操作,而是只能使用提供的 set 方法来进行写操作

  • set
  • start
  • stop
// App.svelte
<script>
	import { time } from './stores.js';

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

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

// store.js
import { readable } from 'svelte/store';

export const time = readable(null, function start(set) {
	// implementation goes here
		const interval = setInterval(() => {
		set(new Date());
	}, 1000);

	return function stop() {
		clearInterval(interval);
	};
});

store 派生

  • derived
import { readable, derived } 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)
);

自定义 store

// App.svelte
<script>
	import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>

<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

// store.js
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();

绑定 store

这个就很厉害了,如果有多个地方都绑定了,就是不知道会不会让数据流变得混乱

// App.svelte
<script>
	import { name, greeting } from './stores.js';
</script>

<h1>{$greeting}</h1>
<input value={$name}>

<button on:click="{() => $name += '!'}">
	Add exclamation mark!
</button>

// store.js
import { writable, derived } from 'svelte/store';

export const name = writable('world');

export const greeting = derived(
	name,
	$name => `Hello ${$name}!`
);