# Nuxt
## References
+ 📑 [**Doc - Nuxt 3**](https://nuxt.com/)
+ 📘 [**Vue 開發者升級指南 - Nuxt 3 入門:打造 SSR 專案**](https://books.google.com.tw/books/about/Vue%E9%96%8B%E7%99%BC%E8%80%85%E5%8D%87%E7%B4%9A%E6%8C%87%E5%8D%97_Nuxt3_%E5%85%A5%E9%96%80.html?id=I6Y4EQAAQBAJ)
+ 🎬 [**Nuxt 3 - Course for Beginners**](https://youtu.be/fTPCKnZZ2dk)
+ 🎬 [**企鵝先生 Mr.Penguin - Nuxt3 怎麼優化網頁的 SEO?**](https://youtu.be/mTfCZ43rG9Y)
+ 🎬 [**企鵝先生 Mr.Penguin - Nuxt3 怎麼做到 API 生成 HTML 做 SSR?**](https://youtu.be/42DKs_tvuhs)
+ 🔗 [**iThome - Nuxt 3 實戰筆記**](https://ithelp.ithome.com.tw/users/20152617/ironman/6959)
+ 🔗 [**Claire's Note - Nuxt 3**](https://clairechang.tw/categories/Nuxt3/)
## Installation
### create project
```
pnpm create nuxt@latest
```
### eslint
詳見 [ESlint (Nuxt)](https://hackmd.io/@RogelioKG/eslint#Nuxt)
若要直接使用大神 Antfu 的設置,詳見 [ESlint (Antfu)](https://hackmd.io/@RogelioKG/eslint#antfueslint-config)
### tailwind
詳見 [Tailwind-Installation (Nuxt)](https://hackmd.io/@RogelioKG/tailwind#Installation)
## Rendering Modes
### [client-side rendering](https://nuxt.com/docs/guide/concepts/rendering#client-side-rendering)
#### about

#### example
詳見:[enable-CSR](#enable-CSR) (只需一行配置解決)
### [universal rendering](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering)
#### about


#### exapmle
```html
<script setup lang="ts">
const counter = ref(0) // 在 server 和 client 端都會執行
const handleClick = () => {
counter.value++ // 只在 client 端執行
}
</script>
<template>
<div>
<p>Count: {{ counter }}</p>
<button @click="handleClick">Increment</button>
</div>
</template>
```
### [hybrid rendering](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering)
#### about
詳細指定不同路由頁面的渲染策略
#### example
詳見:[hybrid rendering example](#hybrid-rendering-example)
## Feature
### auto import
🏔️ <mark><u>Vue API, Nuxt API, components 的自動 import 與 tree-shaking</u></mark>
|🚨 <span class="caution">CAUTION</span>|
|:---|
|在 `.nuxt` 目錄會自動創建一個<br /> + `imports.d.ts` 去聲明使用到的 Vue API, Nuxt API<br /> + `components.d.ts` 去聲明使用到的 components<br />以便提供良好的 IDE 提示 |
| 要 `nuxi dev` 背景運行,才能自動更新這些檔案 |
如此一來,就不用像 Vue 中
+ 使用 Vue API 時,需 `import { ref } from 'vue'`
+ 使用自己寫的 component 時,需 `import Card from '@/components/Card.vue'`
### `pages/`
🏔️ <mark><u>使用檔案系統的巢狀特性,管理巢狀路由</u></mark>
+ ❌ `router/index.ts`:Vue 管理路由的方式,我們不需要寫這個了
+ ✅ `pages/*.vue`:每個在 `pages` 目錄底下的 Vue 檔案,會自動辨識為一個頁面
+ `pages/index.vue`:index.vue 為根頁面
+ `pages/[id].vue`:動態路由,例如 `/123`、`/abc`。可用 `useRoute().params.id` 獲取值
+ `pages/blog/[...slug].vue`:表示捕獲剩餘路由,`/blog` 將導向 index.vue 頁面,假如 blog 目錄底下只有 index.vue,那麼剩餘的路由 `/blog/` 將導向 [...slug].vue。可用 `useRoute().params.slug` 獲取值
### `components/`
🏔️ <mark><u>以 camel-case 命名,管理元件</u></mark>
|📗 <span class="tip">TIP</span>|
|:---|
|若目錄過深導致元件名稱落落長,可手動 import。<br />如 `import Input from '~/components/Form/Input/Group/index.vue'`。|
```
components/
├── Card/
│ ├── index.vue
│ └── Item.vue
└── Todo/
├── index.vue
└── Item.vue
```
Nuxt 會將如上目錄結構視為
+ `Card` 元件:`components/Card/index.vue`
+ `CardItem` 元件:`components/Card/Item.vue`
+ `Todo` 元件:`components/Todo/index.vue`
+ `TodoItem` 元件:`components/Todo/Item.vue`
### `public/` & `assets/`
🏔️ <mark><u>靜態資源</u></mark>
#### `public/`
- 資源不會經過 Vite 或 Webpack 的後處理
- 資源可透過 URL 直接訪問
- `public/logo.png` → `https://example.com/logo.png`
- `public/favicons/favicon.ico` → `https://example.com/favicons/favicon.ico`
#### `assets/`
- 資源會經過 Vite 或 Webpack 的後處理 (壓縮與 hash)
- 資源無法透過 URL 直接訪問
- 會被 import 進元件的資源,通常歸類於此
```html
<template>
<img :src="logo" alt="Logo" />
</template>
<script setup lang="ts">
import logo from '@/assets/logo.png'
</script>
```
### `plugins/`
🏔️ <mark><u>[註冊插件](https://clairechang.tw/2023/07/10/nuxt3/nuxt-v3-plugins/)、注入全域變數</u></mark>
+ 注入全域變數
```ts
// plugins/hello.ts
export default defineNuxtPlugin(nuxtApp => {
// 方法一
nuxtApp.provide('hello', (msg: string) => `Hello ${msg} !`);
// 方法二 (✅有型別提示,較好)
return {
provide: {
hello: (msg: string) => `Hello ${msg} !`
}
};
})
```
```html
<!-- pages/index.vue -->
<script setup lang="ts">
const { $hello } = useNuxtApp() // 以 $hello 調用
</script>
```
+ 註冊插件
```ts
import Notifications, { useNotification } from '@kyvg/vue3-notification'
const { notify } = useNotification()
export default defineNuxtPlugin(nuxtApp => {
// 全域註冊插件,讓所有的元件都能使用插件
nuxtApp.vueApp.use(Notifications)
// 注入全域變數 $notify
return {
provide: {
notify,
},
}
})
```
```html
<script setup lang="ts">
const { $notify } = useNuxtApp()
onMounted(() => {
$notify({
type: 'success',
title: 'Notification Title',
text: 'Notification Text',
})
})
</script>
```
+ 插件僅在應用啟動時載入
+ 僅限於 client 或 server 載入
檔名加上 `.client` 或是 `.server` 後綴即可。
|🚨 <span class="caution">CAUTION</span>|
|:---|
| 當我們使用的第三方套件定義了 window、document 等瀏覽器全域變數,<br />直接定義 plugin,執行時可能會拋錯誤 window is not defined,<br />因為 server 端預渲染時找不到變數,<br />這時候就可以加上 `.client` 後綴來限制載入時機。|
+ [object syntax](https://youtu.be/2aXZyXB1QGQ)
這種寫法可以細項調整插件載入的更多細節,尤其是插件彼此有依賴關係時。
|🚨 <span class="caution">CAUTION</span>|
|:---|
|插件預設的載入順序是按照 alphabetical,而且是同步載入。|
```ts
export default defineNuxtPlugin({
name: 'depends-on-my-plugin', // 插件名稱
dependsOn: ['my-plugin'], // my-plugin 插件載入完後,才會載入這個插件 (依賴關係)
parallel: true, // 當插件載入到一半進入 await 時,先跳出並載入其他插件
async setup (nuxtApp) {
// 插件載入
}
})
```
### `middleware/`
🏔️ <mark><u>攔截 navigation 並做一些處理的中介層</u></mark>
+ [more info....](https://clairechang.tw/2023/09/05/nuxt3/nuxt-v3-middleware/)
### `layouts/`
🏔️ <mark><u>版型模板 (如果只有一種版型,那直接寫在 App.vue 內就好)</u></mark>
+ [more info....](https://clairechang.tw/2023/09/01/nuxt3/nuxt-v3-layouts/)
## Config
`nuxt.config.ts`
### env var
在模板中可直接使用 `$config` 調用,
在 TS 中使用 `useRuntimeConfig()` 調用。
```ini
# .env
NUXT_API_SECRET=your-secure-api-key
NUXT_API_BASE=/new-api
```
```ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 非 public:只能在 server 讀取
apiSecret: process.env.NUXT_API_SECRET,
// public:可在 client 或 server 讀取
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
},
},
})
```
```html
<!-- app.vue -->
<template>
<div id="app">
<p>{{ config.public.apiBase }}</p>
<p>{{ $config.public.apiBase }}</p>
</div>
</template>
<script setup lang="ts">
const config = useRuntimeConfig()
</script>
<style scoped lang="css">
</style>
```
在執行時,都可以在命令行給定環境變數
```
NUXT_API_SECRET=123 pnpm dev
```
### <head> info
由於 Nuxt 沒有 index.html,因此 head 內容都在 config 裡設定<br />(比如 `title`、`meta`、`script` 等)
font awesome
```ts
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: 'https://kit.fontawesome.com/024f8fd2b4.js',
crossorigin: 'anonymous',
defer: true,
},
],
},
},
})
```
bootstrap
```ts
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
link: [
{
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
integrity: "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH",
crossorigin: "anonymous",
},
],
script: [
{
src: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js",
integrity: "sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r",
crossorigin: "anonymous",
defer: true,
},
{
src: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js",
integrity: "sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy",
crossorigin: "anonymous",
defer: true,
},
],
},
},
})
```
### diff config for diff env
```ts
// nuxt.config.ts
export default defineNuxtConfig({
// 全域設定
...
// 開發環境
$development: {
app: {
...
},
},
// 部署環境
$production: {
app: {
...
},
},
// 測試環境
$test: {
app: {
...
},
},
})
```
### enable CSR
```ts
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false
})
```
### hybrid rendering example
```ts
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Homepage pre-rendered at build time
'/': { prerender: true },
// Products page generated on demand, revalidates in background, cached until API response changes
'/products': { swr: true },
// Product pages generated on demand, revalidates in background, cached for 1 hour (3600 seconds)
'/products/**': { swr: 3600 },
// Blog posts page generated on demand, revalidates in background, cached on CDN for 1 hour (3600 seconds)
'/blog': { isr: 3600 },
// Blog post page generated on demand once until next deployment, cached on CDN
'/blog/**': { isr: true },
// Admin dashboard renders only on client-side
'/admin/**': { ssr: false },
// Add cors headers on API routes
'/api/**': { cors: true },
// Redirects legacy urls
'/old-page': { redirect: '/new-page' }
}
})
```
## CLI
`nuxi` 是 Nuxt 的 CLI 工具
### references
+ 📑 [**Doc - nuxi**](https://nuxt.com/docs/api/commands)
+ 🔗 [**iThome - nuxi**](https://ithelp.ithome.com.tw/articles/10319420)
### `nuxi add`
可以快速新增樣板檔案。\
比如 `pnpm exec nuxi add component TheHeader`\
能快速產生 `components/TheHeader.vue` 檔案。
## Built-in Components
### `NuxtLink`
+ `external` : 表示導向外部連結,提示 Nuxt 不要做 client nav
## Built-in API
### references
+ 📑 [**Doc - API**](https://nuxt.com/docs/api)
+ 🔗 [**Nuxt 3 發送 API 請求資料 - $fetch / useAsyncData / useFetch**](https://ithelp.ithome.com.tw/articles/10326675)
+ 🎬 [**Alexander Lichter - useAsyncData vs. useFetch**](https://youtu.be/0X-aOpSGabA)
### components
#### `resolveComponent`
若要在 Nuxt 中使用 dynamic component,使用 import #components 或 resolveComponent 引入元件
```html
<script setup lang="ts">
import { SomeComponent } from '#components'
const MyButton = resolveComponent('MyButton')
</script>
<template>
<component :is="clickable ? MyButton : 'div'" />
<component :is="SomeComponent" />
</template>
```
### data fetching
#### `$fetch`
```ts
// GET /api/result?page=2&otherParam=1
const data = await $fetch('/api/result', {
query: {
page: page.value,
otherParam: otherParam.value,
},
})
```
|📗 <span class="tip">TIP</span> : param|
|:---|
|`options` : 剩餘選項<br />+ `method` : HTTP Method<br />+ `query` : 指定 URL query (<mark>無法給定 ref</mark>)|
|📘 <span class="note">NOTE</span>|
|:---|
|`$fetch` 使用官方自肥的 fetch API - [ofetch](https://github.com/unjs/ofetch) 實現。|
|`$fetch` 基本和原生 fetch API 無異,但有針對 SSR 優化,若在 SSR 階段時使用 `$fetch`,會直接以函數呼叫代替 HTTP request 以節省時間。|
|🚨 <span class="caution">CAUTION</span>|
|:---|
|若在 component 最外層中直接使用 `$fetch`,會造成兩次的 API call,一次是 SSR 階段,一次是 Hydration 階段,造成效能浪費。建議使用 `useFetch` 或 `useAsyncData + $fetch` 的組合。|
#### [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data)
```ts
// GET /api/result?page=2&otherParam=1
const { data } = await useAsyncData('result', async () => $fetch('/api/result', {
query: {
page: page.value,
otherParam: otherParam.value,
},
}), {
watch: [page, otherParam],
})
```
|📗 <span class="tip">TIP</span> : param|
|:---|
|`key` : 唯一 key|
|`handler` : 返回資料 Promise 的函數 (通常是 fetch API)|
|`options` : 剩餘選項<br />+ `lazy` : 若為 true,等到對應的 DOM 元素存在時,才會抓取資料 (<mark>會影響 SEO</mark>)<br />+ `immediate` : 若為 false,先不抓取資料,等到需要時手動執行 `execute()` 抓取資料 (<mark>未抓資料時的狀態為 `idle`</mark>) (<mark>會影響 SEO</mark>)<br />+ `server` : 若為 false,表示此 request 僅限在 client 端執行,不讓它在 SSR 階段先執行 (<mark>會影響 SEO</mark>)<br />+ `watch` : 追蹤 URL query 用到的響應式參數,一旦響應式參數有變化,再重新 request 一次|
|📗 <span class="tip">TIP</span> : return|
|:---|
| `data` : 抓取成功的資料 (響應式) |
| `error` : 抓取失敗的回應 (響應式) |
| `status` : fetch 狀態 (響應式),有 <mark>`'idle'` \| `'pending'` \| `'success'` \| `'error'`</mark> 四種<br />(這很好用,可以利用它來做資料加載動畫) |
| `execute` : 手動發起 request 抓取資料的功能函數 |
|🔮 <span class="important">IMPORTANT</span> : 快取機制|
|:---|
| `useAsyncData()` 會將抓到的資料儲存至快取中<br />(快取即 [Nuxt 環境] `useNuxtApp()` 中的 `payload.data`,也可用 `window.__NUXT__.data` 查看)<br />(更準確地說,`useAsyncData()` 是在 SSR 階段將資料儲存至 `payload.data`,並渲染至 HTML。<mark>在 Hydration 階段時,就不會再重新 request 一次了</mark>,但資料會再藉由 `payload.data` 重新填入 DOM 元素,這是為了讓資料綁定響應式,假如你有指定 `options` 的 `watch` 選項 -- 即監控響應式的參數,自動 refresh,就是重新 request 辣 -- 時,畫面才會顯示新的資料。) |
| 給定唯一 key 做為快取物件的 property,抓到的資料做為快取物件的 value|
| 若不給定 key,將依據檔名與行號自動產生<br />(建議手動提供 key,以確保快取的穩定性)|
| 若一個頁面中多個地方使用相同的 key,`useAsyncData()` 會直接存取快取<br />(避免了重複 request)|
|📘 <span class="note">NOTE</span>|
|:---|
|若某 3rd party lib 附帶自己的 fetch API,使用 `useAsyncData` 是更好的選擇|
#### `useFetch`
```ts
// GET /api/result?page=2&otherParam=1
const { data } = await useFetch('/api/result', {
query: {
page,
otherParam,
},
})
```
|📘 <span class="note">NOTE</span>|
|:---|
|`useFetch(url)` 大致等價於 `useAsyncData(url, () => $fetch(url))`|
### composables
#### `useHead`
設定 `<head>` (可動態配置)
```html
<script setup lang="ts">
useHead({
title: 'My App',
meta: [
{ name: 'description', content: 'My amazing site.' }
],
bodyAttrs: {
class: 'test'
},
script: [ { innerHTML: 'console.log(\'Hello world\')' } ]
})
</script>
```
#### `useSeoMeta`
設定 SEO 標籤 (可動態配置)
```html
<script setup lang="ts">
useSeoMeta({
title: 'My Amazing Site',
ogTitle: 'My Amazing Site',
description: 'This is my amazing site, let me tell you all about it.',
ogDescription: 'This is my amazing site, let me tell you all about it.',
ogImage: 'https://example.com/image.png',
twitterCard: 'summary_large_image',
})
</script>
```
#### `useRoute`
獲取 `route`
```html
<!-- pages/[id].vue -->
<script setup lang="ts">
const route = useRoute()
</script>
<template>
<div>
<h1>id: {{ route.params.id }}</h1>
</div>
</template>
```
#### `useState`
全域狀態,類似 Pinia。
詳見 [Alexander Lichter - Why you should use useState()](https://youtu.be/mv0WcBABcIk)
```ts
// composables/useUser.ts
export default function useUser() {
const userName = useState('username', () => '')
const setUserName = (name: string) => {
userName.value = name
}
return {
userName: readonly(userName),
setUserName
}
}
```
除此之外它還有一個特點。
在 SSR 情況下,當 server 渲染完畢時,\
資料會留下來並藉由 `<script type="application/json"></script>` 傳到 client。
也就是說,<mark>資料必須是 JSON serializable</mark>。
<mark>在 Hydration 階段,client 會直接拿這筆資料直接渲染,並綁定響應式關係</mark>。
而不會像一般情況重新執行一次。
這可以用在資料產生方式是隨機的局面,
避免了 server rendering 與 client hydration 結果不符 (隨機性),
從而意外產生 [Hydration Error](#Hydration-Error)。
```html
<template>
<div id="index">
<ul>
<li v-for="link in links" :key="link.name">{{ link.name }}: {{ link.url }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
type Link = { name: string, url: string }
// 只要使用相同 key,就可以在其他元件到用到這個全域狀態
const links = useState<Link[]>('links', () => generateRandomLinks(10))
function generateRandomLinks (n: number): Link[] {
const links: Link[] = []
for (let i = 0; i < n; i++) {
links.push(generateRandomLink(i))
}
return links
}
function generateRandomLink (serial: number): Link {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const length = 10
let randomChars = ''
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * chars.length)
randomChars += chars[randomIndex]
}
return {
name: `data ${serial}`,
url: `https://example.com/${randomChars}`,
}
}
</script>
```
## Module
+ `@nuxtjs/i18n` 多國語系
+ [Doc - @nuxtjs/i18n](https://v8.i18n.nuxtjs.org/)
+ [Claire's Note - Nuxt.js 3.x 導入 I18n 實作多國語系](https://clairechang.tw/2023/08/29/nuxt3/nuxt-v3-i18n/)
+ 全域翻譯檔要放在 `i18n/locales/` 目錄
+ `@nuxt/content` Markdown Blog
+ [Doc - @nuxt/content](https://content.nuxt.com/)
+ [用 Nuxt Content 重寫我的部落格](https://blog.twjoin.com/%E7%94%A8-nuxt-content-%E9%87%8D%E5%AF%AB%E6%88%91%E7%9A%84%E9%83%A8%E8%90%BD%E6%A0%BC-278ce9e8580c)
+ `@nuxt/ui` UI
+ [Doc - @nuxt/ui](https://ui.nuxt.com/)
[Nuxt 環境]: https://nuxt.dev.org.tw/docs/guide/going-further/nuxt-app#the-nuxt-context
## Console Error
### Hydration Error
+ `Hydration completed but contains mismatches.`