# 🏅 Day 19 - Nuxt3 狀態管理 - Pinia - ( 1 ) ## 今日學習目標 - 學習在 Nuxt3 中安裝並整合 **Pinia** 狀態管理工具 - 學習在 Nuxt3 中建立與操作基礎的 **Pinia** 結構 ## 安裝與環境設定 ### 步驟一 . 安裝 Pinia 使用 npm 或是 yarn 安裝 `pinia` 以及 `@pinia/nuxt` 模組。`pinia` 是狀態管理的核心工具,而 `@pinia/nuxt` 則是將 Pinia 與 Nuxt3 整合的模組,確保其可以在 Nuxt 的伺服器端渲染(SSR)及元件中運作。 ```bash # 使用 npm npm install pinia @pinia/nuxt # 或是使用 yarn yarn add pinia @pinia/nuxt ``` 安裝過程中,有可能遇到 `ERESOLVE could not resolve` 的解析錯誤 ( 如下圖 ) ![day19-1](https://hackmd.io/_uploads/HyBfvapfke.png) 解決方法是修改 `package.json` ,加入`overrides` 設定以指定使用最新版本的 `vue`,接著重新執行 `npm install`。 ```jsx "overrides": { "vue": "latest" } ``` ### 步驟二. 設定 nuxt.config.ts 安裝完成後,打開 `nuxt.config.ts`,將 `@pinia/nuxt` 模組添加至 `modules` 屬性。這樣 Nuxt 啟動時會自動初始化 Pinia,並允許在開發中使用其功能。 ```jsx export default defineNuxtConfig({ // ... 其他設定 // 加入 Pinia 模組 modules: ["@pinia/nuxt"], }); ``` ## @pinia/nuxt 的功能 將 `@pinia/nuxt` 加入 `nuxt.config.ts` 後,我們便可以在 Nuxt 中使用 Pinia。不過在使用之前,了解 `@pinia/nuxt` 提供的功能有助於更高效地運用它。 ### 自動初始化 Pinia 在 Vue CLI 與 Vite 開發時,需要在進入點檔案 ( `main.js` 或 `index.js` ) 中引入 `createPinia()` 來初始化 Pinia,並使用 `app.use()` 掛載 : ```jsx import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; // 初始化 Pinia 狀態管理工具 const pinia = createPinia(); // 建立 Vue 實體 const app = createApp(App); // 將 Pinia 掛載到 Vue 實體中 app.use(pinia); app.mount('#app'); ``` 而 Nuxt3 會透過 `@pinia/nuxt` 自動完成初始化,無需透過插件系統的 `nuxtApp.vueApp.use()` 手動處理邏輯。 ### 自動匯入 stores 資料夾 在 Nuxt 專案的根目錄下建立 `stores` 資料夾並添加 store 檔案,Nuxt 會自動加載並匯入這些 store。 ```bash stores ├── cart.js // 自動匯入 └── admin └── order.js // 不會自動匯入 ``` 需要注意的是,`@pinia/nuxt` 預設只會自動匯入 `stores` 資料夾第一層的檔案,也就是上方結構中的 `cart.js`。如果要自動匯入更深層的檔案 (如 `admin/order.js`),需在 `nuxt.config.ts` 中進行設定 : ```bash // nuxt.config.ts export default defineNuxtConfig({ // ...其他設定 modules: ['@pinia/nuxt'], pinia: { storesDirs: ['./stores/**'], // 自動匯入 /stores 目錄下的所有 store }, }) ``` ### 自動匯入 Pinia 方法 除了自動匯入 store 檔案以外,`@pinia/nuxt` 也會自動引入以下 Pinia 方法,因此在使用這些功能時無需手動導入: - `defineStore()`  : 用於建立 store,定義 store 的資料狀態 ( state ) 、計算屬性(getters)以及操作行為(actions)。 - `storeToRefs()` : 從 store 中提取具有狀態的資料並轉換為單個響應式 `ref`,保持狀態的響應性。 ## 在 Nuxt 建立 store ### 基本結構 Pinia 提供了 `defineStore` 函式來定義 store 。使用時,第一個參數需要傳入一個唯一的 store ID,Pinia 會使用此 ID 與 devtools 連接。第二個參數是用來定義 store 的內容,可以選擇使用 Option 物件或 Setup 函式。 定義 store 後,將其賦予至變數並匯出。建議遵循 Composable 命名的[約定](https://vuejs.org/guide/reusability/composables#naming),使用 `use` 開頭且以 `Store` 結尾的駝峰式命名方式來表明它是狀態管理的 store。 ```jsx // 在 Nuxt3 中已經有將 defineStore 自動匯入,所以不需 import { defineStore } from 'pinia'; export const useExampleStore = defineStore('example',{ /* store 的主體 */ }); ``` ### Option 物件定義 store ( Option Store ) Option Store 可以使用物件的方式定義,將 `state`、`actions` 和 `getters` 作為屬性傳入: - state:存放 store 中的資料狀態,與 Options API 的 `data` 屬性相似。 - actions:包含操作和修改 `state` 的方法。與 Options API 的 `methods`屬性相似。 - getters:計算來自 `state` 的資料,與 Options API 的 `computed` 屬性相似。 ```jsx // /stores/example.js export const useExampleStore = defineStore("example", { state: () => ({ //資料狀態 message: "message", }), actions: { // store 的方法 // 更新 message 的方法 writeMessage(messageText) { this.message = messageText; }, }, getters: { // store 的計算屬性 // 根據 message 資料計算長度 countMessageLength: (state) => // 從 store 的 state 取出 message 屬性的資料 ` this.message : ${state.message.length} 共有幾個字`, }, }); ``` ### Setup 函式定義 store ( Setup Store ) Setup Store 的結構類似於 Composition API 中的 [`setup()`](https://vuejs.org/api/composition-api-setup) 函式,允許我們直接使用 `ref()`、`computed()` 和 `function()` 等 Composition API 來定義 store。 例如下方範例,在 `/stores/todos.js` 中使用 Setup Store 管理待辦事項 (todo) 的新增和刪除功能,並回傳所需的狀態與方法,供外部元件使用。 ```jsx // /stores/todos.js export const useTodoStore = defineStore("todo", () => { // 定義 state const todoList = ref([]); // 定義 actions // 新增待辦事項 const addTodo = (todo) => { todoList.value.push({ id: Date.now(), text: todo, }); }; // 刪除待辦事項 const removeTodo = (id) => { const todoIndex = todoList.value.findIndex((todo) => todo.id === id); if (todoIndex !== -1) { todoList.value.splice(todoIndex, 1); } }; // 定義 getters const todoQuantity = computed( () => `總共有 ${todoList.value.length} 個待辦事項` ); // 回傳 store 的 state, actions 和 getters ,供外部取用 return { todoList, addTodo, removeTodo, todoQuantity, }; }); ``` ## 在 Nuxt 使用 store 由於 Nuxt3 提供了自動匯入機制,`stores` 資料夾中的 store 可以自動注入到元件中,無需手動匯入。因此在 `/pages/todos.vue` 中,我們可以直接操作 `useTodoStore` 提供的狀態與方法,例如操作 `todoQuantity`(getter)以及 `addTodo` 和 `removeTodo` 方法,並將 `todoQuantity` 渲染到模板中。 以下示範如何在 `<script setup>` 中,以 Composition API 的方式來操作 store: ```html <!-- /pages/todos.vue --> <script setup> // 取得 stores/todo.js 定義的 useTodoStore const todoStore = useTodoStore(); const newTodo = ref(""); // 新增 Todo const addNewTodo = () => { if (newTodo.value) { todoStore.addTodo(newTodo.value); newTodo.value = ""; } }; // 移除 Todo const deleteTodo= (id) => { todoStore.removeTodo(id); }; </script> <template> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-lg-8"> <h1 class="text-center mb-4">Todo List</h1> <div class="input-group mb-3"> <input v-model.trim="newTodo" class="form-control" placeholder="新增待辦事項" /> <button @click="addNewTodo" class="btn btn-primary" :disabled="!newTodo.length" > 新增 Todo </button> </div> <ul class="list-group"> <li v-for="todo in todoStore.todoList" :key="todo.id" class="list-group-item d-flex justify-content-between align-items-center" > <span> {{ todo.text }} </span> <button @click="deleteTodo(todo.id)" class="btn btn-danger btn-sm"> 刪除 </button> </li> </ul> <p class="mt-3">{{ todoStore.todoQuantity }}</p> </div> </div> </div> </template> ``` ### 解構 store 的響應性資料 為了方便使用 store 內的響應性屬性,可以使用 `storeToRefs()` 將具有 Ref 響應性的`ref` 與 `computed` 解構出來並維持響應性。而 `reactive` 資料和 actions 的方法不需透過解構取出,可以直接從 `todoStore` 變數解構。 以下是使用解構的方式。`addTodo` 和 `removeTodo` 方法不具備 Ref 的響應性,因此可以直接從 `todoStore` 變數中解構出來。而 `todoList` 和 `todoQuantity` 是響應式的 Ref,需通過 `storeToRefs()` 包裹 `todoStore`,以確保解構後的資料保持響應性。 ```html <!-- /pages/todos.vue --> <script setup> // 取得 stores/todo.js 定義的 useTodoStore const todoStore = useTodoStore(); // actions 的方法可以直接解構 const { addTodo, removeTodo } = todoStore; // state 資料和 getters 具有響應性,需經過 storeToRefs 解構 const { todoList, todoQuantity } = storeToRefs(todoStore); const newTodo = ref(""); // 新增 Todo const addNewTodo = () => { if (newTodo.value) { addTodo(newTodo.value); newTodo.value = ""; } }; // 移除 Todo const deleteTodo = (id) => { removeTodo(id); }; </script> <template> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-lg-8"> <h1 class="text-center mb-4">Todo List</h1> <div class="input-group mb-3"> <input v-model.trim="newTodo" class="form-control" placeholder="新增待辦事項" /> <button @click="addNewTodo" class="btn btn-primary" :disabled="!newTodo.length" > 新增 Todo </button> </div> <ul class="list-group"> <li v-for="todo in todoList" :key="todo.id" class="list-group-item d-flex justify-content-between align-items-center" > <span> {{ todo.text }} </span> <button @click="deleteTodo(todo.id)" class="btn btn-danger btn-sm"> 刪除 </button> </li> </ul> <p class="mt-3">{{ todoQuantity }}</p> </div> </div> </div> </template> ``` <br> > 今日學習的[範例 Code - 資料夾: day19-pinia-example](https://github.com/hexschool/nuxt-daily-tasks-2024) ## 題目 請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day19-pinia) ,完成以下任務 : - 在 `nuxt.config.ts` 添加 `@pinia/nuxt` 模組,讓它在 Nuxt 中可以運作。 - 在 `stores/booking.js` 建立一個名為 `useBookingStore` 的 Pina store ,用來管理訂單資訊。 ```jsx // /stores/booking.js // 建立名稱為 useBookingStore 的 store // export const xxx = ... ; ``` - 使用 `/pages/index.vue` 的 `createOrder()` 方法建立訂單,將被選取的房型 `roomInfo` 和訂房人資料 `userInfo` 整合為 `bookingResult`,並將 `bookingResult` 移至 `useBookingStore` 中進行狀態管理 ( 格式如下 )。完成後,使用 `useRouter` 的方法導引至 `/order` 頁面。 ```jsx /pages/index.vue // 訂單資訊的格式 const bookingResult = ref({}); // 建立訂單 const createOrder = (roomInfo, userInfo) => { // 1. 將選取的房型以及訂房人資訊整合成訂單資訊 ( bookingResult ) /* 格式 { ...roomInfo, // 將被選取的房型以解構的方式合併 user: { ...userInfo, // 將訂房人資料以解構的方式合併 }, }; */ // 2. 將 bookingResult 改成用 pinia 管理狀態 // 3. 使用 router 將頁面導引至 /order }; ``` - 進入 `/order` 頁面後,從 `useBookingStore` 中取出訂單資料 `bookingResult` 並顯示於頁面中。如果沒有訂單資料,顯示 "目前沒有預訂資訊"。 ```jsx // /pages/order <script setup> // 1. 從 useBookingStore 取出資料 bookingResult const bookingResult = ref({}); </script> <template> <div class="container mt-5"> <template v-if="bookingResult.name"> <!-- 2. 渲染至 HTML ( 在模板有提供 HTML 結構 )--> </template> <template v-else> <h1>目前沒有預訂資訊</h1> </template> <NuxtLink class="btn btn-primary" to="/">回上一頁</NuxtLink> </div> </template> ``` ## 回報流程 將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 ! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day19-pinia --> 回報區 --- | # | Discord | Github / 答案| | --- | --- | --- | | 1 |眼睛|[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day19-pinia)| | 2 | Steven |[Github](https://github.com/y7516552/nuxt3-live-question/tree/day19)| | 3 | kevinhes | [Github](https://github.com/kevinhes/nuxt-daily-mission/tree/day19)| | 4 | MOON | [Github](https://github.com/mytrylin/Nuxt3-Day19-Pinia)| | 5 | dragon | [Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day19)| | 6 | MY | [Github](https://github.com/ahmomoz/nuxt3-live-question/tree/day19-pinia-hw)| | 7 | Rocky | [Github](https://github.com/WuRocky/Nuxt-Day19-Pinia-1.git)| | 8 | LinaChen | [Github](https://github.com/Lina-SHU/nuxt3-live-question) | | 9 | Jim Lin | [Github](https://github.com/junhoulin/Nuxt3-hw-day-after10/tree/day19)| | 10 | Johnson| [Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day19-pinia)| | 11 | tanuki狸 |[Github](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day19-pinia)| |12|Ariel|[Github](https://github.com/Ariel0508/nuxt3-hw/tree/day19-pinia)| |13|hsin yu|[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day19-pinia)| <!-- |---|---|[Github]()| -->