Composables
2025年5月7日大约 5 分鐘
第二部分:Vue 3 Composables 與 Firestore CRUD 操作
本部分將介紹如何使用 Vue 3 的 Composition API 創建可重用的函數來處理待辦事項的 CRUD 操作。
學習目標
- 了解 Vue 3 Composition API 和 Composables
- 創建用於 Firestore CRUD 操作的通用函數
- 實現待辦事項的基本操作功能
1. Composition API 與 Composables 簡介
Vue 3 的 Composition API 是一種基於函數的 API,它提供了組合和重用組件邏輯的新方法。
Composables 是利用 Composition API 創建的可重用的函數,其特點是:
- 封裝和重用複雜的邏輯
- 提供反應性數據和方法
- 能夠在多個組件間共享
2. 創建 Firestore CRUD Composables
2.1 獲取待辦事項列表
首先,我們創建一個用於獲取集合數據的 Composable:
// src/composables/getCollection.js
import { ref, watchEffect } from 'vue'
import { collection, onSnapshot, query, orderBy } from 'firebase/firestore'
import { db } from '@/firebase/config'
const getCollection = (collectionName, queryOptions = []) => {
const documents = ref(null)
const error = ref(null)
const isPending = ref(true)
// 設置查詢
const collectionRef = collection(db, collectionName)
const q = query(collectionRef, ...queryOptions)
// 實時監聽資料變更
const unsubscribe = onSnapshot(q, (snapshot) => {
// 將文件轉換為數組
const results = []
snapshot.docs.forEach(doc => {
results.push({ id: doc.id, ...doc.data() })
})
// 更新數據
documents.value = results
error.value = null
isPending.value = false
}, (err) => {
console.error(err.message)
documents.value = null
error.value = err.message
isPending.value = false
})
// 清理函數,停止監聽
watchEffect((onInvalidate) => {
onInvalidate(() => unsubscribe())
})
return { documents, error, isPending }
}
export default getCollection
2.2 用於 CRUD 操作的 Composable
接下來,我們創建一個用於添加、更新和刪除待辦事項的 Composable:
// src/composables/useCollection.js
import { ref } from 'vue'
import { collection, addDoc, doc, deleteDoc, updateDoc, serverTimestamp } from 'firebase/firestore'
import { db } from '@/firebase/config'
const useCollection = (collectionName) => {
const error = ref(null)
const isPending = ref(false)
// 添加文檔
const addDocument = async (data) => {
error.value = null
isPending.value = true
try {
// 添加一個伺服器時間戳
const docToAdd = { ...data, createdAt: serverTimestamp() }
const colRef = collection(db, collectionName)
const docRef = await addDoc(colRef, docToAdd)
isPending.value = false
return docRef.id
} catch (err) {
console.error(err.message)
error.value = '無法添加文檔'
isPending.value = false
return null
}
}
// 刪除文檔
const deleteDocument = async (id) => {
error.value = null
isPending.value = true
try {
const docRef = doc(db, collectionName, id)
await deleteDoc(docRef)
isPending.value = false
} catch (err) {
console.error(err.message)
error.value = '無法刪除文檔'
isPending.value = false
}
}
// 更新文檔
const updateDocument = async (id, updates) => {
error.value = null
isPending.value = true
try {
const docRef = doc(db, collectionName, id)
await updateDoc(docRef, updates)
isPending.value = false
} catch (err) {
console.error(err.message)
error.value = '無法更新文檔'
isPending.value = false
}
}
return { error, isPending, addDocument, deleteDocument, updateDocument }
}
export default useCollection
2.3 使用 Composables 的範例
以下是一個簡單的待辦事項清單組件,展示如何使用這些 Composables:
<!-- src/components/TodoList.vue -->
<script setup>
import { ref } from 'vue'
import { orderBy } from 'firebase/firestore'
import getCollection from '@/composables/getCollection'
import useCollection from '@/composables/useCollection'
// 使用 getCollection 獲取待辦事項列表,按創建時間排序
const { documents: todos, error, isPending } = getCollection(
'todos',
[orderBy('createdAt', 'desc')]
)
// 使用 useCollection 進行 CRUD 操作
const {
addDocument,
deleteDocument,
updateDocument,
error: collectionError,
isPending: collectionIsPending
} = useCollection('todos')
// 新待辦事項標題
const newTodoTitle = ref('')
// 添加新待辦事項
const addTodo = async () => {
if (newTodoTitle.value.trim()) {
const newTodo = {
title: newTodoTitle.value,
completed: false
// createdAt 會在 addDocument 函數中自動添加
}
await addDocument(newTodo)
newTodoTitle.value = '' // 清空輸入框
}
}
// 切換待辦事項完成狀態
const toggleTodo = async (todo) => {
await updateDocument(todo.id, {
completed: !todo.completed
})
}
// 刪除待辦事項
const deleteTodo = async (id) => {
await deleteDocument(id)
}
</script>
<template>
<div class="todo-list">
<h2>待辦事項清單</h2>
<!-- 載入狀態 -->
<div v-if="isPending" class="loading">載入中...</div>
<!-- 錯誤訊息 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 待辦事項列表 -->
<ul v-if="todos">
<li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }">
<span>{{ todo.title }}</span>
<div class="actions">
<button @click="toggleTodo(todo)">{{ todo.completed ? '還原' : '完成' }}</button>
<button @click="deleteTodo(todo.id)" class="delete">刪除</button>
</div>
</li>
<li v-if="todos.length === 0" class="empty">目前沒有待辦事項</li>
</ul>
<!-- 添加新待辦事項 -->
<form @submit.prevent="addTodo">
<input
v-model="newTodoTitle"
placeholder="輸入新的待辦事項..."
required
>
<button type="submit" :disabled="collectionIsPending">新增</button>
</form>
</div>
</template>
<style scoped>
.todo-list {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background-color: #f5f5f5;
border-radius: 4px;
}
.completed {
text-decoration: line-through;
opacity: 0.6;
}
.actions {
display: flex;
gap: 8px;
}
button {
padding: 6px 12px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button.delete {
background-color: #f44336;
}
form {
margin-top: 20px;
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.loading, .error, .empty {
text-align: center;
padding: 10px;
}
.error {
color: #f44336;
}
</style>
3. Firestore 查詢進階功能
3.1 篩選與排序
Firestore 提供了強大的查詢功能。我們可以使用 where
和 orderBy
來篩選和排序數據:
import { where, orderBy } from 'firebase/firestore'
// 獲取已完成的待辦事項,按完成時間排序
const { documents } = getCollection(
'todos',
[
where('completed', '==', true),
orderBy('completedAt', 'desc')
]
)
// 獲取特定類別的待辦事項
const { documents } = getCollection(
'todos',
[
where('category', '==', 'work'),
orderBy('createdAt', 'desc')
]
)
3.2 限制結果數量
使用 limit
可以限制返回的文檔數量:
import { orderBy, limit } from 'firebase/firestore'
// 僅獲取最近 5 個待辦事項
const { documents } = getCollection(
'todos',
[
orderBy('createdAt', 'desc'),
limit(5)
]
)
4. 優化 Composables
我們可以進一步優化 Composables,添加更多功能:
4.1 獲取單個文檔的 Composable
// src/composables/getDocument.js
import { ref, watchEffect } from 'vue'
import { doc, onSnapshot } from 'firebase/firestore'
import { db } from '@/firebase/config'
const getDocument = (collectionName, id) => {
const document = ref(null)
const error = ref(null)
const isPending = ref(true)
// 獲取文檔引用
const docRef = doc(db, collectionName, id)
// 實時監聽文檔變更
const unsubscribe = onSnapshot(docRef, (doc) => {
if (doc.exists()) {
document.value = { id: doc.id, ...doc.data() }
error.value = null
isPending.value = false
} else {
document.value = null
error.value = "文檔不存在"
isPending.value = false
}
}, (err) => {
document.value = null
error.value = err.message
isPending.value = false
})
// 清理函數
watchEffect((onInvalidate) => {
onInvalidate(() => unsubscribe())
})
return { document, error, isPending }
}
export default getDocument
練習
練習 1:基本 Todo 應用
- 使用提供的 Composables 實現一個基本的待辦事項應用
- 實現添加、刪除和標記待辦事項完成的功能
- 添加按完成狀態篩選待辦事項的功能
練習 2:增強的 Todo 應用
- 添加編輯待辦事項的功能
- 實現待辦事項分類功能(工作、個人、學習等)
- 添加按類別篩選待辦事項的功能
- 添加搜索功能
總結
在這一部分,我們學習了:
- 如何使用 Vue 3 的 Composition API 創建可重用的 Composables
- 如何實現待辦事項的基本 CRUD 操作
- 如何使用 Firestore 的進階查詢功能
下一部分我們將開發一個完整的待辦事項應用,綜合運用所學知識,並添加更多功能和最佳實踐。