Vue3 筆記 | Nuxt

2023/11/21閱讀時間約 24 分鐘

Nuxt 可以說是 Vue 這個框架中的框架,它的出現讓大型開發變得更加簡單。

基本上,Nuxt 的出生是為了解決 SPA 在 SEO 方面的不足,因此 Nuxt 主要是用來建立 SSR (Server-Side Rendering) 網頁。不過當然,它還是可以拿來建立 SPA 網頁,畢竟它整合的功能實在太過方便 ,光是路由的設定不用在像 vue-router 要寫一堆就很值得使用。



Nuxt 專案啟動

建立新 Nuxt 專案:

npx nuxi@latest init nuxt-app

進入到新專案路徑底下,記得載入需求套件:

npm install

接著就可以開啟網頁看看啦:

npm run dev
raw-image



Nuxt 環境簡單介紹

在 Nuxt 專案下會發現環境好像異常的空,比之前用 vite 建立專案少了一些資料夾,比如說 components 之類的。這不代表 Nuxt 沒有,但它選擇並不幫你預設建立,而是讓我們自己按照需求新增。

raw-image

比方說在官網的這裡就有說明我們可以建立哪些資料夾來管理檔案,並讓 Nuxt 幫我們自動管理。舉例來說:

  1. components:存放元件的地方。
  2. pages:存放頁面的地方。
  3. composables:把常用的功能寫成一個函數共用儲存的地方。
  4. assets:用來存放需要打包的靜態檔案,諸如各式各樣 CSS、SCSS 或字型。
  5. public:用來存放那些不需要打包的靜態檔案,比如 favicon。
  6. middleware:路由中間件放置處,比如跳轉頁面時要先運行的身分驗證。
raw-image



nuxt.config.ts

我原本打算把這個放最後記錄,但後來還是決定把它放前面先講,因為它似乎有些重要,一些好用的功能都必須在這個 Nuxt 設定檔先做設定,所以還是先介紹一下。

我們在一開始建立完 Nuxt 專案後,打開 nuxt.config.ts 會看到 Nuxt 預先做好了一個設定:

export default defineNuxtConfig({
devtools: { enabled: true }
})

這個是 "是否啟用開發者工具" 的意思,刪掉也不會怎樣。

在這個 Nuxt 設定檔中,通常會因為你需要什麼功能而來這裡進行一些配置,像本文後段的設定網站標頭就會提到要做哪些設定。

這裡僅介紹一個我認為滿好用的設定:runtimeConfig

先給大家看一眼官網的寫法:

// ​nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 只在伺服器可見
apiSecret: '123',
// 伺服器或瀏覽器皆可見
public: {
apiBase: '/api'
}
}
})
// .env
// 可以覆蓋掉​apiSecret的內容
NUXT_API_SECRET=api_secret_token

這樣的設定好處是放在 private 的資訊都不會出現在客戶端的程式碼裡,大大提高了網頁安全性。

想像一下,一個應用程式需要使用一個 API,而該 API 需要 API 金鑰進行身份驗證。在這種情況下,你可以將 API 金鑰放在 private 中,達到伺服器端和客戶端之間共享配置信息,同時保護了一些敏感數據不被公開暴露在客戶端。



pages

單獨抓它出來講,是因為 pages 建立路由這功能太過於方便了,方便到說到 Nuxt 除了 SSR 就是建立路由很方便這件事。

現在我們在專案下建立一個 pages 資料夾,並在裡面建立一個 index.vue 檔案,稍微打些內容:

<template>
<h1>I am main page</h1>
</template>

然後回到 app.vue 把程式碼改成下列:

<template>
<div>
<NuxtPage />
</div>
</template>

我們就會看到畫面成功渲染出 "I am main page" 。

當我們在 pages 裡面建立頁面檔案時,Nuxt 就會幫我們建立好相對應的路由,用的原理其實是 vue-router 的原理,只是我們可以省去自己去寫 path 的那些時間。

而在 pages 中,通常命名為 index 的都會被預設為主頁,然後當我們在 app.vue 這個最終出口設定 <NuxtPage /> 指定渲染位置後,畫面就會跑出來。

現在來試試建立一個 about 頁面。同樣在 pages 下新增一個 about.vue 檔案,也給一些簡單的內容:

<template>
<h1>I aam about page</h1>
<NuxtLink to="/">Home</NuxtLink>
</template>

然後把 index.vue 稍微調整一下,加個去 about 頁面的連結:

<template>
<h1>I am main page</h1>
<!-- 添加我 -->
<NuxtLink to="/about">About</NuxtLink>
</template>

現在可以看到可以很順利在兩個頁面中做跳轉,Nuxt 很輕鬆地幫我們完成路由的設定。

動態路由

肯定會有人問:那 Nuxt 的動態路由怎麼搞?沒錯,那人就是我!

在 Nuxt 玩動態路由只要把檔名用 [ ] 包起來就好,我們可以嘗試在 pages 李建立一隻叫 [id].vue 的檔案,然後輸入下列 code:

<template>
<h1>page: {{ $route.params.id }}</h1>
</template>

