0%

Nuxt3 潮在哪?

文章目的

公司未來專案開發要逐漸轉向使用 Nuxt3,因此利用此文章記錄 Nuxt3 的學習。

前言

撰寫這篇文章時,Nuxt3 還是 Beta 階段,很多新奇潮到爆的技術,但也因此可能會跟 bug 不期而遇,或是遇到部分工具、套件不相容問題,待未來版本越來越成熟,相依套件、工具一定會越來越多,奇怪的 bug 當然相對減少。
此篇文章會記錄一些我在使用 Nuxt3 時遇到的問題,或是如何引入一些我們以前常用的工具。

介紹

Nuxt3 是基於 Vue3 的 SSR 框架,同時 default 支援 TypeScript,在打包處理上也做了相當多的優化,像是引入 Vite 等,相較 Nuxt2 對於專案的瘦身與效能都有相當程度的提升。
更多 Nuxt3 的特色我就不多加介紹,如果有興趣可以去看看官方文件

資料夾結構

主要介紹幾個與 Nuxt2 規則不同的資料夾。

pages

如果你有用過 Nuxt2,對這資料夾應該不陌生,但在 Nuxt3 中 pages 是選用的,原因是如果專案只用到 app.vue 這隻檔案做開發,就不需要 pages,減少專案大小。
但較複雜的專案我們還是會需要 pages 來幫助我們產生不同路徑的頁面,基本上 pages 的結構跟 Nuxt2 沒什麼差異,唯一需要注意的是動態路由的命名從以前的下底線變成中括號,例如 _id.vue => [id].vue
另外 Nuxt3 還提供了一種與 RouterView 相同作用的 component - NuxtChild,在 component 中直接調用 <NuxtChild /> 會有一樣的效果。

components

Nuxt3 會自動幫我們引入 components 底下的元件,因此我們調用元件只需要根據元件的資料夾結構調用即可,例如:

1
2
3
4
| components/
--| base/
----| foo/
------| Button.vue

引入時 tag 名稱就會是 <BaseFooButton />,但這邊會建議將你的元件命名跟調用的名稱一致,意思是將 Button.vue 變更成 BaseFooButton.vue 會是更好的選擇,不用擔心重複的文字因為 Nuxt3 會自己幫我們刪除。

components 現在可以透過加入前綴 Lazy,達到元件延遲載入的效果,使用情境通常會是該元件並不需要馬上出現,而是在特定時機出現,就可以透過 Lazy 的方式提升效能,寫法像是 <LazyComponentName />

composables

這是 Nuxt3 新增的資料夾,這個資料夾專門用來管理 Vue3 的 composition api,而且它會自動 import 裡面的檔案,不需要我們手動處理。
composition api 可參考

layouts

Nuxt3 初始是沒有 layouts 資料夾的,當建立 layouts 資料夾並新增 default.vue 時,所有的頁面都會預設使用 default layout,記得要在 layout 裡使用 slot 做注入才能正確渲染畫面。

1
2
3
4
5
<template>
<div>
<slot />
</div>
</template>

plugins

Nuxt3 plugins 資料夾底下的檔案一樣會自動幫我們 import,另外在命名時增加 .server.client 前綴,就可以讓 plugin 在對應端引入,像是 plugin.client.js
在 plugins 的檔案中起手式需要透過 nuxtApp 對其做對應操作,可以看看下面的程式碼:

1
2
3
4
5
import { defineNuxtPlugin } from '#app';

export default defineNuxtPlugin(nuxtApp => {
// plugin 針對 nuxtApp 的操作都寫在這裡
});

我們也可以為 nuxtApp 寫上 Vue3 提供的 provide 寫法做全局注入:

1
2
3
4
5
6
7
8
9
import { defineNuxtPlugin } from '#app';

export default defineNuxtPlugin(() => {
return {
provide: {
hello: () => 'world'
}
}
});

接著我們就可以在任何地方做全局的調用:

1
2
3
<script setup>
const { $hello } = useNuxtApp();
</script>

另外,plugins 也可以像以前 Nuxt2 一樣引入我們裝的相關套件,寫法只有些微變化:

1
2
3
4
5
6
7
8
9
10
import { defineNuxtPlugin } from '#app'
import VueGtag from 'vue-gtag-next'

export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueGtag, {
property: {
id: 'GA_MEASUREMENT_ID'
}
});
});

