# 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 ![image](https://hackmd.io/_uploads/SkVB5Dkd1l.png) #### example 詳見:[enable-CSR](#enable-CSR) (只需一行配置解決) ### [universal rendering](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering) #### about ![](https://hackmd.io/_uploads/SJBga4wu1x.png) ![](https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png) #### 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 ``` ### &lt;head&gt; 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 請求資料 - &#36;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.`