---
tags: Vue.js
---
# Nuxt3-project1 寶寶英文名字產生器
原始碼:https://github.com/WangShuan/nuxt3-01-name-generator
## 啟動 Nuxt 應用程序
執行命令:
```shell
npm install
```
安裝所有依賴項目,此時會發現專案目錄中生成了 `node_modules` 資料夾
確認您的專案已成功安裝好所有依賴後,即可執行命令:
```shell
npm run dev
```
啟動 Nuxt 應用程序
此時根據提示可通過 `http://localhost:3000/` 開啟畫面預覽
## 主要程式碼 app.vue
在專案目錄中可以看到 `app.vue` 檔案,
該檔案為整個專案的**入口畫面**,
在 `app.vue` 檔案中默認會有初始畫面用的元件 `<NuxtWelcome />`
將該元件移除後即可開始撰寫自己的程式碼
程式碼如同 `Vue.js` 一樣,區分為三大部分:
1. **template** - 撰寫 `HTML` 的部分
2. **script** - 撰寫 `JavaScript` 的部分
3. **style** - 撰寫 `CSS` 的部分
---
首先於 `HTML` 的部分,
設置一個 `h1` 設置標題與一個 `p` 段落說明使用方式:
```htmlembedded
<template>
<h1>
寶寶英文名字產生器
</h1>
<p>
選取您需要的條件,並點擊下方按鈕獲取結果。
</p>
</template>
```
接著設置一個 `div` 裡面放置 `p` 段落說明選擇的內容與一組 `button` 按鈕顯示所有選項
重複拷貝三次,分別用於性別、流行程度以及名字長度:
```htmlembedded
<template>
...
<div class="option-container">
<p>1. 選擇寶寶性別</p>
<div class="option-buttons">
<button>男性</button>
<button>通用</button>
<button>女性</button>
</div>
</div>
<div class="option-container">
<p>2. 選擇流行程度</p>
<div class="option-buttons">
<button>唯一的</button>
<button>常見的</button>
</div>
</div>
<div class="option-container">
<p>3. 選擇長度</p>
<div class="option-buttons">
<button>全部</button>
<button>長的</button>
<button>短的</button>
</div>
</div>
</template>
```
最後新增一個 `button` 按鈕用來執行搜尋與一個列表用來顯示結果:
```htmlembedded
<template>
...
<button class="submit-btn">搜索姓名</button>
<ul>
<li>name</li>
</ul>
</template>
```
---
接著開始撰寫 JS 的部分,
在姓名產生器這個專案中,將使用 `TypeScript` 來撰寫 JS
第一步先建立 `script` 標籤,並設置為 `TypeScript` 語言:
```htmlembedded
<script setup lang="ts">
</script>
```
> 於 script 標籤中**新增屬性 setup 表示要使用 `Composition API` 寫法**
> 由於 Nuxt3 本身就支援 TS 所以可以直接透過 lang 設置語言類型(不設置 lang 默認就是 JS)
接著使用 TS 提供的 `enum` 羅列出性別、流行程度、長度的所有選項:
```typescript
// 性別 Gender: 男性 Boy、女性 Girl、通用 Unisex
enum Gender {
GIRL = "Girl",
BOY = "Boy",
UNISEX = "Unisex",
}
// 流行程度 Popularity: 唯一的 Unique、常見的 Trendy
enum Popularity {
UNIQUE = "Unique",
TRENDY = "Trendy",
}
// 長度 Length: 全部 All、長的 Long、短的 Short
enum Length {
ALL = "All",
LONG = "Long",
SHORT = "Short",
}
```
> 這邊使用 enum 羅列出選項後
> 性別、流行程度、長度的選項值就**被限制在 enum 中列舉出的內容**
> 比如 Gender 中有 GIRL、BOY、UNISEX 三個選項
> Gender **從此就只能是三個選項之中的其中一個,不能是這三個選項以外的內容**
再來使用 TS 提供的 `interface` 初始化物件的類型:
```typescript
// 先定義好選項有哪些、分別是什麼值
interface OptionsState {
// 使用 interface 聲明 OptionsState 物件的類型
gender: Gender; // gender 選項值只能為 eunm Gender 中的選項
popularity: Popularity; // popularity 選項值只能為 eunm Popularity 中的選項
length: Length; // length 選項值只能為 eunm Length 中的選項
}
```
最後即可設置我們的初始值:
```typescript
const options = reactive<OptionsState>({
// 使用 <> 設置物件類型為 OptionsState
gender: Gender.BOY, // 設定性別的預設值為 enum 中的 'Boy'
popularity: Popularity.UNIQUE, // 設定流行程度的預設值為 enum 中的 'Unique'
length: Length.ALL, // 設定長度的預設值為 enum 中的 'All'
});
```
## 將重複內容進行元件化 OptionContainer.vue
在 Nuxt3 中可以通過於項目根目錄中新增資料夾 `components` 來存放元件
且**資料夾名稱必須叫做 components**
主要原因是 Nuxt3 可以利用資料夾名稱實現**自動引入**
而不需要在檔案中多寫 `import` 程式碼
元件的名稱與 `Vue.js` 一樣,必須是**大駝峰表示法**
使用元件時,也與 `Vue.js` 一樣,可以用 `<FooBar />` 或 `<foo-bar />`
另外當有元件重複使用同樣開頭名稱時(EX: `CardTitle.vue` 與 `CardButton.vue`)
可以直接於 `components` 資料夾中再新增一個資料夾 `Card`
並在 `Card` 資料夾中新增檔案 `Title.vue` 與 `Button.vue` 以減少撰寫的程式碼量
(也可以在 `Card` 資料夾中新增檔案 `CardTitle.vue` 與 `CardButton.vue`,Nuxt 會自動幫您刪減掉重複的名稱內容)
**使用元件時則一樣要用 `<CardTitle />` 與 `<CardButton />` 標籤,不得省略 “Card”**
> Card 資料夾名稱建議同元件一樣使用大駝峰表示法命名,但用全小寫也欸通~!
---
在該專案中可以將選擇性別、流行程度、長度的區塊建立成元件:
1. 於項目根目錄中新增資料夾 `components`
2. 於 `components` 資料夾中新增檔案 `OptionContainer.vue`
3. 將 `template` 標籤中的 `.option-container` 區塊剪下放到 `OptionContainer.vue` 中
```htmlembedded
<!-- OptionContainer.vue 中 -->
<template>
<div class="option-container">
<p>1. 選擇寶寶性別</p>
<div class="option-buttons">
<button>男性</button>
<button>通用</button>
<button>女性</button>
</div>
</div>
<div class="option-container">
<p>2. 選擇流行程度</p>
<div class="option-buttons">
<button>唯一的</button>
<button>常見的</button>
</div>
</div>
<div class="option-container">
<p>3. 選擇長度</p>
<div class="option-buttons">
<button>全部</button>
<button>長的</button>
<button>短的</button>
</div>
</div>
</template>
```
接著回到 `app.vue` 中聲明 `OptionContainer.vue` 需要用到的資料:
```typescript
// app.vue 的 script 標籤中
const optionsDatas = [
// 新增 optionsDatas 為一個陣列
{
title: "1. 選擇寶寶性別", // 將 p 段落中的內容設為 title
cate: "gender", // 設定分類為性別
buttons: [
// 新增性別的所有按鈕內容
Gender.BOY, // 依序傳入所有 enum
Gender.UNISEX,
Gender.GIRL,
],
},
{
title: "2. 選擇流行程度", // 將 p 段落中的內容設為 title
cate: "popularity", // 設定分類為流行程度
buttons: [
// 新增流行程度的所有按鈕內容
Popularity.TRENDY, // 依序傳入所有 enum
Popularity.UNIQUE,
],
},
{
title: "3. 選擇長度", // 將 p 段落中的內容設為 title
cate: "length", // 設定分類為長度 length
buttons: [
// 新增長度的所有按鈕內容
Length.LONG, // 依序傳入所有 enum
Length.ALL,
Length.SHORT,
],
},
];
```
然後將 `Gender`、`Popularity`、`Length` 這三個 `enum` 取出,
改放在項目根目錄中新增的檔案 `data.ts` 裡面,
以便於在需要的地方可以通過 `import` 隨時引入使用:
```typescript
// data.ts 中
export enum Gender {
GIRL = "Girl",
BOY = "Boy",
UNISEX = "Unisex",
}
export enum Popularity {
UNIQUE = "Unique",
TRENDY = "Trendy",
}
export enum Length {
ALL = "All",
LONG = "Long",
SHORT = "Short",
}
```
接著把原來放置 `.option-container` 區塊的位置改為放置元件
並藉由 `v-for` 遍歷 `optionsDatas` 將傳遞需要的數據傳進元件中:
```htmlembedded
<!-- app.vue 中 -->
<template>
<div class="container">
<h1>
寶寶英文名字產生器
</h1>
<p>
選取您需要的條件,並點擊下方按鈕獲取結果。
</p>
<div class="options-container">
<!-- 元件化 -->
<OptionContainer
v-for="item in optionsDatas" :key="item.title"
:optionData="item" :options="options"
/>
</div>
</div>
</template>
```
> 這邊傳入 `optionData` 以及 `options` 這兩個 `props`
> `optionData` 用於顯示 `p` 段落以及 `button` 按鈕
> `options` 則用於顯示當前選中的對象
而在 `OptionContainer.vue` 中,則需要接收傳入的 `props`:
```typescript
// OptionContainer.vue 的 script 標籤中
import { Gender, Popularity, Length } from "@/data"; // 引入 data.ts 中的 enum
interface OptionProps {
// 聲明 props 的類型
optionData: {
// 傳入的 props:optionData
title: string; // title 為字串類型
cate: string; // cate 為字串類型
buttons: Gender[] | Popularity[] | Length[]; // buttons 為三種 enum 的陣列
};
options: {
gender: Gender; // gender 為對應的 enum
popularity: Popularity; // popularity 為對應的 enum
length: Length; // length 為對應的 enum
};
}
const props = defineProps<OptionProps>(); // 這邊用 defineProps 接收 props,並設置好類型為 OptionProps
```
接著再將樣式、資料等串接到元件中:
```htmlembedded
<!-- OptionContainer.vue 中 -->
<template>
<div class="option-container">
<p>{{ optionData.title }}</p>
<div class="option-buttons">
<button
v-for="option in optionData.buttons" :key="option"
:class="{ 'active': options[optionData.cate as 'gender' | 'popularity' | 'length'] === option }"
>
{{ option }}
</button>
</div>
</div>
</template>
```
> 這邊需注意型別問題
> 在 `:class` 處,需要設置:
> `optionData.cate as 'gender' | 'popularity' | 'length'`
> 否則會噴型別上的錯誤```
最後即可綁定點擊事件,當按下按鈕時將 `options` 的值進行更新
第一步要先新增函數 `clickOption` 當作點擊事件要執行的內容:
```typescript
// OptionContainer.vue 的 script 標籤中
const clickOption = (option: Gender | Popularity | Length) => {
// 新增函數 clickOption
if (props.optionData.cate === "gender") {
props.options[props.optionData.cate] = option as Gender;
} else if (props.optionData.cate === "popularity") {
props.options[props.optionData.cate] = option as Popularity;
} else if (props.optionData.cate === "length") {
props.options[props.optionData.cate] = option as Length;
}
};
```
> `option: Gender | Popularity | Length` 設置傳入參數 `option` 的類別
> 接著藉由判斷 `optionData.cate` 的值,分別設定好 `option` 是哪個 `enum`
接著在每個按鈕上綁定點擊事件,並將 `option` 傳入 `clickOption` 函數中:
```htmlembedded
<!-- OptionContainer.vue 中 -->
<template>
<div class="option-container">
<p>{{ optionData.title }}</p>
<div class="option-buttons">
<button
v-for="option in optionData.buttons" :key="option"
:class="{ 'active': options[optionData.cate as 'gender' | 'popularity' | 'length'] === option }"
@click="clickOption(option)"
>
{{ option }}
</button>
</div>
</div>
</template>
```
## 處理搜尋結果
在 `app.vue` 中有一個用來執行搜尋的按鈕
首先需要定義好所有資料內容,可於 `data.ts` 中進行撰寫:
```typescript
// data.ts 中
// 這邊是之前聲明的性別、流行程度以及長度的 enum
export enum Gender {
GIRL = 'Girl',
BOY = 'Boy',
UNISEX = 'Unisex'
}
export enum Popularity {
UNIQUE = 'Unique',
TRENDY = 'Trendy'
}
export enum Length {
ALL = 'All',
LONG = 'Long',
SHORT = 'Short'
}
// 這邊往下是要用來查找搜尋結果用的資料內容
export interface Name { // 首先聲明 Name 物件的類型
id: number; // 有 id 為數字
name: string; // 有姓名為字串
gender: Gender; // 有性別為 enum Gender
popularity: Popularity; // 有流行程度為 enum Popularity
length: Length; // 有長度為 enum Length
}
export const names: Name[] = [ // 接著聲明所有 names 資料為 Name 類型的陣列
{ // 開始建立一筆一筆的 Name 資料內容
id: 1,
name: "Laith",
gender: Gender.BOY,
popularity: Popularity.UNIQUE,
length: Length.SHORT,
},
{
id: 2,
name: "Jake",
gender: Gender.BOY,
popularity: Popularity.TRENDY,
length: Length.SHORT,
},
{
id: 3,
name: "Lamelo",
gender: Gender.BOY,
popularity: Popularity.UNIQUE,
length: Length.SHORT,
},
...
];
```
接著回到 `app.vue` 檔案中,撰寫搜尋姓名用的事件與資料:
```typescript
// app.vue 的 script 標籤中
const selectedNames = ref<string[]>([]); // 首先新增一個類型為字串的變數 selectedNames 用於存放搜尋結果
const computedSelectedNames = () => {
// 新增一個函數 computedSelectedNames 用於篩選結果內容
const filterNames = names.filter(
(name) =>
name.gender === options.gender && name.popularity === options.popularity
);
if (options.length !== Length.ALL) {
// 假設長度的選項不為全部 則使用 filter 根據選項進行取值
selectedNames.value = filterNames
.filter((name) => name.length === options.length)
.map((item) => item.name);
} else {
// 假設長度的選項為全部 則直接顯示篩選過姓名與流行程度後的結果
selectedNames.value = filterNames.map((item) => item.name);
}
};
```
> 這邊要注意,輸出的結果 `selectedNames` 類型為字串
> 所以 `filter` 後得到的 `Name` 對象,要再通過 `map` 取出其 `name` 的值,設為最終的 `selectedNames` 結果
接著將函數 `computedSelectedNames` 綁定給搜尋結果的按鈕
最後再把 `li` 改用 `v-for` 的方式遍歷 `selectedNames` 呈現出結果:
```htmlembedded
<!-- app.vue 中 -->
<template>
<h1>
寶寶英文名字產生器
</h1>
<p>
選取您需要的條件,並點擊下方按鈕獲取結果。
</p>
<div class="options-container">
<OptionContainer
v-for="item in optionsDatas" :key="item.title"
:optionData="item" :options="options"
/>
<button @click="computedSelectedNames()" class="submit-btn">搜索姓名</button>
</div>
<ul v-if="selectedNames.length">
<li v-for="name in selectedNames" :key="name">{{ name }}</li>
</ul>
</template>
```
## 搜尋結果元件化 CardName.vue
在搜尋結果中的 `li` 因為是完全相同的結構,就很適合製作成元件
在將 `li` 進行元件化前,先為每個 `li` 添加一個 `span` 標籤,綁定刪除姓名的事件:
```htmlembedded
<!-- app.vue 中 -->
<template>
...
<ul v-if="selectedNames.length">
<li v-for="name in selectedNames" :key="name">
{{ name }}<span @click="removeName(name)">❌</span>
</li>
</ul>
</template>
```
接著撰寫函數 `removeName` 執行的內容:
```typescript
// app.vue 的 script 標籤中
const removeName = (name: string) => {
const i = selectedNames.value.findIndex((item) => item === name); // 獲取當前目標的索引值
selectedNames.value.splice(i, 1); // 通過 splice 方式刪除一個 element
};
```
最後就可以進行元件化了
在資料夾 `components` 中新增檔案 `CardName.vue`
並將 `li` 標籤整個拷貝到檔案 `CardName.vue` 中:
```htmlembedded
<!-- CardName.vue 中 -->
<template>
<li v-for="name in selectedNames" :key="name">
{{ name }}<span @click="removeName(name)">❌</span>
</li>
</template>
```
接著通過 interface 聲明好傳入的 props 類型:
```typescript
interface NameProps {
name: string;
}
const props = defineProps<NameProps>();
```
最後將函數 removeName 通過 emit 的方式傳遞給 CardName.vue:
```htmlembedded
<!-- app.vue 中 -->
<template>
...
<ul>
<CardName
v-for="item in selectedNames" :key="item"
:name="item"
@removeName="() => removeName(item)"
/>
</ul>
</template>
```
```htmlembedded
<!-- CardName.vue 中 -->
<template>
<li>{{ name }}<span @click="emit('removeName')">❌</span></li>
</template>
<script setup lang="ts">
...
const emit = defineEmits<{
(e: 'removeName'): void // e: 函數名稱 'removeName' : 函數類型為 void
}>()
</script>
```
> emit 宣告類型的方式參考:
>
> ```typescript
> const emit = defineEmits<{
> // 用物件包裹內容
> (e: "change", id: number): void; // e: 函數名稱 'change', 參數 id 類型數字 : 函數類型為 void
> (e: "update", value: string): void; // e: 函數名稱 'update', 參數 value 類型字串 : 函數類型為 void
> }>();
> ```