# 🏅 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` 的解析錯誤 ( 如下圖 )

解決方法是修改 `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://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]()|
-->