點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Axios 實作 Todo List

Sam Xiao's Avatar 2024-02-15

使用 Axios 搭配 API 實作 Todo List,並將存取 API 部分重構成 API Function。

Version

Vue 3.4

Axios

todolist001

App.vue

<template>
  <div>
    <input type="text" v-model="newTodo" />
    <button @click="onAdd">Add</button>
  </div>
  <div>
    <ul>
      <li v-for="item in filteredTodos" :key="item.id">
        <input type="checkbox" v-model="item.isCompleted" @change="onCompleted(item)" />
        <span v-if="!item.isEdit">
          <span>{{ item.todo }}</span>
          <button @click="onEdit(item)">edit</button>
          <button @click="onDelete(item)">delete</button>
        </span>
        <span v-else>
          <input type="text" v-model="item.todo_" />
          <button @click="onSave(item)">save</button>
          <button @click="onCancel(item)">cancel</button>
        </span>
      </li>
    </ul>
  </div>
  <div>
    <span>{{ active.length }} items left</span>
    <button @click="onFilterAll">All</button>
    <button @click="onFilterActive">Active</button>
    <button @click="onFilterCompleted">Completed</button>
    <button v-if="completed.length" @click="onClearCompleted">Clear Completed</button>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { getTodos, addTodo, saveTodo, delTodo } from '@/api/todos.js'

let newTodo = ref('')
let todos = ref([])

let filter = ref(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))

let active = computed(() => todos.value.filter((todo) => !todo.isCompleted))
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))

onMounted(async () => (todos.value = await getTodos()))

let onAdd = async () => {
  if (newTodo.value === '') return

  await addTodo(newTodo.value)
  todos.value = await getTodos()

  newTodo.value = ''
}

let onCompleted = async (item) => {
  await saveTodo(item)
}

let onEdit = (item) => {
  item.isEdit = true
  item.todo_ = item.todo
}

let onSave = async (item) => {
  item.isEdit = false
  item.todo = item.todo_

  await saveTodo(item)
}

let onDelete = async (item) => {
  await delTodo(item)
  todos.value = await getTodos()
}

let onClearCompleted = async () => {
  for (let item of completed.value) {
    await delTodo(item)
  }

  todos.value = await getTodos()
}

let onCancel = (item) => (item.isEdit = false)
let onFilterAll = () => (filter.value = () => true)
let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
</script>

Line 3

<input type="text" v-model="newTodo" />
  • v-model:將輸入的新 todo 綁定到 newTodo state

Line 4

<button @click="onAdd">Add</button>
  • 新增一筆 todo,並將 click event 指定到 onAdd()

Line 8

<ul>
  <li v-for="item in filteredTodos" :key="item.id">
    <span>{{ item.todo }}</span>
  </li>
</ul>
  • v-for :列舉 filteredTodos computed

Line 9

<input type="checkbox" v-model="item.isCompleted" @change="onCompleted(item)" />
  • v-model:將 checkbox 綁定到 item.isCompleted
  • @change:當改變 checkbox 改變時,呼叫 onCompleted()

Line 10

<span v-if="!item.isEdit">
  <span>{{ item.todo }}</span>
  <button @click="onEdit(item)">edit</button>
  <button @click="onDelete(item)">delete</button>
</span>
  • v-if:若是 非編輯模式,則直接顯示 todo
  • Button 此時為 editdelete

Line 15

<span v-else>
  <input type="text" v-model="item.todo_" />
  <button @click="onSave(item)">save</button>
  <button @click="onCancel(item)">cancel</button>
</span>
  • v-else:若是 編輯模式,則以 <input> 顯示 todo,注意此時 v-model 綁定到 item.todo_ 而非 item.todo,此為儲存 暫時 的 todo,主要為了 cancel 所用
  • Button 此時為 savecancel

Line 24

<span> {{ active.length }} items left</span>
  • 顯示目前尚未完成的 todo 筆數

Line 25

<button @click="onFilterAll">All</button>
  • 顯示所有 todo,並將 click event 指定到 onFilterAll()

Line 26

<button @click="onFilterActive">Active</button>
  • 顯示尚未完成 todo,並將 click event 指定到 onFilterActive()

Line 27

<button @click="onFilterCompleted">Completed</button>
  • 顯示已完成 todo,並將 click event 指定到 onFilterCompleted()

Line 28

<button v-if="completed.length" @click="onClearCompleted">Clear Completed</button>
  • v-if:若有已完成 todo,則顯示 Clear Completed button
  • click event 指定到 onClearCompleted()

Line 34

import { getTodos, addTodo, saveTodo, delTodo } from '@/api/todos.js'
  • api/todos.js import API function
  • @:Vite 會幫我們替換成 src

Line 36

let newTodo = ref('')
let todos = ref([])
  • newTodo state:儲存新輸入的 todo
  • todos state:儲存所有 todo

Line 38

let filter = ref(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))
  • filter state:儲存 AllActiveCompleted 所需要的 filter function
  • filteredTodos computed:當 todo 改變時,filteredTodos computed 會 reactive 跟著改變,只顯示所有目前 filter state 的 filter function 所指定的 todo

Line 41

let active = computed(() => todos.value.filter(todo => !todo.isCompleted))
  • active computed:當 todo state 改變時,active computed 會 reactive 跟著改變,只顯示所有 未完成 todo

Line 42

let completed = computed(() => todos.value.filter(todo => todo.isCompleted))
  • completed computed:當 todo state 改變時,completed computed 會 reactive 跟著改變,只顯示所有 已完成 todo

Line 45

onMounted(async () => (todos.value = await getTodos()))
  • onMounted():呼叫 getTodos() API function 取得所有 todos

Line 47

