Nuxt 可以說是 Vue 這個框架中的框架,它的出現讓大型開發變得更加簡單。
基本上,Nuxt 的出生是為了解決 SPA 在 SEO 方面的不足,因此 Nuxt 主要是用來建立 SSR (Server-Side Rendering) 網頁。不過當然,它還是可以拿來建立 SPA 網頁,畢竟它整合的功能實在太過方便 ,光是路由的設定不用在像 vue-router 要寫一堆就很值得使用。
建立新 Nuxt 專案:
npx nuxi@latest init nuxt-app
進入到新專案路徑底下,記得載入需求套件:
npm install
接著就可以開啟網頁看看啦:
npm run dev
在 Nuxt 專案下會發現環境好像異常的空,比之前用 vite 建立專案少了一些資料夾,比如說 components 之類的。這不代表 Nuxt 沒有,但它選擇並不幫你預設建立,而是讓我們自己按照需求新增。
比方說在官網的這裡就有說明我們可以建立哪些資料夾來管理檔案,並讓 Nuxt 幫我們自動管理。舉例來說:
我原本打算把這個放最後記錄,但後來還是決定把它放前面先講,因為它似乎有些重要,一些好用的功能都必須在這個 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 建立路由這功能太過於方便了,方便到說到 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 (對,這裡讓我結合一下動態路由玩玩),像下圖這樣:
接著我們為 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
每一層級的資料夾都有一個 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>
現在先嘗試換換看動態參數的部分看看網頁是不是如期運作。
現在來實作驗證的部分,我們現在要動態 :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>
definePageMeta
的 validate
是 Nuxt 提供來做路由匹配的。它所返回的 boolean 值會決定是否渲染這個頁面,所以一定要記得 return true
!不然就會像我一樣發生錯誤匹配進行很順利,但 id 為數字時一值出現 404 error。
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 專案裡面,我們要設定網站標頭,也就是原先寫在 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 提供了三個方便獲得數據的方法:useFetch
、useAsyncData
和 $fetch
,都是使用 Nuxt 打 API 的好夥伴 (默默放下 axios...)。
但在這裡我只想記錄 useFetch
,為什麼呢?
仔細看官網解說,會發現官網說這其實就是useAsyncData
和 $fetch
的結合 :
Indeed,useFetch(url)
is nearly equivalent touseAsyncData(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>
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 只是用來定義他的初始值。
useState
輕輕帶過就是為了這個更重要、更好用的 Pinia!對,Pinia 又出現啦!
在 Nuxt 中使用 Pinia 請先按下列順序做設定:
npm install pinia @pinia/nuxt
export default defineNuxtConfig({
modules: [
[
'@pinia/nuxt',
{
autoImports: [
// 自動引入 `defineStore()`
'defineStore',
// 自動引入 `defineStore()` 并重新命名為 `definePiniaStore()`
['defineStore', 'definePiniaStore'],
],
},
],
],
})
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
<script setup>
import {useCounterStore} from '~/stores/index'
const store = useCounterStore()
</script>
<template>
<h1>{{ store.count }}</h1>
<button @click="store.increment">Add</button>
</template>