然後我們手動更改我們的網址進入 http://localhost:3000/55、http://localhost:3000/62 之類的,後面的參數自己換,可以看到路由確實可以切換,並且也成功渲染出動態參數到頁面上。

巢狀路由

在 Nuxt 裡建立巢狀路由,方便的程度超乎你我想像。我們先在 pages 下建立一個 user.vue 和 user 資料夾,並在 user 資料夾中新增一個 [user].vue (對,這裡讓我結合一下動態路由玩玩),像下圖這樣:

raw-image

接著我們為 user.vue 加點東西:

<template>
<h1>default user page</h1>
<NuxtPage/>
</template>

也為 [user].vue 加點內容:

<template>
<h1>{{ $route.params.user }}</h1>
</template>

現在我們前往 http://localhost:3000/user 跟 http://localhost:3000/user/jeremy 就會看到畫面的改變:

可以發現路由的嵌套在 Nuxt 中變成只要手手點一下,創個資料夾、建立個檔案就完成的事。瞧瞧,我們只要在專案路徑下建立這樣的結構:

-| pages/
---| parent/
------| child.vue
---| parent.vue

就等同在 vue-router 中寫這樣的內容:

[
{
path: '/parent',
component: '~/pages/parent.vue',
name: 'parent',
children: [
{
path: 'child',
component: '~/pages/parent/child.vue',
name: 'parent-child'
}
]
}
]

而實際上,我們只是把手寫這些定義路由內容的部分交給 Nuxt 處理而已。但是!但是!請務必記得在父層加一個<NuxtPage/>,不然就會像我一樣找半天找不到為何子路由渲染不出來的問題 @@

多層級路由

在 Nuxt 中做多層級路由就是用資料夾一個套住一個,比方說我想建立一個 /plant/flower/lily 這樣的路由,並且在每一層的時候渲染的東西都不一樣,也就是說 /plant 有屬於他自己的頁面、/plant/flower 也有自己的頁面 (跟巢狀不一樣,不要搞混)。

實作上會建立出像下圖這樣的資料夾結構:

-| pages/
---| plant/
------| index.vue
------| flower/
-----------| index.vue
-----------| lily/
----------------| index.vue
raw-image

每一層級的資料夾都有一個 index.vue 去代表這個路由下該渲染的畫面,然後再包其他資料夾這樣一層疊一層。

路由匹配

因為前面的太方便了,所以路由驗證就要與眾不同一點 (x

好啦,反正各位都知道在 vue-router 中如果想要依動態路由匹配去渲染不同畫面是這樣做 (讓我抄一下我前面文章的 code):

{
path: '/about/:afterAbout',
component: AboutView
},
{
path: '/about/:afterAbout(\\d+)',
component: NotFound
}

但很明顯,在 Nuxt 中我們不可能在檔案名稱上寫正規表達式。所以這件事情就落到要去 <script setup> 中來使用 Nuxt 提供的方法。

現在來做一個路由結合多層路由的例子,我們先建出下列這個結構:

-| pages/
---| books/
------| index.vue
------| [id]/
-----------| index.vue

注意喔,有一個資料夾是命名為 [id],表示要套用動態路由。接著我們在 /books/index.vue 加一些內容:

<template>
<h1>books page</h1>
</template>

然後在 /books/[id]/index.vue (這個不是路由) 下加一些內容:

<script setup>
const route = useRoute()
</script>

<template>
<h1>{{ route.params.id }}</h1>
</template>

現在先嘗試換換看動態參數的部分看看網頁是不是如期運作。

raw-image

現在來實作驗證的部分,我們現在要動態 :id 為數字時才出現像剛剛的內容,如果不是數字就幫我導引回 /books 這個路由。

重新回到/books/[id]/index.vue,我們修改如下:

<script setup>
const route = useRoute()

// 驗證如下​
definePageMeta({
validate: async (route) => {
// 檢查id是否由數字組成
if(/^\d+$/.test(route.params.id)){
return true
}else{
return {path: `/books`}
}
}
})
</script>

<template>
<h1>{{ route.params.id }}</h1>
</template>

definePageMetavalidate 是 Nuxt 提供來做路由匹配的。它所返回的 boolean 值會決定是否渲染這個頁面,所以一定要記得 return true!不然就會像我一樣發生錯誤匹配進行很順利,但 id 為數字時一值出現 404 error。



layouts

layouts 單獨拿出來講也必有其獨特之處。通常來說,會用到 layout 的情況就是複數頁面有共用的組件或排版樣式,比如說導覽列。

layouts 資料夾中,命名為 default.vue 的會是預設為全局 layout,我們只要在 app.vue 中給予 <NuxtLayout> 設定,就完成了一半的 layout 引用:

<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

然後我們必須在 layout (以 default.vue 為例) 中,在想要套入其他頁面或元件的地方加一個 <slot/>

<template>
<div>
<slot />
</div>
</template>

而如果今天有某特定頁面想引用一個特定的 layout,比如說登入、註冊頁面想套用同一個 layout (比如 sign-in-up.vue) 而不是 default.vue,我們可以在 <script setup> 中進行頁面設定:

<script setup lang="ts">
definePageMeta({
layout: 'sign-in-up'
})
</script>



Nuxt 中設定網站標頭

在 Nuxt 專案裡面,我們要設定網站標頭,也就是原先寫在 HTML head 的那些東西必須透過兩道程序做設定。

我們必須先到 nuxt.config.ts 中設定下列內容:

export default defineNuxtConfig({
// 不要管我,我是開發者工具
devtools: { enabled: true },
// 添加我!!!
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
}
}
})

