Svelte 组件之间通讯的 6 种方法

原创
2021/12/31 09:01
阅读数 1.3W

使用组件设计用户界面的主要挑战是管理不同组件上的应用状态。而 Svelte 提供了强大的能力实现在组件中进行数据传递。

“Great communication begins with connection.“

— Oprah Winfrey

译注:奥普拉·盖尔·温弗里(英语:Oprah Gail Winfrey,1954年1月29日-),生于美国密西西比州,美国电视脱口秀主持人、制作人、投资家、慈善家及演员,美国最具影响力的非洲裔名人之一,时代百大人物。

她是入选时代百大人物次数最多者,总共9次。2005年美国在线举办票选活动—《最伟大的美国人》,她被选为美国最伟大的人物中的第九位。

接下来开始来了解 6 种实现 Svelte 组件间通讯的方法,他们分别是:

  1. 将数据发送到子组件: Props
  2. 在组件内渲染 HTML: Slots
  3. 子组件将数据通过事件方式通知父组件: Events
  4. 通过上下文 API 传递数据: Context API
  5. 在所有组件的实例之间共享数据: Module Context
  6. 在任意组件之间共享数据: Store

在本文中,我们写了一些简单的示例,应用了不同的 Svelte 组件通讯技术。这些代码不会包含所有零碎的东西,只是为了让你了解这些技术的本质。

1. Props

在真实应用中,经常需要将数据从一个组件传递到其他的子组件。最基本的方法就是使用组件属性,在 Svelte 中可以使用 export 关键字来定义组件属性。

请注意下图代码中高亮部分的 export 关键字:

Card.svelte with props

简单的引入 Card 组件,并通过属性将数据传递给 Card 。

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

​​​​​​​<Card userid="#2312312" name="Jhon Doe"/>

提示: 在 Svelte 中使用 Typescript 可以解决组件内的属性类型检查问题。

2. Slots

Slots (槽) —— 子组件可以通过使用槽来传递要渲染的内容,这是基于 Web 组件槽的建议。

<div class="card">
  <slot/>
</div>

槽的默认定义方法是在子组件中使用 <slot> 这个 HTML 标签。

槽有助于将组件设计为一个模板,同时也可以基于命名的方式来注入 HTML 内容。下面栗子展示命名槽的做法:

Card component with slots

3. Events

在设计组件时,捕获来自子组件的事件是一件很有意思的事情。继续更新上述的代码,当我们点击心图标的时候就会在喜欢与不喜欢之间做切换。如图所示:

Card with toggle heart button
为了分发事件,需要引入Svelte 的   {createEventDispatcher}  函数。然后在子组件中通过这个函数来触发事件。来看看代码:
<script>
	import { createEventDispatcher } from 'svelte';
	
	//Declare the dispatch
	const dispatch = createEventDispatcher();
	
	export let id = 'ID';
	export let name = 'unknown'
	export let favor = false;
	
	let hearts = ['♡','♥'];	
	let heartIcon = hearts[0];
	
	//if you wonder $: its svelte way make reactive statement
	$: heartIcon = favor ? hearts[1] : hearts[0];
	
	const favorite = (id) => {
		favor = !favor;
		
		//Dispatch the favorite event with object data
		dispatch('favorite', {id, favor});
	}
</script>

<div class="card">
	<i>{id}</i>
	<h2>{name}</h2>
	<span class="btnHeart" on:click={() => favorite(id)}>{heartIcon}</span>
</div>

