將 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部分的頂部新增以下行:jsimport FilterButton from "./FilterButton.svelte"; - 現在用對
FilterButton元件的呼叫替換svelte<FilterButton {filter} />
注意:請記住,當 HTML 屬性名稱和變數匹配時,它們可以用{variable}替換。這就是為什麼我們可以用替換。
到目前為止,一切都很好!現在讓我們試用一下應用程式。您會注意到,當您點選過濾器按鈕時,它們會被選中並且樣式會相應更新。但是我們有一個問題:待辦事項沒有被過濾。這是因為filter變數透過 prop 從Todos元件向下傳遞到FilterButton元件,但FilterButton元件中發生的更改不會向上回傳到其父元件——預設情況下資料繫結是單向的。讓我們看看解決這個問題的方法。
元件之間共享資料:將處理程式作為 prop 傳遞
讓子元件通知其父元件任何更改的一種方法是將處理程式作為 prop 傳遞。子元件將執行處理程式,並將所需的資訊作為引數傳遞,處理程式將修改父元件的狀態。
在我們的例子中,FilterButton元件將從其父元件接收一個onclick處理程式。每當使用者點選任何過濾器按鈕時,子元件都會呼叫onclick處理程式,並將選定的過濾器作為引數回傳到其父元件。
我們將只宣告onclick prop 併為其分配一個虛擬處理程式以防止錯誤,如下所示:
export let onclick = (clicked) => {};
並且我們將宣告反應式語句$: onclick(filter),以便每當filter變數更新時呼叫onclick處理程式。
部分的FilterButton元件最終應如下所示。立即更新它: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 為我們提供了一種更簡單、更直接的方法來實現雙向資料繫結。資料通常使用 props 從父元件向下傳遞到子元件。如果我們希望它也以相反的方式流動,從子元件到父元件,我們可以使用bind:指令。
使用bind,我們將告訴 Svelte FilterButton元件中對filter prop 所做的任何更改都應傳播回父元件Todos。也就是說,我們將父元件中filter變數的值繫結到子元件中的值。
- 在
Todos.svelte中,按如下方式更新對FilterButton元件的呼叫:像往常一樣,Svelte 為我們提供了一個簡潔的簡寫形式:svelte<FilterButton bind:filter={filter} />bind:value={value}等效於bind:value。因此,在上面的示例中,您可以只寫。 - 子元件現在可以修改父元件的
filter變數的值,因此我們不再需要onclickprop。像這樣修改FilterButton的元素: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中。現在轉到此檔案,並在您之前的匯入語句下方新增以下匯入語句:jsimport Todo from "./Todo.svelte"; - 接下來,我們需要更新
{#each}塊,以便為每個待辦事項包含一個元件,而不是已移出到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()函式,該函式將允許我們發出自定義事件。當您排程事件時,您必須傳遞事件的名稱,以及可選地傳遞一個包含您想要傳遞給每個偵聽器的其他資訊的object。這些其他資料將在事件物件的detail屬性中可用。
注意:Svelte 中的自定義事件與常規 DOM 事件共享相同的 API。此外,您可以透過指定on:event(不帶任何處理程式)將事件向上冒泡到您的父元件。
我們將編輯我們的Todo元件以發出remove事件,並將要刪除的待辦事項作為其他資訊傳遞。
- 首先,將以下幾行新增到
Todo元件的部分的頂部: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內部監聽該事件並採取相應措施。返回此檔案並像這樣更新您的元件呼叫:我們的處理程式接收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 } - 我們可以為狀態切換和名稱編輯發出不同的事件(例如,
- 接下來,我們將建立不同的函式來處理每個使用者操作。當待辦事項處於編輯模式時,使用者可以儲存或取消更改。當它不處於編輯模式時,使用者可以刪除待辦事項、編輯它或在其已完成和活動狀態之間切換。在您之前的函式下方新增以下函式集以處理這些操作js
function 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的響應性,分配editing變數的值就足以顯示正確的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 }; }id在todos陣列中找到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中很容易實現“prop向下,事件向上”模式。但是,對於簡單的元件,bind可能是一個不錯的選擇;Svelte將讓您選擇。
注意:Svelte提供了更高階的機制來在元件之間共享資訊:Context API和Stores。Context API提供了一種機制,允許元件及其後代相互“通訊”,而無需將資料和函式作為prop傳遞,或分派大量事件。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