將 Svelte 應用元件化
在上一篇文章中,我們開始開發待辦事項應用。本文的核心目標是探討如何將應用分解為可管理的元件,並在它們之間共享資訊。我們將把應用元件化,然後新增更多功能,允許使用者更新現有元件。
| 預備知識 |
至少,建議您熟悉核心 HTML、CSS 和 JavaScript 語言,並瞭解 終端/命令列。 您需要安裝了 Node 和 npm 的終端才能編譯和構建您的應用程式。 |
|---|---|
| 目標 | 學習如何將我們的應用程式分解為元件並在它們之間共享資訊。 |
與我們一起編寫程式碼
Git
使用以下命令克隆 GitHub 倉庫(如果您尚未這樣做)
git clone https://github.com/opensas/mdn-svelte-tutorial.git
然後執行以下命令以獲取當前應用程式狀態
cd mdn-svelte-tutorial/04-componentizing-our-app
或者直接下載資料夾內容
npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app
請記住執行 npm install && npm run dev 以開發模式啟動您的應用程式。
REPL
要使用 REPL 與我們一起編碼,請從以下地址開始
https://svelte.dev/repl/99b9eb228b404a2f8c8959b22c0a40d3?version=3.23.2
將應用分解為元件
在 Svelte 中,應用程式由一個或多個元件組成。元件是可重用、自包含的程式碼塊,它封裝了 HTML、CSS 和 JavaScript,並將它們寫入一個 .svelte 檔案中。元件可大可小,但通常定義清晰:最有效的元件只服務於一個單一、明確的目的。
定義元件的好處與將程式碼組織成可管理部分的更普遍的最佳實踐相媲美。它將幫助您理解它們之間的關係,促進重用,並使您的程式碼更易於理解、維護和擴充套件。
但是,您怎麼知道什麼應該拆分為自己的元件呢?
對此沒有硬性規定。有些人喜歡直觀的方法,開始檢視標記,並在每個似乎有自己邏輯的元件和子元件周圍繪製框。
另一些人則採用與決定是否建立新函式或物件相同的技術。其中一種技術是單一職責原則——也就是說,一個元件理想情況下應該只做一件事。如果它最終變得龐大,就應該拆分為更小的子元件。
這兩種方法應該相互補充,幫助您決定如何更好地組織元件。
最終,我們將把我們的應用拆分為以下元件
Alert.svelte:用於傳達已發生操作的通用通知框。NewTodo.svelte:允許您輸入新待辦事項的文字輸入和按鈕。FilterButton.svelte:允許您對顯示的待辦事項應用篩選器的“全部”、“活動”和“已完成”按鈕。TodosStatus.svelte:”x 條待辦事項中有 y 條已完成“的標題。Todo.svelte:單個待辦事項。每個可見的待辦事項將在此元件的單獨副本中顯示。MoreActions.svelte:UI 底部的“全選”和“刪除已完成”按鈕,允許您對待辦事項執行批次操作。