let onAdd = async () => {
  if (newTodo.value === '') return

  await addTodo(newTodo.value)
  todos.value = await getTodos()

  newTodo.value = ''
}
  • 新增一筆 todo
  • 呼叫 addTodo() API function 新增一筆 todo
  • 新增完呼叫 getTodos() API function 重新取得所有 todo

Line 56

let onCompleted = async (item) => {
  await saveTodo(item)
}
  • 改變 todo 狀態為 完成未完成
  • 呼叫 saveTodo() API function 儲存目前 todo 完成狀態

Line 60

let onEdit = (item) => {
  item.isEdit = true
  item.todo_ = item.todo
}
  • 編輯一筆 todo
  • 將 item 的 isEdit 設定為 true 表示為 編輯模式,此時 button 會改為顯示 savecancel
  • 在 item 下新增 todo_ property 作為臨時 state,並將目前 toto 值寫入 todo_,作為綁定 <input>

由於 JavaScript 為 動態語言,我們可對 Object 動態新增 property 作為臨時變數,此為 JavaScript 慣用手法

Line 65

let onSave = async (item) => {
  item.isEdit = false
  item.todo = item.todo_

  await saveTodo(item)
}
  • 儲存一筆 todo
  • 將 item 的 isEdit 設定為 false 表示為 非編輯模式,此時 button 會改為顯示 editdelete
  • 因為目前編輯的 <input> 綁定到 item.todo_,將資料從 item.todo_ 複製到 item.todo 完成儲存
  • 最後呼叫 saveTodo() API function 儲存 todo

Line 72

let onDelete = async (item) => {
  await delTodo(item)
  todos.value = await getTodos()
}
  • 刪除一筆 todo
  • 呼叫 delTodo() API function 刪除 todo
  • 刪除完呼叫 getTodos() API function 重新取得所有 todo

Line 77

let onClearCompleted = async () => {
  for (let item of completed.value) {
    await delTodo(item)
  }

  todos.value = await getTodos()
}
  • 使用 for loop 一筆一筆呼叫 delTodo() API function 刪除 todo
  • 刪除完呼叫 getTodos() API function 重新取得所有 todo

Q:各位可思考若改成以下寫法會變成如何?

let onClearCompleted = async () => {
  completed.value.forEach(async item => {
    await delTodo(item)
  })

  todos.value = await getTodos()
}

Line 85

let onCancel = (item) => (item.isEdit = false)
  • 取消編輯一筆 todo
  • 將 item 的 isEdit 設定為 false 表示為 非編輯模式,此時 button 會改為顯示 editdelete

亦可以使用 delete item.todo_ 動態刪除 _todo property 或 item.todo_ = '' 清空,也可以不處理,反正每次 onEdit() 都會重新動態新增 todo_ property

Line 86

let onFilterAll = () => (filter.value = () => true)
  • 顯示所有 todo,直接提供 filter function

Line 87

let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
  • 顯示尚未完成 todo,直接提供 filter function

Line 88

let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
  • 顯示已完成 todo,直接提供 filter function

API Function

api/todos.js

import axios from 'axios'

export let getTodos = async () => {
  let url = 'http://localhost:3000/todos'

  try {
    let { data } = await axios.get(url)
    return data
  } catch (e) {
    console.error(e)
  }
}

export let addTodo = async (todo) => {
  let url = 'http://localhost:3000/todos'

  let body = {
    isCompleted: false,
    todo
  }

  try {
    let { data } = await axios.post(url, body)
    return data
  } catch (e) {
    console.error(e)
  }
}

export let saveTodo = async (item) => {
  let url = `http://localhost:3000/todos/${item.id}`

  let body = {
    isCompleted: item.isCompleted,
    todo: item.todo
  }

  try {
    let { data } = await axios.put(url, body)
    return data
  } catch (e) {
    console.error(e)
  }
}

export let delTodo = async (item) => {
  let url = `http://localhost:3000/todos/${item.id}`

  try {
    let { data } = await axios.delete(url)
    return data
  } catch (e) {
    console.error(e)
  }
}

Line 1

import axios from 'axios'
  • 引用 Axios

Line 3

export let getTodos = async () => {
  let url = 'http://localhost:3000/todos'

  try {
    let { data } = await axios.get(url)
    return data
  } catch (e) {
    console.error(e)
  }
}
  • 取得目前所有 todo
  • 使用 axios.get() 呼叫 GET API
  • axios.get() 所回傳的資料都放在 data property 下,可使用 { data } 直接 Object Destructure

Line 14

export let addTodo = async (todo) => {
  let url = 'http://localhost:3000/todos'

  let body = {
    isCompleted: false,
    todo
  }

  try {
    let { data } = await axios.post(url, body)
    return data
  } catch (e) {
    console.error(e)
  }
}
  • 新增一筆 todo
  • 使用 axios.post() 呼叫 POST API

Line 30

export let saveTodo = async (item) => {
  let url = `http://localhost:3000/todos/${item.id}`

  let body = {
    isCompleted: item.isCompleted,
    todo: item.todo
  }

  try {
    let { data } = await axios.put(url, body)
    return data
  } catch (e) {
    console.error(e)
  }
}
  • 儲存一筆 todo
  • 在 url 提供 id
  • 使用 axios.put() 呼叫 PUT API

Line 46

export let delTodo = async (item) => {
  let url = `http://localhost:3000/todos/${item.id}`

  try {
    let { data } = await axios.delete(url)
    return data
  } catch (e) {
    console.error(e)
  }
}
  • 刪除一筆 todo
  • 在 url 提供 id
  • 使用 axios.delete() 呼叫 DELETE API

Conclusion

  • 雖然可將呼叫 API 部分也寫在 .vue 內,但此部分通常會重複使用,實務上會將此部分重構成 API fuction