新用法

Nuxt3 重新定義了一些新方法,或是將舊有的 Nuxt2 方法重新包裝,我們來看看改變了什麼?
目前我還沒嘗試過所有新方法,以下僅列出我有嘗試過的方法做分享,若有興趣可以參考官方文件,未來有接觸更多方法,我會再補上。

State

Nuxt3 新增了一個 useState 的方法,如果你有寫過 React hook 對這個名字應該不陌生。
在 Nuxt3 中 useState 可以說是一個輕量版的 vuex,它可以幫我們管理 global state,並隨時做取用、更改。

useState 的優勢在於在 server 端時會將 state 的值作保留並沿用到 client 端,寫法也很簡易、取用方便。
缺點方面,在我目前看來,是有點太過簡易了,改變值的方法沒有統一的規範,容易造成不同頁面用不同方法去改變 state,管理上可能會有瑕疵。

useState 創立時基本上只需要提供一個唯一的 key,並且賦予它 init 的值即可。

接下來看看範例:

1
2
3
4
5
// useState 僅能作用在 setup 或是其他生命週期中
<script setup>
const counter = useState('counter', () => 0);
// conter 是 key,init 值是 0
</script>

上面的範例中,我們宣告了一個 counter 的 useState,接著這個 state 就可以在任一個 component 透過 useState('counter') 一起共享這個狀態。
若要更改 state 的值也很簡單,可以直接針對它做處理,甚至直接賦值,像是:

1
2
3
4
5
6
7
<template>
<p>{{ counter }}</p>
<!-- 點擊之後,counter + 1 -->
<button @click="counter++">counter increment<button>
<!-- 點擊之後,counter 變成 '我是字串' -->
<button @click="counter='我是字串'">counter to string<button>
</template>

相信看了改變 state 的方法,你應該就懂我的疑慮了,state 的改變過於簡單、容易,在比較複雜的專案上可能會導致管理不易。
但在較小的專案上利用 useState 處理一些 global 的狀態,會是一個替代 vuex 很好的選擇。

除了直接在 setup 定義外,也可以將 useState 的定義全部放在 composables 這個資料夾做管理,來看看範例吧:

1
2
3
4
5
6
7
8
9
10
// 將所有的 state 放在 state.js 這隻檔案中
// 路徑 /composables/state.js
export const useCounter = () => useState('counter', () => 0);
export const useColor = () => useState('color', () => 'pink');

// 在任何一個 component 中取用
<script setup>
const counter = useCounter();
const color = useColor();
</script>

NuxtApp

如果你有用過 Nuxt2 相信你對 context 這個參數不陌生,在 Nuxt 2 中我們透過 context 取得我們的 nuxtApp。
在 Nuxt3 中重新定義了這個參數,我們透過 useNuxtApp 來取得所謂的 context,我們趕緊來看看範例:

1
2
3
import { useNuxtApp } from '#app';

const nuxtApp = useNuxtApp();

nuxtApp 可以在 composables、components 還有 plugins 取用,跟 Nuxt2 一樣,在 plugins 中會當作第一個參數做代入。

若你需要註冊一些全局方法,可以利用 provide 做註冊,若與 Nuxt2 做對應就是 inject

1
2
3
4
const nuxtApp = useNuxtApp();
nuxtApp.provide('hello', (name) => `Hello ${name}!`);

console.log(nuxtApp.$hello('name')); // Prints "Hello name!"

Data Fetching

Nuxt3 新增了幾個 fetch 功能,這些功能可以讓我們在不需要安裝其他 api 套件的情況下實踐 fetch 資料。
提供的 fetch 功能如下:

  • useAsyncData
  • useLazyAsyncData
  • useFetch
  • useLazyFetch

使用它們的時機一樣是只有在 setup 或是其他生命週期。

useAsyncData

useAsyncData 是這四個功能裡最基本的一個,另外三個都可以說是它的衍生,我們來看看它的用法吧:

1
2
3
4
5
6
const { data, pending, refresh, error } = useAsyncData(key, fn, options);

// example
<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'));
</script>

