Svelte 中的動態行為:使用變數和 props

現在我們的標記和樣式已準備就緒,我們可以開始為 Svelte 待辦事項列表應用程式開發所需的功能。在本文中,我們將使用變數和屬性來使我們的應用程式動態化,允許我們新增和刪除待辦事項,將它們標記為已完成,並按狀態對其進行過濾。

先決條件

至少建議您熟悉核心 HTMLCSSJavaScript 語言,並瞭解 終端/命令列

您需要一個安裝了 node 和 npm 的終端來編譯和構建您的應用程式。

目標 學習並實踐一些基本的 Svelte 概念,例如建立元件、使用屬性傳遞資料、將 JavaScript 表示式渲染到我們的標記中、修改元件的狀態以及遍歷列表。

與我們一起編寫程式碼

Git

克隆 GitHub 倉庫(如果您還沒有這樣做)

bash
git clone https://github.com/opensas/mdn-svelte-tutorial.git

然後,要進入當前應用程式狀態,請執行

bash
cd mdn-svelte-tutorial/03-adding-dynamic-behavior

或直接下載資料夾的內容

bash
npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior

請記住執行 npm install && npm run dev 以在開發模式下啟動您的應用程式。

REPL

要使用 REPL 與我們一起編寫程式碼,請從以下地址開始

https://svelte.dev/repl/c862d964d48d473ca63ab91709a0a5a0?version=3.23.2

使用待辦事項

我們的 Todos.svelte 元件目前僅顯示靜態標記;讓我們開始使其更具動態性。我們將從標記中獲取任務資訊並將其儲存在 todos 陣列中。我們還將建立兩個變數來跟蹤任務總數和已完成的任務。

元件的狀態將由這三個頂級變量表示。

  1. src/components/Todos.svelte 的頂部建立一個 <script> 部分,併為其提供一些內容,如下所示
    svelte
    <script>
      let todos = [
        { id: 1, name: "Create a Svelte starter app", completed: true },
        { id: 2, name: "Create your first component", completed: true },
        { id: 3, name: "Complete the rest of the tutorial", completed: false }
      ];
      let totalTodos = todos.length;
      let completedTodos = todos.filter((todo) => todo.completed).length;
    </script>
    
    現在讓我們對這些資訊做點什麼。
  2. 讓我們從顯示狀態訊息開始。找到 idlist-heading<h2> 標題,並用動態表示式替換硬編碼的活動任務和已完成任務的數量
    svelte
    <h2 id="list-heading">{completedTodos} out of {totalTodos} items completed</h2>
    
  3. 轉到應用程式,您應該會看到之前顯示的“已完成 3 個專案中的 2 個”訊息,但這次資訊來自 todos 陣列。
  4. 為了證明這一點,請轉到該陣列,嘗試更改某些待辦事項物件的 completed 屬性值,甚至新增新的待辦事項物件。觀察訊息中的數字如何被相應地更新。

從資料動態生成待辦事項

目前,我們顯示的待辦事項都是靜態的。我們希望遍歷 todos 陣列中的每個專案併為每個任務渲染標記,所以現在讓我們這樣做。

