---
tags: Vue.js
---
# Nuxt3-project2 世界前10排名餐廳
原始碼:https://github.com/WangShuan/nuxt3-02-top10-restaurants
## 建立與啟動 Nuxt 專案
開啟終端機,`cd` 到桌面或任何希望創建該專案的位置
執行命令:
```shell
npx nuxi init 02-top10-restaurants
```
完成後,根據提示
先 `cd` 到專案目錄 `02-top10-restaurants` 中
執行命令:
```shell
npm install
```
安裝所有依賴項目
此時會發現專案目錄中**生成了 `node_modules` 資料夾**
確認您的專案已成功安裝好所有依賴後
即可執行命令:
```shell
npm run dev
```
啟動 Nuxt 應用程序。
## 主要架構
### 引入 Bootstrap
在本專案中將使用 Bootstrap 來處理樣式與架構,
所以需要透過 CDN 引入 CSS 與 JS 至 head 標籤中。
由於在 Nuxt 開發過程中並沒有 index.html 檔案,
如果要為編譯後的 head 標籤設置 meta 等資訊,
則需要將該資訊設置在 nuxt.config.ts 檔案中:
```typescript
export default defineNuxtConfig({
app: {
head: {
link: [ // 新增 link 標籤
{
rel: "stylesheet", // 設置 rel
href: "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css", // 設置 href
crossorigin: "anonymous", // 設置 crossorigin
},
],
script: [ // 新增 script
{
src: "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js", // 設置 src
crossorigin: "anonymous", // 設置 crossorigin
},
],
},
},
});
```
### 路由設置 Pages
接著我們需要創建路由,才可以連結到特定的頁面
在 Nuxt 中可以通過資料夾規則來處理路由
該規則有以下幾個重點:
1. 必須在項目根目錄中新增資料夾,且資料夾名稱規定要是 `pages`(與 `components` 資料夾一樣不能使用其他名稱否則 Nuxt 無法辨識)
2. `pages` 底下的每個 `.vue` 檔案名稱即為路徑的名稱,所以假設檔案為 `about.vue` 則在網址中輸入 `/about` 即可開啟 `about.vue` 的頁面
3. 網址中的每個 `/` 就表示一個資料夾,所以假設網址路徑為 `/restaurants/:restaurantId`,`restaurants` 就會是 `pages` 底下的 `restaurants` 資料夾
4. 動態路由的建立方式為將參數 `id` 用 `[]` 包裹,即新增檔案 `[id].vue` 於 `/pages/restaurants` 底下
現在有兩種做法可以開始建立路由檔案,
第一種是沿用 app.vue 檔案,將檔案中的內容更改為:
```htmlembedded
<!-- app.vue -->
<template>
<Navbar />
<div>
<NuxtPage />
</div>
</template>
```
並開始新增 pages 底下的檔案
第二種是刪除 app.vue 檔案,並在 pages 底下新增檔案(比如 index.vue 與 restaurants.vue 等等)
>兩者的差別在於 app.vue 是 Nuxt 的入口頁,且可以設置全局內容
>比如本專案中的 `<Navbar />` 元件要顯示於每個頁面的上方,就適合採用 app.vue 檔案將其設置為全局內容
>而如果刪除 app.vue 檔案,則不存在全局內容,新增於 pages 底下的每個 .vue 檔案就都是獨立頁面
這邊不管採用的是哪一種做法,都需要在 pages 底下新增檔案
1. /pages/index.vue 檔案為首頁
2. /pages/restaurants.vue 檔案為餐廳列表頁
3. /pages/restaurants/[id].vue 檔案為餐廳細節頁
#### 首頁 /pages/index.vue
首頁簡單顯示個標題與餐廳列表頁連結即可,
其內容如下:
```htmlembedded
<!-- /pages/index.vue -->
<template>
<Navbar />
<div class="bg-white vh-100 text-center text-dark d-flex flex-column justify-content-center position-relative p-3">
<h1 class="fw-bolder">Welcome To Top 30 Restaurants</h1>
<NuxtLink class="link-success fw-bold fs-4" to="/restaurants">👉 Go To See The Top 30 Restaurant List</NuxtLink>
</div>
</template>
```
#### 餐廳列表頁 /pages/restaurants.vue
餐廳列表頁主要會按照排名依序顯示餐廳名稱、點擊後可導連至餐廳細節頁,
其內容如下:
```htmlembedded
<!-- /pages/restaurants.vue -->
<template>
<Navbar />
<div class="my-5 pt-5 text-center text-dark d-flex flex-column justify-content-center container">
<h1 class="lh-lg fw-bolder">Top 30 Restaurant List</h1>
<div class="row">
<div class="col-md-6 col-xl-4" v-for="item in restaurants" :key="item">
<NuxtLink class="text-decoration-none" :to="`/restaurants/${item.id}`">
<div class="card my-3 overflow-hidden">
<div class="ratio ratio-16x9 card-img">
<img :src="item.imageUrl" class="img-fluid">
</div>
<div class="d-flex flex-column justify-content-end">
<div class="d-flex align-items-center justify-content-center bg-white px-2 py-1">
<span class="fw-bolder rounded-pill badge" style="font-size: 14px;">
Top {{ item.rank }}
</span>
<h2 class="fs-5 fw-bolder px-2 py-1 m-0">
{{ item.name }}
</h2>
</div>
</div>
</div>
</NuxtLink>
</div>
</div>
</div>
</template>
```
#### 餐廳細節頁 /pages/restaurants/[id].vue
餐廳細節頁主要顯示所有當前餐廳的相關資訊,
其內容如下:
```htmlembedded
<!-- /pages/restaurants/[id].vue -->
<template>
<Navbar />
<div class="container my-5 pt-5">
<div class="row g-0 justify-content-center">
<div class="col-xl-8">
<img class="img-fluid w-100" :src="restaurant.imageUrl" alt="">
</div>
<div class="col-xl-8 p-3 p-md-5 bg-light d-flex flex-column justify-content-between">
<div class="d-flex flex-column align-items-start">
<small class="badge rounded-pill text-bg-danger">TOP {{ restaurant.rank }}</small>
<h1 class="text-success fw-bolder my-2 fs-3">{{ restaurant.name }}</h1>
</div>
<p class="lh-lg my-3 me-0 me-xl-5">{{ restaurant.content }}</p>
<span>
年營收:{{ restaurant.revenue }} 億美元
/
店舖數量:{{ restaurant.numberOfStores }} 家
</span>
</div>
</div>
</div>
</template>
```
#### 餐廳的數據 data.json
接著需要建立餐廳列表的數據資料,於項目根目錄中新增檔案 data.json:
```json
[
{
"id": 1,
"rank": 1,
"name": "麥當勞 McDonalds",
"content": "麥當勞是源自美國南加州的跨國連鎖速食店,也是世界最大的速食連鎖店,主要販售漢堡及薯條、炸雞、碳酸飲料、冰品、沙拉、水果、咖啡等速食食品,目前總部位於美國芝加哥,根據麥當勞2022年公布的年報,截至2021年底全球共有40,031間店,分佈在119個國家及地區。",
"revenue": 42.4,
"numberOfStores": "39,222",
"imageUrl": "https://images.unsplash.com/photo-1602400236316-f5e3b6d2314c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=5070&q=80"
},
{
"id": 2,
"rank": 2,
"name": "星巴克 Starbucks",
"content": "星巴克股份有限公司,簡稱星巴克,又譯史塔巴克斯,是美國一家跨國連鎖咖啡店,也是全球最大的連鎖咖啡店,成立於1971年,發源地與總部位於美國華盛頓州西雅圖。除咖啡之外,亦有茶飲等飲料,以及三明治、糕點等點心類食品。",
"revenue": 26.5,
"numberOfStores": "33,295",
"imageUrl": "https://images.unsplash.com/photo-1592321675774-3de57f3ee0dc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTV8fHN0YXJidWNrc3xlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60"
},
...
]
```
#### 數據 data.json 使用方式
在餐廳列表頁面中引入數據資料:
```htmlembedded
<!-- /pages/restaurants.vue -->
<script setup lang="ts">
import restaurants from "@/data.json"; // 引入數據資料
</script>
```
在餐廳細節頁面中接收傳入的 props 並為其設置好類型:
```htmlembedded
<!-- /pages/restaurants/[id].vue -->
<script setup lang="ts">
interface Restaurant {
restaurant: {
id: number;
rank: number;
name: string;
content: string;
revenue: number;
numberOfStores: string;
imageUrl: string;
}
}
const props = defineProps<Restaurant>()
</script>
```
## 元件化
### Navbar 元件化
在本專案中,頁面的部分主要會有首頁、餐廳列表頁、餐廳細節頁三種頁面
且每個頁面的頂部都有 Navbar,這邊可以直接從 [Bootstrap](https://bootstrap5.hexschool.com/docs/5.1/components/navbar/) 中拷貝來使用
首先在根目錄中新增一個叫做 components 的資料夾,接著創建一個 Navbar.vue 檔案放置在 components 資料夾中
最後再將 Bootstart 中拷貝的導覽列放置到 Navbar.vue 檔案中即可
### 餐廳列表頁卡片元件化
在餐廳列表頁面中可以將 v-for 中的內容封裝成元件使用,
修改如下:
```htmlembedded
<!-- /pages/restaurants.vue -->
<template>
<Navbar />
<div class="my-5 pt-5 text-center text-dark d-flex flex-column justify-content-center container">
<h1 class="lh-lg fw-bolder">Top 30 Restaurant List</h1>
<div class="row">
<div class="col-md-6 col-xl-4" v-for="item in restaurants" :key="item">
<RestaurantCard :restaurant="item" />
</div>
</div>
</div>
</template>
```
於 components 資料夾中新增的檔案 RestaurantCard.vue
並在檔案中接收傳入的 props 為其設置類型:
```htmlembedded
<!-- /components/RestaurantCard.vue -->
<template>
<NuxtLink class="text-decoration-none" :to="`/restaurants/${restaurant.id}`">
<div class="card my-3 overflow-hidden">
<div class="ratio ratio-16x9 card-img">
<img :src="restaurant.imageUrl" class="img-fluid">
</div>
<div class="d-flex flex-column justify-content-end">
<div class="d-flex align-items-center justify-content-center bg-white px-2 py-1">
<span class="fw-bolder rounded-pill badge" style="font-size: 14px;">
Top {{ restaurant.rank }}
</span>
<h2 class="fs-5 fw-bolder px-2 py-1 m-0">
{{ restaurant.name }}
</h2>
</div>
</div>
</div>
</NuxtLink>
</template>
<script setup lang="ts">
interface Restaurant {
restaurant: {
id: number;
rank: number;
name: string;
content: string;
revenue: number;
numberOfStores: string;
imageUrl: string;
}
}
const props = defineProps<Restaurant>()
</script>
```
## 結構化 layouts
在每個頁面中最常見的重複區域不外乎 header 與 footer
本專案中的三種頁面都具有 `<Navbar />` 元件在頁面最上方
此時適合使用 layout 來進行處理
1. 在項目根目錄中新增名稱為 layouts 的資料夾
2. 在 layouts 的資料夾中建立預設結構的檔案,名稱為 default.vue
3. dafault.vue 中內容如下:
```htmlembedded
<template>
<!-- 放置共用的 Navbar 元件 -->
<Navbar />
<!-- 使用插槽表示 pages 內容要顯示的位置 -->
<slot></slot>
<!-- 放置共用的 footer -->
<footer class="bg-dark text-light m-0 p-2 text-center">
<small>此網站僅供學習不含商業用途。</small>
</footer>
</template>
```
接著在 app.vue 檔案中將所有內容使用 <NuxtLayout> 標籤包住即可:
```htmlembedded
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
```
除了預設的 default.vue 檔案外
也可在其他有重複佈局的地方新增其他客製化佈局
舉例來說假設每個頁面的 container 都是使用 div 搭配固定的 class
此時可以新增檔案 custom.vue(名稱隨意) 將 container 做成客製化佈局
接著在需要使用的頁面中一樣通過 <NuxtLayout> 標籤包住所有內容
並在 <NuxtLayout> 標籤中新增屬性 name 設置 layout 名稱(值為 .vue 檔案的名稱)
舉例如下:
```htmlembedded
<template>
<div>
<NuxtLayout name="custom">
...
</NuxtLayout>
</div>
</template>
```
>假設新增的客製化佈局名稱為 custom.vue 則 name 屬性值就為 custom
>依此類推,假設客製化佈局名稱為 foobar.vue 則 name 屬性值就為 foobar
## 錯誤處理 error.vue
在創建 Nuxt3 項目後,預設提供了一個錯誤頁面
對於常規的錯誤處理可使用預設的頁面滿足大部分需求
假設需要自定義錯誤頁面則可於項目根目錄中新增檔案 error.vue 自行設置頁面
另外在需要手動拋出錯誤的地方可以使用 `createError` 自定義錯誤內容
比如餐廳細節頁面,假設輸入不存在的餐廳網址則拋出自定義錯誤以顯示 error.vue 畫面:
```htmlembedded=
<script setup>
import restaurants from "@/data.json";
interface Restaurant {
id: number;
rank: number;
name: string;
content: string;
numberOfStores: string;
imageUrl: string;
}
const currentId = Number(useRoute().params.id);
const restaurant = restaurants.find(item => item.id === currentId) as Restaurant;
if (!restaurant) { // 當 restaurant 不存在
throw createError({ // 拋出自定義錯誤,Nuxt 將自動切換為錯誤頁面
statusCode: 404, // 設置狀態碼
message: "This ID not found, please try again." // 設置訊息
})
}
</script>
```
假設使用了 `throw createError` 則必須在 `error.vue` 中通過 `clearError` 清除錯誤並跳轉至首頁或其他頁面:
```htmlembedded=
<template>
<NuxtLayout>
<div
class="bg-opacity-25 bg-success vh-100 text-center text-dark d-flex flex-column justify-content-center align-items-center p-3">
<h1 class="fw-bolder">{{ err.statusCode }}</h1>
<p>{{ err.message }}</p>
<button @click="handleError()" class="btn btn-outline-success fw-bold" to="/restaurants">Go Home</button>
</div>
</NuxtLayout>
</template>
<script setup>
const err = useError();
const handleError = () => clearError({ redirect: '/' })
</script>
```
## SEO 等 meta 設置
關於網頁中的 SEO 相關設定方式有兩種:
1. 使用 Nuxt3 提供的元件(Html, Title, Link 等等)
2. 使用 Nuxt3 提供的 useHead 方法