使用組件設計用戶界面的主要挑戰是管理不同組件上的應用狀態。而 Svelte 提供了強大的能力實現在組件中進行數據傳遞。
“Great communication begins with connection.“
— Oprah Winfrey
譯註:奧普拉·蓋爾·溫弗裏(英語:Oprah Gail Winfrey,1954年1月29日-),生於美國密西西比州,美國電視脫口秀主持人、製作人、投資家、慈善家及演員,美國最具影響力的非洲裔名人之一,時代百大人物。
她是入選時代百大人物次數最多者,總共9次。2005年美國在線舉辦票選活動—《最偉大的美國人》,她被選爲美國最偉大的人物中的第九位。
接下來開始來了解 6 種實現 Svelte 組件間通訊的方法,他們分別是:
- 將數據發送到子組件: Props
- 在組件內渲染 HTML: Slots
- 子組件將數據通過事件方式通知父組件: Events
- 通過上下文 API 傳遞數據: Context API
- 在所有組件的實例之間共享數據: Module Context
- 在任意組件之間共享數據: 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
在設計組件時,捕獲來自子組件的事件是一件很有意思的事情。繼續更新上述的代碼,當我們點擊心圖標的時候就會在喜歡與不喜歡之間做切換。如圖所示:
{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()
函數執行並設置值到父對象中。
黑科技
- 在多級嵌套的組件中,可以使用
<card on:favorite />
來轉發事件,它將事件傳遞給父組件,這個機制同樣也適用於 DOM 的事件。 - 可以將子組件作爲一個引用傳遞給父組件,並在父組件中調用所有子組件輸出的方法。例如可以使用
<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>
之間即可。
來看這麼一個例子,當點擊標籤名詞的時候,其他組件實例也會相應的展示相同效果。
首先在 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