HTML 無法表達邏輯——例如條件和迴圈。Svelte 可以。在這種情況下,我們使用 {#each} 指令來迭代 todos 陣列。如果提供,第二個引數將包含當前專案的索引。此外,可以提供一個鍵表示式,該表示式將唯一標識每個專案。Svelte 將在資料更改時使用它來比較列表,而不是在末尾新增或刪除專案,並且始終指定一個鍵表示式是一個好習慣。最後,可以提供一個 :else 塊,該塊將在列表為空時呈現。

讓我們試一試。

  1. 將現有的 <ul> 元素替換為以下簡化版本,以瞭解其工作原理
    svelte
    <ul>
    {#each todos as todo, index (todo.id)}
      <li>
        <input type="checkbox" checked={todo.completed}/> {index}. {todo.name} (id: {todo.id})
      </li>
    {:else}
      Nothing to do here!
    {/each}
    </ul>
    
  2. 返回應用程式;您將看到如下內容:使用 each 塊建立的非常簡單的待辦事項列表輸出
  3. 現在我們已經看到它可以工作了,讓我們為 {#each} 指令的每個迴圈生成一個完整的待辦事項,並在其中嵌入來自 todos 陣列的資訊:idnamecompleted。將您現有的 <ul> 塊替換為以下內容
    svelte
    <!-- To-dos -->
    <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
      {#each todos as todo (todo.id)}
      <li class="todo">
        <div class="stack-small">
          <div class="c-cb">
            <input
              type="checkbox"
              id="todo-{todo.id}"
              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">
              Delete <span class="visually-hidden">{todo.name}</span>
            </button>
          </div>
        </div>
      </li>
      {:else}
      <li>Nothing to do here!</li>
      {/each}
    </ul>
    
    請注意,我們如何使用花括號將 JavaScript 表示式嵌入 HTML 屬性中,就像我們在複選框的 checkedid 屬性中所做的那樣。

我們已將靜態標記轉換為動態模板,準備顯示元件狀態中的任務。太棒了!我們正在接近目標。

使用 props

使用硬編碼的待辦事項列表,我們的 Todos 元件並不是很有用。為了將我們的元件轉變為通用的待辦事項編輯器,我們應該允許該元件的父元件傳入要編輯的待辦事項列表。這將允許我們將它們儲存到 Web 服務或本地儲存中,並在以後檢索它們以進行更新。所以讓我們將陣列轉換為 prop

  1. Todos.svelte 中,用 export let todos = [] 替換現有的 let todos = … 塊。
    js
    export let todos = [];
    
    這起初可能感覺有點奇怪。這不是 JavaScript 模組中 export 的正常工作方式!這是 Svelte 如何透過採用有效語法並賦予其新的用途來“擴充套件”JavaScript 的。在這種情況下,Svelte 使用 export 關鍵字將變數宣告標記為屬性或 prop,這意味著它對元件的使用者變得可用。您還可以為 prop 指定預設初始值。如果元件的使用者在例項化元件時沒有在元件上指定 prop——或者如果其初始值為未定義——則將使用此值。因此,使用 export let todos = [],我們告訴 Svelte 我們的 Todos.svelte 元件將接受一個 todos 屬性,如果省略該屬性,則將初始化為空陣列。
  2. 看看應用程式,您將看到“此處無事可做!”訊息。這是因為我們目前沒有從 App.svelte 中向其傳遞任何值,因此它正在使用預設值。
  3. 現在讓我們將待辦事項移至 App.svelte 並將它們作為 prop 傳遞給 Todos.svelte 元件。更新 src/App.svelte 如下所示
    svelte
    <script>
      import Todos from "./components/Todos.svelte";
    
      let todos = [
        { id: 1, name: "Create a Svelte starter app", completed: true },
        { id: 2, name: "Create your first component", completed: true },
        { id: 3, name: "Complete the rest of the tutorial", completed: false }
      ];
    </script>
    
    <Todos todos={todos} />
    
  4. 當屬性和變數具有相同的名稱時,Svelte 允許您僅指定變數作為便捷快捷方式,因此我們可以將最後一行重寫為如下所示。現在試試看。
    svelte
    <Todos {todos} />
    

此時,您的待辦事項應該像以前一樣呈現,只是現在我們是從 App.svelte 元件中傳入的。

切換和刪除待辦事項

讓我們新增一些功能來切換任務狀態。Svelte 具有 on:eventname 指令用於監聽 DOM 事件。讓我們為複選框輸入的 on:click 事件新增一個處理程式以切換 completed 值。

  1. 更新 src/components/Todos.svelte 中的 <input type="checkbox"> 元素,如下所示
    svelte
    <input type="checkbox" id="todo-{todo.id}"
      on:click={() => todo.completed = !todo.completed}
      checked={todo.completed}
    />
    
  2. 接下來,我們將新增一個函式以從我們的 todos 陣列中刪除一個待辦事項。在 Todos.svelte<script> 部分底部,新增 removeTodo() 函式,如下所示
    js
    function removeTodo(todo) {
      todos = todos.filter((t) => t.id !== todo.id);
    }
    
  3. 我們將透過“刪除”按鈕呼叫它。使用 click 事件更新它,如下所示
    svelte
    <button type="button" class="btn btn__danger"
      on:click={() => removeTodo(todo)}
    >
      Delete <span class="visually-hidden">{todo.name}</span>
    </button>
    
    Svelte 中處理程式的一個非常常見的錯誤是將函式執行的結果作為處理程式傳遞,而不是傳遞函式本身。例如,如果您指定 on:click={removeTodo(todo)},它將執行 removeTodo(todo),並且結果將作為處理程式傳遞,這不是我們想要的。在這種情況下,您必須將 on:click={() => removeTodo(todo)} 指定為處理程式。如果 removeTodo() 沒有接收引數,您可以使用 on:event={removeTodo},但不能使用 on:event={removeTodo()}。這不是某些特殊的 Svelte 語法——在這裡我們只是使用常規的 JavaScript 箭頭函式

同樣,這是一個很好的進展——此時,我們現在可以刪除任務了。當按下待辦事項的“刪除”按鈕時,相關待辦事項將從 todos 陣列中刪除,並且 UI 將更新為不再顯示它。此外,我們現在可以選中複選框,並且相關待辦事項的已完成狀態現在將在 todos 陣列中更新。

但是,“x 個專案中的 y 個已完成”標題沒有更新。繼續閱讀以瞭解為什麼會發生這種情況以及我們如何解決它。

反應式待辦事項

正如我們已經看到的,每次修改元件頂級變數的值時,Svelte 都知道如何更新 UI。在我們的應用程式中,每次切換或刪除待辦事項時,都會直接更新 todos 陣列的值,因此 Svelte 將自動更新 DOM。

但是,totalTodoscompletedTodos 並非如此。在以下程式碼中,在例項化元件並執行指令碼時,會為它們分配一個值,但在此之後,不會修改它們的值

js
let totalTodos = todos.length;
let completedTodos = todos.filter((todo) => todo.completed).length;

我們可以在切換和刪除待辦事項後重新計算它們,但有一種更簡單的方法。

我們可以告訴 Svelte 我們希望我們的 totalTodoscompletedTodos 變數具有反應性,方法是在它們前面加上 $:。每當它們依賴的資料發生更改時,Svelte 將生成程式碼以自動更新它們。

注意:Svelte 使用 $: JavaScript 標籤語句語法 來標記反應性語句。就像使用 export 關鍵字宣告 prop 一樣,這可能看起來有點陌生。這是另一個例子,其中 Svelte 利用有效的 JavaScript 語法並賦予其新的用途——在這種情況下,表示“每當任何引用的值更改時重新執行此程式碼”。一旦你習慣了它,就再也回不去了。

更新 src/components/Todos.svelte 中的 totalTodoscompletedTodos 變數定義,使其如下所示

js
$: totalTodos = todos.length;
$: completedTodos = todos.filter((todo) => todo.completed).length;

如果您現在檢查您的應用程式,您將看到標題的數字在待辦事項完成或刪除時會更新。不錯!

在幕後,Svelte 編譯器將解析和分析我們的程式碼以建立依賴項樹,然後它將生成 JavaScript 程式碼以在其中一個依賴項更新時重新評估每個反應性語句。Svelte 中的反應性以非常輕量級且高效的方式實現,無需使用偵聽器、設定器、獲取器或任何其他複雜機制。

新增新的待辦事項

現在轉到本文的下一個主要任務——讓我們新增一些新增新待辦事項的功能。

  1. 首先,我們將建立一個變數來儲存新待辦事項的文字。將此宣告新增到 Todos.svelte 檔案的 <script> 部分
    js
    let newTodoName = "";
    
  2. 現在,我們將在新增新任務的 <input> 中使用此值。為此,我們需要將我們的 newTodoName 變數繫結到 todo-0 輸入,以便 newTodoName 變數值與輸入的 value 屬性保持同步。我們可以這樣做
    svelte
    <input value={newTodoName} on:keydown={(e) => newTodoName = e.target.value} />
    
    每當 newTodoName 變數的值發生變化時,它都會反映在輸入的 value 屬性中,並且每當在輸入中按下鍵時,我們都會更新 newTodoName 變數的內容。這是輸入框雙向資料繫結的手動實現。但我們不需要這樣做——Svelte 提供了一種更簡單的方法來將任何屬性繫結到變數,使用 bind:property 指令
    svelte
    <input bind:value={newTodoName} />
    
    因此,讓我們實現這一點。更新 todo-0 輸入,如下所示
    svelte
    <input
      bind:value={newTodoName}
      type="text"
      id="todo-0"
      autocomplete="off"
      class="input input__lg" />
    
  3. 測試此功能是否有效的一種簡單方法是新增一個反應性語句來記錄 newTodoName 的內容。在 <script> 部分末尾新增此程式碼段
    js
    $: console.log("newTodoName: ", newTodoName);
    

    注意:您可能已經注意到,反應性語句不限於變數宣告。您可以在 $: 符號後放置任何 JavaScript 語句。

  4. 現在嘗試返回 localhost:5042,按下 Ctrl + Shift + K 開啟瀏覽器控制檯,並在輸入欄位中輸入一些內容。你應該會看到你的輸入被記錄下來。此時,如果願意,你可以刪除響應式的 console.log()
  5. 接下來,我們將建立一個函式來新增新的待辦事項——addTodo()——它會將一個新的 todo 物件推送到 todos 陣列中。將此新增到 src/components/Todos.svelte<script> 塊的底部。
    js
    function addTodo() {
      todos.push({ id: 999, name: newTodoName, completed: false });
      newTodoName = "";
    }
    

    注意:目前我們只是為每個待辦事項分配相同的 id,但不用擔心,我們很快就會解決這個問題。

  6. 現在我們希望更新我們的 HTML,以便在表單提交時呼叫 addTodo()。像這樣更新 NewTodo 表單的起始標籤
    svelte
    <form on:submit|preventDefault={addTodo}>
    
    on:eventname 指令支援使用 | 字元向 DOM 事件新增修飾符。在本例中,preventDefault 修飾符告訴 Svelte 生成呼叫 event.preventDefault() 的程式碼,然後再執行處理程式。瀏覽之前的連結以檢視可用的其他修飾符。
  7. 如果你現在嘗試新增新的待辦事項,新的待辦事項會新增到待辦事項陣列中,但我們的 UI 不會更新。請記住,在 Svelte 中,響應性是由賦值觸發的。這意味著 addTodo() 函式被執行,元素被新增到 todos 陣列中,但 Svelte 不會檢測到 push 方法修改了陣列,因此它不會重新整理任務 <ul>。只需在 addTodo() 函式的末尾新增 todos = todos 就可以解決問題,但這看起來很奇怪,因為必須在函式的末尾包含它。相反,我們將取出 push() 方法,並使用展開語法來實現相同的結果:我們將為 todos 陣列分配一個值,該值等於 todos 陣列加上新物件。

    注意:Array 有幾個可變操作:push()pop()splice()shift()unshift()reverse()sort()。使用它們通常會導致難以跟蹤的副作用和錯誤。透過使用展開語法而不是 push(),我們避免了修改陣列,這被認為是一種良好的實踐。

    像這樣更新你的 addTodo() 函式
    js
    function addTodo() {
      todos = [...todos, { id: 999, name: newTodoName, completed: false }];
      newTodoName = "";
    }
    

為每個待辦事項提供唯一的 ID

如果你現在嘗試在你的應用中新增新的待辦事項,你將能夠新增一個新的待辦事項並使其出現在 UI 中——一次。如果你第二次嘗試,它將無法工作,並且你會收到一條控制檯訊息,提示“錯誤:帶鍵的 each 中不能有重複的鍵”。我們需要為我們的待辦事項提供唯一的 ID。

  1. 讓我們宣告一個 newTodoId 變數,該變數根據待辦事項的數量加 1 計算得出,並使其具有響應性。將以下程式碼片段新增到 <script> 部分
    js
    let newTodoId;
    $: {
      if (totalTodos === 0) {
        newTodoId = 1;
      } else {
        newTodoId = Math.max(...todos.map((t) => t.id)) + 1;
      }
    }
    

    注意:如你所見,響應式語句不限於單行語句。以下方法也可以使用,但可讀性稍差:$: newTodoId = totalTodos ? Math.max(...todos.map((t) => t.id)) + 1 : 1

  2. Svelte 是如何實現這一點的?編譯器解析整個響應式語句,並檢測到它依賴於 totalTodos 變數和 todos 陣列。因此,每當其中任何一個被修改時,此程式碼就會重新計算,相應地更新 newTodoId。讓我們在 addTodo() 函式中使用它。像這樣更新它
    js
    function addTodo() {
      todos = [...todos, { id: newTodoId, name: newTodoName, completed: false }];
      newTodoName = "";
    }
    

按狀態篩選待辦事項

最後,對於本文,讓我們實現按狀態過濾待辦事項的功能。我們將建立一個變數來儲存當前過濾器,以及一個將返回過濾後的待辦事項的輔助函式。

  1. 在我們的 <script> 部分底部新增以下內容
    js
    let filter = "all";
    const filterTodos = (filter, todos) =>
      filter === "active"
        ? todos.filter((t) => !t.completed)
        : filter === "completed"
          ? todos.filter((t) => t.completed)
          : todos;
    
    我們使用 filter 變數來控制活動過濾器:allactivecompleted。只需將這些值之一分配給 filter 變數,就會啟用過濾器並更新待辦事項列表。讓我們看看如何實現這一點。filterTodos() 函式將接收當前過濾器和待辦事項列表,並返回一個相應過濾後的新待辦事項陣列。
  2. 讓我們更新過濾器按鈕標記,使其成為動態的,並在使用者按下其中一個過濾器按鈕時更新當前過濾器。像這樣更新它
    svelte
    <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>
    
    此標記中發生了一些事情。我們將透過將 btn__primary 類應用於活動過濾器按鈕來顯示當前過濾器。為了有條件地將樣式類應用於元素,我們使用 class:name={value} 指令。如果值表示式計算結果為真值,則會應用類名。你可以在同一個元素上新增許多此類指令,並使用不同的條件。因此,當我們發出 class:btn__primary={filter === 'all'} 時,如果 filter 等於 all,則 Svelte 將應用 btn__primary 類。

    注意:當類與變數名稱匹配時,Svelte 提供了一個快捷方式,允許我們將 <div class:active={active}> 簡化為 <div class:active>

    aria-pressed={filter === 'all'} 也是類似的情況:當花括號之間傳遞的 JavaScript 表示式計算結果為真值時,aria-pressed 屬性將被新增到按鈕中。每當我們點選按鈕時,我們都會透過發出 on:click={() => filter = 'all'} 來更新 filter 變數。繼續閱讀以瞭解 Svelte 響應性將如何處理其餘部分。
  3. 現在我們只需要在 {#each} 迴圈中使用輔助函式;像這樣更新它
    svelte
    <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
      {#each filterTodos(filter, todos) as todo (todo.id)}
    在分析我們的程式碼後,Svelte 檢測到我們的 filterTodos() 函式依賴於變數 filtertodos。並且,就像嵌入在標記中的任何其他動態表示式一樣,每當這些依賴項中的任何一個發生變化時,DOM 都會相應地更新。因此,每當 filtertodos 發生變化時,filterTodos() 函式將重新計算,迴圈內的專案將更新。

注意:響應性有時可能很棘手。Svelte 將 filter 識別為依賴項,因為我們在 filterTodos(filter, todo) 表示式中引用了它。filter 是一個頂級變數,因此我們可能會傾向於將其從輔助函式引數中刪除,並像這樣呼叫它:filterTodos(todo)。這可以工作,但現在 Svelte 無法知道 {#each filterTodos(todos) } 依賴於 filter,並且當過濾器發生變化時,過濾後的待辦事項列表不會更新。始終記住,Svelte 會分析我們的程式碼以找出依賴項,因此最好明確說明它,而不是依賴於頂級變數的可見性。此外,使我們的程式碼清晰並明確它正在使用哪些資訊是一種良好的實踐。

目前的程式碼

Git

要檢視本文結尾處程式碼的狀態,請像這樣訪問你的儲存庫副本

bash
cd mdn-svelte-tutorial/04-componentizing-our-app

或直接下載資料夾的內容

bash
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

總結

現在就到這裡!在這篇文章中,我們已經實現了我們的大部分所需功能。我們的應用可以顯示、新增和刪除待辦事項,切換其已完成狀態,顯示其中有多少已完成,並應用過濾器。

概括地說,我們涵蓋了以下主題

  • 建立和使用元件
  • 將靜態標記轉換為即時模板
  • 在標記中嵌入 JavaScript 表示式
  • 使用 {#each} 指令迭代列表
  • 使用 props 在元件之間傳遞資訊
  • 偵聽 DOM 事件
  • 宣告響應式語句
  • 使用 console.log() 和響應式語句進行基本除錯
  • 使用 bind:property 指令繫結 HTML 屬性
  • 使用賦值觸發響應性
  • 使用響應式表示式過濾資料
  • 明確定義我們的響應式依賴項

在下一篇文章中,我們將新增更多功能,允許使用者編輯待辦事項。