iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0
Modern Web

Svelte 的奇妙冒險系列 第 8

[Svelte 的奇妙冒險] Day 08 - 如何寫一個 Component

  • 分享至 

  • xImage
  •  

今天來介紹 Svelte 中怎麼寫一個 component,在前幾天的文章中有提過每一個 .svelte 檔就是一個 component ,而那個 Svelte 會自動 default export 整個 component 我們要使用時 import 它的 default export 即可。

像是我這邊封裝一個 Input component

<!-- in Input.svelte -->

<script lang="ts">
	let value = $state('');
	let label = 'name';
</script>

<label>
	<span>{label}</span>
	<input bind:value />
</label>

<style>
	input {
		border: 1px solid var(--border);
		border-radius: 4px;
		padding: 0.5rem;
		background-color: var(--background);
		color: var(--text);
	}

	label {
		display: flex;
		flex-direction: column;
		color: var(--text);
	}

	label span {
		display: block;
		margin-bottom: 0.5rem;
		color: var(--text);
	}
</style>

然後我在其他地方要使用時只要 import Input from './Input.svelte'; 即可

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

<div class="inputContainer">
	<Input label="name" {value} />
</div>

<style>
	:root {
		--background: #fff;
		--text: #000;
		--border: #ccc;
		--body-background: #f5f5f5;
	}

	@media (prefers-color-scheme: dark) {
		:root {
			--background: #333;
			--text: #fff;
			--border: #555;
			--body-background: #222;
		}
	}

	:global(body) {
		background-color: var(--body-background);
		margin: 0;
		padding: 0;
	}

  
	.inputContainer {
		width: 40%;
	}
</style>

$props

那既然是 component 勢必有時候我們會需要從外部控制它的行為或者樣式等等的東西,這時候我們可以用 $props 這個 rune 來達成這件事情。

像是如果想要讓 label 變成外部控制的話就能這麼寫

<!-- in Input.svelte -->

<script lang="ts">
	let { label }: { label: string } = $props();

	let value = $state('');
</script>

然後要傳入 props 時只要這樣即可

<Input label="name" />

當然 props 也可以有預設值

let { label='name' }: { label: string } = $props();

也可以選擇不解構

let label: { label: string } = $props();

那當然也可以使用 spread

let { ...restProps } : { label: string } = $props();

value 呢? 直覺上我們會想這麼做

<!-- in Input.svelte -->

<script lang="ts">
	let { label, value }: { label: string; value: string } = $props();
    $effect(() => {
  		console.log('[Input]', value);
  	});
</script>

<label>
	<span>{label}</span>
	<input bind:value />
</label>

然後讓 value 從外部傳入

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

	let value = $state('');
</script>

<Input label="name" {value} />

看起來沒什麼問題,但當我們加上 $effect 來看一下狀態更新的情況就會發現有點怪怪的,只有 Input 裡面的 value 有被更新,但我們外面的 state 沒有跟著被更新。

沒錯這裡也需要使用 bind:value 讓外部的 state 可以一起被更新,但當我們這麼寫的時候會發現網站 crash 了

<div class="inputContainer">
	<Input label="name" bind:value />
</div>

這時打開 console 會發現其實 Svelte 已經告訴我們答案了

$bindable

為了解決這個問題我們會需要 $bindable 這個 rune,它是可以讓 component 的 props 可以跟外部的 state 去做 binding 。

let { label = 'name', value = $bindable() }: { label: string; value: string } = $props();

會發現效果已經如我們預期了!

bind:this

跟一般的 HTML tag 一樣我們也能對 Svelte component 進行 binding,首先在 Input.svelte 裡新增一個 functionexport 出去

<!-- in Input.svelte -->

<script lang="ts">
	let { label = 'name', value = $bindable() }: { label: string; value: string } = $props();

	$effect(() => {
		console.log('\x1b[34m%s\x1b[0m', '[Input.svelte]', value);
	});

  	export const CONSTANT = 'CONSTANT';

	export const greet = () => {
		alert(`Hello, ${value}`);
	};

	export { greet };
</script>

請不要使用 export let ,那是 Svelte 4 中用來描述 props 的特殊語法

然後就跟對 HTML tag binding 一樣用一個 $state 並傳入到 <Input />bind:this

<script lang="ts">
	import { onMount } from 'svelte';
	import Input from './Input.svelte';

	let value = $state('');
	let inputElement: Input | undefined = $state(undefined);
	onMount(() => {
		console.log('\x1b[33m%s\x1b[0m', '[onMount]', $state.snapshot(inputElement));
	});
	$effect(() => {
		console.log('\x1b[32m%s\x1b[0m', '[+page.svelte]', value);
	});
</script>

<div class="inputContainer">
	<Input bind:this={inputElement} label="name" bind:value />
</div>

<button
	onclick={() => {
		inputElement?.greet();
	}}
>
	Input.greet</button
>

當我們拿到 Input 的 instance 時我們就可以使用它所 exportconst variable ,所以我們就能做到在 parent 層使用 child 層所封裝的 function 或 constant 。

與 Svelte 4 的粗略對比

與 v4 相比 v5 對於 props 的寫法好懂很多

宣告 props

// 宣告 props
//v5
let { lable = ''} = $props()
// v4 
export let lable = ''

//取得所有 props
//v5
let lable  = $props()
//v4 
$$props

但最大的不同是 event 這件事情在 v4 會建議使用 createEventDispatcher 來進行事件處理的封裝。

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

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

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

<Inner on:message={handleMessage} />

簡單外部的 component 透過用 on: 來監聽內部 component 是否有 dispatch 對應的 event。以這個例子來說裡面 dispatch 名稱為 message 的 event ,並帶了 {text: 'Hello!'} 所以我外部使用的時候就可以從 function 中的 event 取出來。

但如果是 v5 會建議使用 function 的 props 來達成這件事情

<!-- Inner.svelte -->
<script lang='ts'>
    let { sayHello }  = $props()
</script>

<button onclick={()=>{
   sayHello({text:'hello'})
}}>
	Click to say hello
</button>

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

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

<Inner sayHello={handleMessage} />

當然在 v4 中也可以將 function 做為 props 傳入 component 中,但會變成這樣子

<!-- Inner.svelte -->
<script>
	import { createEventDispatcher } from 'svelte';
	export let onclick 
</script>

<button on:click={onclick}>
	Click to say hello
</button>
<script>
	import Inner from './Inner.svelte';

	function onclick() {
      	console.log('clicked')
	}
</script>

<Inner onclick={onclick} />

因為 Svelte 4都是使用 on: 來監聽事件而不是使用 onclick 之類的 HTML 的 attribute 來控制,所以如果直接使用上面的形式會變成一下子用 on:click 一下子用 onclick 的感覺。

以及 <slot /> 在 v5 後 deprecated 了,之後會建議使用 {#snippet} 這個特殊語法,只是它不只可以用在 component 的 children 的用途所以未來有用到再來說明吧。

所以我自己的感覺是在 component 撰寫這件事情來說 v5 比 v4 更為貼近在寫 JS 而不是根據 Svelte 自己的規則在寫,這點對於從 React 來的開發者友善許多 XDD 。


參考資料

source code

https://github.com/toddLiao469469/30days-for-svelte5/tree/main/src/routes/day08


上一篇
[Svelte 的奇妙冒險] Day 07 - 常用的 directives
下一篇
[Svelte 的奇妙冒險] Day 09 - 先寫一個最陽春的 Todo list
系列文
Svelte 的奇妙冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言