Svelte 高階教程:響應式、生命週期、可訪問性
在上一篇文章中,我們為待辦事項列表添加了更多功能,並開始將我們的應用程式組織成元件。在本文中,我們將新增應用程式的最終功能並進一步將我們的應用程式元件化。我們將學習如何處理與更新物件和陣列相關的反應性問題。為了避免常見的陷阱,我們將不得不更深入地瞭解 Svelte 的反應性系統。我們還將探討解決一些可訪問性焦點問題,以及其他更多內容。
| 先決條件 |
至少,建議您熟悉核心 HTML、CSS 和 JavaScript 語言,並瞭解 終端/命令列。 您需要一個安裝了 Node 和 npm 的終端來編譯和構建您的應用程式。 |
|---|---|
| 目標 | 學習一些高階的 Svelte 技術,包括解決反應性問題、與元件生命週期相關的鍵盤可訪問性問題等等。 |
我們將重點關注一些涉及焦點管理的可訪問性問題。為此,我們將利用一些訪問 DOM 節點和執行 focus() 和 select() 等方法的技術。我們還將瞭解如何在 DOM 元素上宣告和清理事件監聽器。
我們還需要了解一些關於元件生命週期的知識,以瞭解這些 DOM 節點何時從 DOM 中掛載和解除安裝,以及我們如何訪問它們。我們還將學習 action 指令,它將允許我們以可重用和宣告的方式擴充套件 HTML 元素的功能。
最後,我們將進一步瞭解元件。到目前為止,我們已經瞭解了元件如何使用 props 共享資料,以及如何使用事件和雙向資料繫結與父元件通訊。現在我們將瞭解元件如何公開方法和變數。
在本文的過程中,將開發以下新元件
MoreActions:顯示“全選”和“刪除已完成”按鈕,併發出處理其功能所需的相關事件。NewTodo:顯示用於新增新待辦事項的<input>欄位和“新增”按鈕。TodosStatus:顯示“已完成 x 項,共 y 項”狀態標題。
與我們一起編寫程式碼
Git
克隆 GitHub 倉庫(如果您還沒有這樣做)使用
git clone https://github.com/opensas/mdn-svelte-tutorial.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
處理 MoreActions 元件
現在我們將處理“全選”和“刪除已完成”按鈕。讓我們建立一個元件來負責顯示按鈕併發出相應的事件。
- 建立一個新檔案,
components/MoreActions.svelte。 - 當第一個按鈕被點選時,我們將發出一個
checkAll事件來表示所有待辦事項應該被選中/取消選中。當第二個按鈕被點選時,我們將發出一個removeCompleted事件來表示所有已完成的待辦事項應該被刪除。將以下內容放入您的MoreActions.svelte檔案中我們還包含了一個svelte<script> import { createEventDispatcher } from "svelte"; const dispatch = createEventDispatcher(); let completed = true; const checkAll = () => { dispatch("checkAll", completed); completed = !completed; }; const removeCompleted = () => dispatch("removeCompleted"); </script> <div class="btn-group"> <button type="button" class="btn btn__primary" on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" on:click={removeCompleted}>Remove completed</button> </div>completed變數來在選中和取消選中所有任務之間切換。 - 回到
Todos.svelte中,我們將匯入我們的MoreActions元件並建立兩個函式來處理MoreActions元件發出的事件。在現有的匯入語句下方新增以下匯入語句jsimport MoreActions from "./MoreActions.svelte"; - 然後在
<script>部分的末尾新增描述的函式jsconst checkAllTodos = (completed) => todos.forEach((t) => (t.completed = completed)); const removeCompletedTodos = () => (todos = todos.filter((t) => !t.completed)); - 現在轉到
Todos.svelte標記部分的底部,並將我們複製到MoreActions.svelte中的<div class="btn-group">元素替換為對MoreActions元件的呼叫,如下所示svelte<!-- MoreActions --> <MoreActions on:checkAll={(e) => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} /> - 好的,讓我們回到應用程式中並試一試。您會發現“刪除已完成”按鈕工作正常,但“全選”/“取消全選”按鈕只是靜默失敗。
要找出這裡發生了什麼,我們將不得不更深入地瞭解 Svelte 的反應性。
響應式陷阱:更新物件和陣列
要檢視發生了什麼,我們可以將 checkAllTodos() 函式中的 todos 陣列記錄到控制檯。
- 將您現有的
checkAllTodos()函式更新為以下內容jsconst checkAllTodos = (completed) => { todos.forEach((t) => (t.completed = completed)); console.log("todos", todos); }; - 返回瀏覽器,開啟您的 DevTools 控制檯,然後點選“全選”/“取消全選”幾次。
您會注意到每次按下按鈕時陣列都會成功更新(todo 物件的 completed 屬性在 true 和 false 之間切換),但 Svelte 並不知道這一點。這也意味著在這種情況下,像 $: console.log('todos', todos) 這樣的反應性語句不會很有用。
要找出為什麼會發生這種情況,我們需要了解當更新陣列和物件時 Svelte 中的反應性是如何工作的。
許多 Web 框架使用虛擬 DOM 技術來更新頁面。基本上,虛擬 DOM 是網頁內容的記憶體副本。框架更新此虛擬表示,然後將其與“真實”DOM 同步。這比直接更新 DOM 快得多,並允許框架應用許多最佳化技術。
這些框架預設情況下基本上會在每次更改時針對此虛擬 DOM 重新執行我們所有的 JavaScript,並應用不同的方法來快取昂貴的計算並最佳化執行。它們幾乎沒有嘗試理解我們的 JavaScript 程式碼在做什麼。
Svelte 不使用虛擬 DOM 表示。相反,它解析和分析我們的程式碼,建立依賴樹,然後生成所需的 JavaScript 以僅更新需要更新的 DOM 部分。這種方法通常會生成具有最少開銷的最佳 JavaScript 程式碼,但它也有其侷限性。
有時 Svelte 無法檢測到正在監視的變數的更改。請記住,要告訴 Svelte 變數已更改,您必須為其分配一個新值。一個需要記住的簡單規則是:**更新的變數的名稱必須出現在賦值的左側。**
例如,在以下程式碼段中
const foo = obj.foo;
foo.bar = "baz";
Svelte 不會更新對 obj.foo.bar 的引用,除非您隨後使用 obj = obj。這是因為 Svelte 無法跟蹤物件引用,因此我們必須透過發出賦值來明確告訴它 obj 已更改。
**注意:**如果 foo 是一個頂級變數,您可以輕鬆地告訴 Svelte 在 foo 更改時更新 obj,使用以下反應性語句:$: foo, obj = obj。透過此操作,我們定義了 foo 作為依賴項,並且每當它更改時,Svelte 都會執行 obj = obj。
在我們的 checkAllTodos() 函式中,當我們執行
todos.forEach((t) => (t.completed = completed));
Svelte 不會將 todos 標記為已更改,因為它不知道當我們在 forEach() 方法中更新我們的 t 變數時,我們也在修改 todos 陣列。這很有道理,因為否則 Svelte 將會了解 forEach() 方法的內部工作原理;因此,對於附加到任何物件或陣列的任何方法來說也是如此。
然而,我們可以應用不同的技術來解決此問題,並且所有這些技術都涉及為正在監視的變數分配一個新值。
正如我們已經看到的,我們可以簡單地告訴 Svelte 使用自我賦值來更新變數,如下所示
const checkAllTodos = (completed) => {
todos.forEach((t) => (t.completed = completed));
todos = todos;
};
這將解決問題。在內部,Svelte 將 todos 標記為已更改並刪除明顯多餘的自我賦值。除了看起來很奇怪之外,使用這種技術完全沒問題,有時它是做到這一點最簡潔的方式。
我們還可以透過索引訪問 todos 陣列,如下所示
const checkAllTodos = (completed) => {
todos.forEach((t, i) => (todos[i].completed = completed));
};
對陣列和物件的屬性的賦值(例如 obj.foo += 1 或 array[i] = x)與對值本身的賦值的工作方式相同。當 Svelte 分析此程式碼時,它可以檢測到 todos 陣列正在被修改。
另一種解決方案是為 todos 分配一個新陣列,其中包含所有待辦事項的副本,並相應地更新了 completed 屬性,如下所示
const checkAllTodos = (completed) => {
todos = todos.map((t) => ({ ...t, completed }));
};
在這種情況下,我們使用的是 map() 方法,該方法返回一個新陣列,其中包含對每個專案執行提供的函式的結果。該函式使用 擴充套件語法 返回每個待辦事項的副本,並相應地覆蓋 completed 值的屬性。此解決方案的額外好處是返回一個包含新物件的新陣列,完全避免了修改原始 todos 陣列。
**注意:**Svelte 允許我們指定影響編譯器工作方式的不同選項。<svelte:options immutable={true}/> 選項告訴編譯器您承諾不會更改任何物件。這允許它在檢查值是否已更改方面不那麼保守,並生成更簡單且效能更高的程式碼。有關 <svelte:options> 的更多資訊,請檢視 Svelte 選項文件。
所有這些解決方案都涉及一個賦值,其中更新的變數位於等式的左側。任何這些技術都將允許 Svelte 注意到我們的 todos 陣列已被修改。
選擇一個,並根據需要更新您的 checkAllTodos() 函式。現在您應該能夠一次選中和取消選中所有待辦事項。試試看!
完成我們的 MoreActions 元件
我們將向我們的元件新增一個可用性細節。當沒有要處理的任務時,我們將停用按鈕。要建立此功能,我們將接收 todos 陣列作為 prop,並相應地設定每個按鈕的 disabled 屬性。
- 像這樣更新您的
MoreActions.svelte元件我們還聲明瞭一個反應性的svelte<script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); export let todos; let completed = true; const checkAll = () => { dispatch('checkAll', completed); completed = !completed; } const removeCompleted = () => dispatch('removeCompleted'); $: completedTodos = todos.filter((t) => t.completed).length; </script> <div class="btn-group"> <button type="button" class="btn btn__primary" disabled={todos.length === 0} on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" disabled={completedTodos === 0} on:click={removeCompleted}>Remove completed</button> </div>completedTodos變數來啟用或停用“刪除已完成”按鈕。 - 不要忘記在
Todos.svelte中呼叫元件的位置將 prop 傳遞到MoreActions中svelte<MoreActions {todos} on:checkAll={(e) => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} />
使用 DOM:關注細節
現在我們已經完成了應用程式所需的所有功能,我們將專注於一些可訪問性功能,這些功能將提高鍵盤使用者和螢幕閱讀器使用者的應用程式可用性。
在當前狀態下,我們的應用程式存在一些涉及焦點管理的鍵盤可訪問性問題。讓我們看看這些問題。
探索待辦事項應用程式中的鍵盤可訪問性問題
現在,鍵盤使用者會發現我們應用程式的焦點流程不太可預測或連貫。
如果您點選應用程式頂部的輸入框,您會看到該輸入框周圍有一個粗的虛線輪廓。此輪廓是您瀏覽器當前聚焦於此元素的視覺指示器。
如果您是滑鼠使用者,您可能已經跳過了此視覺提示。但是,如果您只使用鍵盤工作,則瞭解哪個控制元件具有焦點至關重要。它告訴我們哪個控制元件將接收我們的按鍵。
如果您反覆按下 Tab 鍵,您會看到虛線焦點指示器在頁面上的所有可聚焦元素之間迴圈。如果您將焦點移動到“編輯”按鈕並按下 Enter,焦點會突然消失,您將無法再判斷哪個控制元件將接收我們的按鍵。
此外,如果您按下Escape或Enter鍵,則不會發生任何事情。如果您點選取消或儲存,焦點又會消失。對於使用鍵盤的使用者來說,這種行為充其量只會令人困惑。
我們還希望新增一些可用性功能,例如在必填欄位為空時停用儲存按鈕,將焦點置於某些HTML元素或在文字輸入獲得焦點時自動選擇內容。
為了實現所有這些功能,我們需要以程式設計方式訪問DOM節點以執行諸如focus()和select()之類的函式。我們還必須使用addEventListener()和removeEventListener()在控制元件獲得焦點時執行特定的任務。
問題是所有這些DOM節點都是由Svelte在執行時動態建立的。因此,我們必須等待它們被建立並新增到DOM中才能使用它們。為此,我們必須瞭解元件生命週期以瞭解何時可以訪問它們——稍後將詳細介紹。
建立 NewTodo 元件
讓我們首先將新的待辦事項表單提取到它自己的元件中。根據我們目前所知,我們可以建立一個新的元件檔案並調整程式碼以發出addTodo事件,並將新待辦事項的名稱與其他詳細資訊一起傳遞。
- 建立一個新檔案,
components/NewTodo.svelte。 - 將以下內容放入其中在這裡,我們使用svelte
<script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let name = ''; const addTodo = () => { dispatch('addTodo', name); name = ''; } const onCancel = () => name = ''; </script> <form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg">What needs to be done?</label> </h2> <input bind:value={name} type="text" id="todo-0" autoComplete="off" class="input input__lg" /> <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button> </form>bind:value={name}將<input>繫結到name變數,並在其為空(即沒有文字內容)時使用disabled={!name}停用新增按鈕。我們還使用on:keydown={(e) => e.key === 'Escape' && onCancel()}處理Escape鍵。每當按下Escape鍵時,我們都會執行onCancel(),它只會清除name變數。 - 現在,我們必須從
Todos元件內部import並使用它,並更新addTodo()函式以接收新待辦事項的名稱。在Todos.svelte中的其他import語句下方新增以下import語句jsimport NewTodo from "./NewTodo.svelte"; - 並像這樣更新
addTodo()函式jsfunction addTodo(name) { todos = [...todos, { id: newTodoId, name, completed: false }]; }addTodo()現在直接接收新待辦事項的名稱,因此我們不再需要newTodoName變數來賦予它值。我們的NewTodo元件負責處理這一點。注意:
{ name }語法只是{ name: name }的簡寫。這來自JavaScript本身,與Svelte無關,除了為Svelte自己的簡寫提供了一些靈感。 - 最後,對於本節,用對
NewTodo元件的呼叫替換NewTodo表單標記,如下所示svelte<!-- NewTodo --> <NewTodo on:addTodo={(e) => addTodo(e.detail)} />
使用 `bind:this={dom_node}` 指令處理 DOM 節點
現在,我們希望NewTodo元件的<input>元素在每次按下新增按鈕時重新獲得焦點。為此,我們需要對輸入的DOM節點進行引用。Svelte提供了一種使用bind:this={dom_node}指令執行此操作的方法。指定後,一旦元件被掛載並且DOM節點被建立,Svelte就會將對DOM節點的引用分配給指定的變數。
我們將建立一個nameEl變數並使用bind:this={nameEl}將其繫結到輸入。然後在addTodo()內部,在新增新待辦事項後,我們將呼叫nameEl.focus()以再次將焦點重新置於<input>上。當用戶按下Escape鍵時,我們將使用onCancel()函式執行相同的操作。
像這樣更新NewTodo.svelte的內容
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let name = '';
let nameEl; // reference to the name input DOM node
const addTodo = () => {
dispatch('addTodo', name);
name = '';
nameEl.focus(); // give focus to the name input
}
const onCancel = () => {
name = '';
nameEl.focus(); // give focus to the name input
}
</script>
<form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}>
<h2 class="label-wrapper">
<label for="todo-0" class="label__lg">What needs to be done?</label>
</h2>
<input bind:value={name} bind:this={nameEl} type="text" id="todo-0" autoComplete="off" class="input input__lg" />
<button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button>
</form>
試用一下應用程式:在<input>欄位中鍵入新的待辦事項名稱,按tab將焦點賦予新增按鈕,然後按Enter或Escape檢視輸入如何恢復焦點。
自動聚焦我們的輸入
下一個功能將新增到我們的NewTodo元件中,它是一個autofocus屬性,它允許我們指定我們希望在頁面載入時將焦點置於<input>欄位上。
- 我們的第一次嘗試如下:讓我們嘗試新增
autofocus屬性,並僅從<script>塊中呼叫nameEl.focus()。更新NewTodo.svelte的<script>部分的第一部分(前四行),使其如下所示svelte<script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); export let autofocus = false; let name = ''; let nameEl; // reference to the name input DOM node if (autofocus) nameEl.focus(); - 現在返回到
Todos元件,並將autofocus屬性傳遞到<NewTodo>元件呼叫中,如下所示svelte<!-- NewTodo --> <NewTodo autofocus on:addTodo={(e) => addTodo(e.detail)} /> - 如果您現在嘗試使用您的應用程式,您會看到頁面現在是空白的,並且在您的DevTools Web控制檯中,您會看到類似以下內容的錯誤:
TypeError: nameEl is undefined。
為了理解這裡發生了什麼,讓我們再談談我們之前提到的元件生命週期。
元件生命週期和 `onMount()` 函式
當元件被例項化時,Svelte會執行初始化程式碼(即元件的<script>部分)。但在那一刻,構成元件的所有節點都未附加到DOM,事實上,它們甚至不存在。
那麼您如何知道元件何時已被建立並掛載到DOM上呢?答案是每個元件都有一個生命週期,它從建立開始,到銷燬結束。有一些函式允許您在生命週期的關鍵時刻執行程式碼。
您最常使用的函式是onMount(),它允許我們在元件掛載到DOM上後立即執行回撥。讓我們試一試,看看nameEl變數發生了什麼。
- 首先,在
NewTodo.svelte的<script>部分開頭新增以下行jsimport { onMount } from "svelte"; - 並在其末尾新增以下行js
console.log("initializing:", nameEl); onMount(() => { console.log("mounted:", nameEl); }); - 現在刪除
if (autofocus) nameEl.focus()行以避免丟擲我們之前看到的錯誤。 - 應用程式現在將再次工作,並且您將在控制檯中看到以下內容
initializing: undefined mounted: <input id="todo-0" class="input input__lg" type="text" autocomplete="off">
如您所見,在元件初始化時,nameEl未定義,這是有道理的,因為<input>節點甚至還不存在。元件掛載後,Svelte將對<input>DOM節點的引用分配給nameEl變數,這要歸功於bind:this={nameEl}指令。 - 要使自動聚焦功能正常工作,請將您新增的之前的
console.log()/onMount()塊替換為此jsonMount(() => autofocus && nameEl.focus()); // if autofocus is true, we run nameEl.focus() - 再次訪問您的應用程式,您現在將看到
<input>欄位在頁面載入時處於焦點狀態。
使用 `tick()` 函式等待 DOM 更新
現在,我們將處理Todo元件的焦點管理細節。首先,我們希望在按下其編輯按鈕進入編輯模式時,Todo元件的編輯<input>獲得焦點。與我們之前看到的相同,我們將在Todo.svelte內部建立一個nameEl變數,並在將editing變數設定為true後呼叫nameEl.focus()。
- 開啟檔案
components/Todo.svelte並在您的editing和name宣告下方新增nameEl變數宣告jslet nameEl; // reference to the name input DOM node - 現在像這樣更新您的
onEdit()函式jsfunction onEdit() { editing = true; // enter editing mode nameEl.focus(); // set focus to name input } - 最後,透過像這樣更新它將
nameEl繫結到<input>欄位svelte<input bind:value={name} bind:this={nameEl} type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> - 但是,當您嘗試更新的應用程式時,當您按下待辦事項的編輯按鈕時,您會在控制檯中收到類似“TypeError: nameEl is undefined”的錯誤。
那麼,這裡發生了什麼?當您更新Svelte中元件的狀態時,它不會立即更新DOM。相反,它會等到下一個微任務,看看是否有任何其他需要應用的更改,包括其他元件中的更改。這樣做可以避免不必要的工作,並允許瀏覽器更有效地批次處理事物。
在這種情況下,當editing為false時,編輯<input>不可見,因為它不存在於DOM中。在onEdit()函式內部,我們設定editing = true,然後立即嘗試訪問nameEl變數並執行nameEl.focus()。這裡的問題是Svelte尚未更新DOM。
解決此問題的一種方法是使用setTimeout()延遲對nameEl.focus()的呼叫,直到下一個事件迴圈,並給Svelte機會更新DOM。
現在試試這個
function onEdit() {
editing = true; // enter editing mode
setTimeout(() => nameEl.focus(), 0); // asynchronous call to set focus to name input
}
以上解決方案有效,但有點笨拙。Svelte提供了一種更好的方法來處理這些情況。tick()函式返回一個promise,一旦任何掛起的狀態更改已應用於DOM(或立即,如果沒有任何掛起的狀態更改),該promise就會解析。讓我們現在試試。
- 首先,在
<script>部分頂部匯入tick,與您現有的匯入一起jsimport { tick } from "svelte"; - 接下來,使用來自非同步函式的
await呼叫tick();像這樣更新onEdit()jsasync function onEdit() { editing = true; // enter editing mode await tick(); nameEl.focus(); } - 如果您現在嘗試,您會發現一切按預期工作。
注意:要檢視使用tick()的另一個示例,請訪問Svelte教程。
使用 `use:action` 指令向 HTML 元素新增功能
接下來,我們希望名稱<input>在獲得焦點時自動選擇所有文字。此外,我們希望以一種可以輕鬆地重用於任何HTML <input>並以宣告方式應用的方式開發它。我們將使用此需求作為藉口來展示Svelte提供給我們的一個非常強大的功能,用於向常規HTML元素新增功能:操作。
要選擇DOM輸入節點的文字,我們必須呼叫select()。為了在節點獲得焦點時呼叫此函式,我們需要一個類似以下內容的事件偵聽器
node.addEventListener("focus", (event) => node.select());
而且,為了避免記憶體洩漏,我們還應該在節點被銷燬時呼叫removeEventListener()函式。
注意:所有這些都只是標準的WebAPI功能;這裡沒有什麼是特定於Svelte的。
我們可以在我們的Todo元件中實現所有這些,無論何時我們將<input>新增到DOM中或從DOM中刪除,但我們必須非常小心地在節點新增到DOM後新增事件偵聽器,並在節點從DOM中刪除之前刪除偵聽器。此外,我們的解決方案的可重用性不高。
這就是Svelte操作發揮作用的地方。基本上,它們允許我們在元素新增到DOM後以及從DOM中刪除後執行函式。
在我們的直接用例中,我們將定義一個名為selectOnFocus()的函式,該函式將接收一個節點作為引數。該函式將向該節點新增一個事件偵聽器,以便每當它獲得焦點時,它都會選擇文字。然後它將返回一個具有destroy屬性的物件。destroy屬性是Svelte在從DOM中刪除節點後將執行的內容。在這裡,我們將刪除偵聽器以確保我們不會留下任何記憶體洩漏。
- 讓我們建立函式
selectOnFocus()。將以下內容新增到Todo.svelte的<script>部分底部jsfunction selectOnFocus(node) { if (node && typeof node.select === "function") { // make sure node is defined and has a select() method const onFocus = (event) => node.select(); // event handler node.addEventListener("focus", onFocus); // when node gets focus call onFocus() return { destroy: () => node.removeEventListener("focus", onFocus), // this will be executed when the node is removed from the DOM }; } } - 現在我們需要告訴
<input>使用use:action指令使用該函式透過這個指令,我們告訴 Svelte 在元件掛載到 DOM 上時立即執行此函式,並將svelte<input use:selectOnFocus /><input>的 DOM 節點作為引數傳遞。它還負責在元件從 DOM 中移除時執行destroy函式。因此,使用use指令,Svelte 為我們處理了元件的生命週期。在我們的例子中,我們的<input>最終會變成這樣:如下更新元件的第一個標籤/輸入對(在編輯模板內)svelte<label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> - 讓我們試一試。轉到你的應用程式,按下待辦事項的“編輯”按鈕,然後按 Tab 將焦點從
<input>移開。現在點選<input>,你會看到整個輸入文字被選中。
使操作可重用
現在讓我們使這個函式真正地在元件之間可重用。selectOnFocus() 只是一個函式,沒有任何依賴於 Todo.svelte 元件,所以我們可以將其提取到一個檔案中並在那裡使用它。
- 在
src資料夾內建立一個新的檔案actions.js。 - 給它以下內容js
export function selectOnFocus(node) { if (node && typeof node.select === "function") { // make sure node is defined and has a select() method const onFocus = (event) => node.select(); // event handler node.addEventListener("focus", onFocus); // when node gets focus call onFocus() return { destroy: () => node.removeEventListener("focus", onFocus), // this will be executed when the node is removed from the DOM }; } } - 現在從
Todo.svelte內部匯入它;在其他匯入語句下方新增以下匯入語句jsimport { selectOnFocus } from "../actions.js"; - 並刪除
Todo.svelte中的selectOnFocus()定義,因為我們不再需要它了。
重用我們的操作
為了演示我們操作的可重用性,讓我們在 NewTodo.svelte 中使用它。
- 像之前一樣,從
actions.js中匯入selectOnFocus()到此檔案。jsimport { selectOnFocus } from "../actions.js"; - 將
use:selectOnFocus指令新增到<input>,如下所示svelte<input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-0" autocomplete="off" class="input input__lg" />
通過幾行程式碼,我們可以以非常可重用和宣告的方式為常規 HTML 元素新增功能。它只需要一個 import 和一個簡短的指令,如 use:selectOnFocus,它清楚地描述了其目的。而且我們可以實現這一點,而無需建立像 TextInput、MyInput 或類似的自定義包裝器元素。此外,您可以向一個元素新增任意數量的 use:action 指令。
此外,我們不必費力地使用 onMount()、onDestroy() 或 tick()——use 指令為我們處理了元件的生命週期。
其他操作改進
在上一節中,在使用 Todo 元件時,我們不得不處理 bind:this、tick() 和 async 函式,才能在 <input> 新增到 DOM 後立即為其賦予焦點。
- 以下是如何使用操作實現它js
const focusOnInit = (node) => node && typeof node.focus === "function" && node.focus(); - 然後在我們的標記中,我們只需要新增另一個
use:指令svelte<input bind:value={name} use:selectOnFocus use:focusOnInit /> - 我們的
onEdit()函式現在可以變得簡單得多jsfunction onEdit() { editing = true; // enter editing mode }
在繼續之前,讓我們舉最後一個例子,回到我們的 Todo.svelte 元件,並在使用者按下“儲存”或“取消”後將焦點放在“編輯”按鈕上。
我們可以嘗試再次重用我們的 focusOnInit 操作,將 use:focusOnInit 新增到“編輯”按鈕上。但是我們會引入一個細微的錯誤。當你新增一個新的待辦事項時,焦點將放在最近新增的待辦事項的“編輯”按鈕上。這是因為在建立元件時 focusOnInit 操作正在執行。
這不是我們想要的——我們希望“編輯”按鈕僅在使用者按下“儲存”或“取消”後才獲得焦點。
- 因此,返回你的
Todo.svelte檔案。 - 首先,我們將建立一個名為
editButtonPressed的標誌並將其初始化為false。將其新增到其他變數定義的下方jslet editButtonPressed = false; // track if edit button has been pressed, to give focus to it after cancel or save - 接下來,我們將修改“編輯”按鈕的功能以儲存此標誌,併為其建立操作。像這樣更新
onEdit()函式jsfunction onEdit() { editButtonPressed = true; // user pressed the Edit button, focus will come back to the Edit button editing = true; // enter editing mode } - 在它下面,新增以下
focusEditButton()的定義jsconst focusEditButton = (node) => editButtonPressed && node.focus(); - 最後,我們在“編輯”按鈕上
usefocusEditButton操作,如下所示svelte<button type="button" class="btn" on:click={onEdit} use:focusEditButton> Edit<span class="visually-hidden"> {todo.name}</span> </button> - 返回並再次嘗試你的應用程式。此時,每次“編輯”按鈕新增到 DOM 時,都會執行
focusEditButton操作,但它只會將焦點放在按鈕上,前提是editButtonPressed標誌為true。
注意:我們在這裡僅僅觸及了操作的皮毛。操作還可以具有響應式引數,並且 Svelte 允許我們檢測這些引數中的任何一個何時發生變化。因此,我們可以新增與 Svelte 響應式系統很好地整合的功能。有關操作的更詳細介紹,請考慮檢視 Svelte 互動式教程 或 Svelte use:action 文件。
元件繫結:使用 `bind:this={component}` 指令公開元件方法和變數
還有一個輔助功能上的小問題。當用戶按下“刪除”按鈕時,焦點消失了。
本文中我們將要介紹的最後一個功能涉及在待辦事項被刪除後將焦點設定到狀態標題上。
為什麼是狀態標題?在這種情況下,具有焦點的元素已被刪除,因此沒有明確的候選物件來接收焦點。我們選擇狀態標題是因為它靠近待辦事項列表,並且它是一種提供刪除任務的視覺反饋的方式,以及向螢幕閱讀器使用者指示發生了什麼。
首先,我們將狀態標題提取到它自己的元件中。
- 建立一個新檔案
components/TodosStatus.svelte。 - 將以下內容新增到其中svelte
<script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; </script> <h2 id="list-heading"> {completedTodos} out of {totalTodos} items completed </h2> - 在
Todos.svelte的開頭匯入檔案,在其他import語句下方新增以下import語句jsimport TodosStatus from "./TodosStatus.svelte"; - 用對
TodosStatus元件的呼叫替換Todos.svelte內的<h2>狀態標題,並將todos作為 prop 傳遞給它,如下所示svelte<TodosStatus {todos} /> - 你還可以進行一些清理,從
Todos.svelte中刪除totalTodos和completedTodos變數。只需刪除$: totalTodos = …和$: completedTodos = …行,並在我們計算newTodoId時刪除對totalTodos的引用,並改為使用todos.length。為此,請用以下內容替換以let newTodoId開頭的程式碼塊js$: newTodoId = todos.length ? Math.max(...todos.map((t) => t.id)) + 1 : 1; - 一切按預期工作——我們只是將最後一段標記提取到它自己的元件中。
現在我們需要找到一種方法,在待辦事項被刪除後將焦點賦予 <h2> 狀態標籤。
到目前為止,我們已經瞭解瞭如何透過 prop 向元件傳送資訊,以及元件如何透過發出事件或使用雙向資料繫結與父元件通訊。子元件可以使用 bind:this={dom_node} 獲取對 <h2> 節點的引用,並使用雙向資料繫結將其暴露到外部。但是這樣做會破壞元件的封裝;設定焦點應該是它自己的責任。
因此,我們需要 TodosStatus 元件公開一個方法,其父元件可以呼叫該方法以將其賦予焦點。這是一種非常常見的情況,元件需要向使用者公開某些行為或資訊;讓我們看看如何用 Svelte 實現它。
我們已經看到 Svelte 使用 export let varname = … 來 宣告 prop。但是,如果你不使用 let 而是匯出 const、class 或 function,則它在元件外部是隻讀的。但是,函式表示式是有效的 prop。在以下示例中,前三個宣告是 prop,其餘是匯出的值
<script>
export let bar = "optional default initial value"; // prop
export let baz = undefined; // prop
export let format = (n) => n.toFixed(2); // prop
// these are readonly
export const thisIs = "readonly"; // read-only export
export function greet(name) {
// read-only export
alert(`Hello, ${name}!`);
}
export const greet = (name) => alert(`Hello, ${name}!`); // read-only export
</script>
考慮到這一點,讓我們回到我們的用例。我們將建立一個名為 focus() 的函式,該函式將焦點賦予 <h2> 標題。為此,我們需要一個 headingEl 變數來儲存對 DOM 節點的引用,並且我們必須使用 bind:this={headingEl} 將其繫結到 <h2> 元素。我們的焦點方法只會執行 headingEl.focus()。
- 像這樣更新
TodosStatus.svelte的內容請注意,我們已向svelte<script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; let headingEl; export function focus() { // shorter version: export const focus = () => headingEl.focus() headingEl.focus(); } </script> <h2 id="list-heading" bind:this={headingEl} tabindex="-1"> {completedTodos} out of {totalTodos} items completed </h2><h2>添加了一個tabindex屬性,以允許元素以程式設計方式接收焦點。正如我們之前看到的,使用bind:this={headingEl}指令使我們能夠在headingEl變數中獲得對 DOM 節點的引用。然後,我們使用export function focus()公開一個將焦點賦予<h2>標題的函式。我們如何從父級訪問這些匯出的值?就像你可以使用bind:this={dom_node}指令繫結到 DOM 元素一樣,你也可以使用bind:this={component}繫結到元件例項本身。因此,當你在 HTML 元素上使用bind:this時,你會獲得對 DOM 節點的引用,而當你對 Svelte 元件執行此操作時,你會獲得對該元件例項的引用。 - 因此,要繫結到
TodosStatus的例項,我們首先將在Todos.svelte中建立一個todosStatus變數。在你的import語句下方新增以下行jslet todosStatus; // reference to TodosStatus instance - 接下來,向呼叫中新增
bind:this={todosStatus}指令,如下所示svelte<!-- TodosStatus --> <TodosStatus bind:this={todosStatus} {todos} /> - 現在我們可以從
removeTodo()函式呼叫exported focus()方法jsfunction removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id); todosStatus.focus(); // give focus to status heading } - 返回你的應用程式。現在,如果你刪除任何待辦事項,狀態標題將獲得焦點。這有助於突出待辦事項數量的變化,對有視力的人和螢幕閱讀器使用者都有用。
注意:你可能想知道為什麼我們需要為元件繫結宣告一個新變數。為什麼我們不能簡單地呼叫 TodosStatus.focus()?你可能有多個 TodosStatus 例項處於活動狀態,因此你需要一種方法來引用每個特定的例項。這就是為什麼你必須指定一個變數來將每個特定例項繫結到的原因。
目前的程式碼
Git
要檢視本文結束時程式碼的狀態,請像這樣訪問你的 repo 副本
cd mdn-svelte-tutorial/06-stores
或者直接下載資料夾的內容
npx degit opensas/mdn-svelte-tutorial/06-stores
請記住執行 npm install && npm run dev 以在開發模式下啟動您的應用程式。
REPL
要檢視 REPL 中程式碼的當前狀態,請訪問
https://svelte.dev/repl/d1fa84a5a4494366b179c87395940039?version=3.23.2
總結
在本文中,我們已經完成了向我們的應用程式新增所有必需功能的工作,並且我們還解決了許多輔助功能和可用性問題。我們還完成了將我們的應用程式拆分為可管理的元件,每個元件都具有唯一的職責。
同時,我們看到了幾個高階的 Svelte 技術,例如
- 更新物件和陣列時處理響應式問題
- 使用
bind:this={dom_node}(繫結 DOM 元素)處理 DOM 節點 - 使用元件生命週期
onMount()函式 - 使用
tick()函式強制 Svelte 解決掛起的狀態更改 - 使用
use:action指令以可重用和宣告的方式向 HTML 元素新增功能 - 使用
bind:this={component}(繫結元件)訪問元件方法
在下一篇文章中,我們將瞭解如何使用儲存在元件之間進行通訊,以及如何向我們的元件新增動畫。