傳入參數:

  • key: 確保此 fetch 唯一,避免與其他 request 重疊。
  • fn: return 非同步函數的值。
  • options:
    • lazy: Boolean,true 的話調用此方法,在進入 router 時並不會因為還在 fetch 而 pending,而是直接進入 router。
    • default: 在 fetch 完成之前,設定預設 data,通常會搭配 lazy: true 做使用。
    • server: Boolean,是否在 server 端 fetch 資料,預設為 true。
    • transform: 函數,用來更改 fn 回傳的結果。
    • pick: Array,fetch 結果只取用寫在 array 裡的 key。

回傳結果:

  • data: fetch response 的資料。
  • pending: Boolean,告訴是否還在 fetch。
  • refresh: 用來強制刷新 data 的函數。
  • error: 當 response 失敗時,產生的 object。

useLazyAsyncData

這方法是 useAsyncData + lazy: true 的組合,其他的參數與回傳的值都會與 useAsyncData 相同。

useFetch

它是 useAsyncData$fetch 的結合,並且它也不需要 key,key 會自動根據 url 與選項幫你產生。
因此傳入此方法的參數只有兩個,一個是 url,另一個是 options。

1
2
3
<script setup>
const { data } = await useFetch('/api/count');
</script>

你一定會問說 $fetch 是啥?這是套件 ohmyfetch 的方法,useFetch 簡單來講就是把這套件包裝進去。

如果專案 fetch 資料的需求沒有很複雜的話,可以直接用包好的 useFetch 來處理 CRUD。
但如果有一定複雜度,目前不建議這樣用,原因是 ohmyfetch 這套件還很新,很多功能沒有很成熟。

useAsyncData 相比,它的 options 多了 methodparamsheadersbaseURL 可以提供。
這四個選項都是 ohmyfetch 的選項,有興趣可以看看文件。

useLazyFetch

這方法是 useFetch + lazy: true 的組合,其他的參數與回傳的值都會與 useFetch 相同。

套件使用

這邊會介紹幾個套件如何在 Nuxt3 中使用,並解釋它們帶來的功用。

axios

如果你是一個有經驗的前端開發者,我相信你絕對對這套件不陌生。
axios 應該是現在 fetch 資料,最熱門的套件之一,我們就來看看如何在 Nuxt3 中使用 axios 吧。

透過 yarn add axiosnpm install axios 為專案安裝 axios,接著我們就可以在任何需要 fetch 資料的 component 引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
import axios from 'axios';
export default {
async setup() {
const { data } = await useAsyncData('fecthData', () => {
return axios.get('https://jsonplaceholder.typicode.com/posts/1').then(({ data }) => {
return data;
})
});

return { data };
}
}
<script>

這邊可以看到用了前面提到的 useAsyncData 來調用 axios,至於為什麼要這麼做呢?
因為 Nuxt3 的 useAsyncData 其實跟 Nuxt2 的 asyncData 很像,在這裡調用可以幫助我們在 server 端也可以正確 fetch 資料。
這邊要注意的是 useAsyncData 的 fn 參數必須回傳一個值出來

用上面的方法調用 axios,在較大專案時會很難管理我們 fetch 的方法。

既然都用 Nuxt3 了,我們就來嘗試看看用 composition api 管理 axios 吧!
composables 底下創建一隻 api.js (命名可以調整):

1
2
3
4
5
6
7
8
9
10
import axios from 'axios';

export const useApi = () => {
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
timeout: 1000,
});

return { api }
}

我們透過 composition api 幫我們管理創建出來的 axios 實例,接著只需要在 component 中呼叫這個實例即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
async setup() {
const { api } = useApi();
const { data } = await useAsyncData('fecthData', () => {
return api.get('posts/1').then(({ data }) => {
return data;
})
});

return { data };
}
}
<script>

再進階一點我們可以針對 axios 的方法做封裝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import axios from 'axios';

export const useApi = () => {
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
timeout: 1000,
});

const fetch = (method='get', url='', data=null, config) => {
switch(method) {
case 'get':
return api.get(url, { params: data });
case 'post':
return api.post(url, data, config);
case 'put':
return api.put(url, data);
case 'delete':
return api.delete(url, { params: data });
default:
return console.log('method error')
}
}

return { fetch }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
async setup() {
const { fetch } = useApi();
const { data } = await useAsyncData('fecthData', () => {
return fetch('get', 'posts/1').then(({ data }) => {
return data;
})
});

return { data };
}
}
<script>

每次呼叫我們還需要用 then 去回傳我們的 data,既然這樣我們就把它一起放入封裝方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import axios from 'axios';

export const useApi = () => {
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
timeout: 1000,
});