<style>
	.card{position:relative;margin:50px auto;width:300px;padding:20px;border-radius:9px;background-color:#F5F7F0;	}
	i{ color:#999;}
	h2{margin:0; color:#491D3C;}
	.btnHeart{position:absolute;right:20px;	top:10px;font-size:32px;cursor:pointer;	}
</style>

CardEvent.svelte 文件根据点击来分发事件。

这里我们获取到三个值,分别是 id, name, favor ,然后通过 dispatch 分发给父组件:

<script>
 import Card from './CardEvent.svelte'
 
 let user ={
  id : '#1212',
  name: 'Jhon Doe',
  favor : false
 }
 
 const whenFavored = (event) => user.favor = event.detail.favor;
 
</script>

<Card {...user} on:favorite={whenFavored}/>

App.svelte using CardEvent.svelte

这里使用了 {...}操作符来设置 user 对象作为子组件的属性,并侦听 favorite 点击事件,当有点击的时候触发 whenFavored()函数执行并设置值到父对象中。

黑科技

  1. 在多级嵌套的组件中,可以使用 <card on:favorite />来转发事件,它将事件传递给父组件,这个机制同样也适用于 DOM 的事件。
  2. 可以将子组件作为一个引用传递给父组件,并在父组件中调用所有子组件输出的方法。例如可以使用 <Card bind:this={userCard} />来引用 Card 组件为一个 userCard 对象。

4. Context API

上下文 API。 现在我们进入 Svelte 通讯技术里高级部分,这部分非常有用。上下文 API 提供了一个强大的机制实现组件间的对话。

“Communication is only effective when we communicate in a way that is meaningful to the recipient, not ourselves.“
"只有当我们以对接收者而不是我们自己有意义的方式进行沟通时,沟通才有效。“

— Rich Simmonds (LinkedIn 联合创始人兼 CEO)

首先顶层的组件需要使用 setContext() 来设置数据,然后其他组件通过 getContext() 来获取数据。是不是相当简单?

//App.svelte

<script>

import Card from './CardContext.svelte' 
import {setContext} from 'svelte'; 

let user ={
  id:123456,
  name:'Jhon Doe',
  favor : true
} 

setContext('user', user);

</script>

<Card/>

我们通过上下文 API 将对象 user 传递给所有子组件:

<script>
	import {getContext} from 'svelte';
  
	//Get the Ancestor user object
        const user = getContext('user');
  
	let hearts = ['♡','♥'];	
	let heartIcon = hearts[0];
  
	//Apply the favor value to reactive heartIcon
        $: heartIcon = user.favor ? hearts[1] : hearts[0];	
</script>

<div class="card">
	<i>{user.id}</i>
	<h2>{user.name}</h2>
	<span class="btnHeart">{heartIcon}</span>
</div>

<style>
	.card{	position:relative; margin:50px auto; width:300px; padding:20px; border-radius:9px;background-color:#F5F7F0;}
	i{ color:#999;}
	h2{margin:0; color:#491D3C;}
	.btnHeart{ position:absolute; right:20px; top:10px; font-size:32px; cursor:pointer; }
</style>

CardContext.svelte

注意,上下文中的状态仅对子组件有效,如果要使用组件的多个实例,而不使一个组件的状态干扰其他组件的状态,这将非常有用。

5. Module Context (模块上下文)

在 Svelte 中,同一个组件的多个实例想要共享相同数据是非常简单的,只需要将这些变量定义在 <script context='module'></script> 之间即可。

来看这么一个例子,当点击标签名词的时候,其他组件实例也会相应的展示相同效果。

点击 Clear All 可以清除所有按钮状态。

首先在 App.svelte 中使用 users 对象创建 Card 实例并使用属性来传递 user 对象。

<script>
 import Card,{ clearAll } from './CardWithModuleContext.svelte'
 let users=[
  {id:101, name:'Jhon Doe', tags:['Javascript','HTML','PHP']},
  {id:102, name:'Mark Dane', tags:['C++','HTML','CSS']},
  {id:103, name:'Clark Jose', tags:['Javascript','HTML','CSS']},
 ]; 
</script>
<button on:click={clearAll} 
    style="border-radius:9px;"
    >Clear All
</button>
{#each users as user}
  <Card {user}/>
{/each}

App.svelte

同时添加一个模块方法 {clearAll} 来清除所有高亮的标签:

<script context="module">	
       //data shared accross each instance of this componenet
	let tagSelected; 
	export function clearAll() { tagSelected = "" }
</script>

<script>
	import { onMount, onDestroy } from "svelte";  
	export let user;  
	let choosed;	
	let interval;
  
	const selectIt = (tag) => {		
		tagSelected = tag;
		choosed = tagSelected		
	}
	
  	//read the data on interval
	onMount(() => {
	    interval = setInterval(()=> choosed = tagSelected, 100);		
	  });
  
	//destroy the timer
  	onDestroy(() => clearInterval(interval));
</script>

<div class="card">
	<i>#{user.id}</i>
	<h2>{user.name}</h2>	
	<dl>
		{#each user.tags as tag}		
		<dd on:click={()=>selectIt(tag)} class:apply={choosed == tag}>{tag}</dd>	
		{/each}
	</dl>
</div>

<style>
	.card{width:300px;padding:20px;border-radius:9px;background-color:#F5F7F0;margin-bottom:10px;}
	i{ color:#999;}
	h2{margin:0; color:#491D3C;}
	dl{margin:0; padding:0; margin-top:10px;}
	dd{display:inline-block;margin:0; margin-right:10px; 
		color:#999; padding:1px 10px; border:1px solid #ccc; 
		border-radius:15px; font-size:14px;cursor:pointer;
	}
	.apply{color:#900; background:#ccc;}
	
</style>

CardWithModuleContext.svelte

变量 tagSelected 将在组件的不同实例间共享,理解起来非常有趣,这里添加一个 100 毫秒的定时器来更新标签的高亮。如你所见,子组件中的所有逻辑就是用来和其他实例通讯的。

6. Store

随着应用越来越复杂,增加越来越多的特性,越来越多的组件,复杂度日益增加。这个时候我们需要在组件的层次结构之外保存一些应用的状态。而 Svelte 内建的 store 就可以实现这个。

在 Svelte 存储中,可以存单个对象、数组等。Svelte 提供了多种存储,包括  writable, readable, derived, 或者 custom.

接下来做一个简单例子实现图书列表:

BookStore.js

import { writable } from 'svelte/store'
export let bookStore = writable([
 {name:"Hamlet",author:"William Shakespeare"},
 {name:"The Great Gatsby",author:"F. Scott Fitzgerald"}
]);

BookList.svelte

<script>
 import { bookStore } from './BookStore.js' 
</script>
<ul>
 {#each $bookStore as book}
  <li>{book.name} - {book.author}</li>
 {/each} 
</ul>

BookForm.svelte

<script>
 import { bookStore } from './BookStore.js' 
 let bookName;
 let author;
 const addNew = ()=>{
  $bookStore = [{name:bookName, author:author},...$bookStore,];
 }
</script>
<input type="text" bind:value={bookName} placeholder="Book Name"/>
<input type="text" bind:value={author} placeholder="Author Name"/>
<button on:click={addNew}>+ Add Book</button>

App.svelte

<script>
 import BookList from './BookList.svelte'
 import BookForm from './BookForm.svelte' 
</script>
<BookForm/>
<BookList/>

这里创建了一个 bookStore作为 writable 数组,通过 $ 这个语法糖在表单和列表组件中引用来访问数据。

Context vs Store

Context 和 Store 二者比较类似,区别在于 Store 可在应用的任一组件中访问,而 Context 只能用于子组件。

示例代码

了解本文中介绍的所有示例,这些示例在 Svelte REPL 中可用。要进行测试,请通过导入不同的卡组件来更新 App.svelte 文件,以检查结果。 (demo)

结论

构建组件之间的通信是应用程序设计中最重要的部分。在Svelte中,我们有用于状态管理的内置功能,可以给我们很好的灵活性来设计更好的应用程序。

本文翻译自 https://betterprogramming.pub/6-ways-to-do-component-communications-in-svelte-b3f2a483913c

展开阅读全文
加载中

作者的其它热门文章

打赏
6
10 收藏
分享
打赏
1 评论
10 收藏
6
分享
返回顶部
顶部