在本文中,我們將專注於建立 FilterButton 和 Todo 元件;我們將在未來的文章中介紹其他元件。
讓我們開始吧。
注意:在建立前幾個元件的過程中,我們還將學習元件之間通訊的不同技術,以及每種技術的優缺點。
提取我們的篩選器元件
我們將從建立 FilterButton.svelte 開始。
-
首先,建立一個新檔案
components/FilterButton.svelte。 -
在這個檔案中,我們將宣告一個
filterprop,然後將Todos.svelte中相關的標記複製到其中。將以下內容新增到檔案中svelte<script> export let filter = 'all' </script> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={() => filter = 'all'} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={() => filter = 'active'} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={() => filter = 'completed'} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> -
回到我們的
Todos.svelte元件中,我們希望使用我們的FilterButton元件。首先,我們需要匯入它。在Todos.svelte <script>部分的頂部新增以下行jsimport FilterButton from "./FilterButton.svelte"; -
現在將
<div class="filters...元素替換為對FilterButton元件的呼叫,該元件將當前篩選器作為 prop。下面的行就是您所需要的一切svelte<FilterButton {filter} />
注意:請記住,當 HTML 屬性名稱和變數匹配時,它們可以用 {variable} 替換。這就是為什麼我們可以將 <FilterButton filter={filter} /> 替換為 <FilterButton {filter} />。
到目前為止一切順利!現在讓我們嘗試一下這個應用。您會注意到,當您單擊篩選按鈕時,它們會被選中,並且樣式會相應更新。但我們有一個問題:待辦事項未被篩選。這是因為 filter 變數透過 prop 從 Todos 元件流向 FilterButton 元件,但 FilterButton 元件中發生的變化不會流回其父級——資料繫結預設是單向的。讓我們看看解決這個問題的方法。
元件之間共享資料:將處理程式作為 prop 傳遞
讓子元件通知其父元件任何更改的一種方法是將處理程式作為 prop 傳遞。子元件將執行處理程式,將所需資訊作為引數傳遞,處理程式將修改父元件的狀態。
在我們的案例中,FilterButton 元件將從其父元件接收一個 onclick 處理程式。每當使用者單擊任何篩選按鈕時,子元件將呼叫 onclick 處理程式,將選定的篩選器作為引數傳回其父元件。
我們將只宣告 onclick prop,並分配一個虛擬處理程式以防止錯誤,如下所示
export let onclick = (clicked) => {};
我們將宣告響應式語句 $: onclick(filter),以便在 filter 變數更新時呼叫 onclick 處理程式。
-
FilterButton元件的<script>部分最終應如下所示。立即更新它jsexport let filter = "all"; export let onclick = (clicked) => {}; $: onclick(filter); -
現在,當我們在
Todos.svelte內部呼叫FilterButton時,我們需要指定處理程式。像這樣更新它svelte<FilterButton {filter} onclick={ (clicked) => filter = clicked }/>
當任何篩選按鈕被點選時,我們只需用新的篩選器更新篩選器變數。現在我們的 FilterButton 元件將再次工作。
使用 bind 指令更輕鬆地實現雙向資料繫結
在前面的示例中,我們發現我們的 FilterButton 元件無法工作,因為我們的應用程式狀態透過 filter prop 從父級流向子級,但它沒有流回。因此,我們添加了一個 onclick prop,讓子元件將新的 filter 值傳達給其父級。
它工作正常,但 Svelte 為我們提供了一種更簡單、更直接的方式來實現雙向資料繫結。資料通常使用 prop 從父級流向子級。如果我們也希望它從子級流向父級,我們可以使用 bind: 指令。
使用 bind,我們將告訴 Svelte,在 FilterButton 元件中對 filter prop 所做的任何更改都應傳播回父元件 Todos。也就是說,我們將父元件中 filter 變數的值繫結到其子元件中的值。
-
在
Todos.svelte中,如下更新對FilterButton元件的呼叫svelte<FilterButton bind:filter={filter} />像往常一樣,Svelte 為我們提供了一個很好的簡寫:
bind:value={value}等效於bind:value。因此,在上面的示例中,您只需編寫<FilterButton bind:filter />。 -
子元件現在可以修改父元件的 filter 變數的值,因此我們不再需要
onclickprop。像這樣修改FilterButton的<script>元素svelte<script> export let filter = "all"; </script> -
再次嘗試您的應用,您應該仍然看到您的篩選器正常工作。
建立我們的 Todo 元件
現在我們將建立一個 Todo 元件來封裝每個單獨的待辦事項,包括複選框和一些編輯邏輯,以便您可以更改現有待辦事項。
我們的 Todo 元件將接收一個 todo 物件作為 prop。讓我們宣告 todo prop 並將程式碼從 Todos 元件中移出。暫時,我們將把對 removeTodo 的呼叫替換為警報。我們稍後會新增該功能。
-
建立一個新的元件檔案
components/Todo.svelte。 -
將以下內容放入此檔案中
svelte<script> export let todo </script> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={() => todo.completed = !todo.completed} checked={todo.completed} /> <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">{todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={() => alert('not implemented')}> Delete <span class="visually-hidden">{todo.name}</span> </button> </div> </div> -
現在我們需要將
Todo元件匯入到Todos.svelte中。現在轉到此檔案,並在您之前的匯入語句下方新增以下import語句jsimport Todo from "./Todo.svelte"; -
接下來,我們需要更新我們的
{#each}塊,為每個待辦事項包含一個<Todo>元件,而不是已移出到Todo.svelte的程式碼。我們還將當前的todo物件作為 prop 傳遞給元件。像這樣更新
Todos.svelte中的{#each}塊svelte<ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} /> </li> {:else} <li>Nothing to do here!</li> {/each} </ul>
待辦事項列表顯示在頁面上,並且複選框應該可以工作(嘗試選中/取消選中幾個,然後觀察篩選器是否仍按預期工作),但是我們的“x 個專案中有 y 個已完成”的狀態標題將不再相應更新。這是因為我們的 Todo 元件透過 prop 接收待辦事項,但它沒有將任何資訊傳送回其父級。我們將在稍後修復此問題。
元件之間共享資料:props-down, events-up 模式
bind 指令非常簡單,允許您在父元件和子元件之間以最小的麻煩共享資料。但是,當您的應用程式變得更大更復雜時,很容易難以跟蹤所有繫結的值。另一種方法是“props-down, events-up”通訊模式。
基本上,這種模式依賴於子元件透過 props 從其父元件接收資料,以及父元件透過處理從子元件發出的事件來更新其狀態。因此,props 向下流 從父元件到子元件,而事件向上冒泡 從子元件到父元件。這種模式建立了雙向資訊流,它是可預測且更易於理解的。
讓我們看看如何發出我們自己的事件來重新實現缺失的“刪除”按鈕功能。
要建立自定義事件,我們將使用 createEventDispatcher 實用程式。這將返回一個 dispatch() 函式,該函式將允許我們發出自定義事件。當您分派事件時,您必須傳遞事件的名稱,並且可選地傳遞一個包含您想要傳遞給每個監聽器的附加資訊的物件。此附加資料將在事件物件的 detail 屬性上可用。
注意:Svelte 中的自定義事件與常規 DOM 事件共享相同的 API。此外,您可以透過指定 on:event 而不帶任何處理程式來將事件冒泡到父元件。
我們將編輯我們的 Todo 元件以發出一個 remove 事件,將要刪除的待辦事項作為附加資訊傳遞。
-
首先,將以下行新增到
Todo元件的<script>部分的頂部jsimport { createEventDispatcher } from "svelte"; const dispatch = createEventDispatcher(); -
現在,更新同一檔案標記部分中的“刪除”按鈕,使其看起來像這樣
svelte<button type="button" class="btn btn__danger" on:click={() => dispatch('remove', todo)}> Delete <span class="visually-hidden">{todo.name}</span> </button>透過
dispatch('remove', todo),我們正在發出一個remove事件,並傳遞要刪除的todo作為附加資料。處理程式將與一個可用的事件物件一起呼叫,其中附加資料可在event.detail屬性中獲取。 -
現在我們必須在
Todos.svelte內部監聽該事件並採取相應行動。返回到該檔案,並像這樣更新您的<Todo>元件呼叫svelte<Todo {todo} on:remove={(e) => removeTodo(e.detail)} />我們的處理程式接收
e引數(事件物件),如前所述,它在detail屬性中儲存了要刪除的待辦事項。 -
此時,如果您再次嘗試您的應用程式,您應該會看到“刪除”功能現在又可以工作了。所以我們的自定義事件按我們希望的方式工作了。此外,
remove事件監聽器正在將資料更改傳送回父級,因此當待辦事項被刪除時,我們的“x 個專案中 y 個已完成”的狀態標題現在將相應更新。
現在我們將處理 update 事件,以便我們的父元件可以收到任何修改過的待辦事項的通知。
更新待辦事項
我們仍需實現允許我們編輯現有待辦事項的功能。我們必須在 Todo 元件中包含一個編輯模式。進入編輯模式時,我們將顯示一個 <input> 欄位,允許我們編輯當前待辦事項名稱,並帶有兩個按鈕來確認或取消我們的更改。
處理事件
-
我們需要一個變數來跟蹤我們是否處於編輯模式,另一個變數來儲存正在更新的任務名稱。在
Todo元件的<script>部分底部新增以下變數定義jslet editing = false; // track editing mode let name = todo.name; // hold the name of the to-do being edited -
我們必須決定我們的
Todo元件將發出哪些事件- 我們可以為狀態切換和名稱編輯發出不同的事件(例如,
updateTodoStatus和updateTodoName)。 - 或者我們可以採取更通用的方法,為這兩個操作發出一個單獨的
update事件。
我們將採取第二種方法,以便我們能夠演示一種不同的技術。這種方法的優點是,稍後我們可以為待辦事項新增更多欄位,並且仍然可以使用相同的事件處理所有更新。
讓我們建立一個
update()函式,它將接收更改併發出一個帶有修改後的待辦事項的更新事件。在<script>部分的底部再次新增以下內容jsfunction update(updatedTodo) { todo = { ...todo, ...updatedTodo }; // applies modifications to todo dispatch("update", todo); // emit update event }這裡我們使用展開語法返回已應用修改的原始待辦事項。
- 我們可以為狀態切換和名稱編輯發出不同的事件(例如,
-
接下來,我們將建立不同的函式來處理每個使用者操作。當待辦事項處於編輯模式時,使用者可以儲存或取消更改。當它不處於編輯模式時,使用者可以刪除待辦事項、編輯它或切換其狀態(在已完成和活動之間)。
在您之前的函式下方新增以下一組函式來處理這些操作
jsfunction onCancel() { name = todo.name; // restores name to its initial value and editing = false; // and exit editing mode } function onSave() { update({ name }); // updates todo name editing = false; // and exit editing mode } function onRemove() { dispatch("remove", todo); // emit remove event } function onEdit() { editing = true; // enter editing mode } function onToggle() { update({ completed: !todo.completed }); // updates todo status }
更新標記
現在我們需要更新 Todo 元件的標記,以便在採取適當的操作時呼叫上述函式。
為了處理編輯模式,我們正在使用 editing 變數,它是一個布林值。當它為 true 時,它應該顯示用於編輯待辦事項名稱的 <input> 欄位,以及“取消”和“儲存”按鈕。當它不處於編輯模式時,它將顯示覆選框、待辦事項名稱以及編輯和刪除待辦事項的按鈕。
為了實現這一點,我們將使用一個 if 塊。if 塊有條件地渲染一些標記。請注意,它不僅僅是根據條件顯示或隱藏標記——它會根據條件動態地從 DOM 中新增和刪除元素。
例如,當 editing 為 true 時,Svelte 將顯示更新表單;當它為 false 時,它將從 DOM 中刪除它並新增複選框。由於 Svelte 的響應性,分配編輯變數的值就足以顯示正確的 HTML 元素。
以下內容讓您瞭解基本的 if 塊結構是什麼樣的
<div class="stack-small">
{#if editing}
<!-- markup for editing to-do: label, input text, Cancel and Save Button -->
{:else}
<!-- markup for displaying to-do: checkbox, label, Edit and Delete Button -->
{/if}
</div>
非編輯部分——也就是 if 塊的 {:else} 部分(下半部分)——將與我們在 Todos 元件中擁有的非常相似。唯一的區別在於我們根據使用者操作呼叫 onToggle()、onEdit() 和 onRemove()。
{:else}
<div class="c-cb">
<input type="checkbox" id="todo-{todo.id}"
on:click={onToggle} checked={todo.completed}
>
<label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
</div>
<div class="btn-group">
<button type="button" class="btn" on:click={onEdit}>
Edit<span class="visually-hidden"> {todo.name}</span>
</button>
<button type="button" class="btn btn__danger" on:click={onRemove}>
Delete<span class="visually-hidden"> {todo.name}</span>
</button>
</div>
{/if}
</div>
值得注意的是
- 當用戶按下“編輯”按鈕時,我們執行
onEdit(),它只是將editing變數設定為true。 - 當用戶點選複選框時,我們呼叫
onToggle()函式,該函式執行update(),並將一個包含新completed值作為引數的物件傳遞給它。 update()函式發出update事件,將原始待辦事項的副本與應用的更改作為附加資訊傳遞。- 最後,
onRemove()函式發出remove事件,將要刪除的todo作為附加資料傳遞。
編輯 UI(上半部分)將包含一個 <input> 欄位和兩個按鈕,用於取消或儲存更改
<div class="stack-small">
{#if editing}
<form on:submit|preventDefault={onSave} class="stack-small" on:keydown={(e) => e.key === 'Escape' && onCancel()}>
<div class="form-group">
<label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
<input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
</div>
<div class="btn-group">
<button class="btn todo-cancel" on:click={onCancel} type="button">
Cancel<span class="visually-hidden">renaming {todo.name}</span>
</button>
<button class="btn btn__primary todo-edit" type="submit" disabled={!name}>
Save<span class="visually-hidden">new name for {todo.name}</span>
</button>
</div>
</form>
{:else}
[...]
當用戶按下“編輯”按鈕時,editing 變數將被設定為 true,Svelte 將從 DOM 中移除 {:else} 部分中的標記,並將其替換為 {#if} 部分中的標記。
<input> 的 value 屬性將繫結到 name 變數,取消和儲存更改的按鈕分別呼叫 onCancel() 和 onSave()(我們之前添加了這些函式)
- 呼叫
onCancel()時,name會恢復其原始值(作為 prop 傳入時),然後我們退出編輯模式(透過將editing設定為false)。 - 當
onSave()被呼叫時,我們執行update()函式——將修改後的name傳遞給它——並退出編輯模式。
當 <input> 為空時,我們還使用 disabled={!name} 屬性停用“儲存”按鈕,並允許使用者使用 Escape 鍵取消編輯,如下所示
on:keydown={(e) => e.key === 'Escape' && onCancel()}
我們還使用 todo.id 為新的輸入控制元件和標籤建立唯一的 ID。
-
我們
Todo元件的完整更新標記如下所示。現在更新您的svelte<div class="stack-small"> {#if editing} <!-- markup for editing todo: label, input text, Cancel and Save Button --> <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={(e) => e.key === 'Escape' && onCancel()}> <div class="form-group"> <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" on:click={onCancel} type="button"> Cancel<span class="visually-hidden">renaming {todo.name}</span> </button> <button class="btn btn__primary todo-edit" type="submit" disabled={!name}> Save<span class="visually-hidden">new name for {todo.name}</span> </button> </div> </form> {:else} <!-- markup for displaying todo: checkbox, label, Edit and Delete Button --> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={onToggle} checked={todo.completed} > <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn" on:click={onEdit}> Edit<span class="visually-hidden"> {todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={onRemove}> Delete<span class="visually-hidden"> {todo.name}</span> </button> </div> {/if} </div>注意:我們可以進一步將其拆分為兩個不同的元件,一個用於編輯待辦事項,另一個用於顯示待辦事項。最終,這取決於您在一個元件中處理此級別複雜度的舒適程度。您還應該考慮進一步拆分是否能夠在不同的上下文中重用此元件。
-
為了使更新功能正常工作,我們必須處理
Todos元件中的update事件。在其<script>部分中,新增此處理程式jsfunction updateTodo(todo) { const i = todos.findIndex((t) => t.id === todo.id); todos[i] = { ...todos[i], ...todo }; }我們在
todos陣列中透過id找到todo,並使用擴充套件語法更新其內容。在這種情況下,我們也可以只使用todos[i] = todo,但這種實現更穩健,允許Todo元件只返回待辦事項的更新部分。 -
接下來,我們必須在我們的
<Todo>元件呼叫上監聽update事件,並在發生時執行我們的updateTodo()函式以更改name和completed狀態。像這樣更新您的 <Todo> 呼叫svelte{#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} on:update={(e) => updateTodo(e.detail)} on:remove={(e) => removeTodo(e.detail)} /> </li> -
再次嘗試您的應用,您應該會看到您可以刪除、新增、編輯、取消編輯以及切換待辦事項的完成狀態。而且,當待辦事項完成時,我們的“x 條待辦事項中有 y 條已完成”的狀態標題現在將相應更新。
如您所見,在 Svelte 中實現“props-down, events-up”模式很容易。然而,對於簡單的元件,bind 可能是一個不錯的選擇;Svelte 會讓您選擇。
注意: Svelte 提供了更高階的機制來在元件之間共享資訊:Context API 和Stores。Context API 提供了一種機制,使元件及其後代能夠相互“交談”,而無需透過 props 傳遞資料和函式,或分派大量事件。Stores 允許您在沒有層級關係的元件之間共享響應式資料。我們將在本系列的後續文章中介紹 Stores。
目前的全部程式碼
Git
要檢視本文末尾程式碼的狀態,請像這樣訪問您倉庫的副本
cd mdn-svelte-tutorial/05-advanced-concepts
或者直接下載資料夾內容
npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts
請記住執行 npm install && npm run dev 以開發模式啟動您的應用程式。
REPL
要檢視 REPL 中的當前程式碼狀態,請訪問
https://svelte.dev/repl/76cc90c43a37452e8c7f70521f88b698?version=3.23.2
總結
現在我們已經具備了應用程式所有必需的功能。我們可以顯示、新增、編輯和刪除待辦事項,將它們標記為已完成,並按狀態進行篩選。
在本文中,我們涵蓋了以下主題
- 將功能提取到新元件
- 使用作為 prop 接收的處理程式從子元件向父元件傳遞資訊
- 使用
bind指令從子元件向父元件傳遞資訊 - 使用
if塊有條件地渲染標記塊 - 實現“props-down, events-up”通訊模式
- 建立和監聽自定義事件
在下一篇文章中,我們將繼續將我們的應用元件化,並研究一些處理 DOM 的高階技術。