const fetch = (method='get', url='', data=null, config) => {
switch(method) {
case 'get':
return api.get(url, { params: data })
.then(({ data }) => {
return data
})
.catch((error) => {
console.log('error:', error)
return false
});
case 'post':
return api.post(url, data, config)
.then(({ data }) => {
return data
})
.catch((error) => {
console.log('error:', error)
return false
});
case 'put':
return api.put(url, data)
.then(({ data }) => {
return data
})
.catch((error) => {
console.log('error:', error)
return false
});
case 'delete':
return api.delete(url, { params: data })
.then(({ data }) => {
return data
})
.catch((error) => {
console.log('error:', error)
return false
});
default:
return console.log('method error')
}
}

return { fetch }
}

這樣子呼叫時寫法就更簡潔了:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
export default {
async setup() {
const { fetch } = useApi();
const { data } = await useAsyncData('fecthData', () => {
return fetch('get', 'posts/1');
});

return { data };
}
}
<script>

最後你也可以將每個 api 分類管理,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 路徑 composables > useUserApi.js
export const useUserApi = () => {
const { fetch } = useApi();

// 把所有跟 user 有關的 api 統一寫在這裡管理
const getUserInfo = (data) => {
return fetch('get', 'posts/1', data);
}

const postUserInfo = (data) => {
return fetch('post', 'posts/1', data);
}

return {
getUserInfo,
postUserInfo
};
}

// user component
export default {
async setup() {
const { getUserInfo } = useUserApi();

const { data } = await useAsyncData('fecthData', () => getUserInfo());

return { userInfo: data };
}
}

axios 的管理方法非常多元,以上分享我整理的管理方法並與 Nuxt3 做結合以供參考。

pinia

如果你寫過 Vue 的專案,相信你對 vuex 不陌生,vuex 是全局 state 的管理工具。
這邊要介紹的是 pinia,它是更為輕量的全局管理工具,跟 vuex 相比寫法也更加簡潔。
目前 Nuxt3 官方也推薦使用 pinia 作為 state 處裡的套件,pinia 在 SSR 這方面有足夠的支援,讓我們免除一些安全性問題。
讓我們來看看 pinia 的使用吧。

在終端機輸入 yarn add pinia @pinia/nuxt 或是 npm install pinia @pinia/nuxt 將套件安裝至專案中。
接著在 nuxt.config.ts 引入:

1
2
3
4
5
import { defineNuxtConfig } from 'nuxt3'

export default defineNuxtConfig({
"buildModules": ["@pinia/nuxt"]
})

我們可以在專案創建一個 stores 資料夾,專門放我們創建的 store
現在命名一個 useCounter 的 js,接著來創建 store 吧:

1
2
3
4
5
6
7
import { defineStore } from 'pinia'

export const useCounter = defineStore('counter', {
state: () => ({
count: 0,
}),
});

透過 defineStore 創立一個新的 store,並且在第一個參數賦予它一個命名,state 則是透過 function return 的方式建立。
接著在需要使用到的 component 中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import { useCounter } from '@/stores/useCounter';

export default {
setup() {
const counterStore = useCounter();

return {
counterStore
}
}
}
</script>

當你在 component 中使用 store 時,store 都是帶有 composition api 的 reactive 屬性。
若我們使用解構的方式獲得 store 裡的 state 會讓響應性失效,因此可以利用 pinia 的 storeToRefs 來幫助我們維持 state 的響應性:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { storeToRefs } from 'pinia';
import { useCounter } from '@/stores/useCounter';

export default {
setup() {
const counterStore = useCounter();
const { count } = storeToRefs(counterStore);

return {
count
}
}
}

同時我們可以透過定義 actions 幫助我們操作 store 的 state,讓其符合商業邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia'

export const useCounter = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.counter++
},
}
});

在 component 中可以很輕鬆的取得 actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
import { storeToRefs } from 'pinia';
import { useCounter } from '@/stores/useCounter';

export default {
setup() {
const counterStore = useCounter();
const { count } = storeToRefs(counterStore);
const { increment } = useCounter(); // const incrementCounter = counterStore.increment

return {
count,
increment,
}
}
}
</script>

以上是 Nuxt3 引入 pinia 以及簡單的使用方法,其他方法可以參考官方文件

參考資料

Nuxt3 官方文件
用 Axios Instance 管理 API