然後到我們一切路由與元件的出入口 app.vue,添加下面這這一段:

<script setup lang="ts">
useHead({
title: '這裡是title',
meta: [
{ name: 'description', content: '這是測試用網站' }
],
bodyAttrs: {
class: 'test'
},
script: [ { innerHTML: 'console.log(`Hello world`)' } ]
})
</script>

這樣可以動態生成 HTML 元素,以配置網頁的標題、描述、body 屬性和一段內嵌的 JavaScript 代碼。這樣的設置通常用於簡化網頁開發過程,讓開發者能夠更輕鬆地管理和設定網頁的相關資訊。



轉場效果

Vue 和 vue-router 都有的轉場效果沒道理 Nuxt 沒有,只是要使用就必須先在 nuxt.config.ts 中先做一些設定:

export default defineNuxtConfig({
app: {
pageTransition: { name: 'page', mode: 'out-in' }
}
})

然後到 app.vue 添加樣式:

<style>
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
</style>

這時就可以觀察到在頁面切換時會有轉場效果了。

如果後臺噴出這個錯誤:"... does not have a single root node and will cause errors when navigating between routes.",請把各頁面的 <template> 中再加入一層 <div>,像這樣:

<template>
<div>
<h1>I am main page</h1>
<NuxtLink to="/about">About</NuxtLink>
</div>
</template>

我們也可以僅針對一個頁面做轉場,比如進到 about 頁面才會有轉場效果,這時必須先在 /page/about.vue 中做設定:

<script setup lang="ts">
definePageMeta({
pageTransition: {
name: 'rotate'
}
})
</script>

然後回 app.vue 添加樣式,記得使用剛剛定義的 name:

<style>
// 添加下列
.rotate-enter-active,
.rotate-leave-active {
transition: all 0.4s;
}
.rotate-enter-from,
.rotate-leave-to {
opacity: 0;
transform: rotate3d(1, 1, 1, 15deg);
}
</style>

這樣就可以讓 about 頁面有自己的轉場效果了。

不過其實 Nuxt 官網提供了許多轉場的作法,可以視使用情況去看一下該做哪些設定。



數據獲取

Nuxt 提供了三個方便獲得數據的方法:useFetchuseAsyncData$fetch,都是使用 Nuxt 打 API 的好夥伴 (默默放下 axios...)。

但在這裡我只想記錄 useFetch,為什麼呢?

仔細看官網解說,會發現官網說這其實就是useAsyncData$fetch的結合 ​:

Indeed, useFetch(url) is nearly equivalent to useAsyncData(url, () => $fetch(url)) - it's developer experience sugar for the most common use case.

官網自己都說了,useFectch 在絕大多數情況下就是這兩者的語法糖,所以就大膽用吧!(遇到例外狀況再去看說明書 www)

使用上也非常簡單:

<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>



狀態管理

useState

Nuxt 提供了 useState 來管理狀態,這個名詞如果是從 React 來的應該都不陌生 XD

先來看官網例子:

<script setup lang="ts">
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

<template>
<div>
Counter: {{ counter }}
<button @click="counter++">
+
</button>
<button @click="counter--">
-
</button>
</div>
</template>

可以看到 useState 定義的 counter 就是用來儲存狀態的地方,後面的 arrow function 只是用來定義他的初始值。

Pinia

useState 輕輕帶過就是為了這個更重要、更好用的 Pinia!對,Pinia 又出現啦!

在 Nuxt 中使用 Pinia 請先按下列順序做設定:

  1. 安裝 Pinia
npm install pinia @pinia/nuxt
  1. 去 nuxt.config.js 進行設定:
export default defineNuxtConfig({
modules: [
[
'@pinia/nuxt',
{
autoImports: [
// 自動引入 `defineStore()`
'defineStore',
// 自動引入 `defineStore()` 并重新命名為 `definePiniaStore()`
['defineStore', 'definePiniaStore'],
],
},
],
],
})
  1. 建立一個 stores 資料夾,並在裡面建立一個 index.ts:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}

return { count, doubleCount, increment }
})
  1. 來引入 store (index.ts) 做使用,比如在 index.vue 中使用:
<script setup>
import {useCounterStore} from '~/stores/index'

const store = useCounterStore()
</script>

<template>
<h1>{{ store.count }}</h1>
<button @click="store.increment">Add</button>
</template>



參考資料

  1. Nuxt3 官方文件 (中文版 / 英文版)
18會員
37內容數
這個專題用來存放我在學習網頁開發時的心得及知識。
留言0
查看全部
發表第一個留言支持創作者!