---
# System prepended metadata

title: Nuxt3-project2 世界前10排名餐廳
tags: [Nuxt3.js]

---

---
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 方法