# Nuxt3 重點整理 我們知道 `React` `Angular` `Vue` 是為了原生 JS 所誕生的框架, 它幫助我們可以更快更方便的建立專案, 而 `Nuxt` 則是基於 `Vue` 所產生的框架, 讓我們可以比使用 `Vue` 更快更方便的建立專案。 ## Nuxt3 主要特點 1. **file system router**: 在 Nuxt 中使用命名約定來創建動態和嵌套路由,根據特定的檔案、文件夾命名方式即可實現絕大部分會用到的路由而不需要撰寫額外的程式碼! 2. **Auto Imports**: 當我們要使用 `components`、`composables`、`plugins` 等資料夾中的內容時,Nuxt 會自動幫我們從中載入,而不需要另外撰寫 `import` 程式碼! 3. **Composables**: 結合 `Vue3` 的 `Composition API` 管理全局狀態,將函數或變數封裝好,放在 `composables` 資料夾中,以便於各個頁面共同使用~ 4. **Server**: 在 Nuxt 中我們可以創建一個 `http server` 而不需要使用 `Django` 或 `Express` 等框架 5. **Middleware**: 類似路由守衛的用途 6. **Universal Rendering**: 結合了一部分的 `SSR` 以及一部分的 `CSR` 進行渲染 ## 建立項目與啟動 首先要確認 `node` 版本大於 `20` 接下來執行 `npm create nuxt@latest <project-name> -- -t v3` 生成項目資料夾 然後 `cd <project-name>` 移至項目資料夾中 執行 `npm install` 安裝依賴 最後執行 `npm run dev` 就可開啟服務 透過瀏覽器輸入網址 `http://localhost:3000/` 即可正確看到畫面。 ## 關於路由的各種規則 當項目是多頁面的,需要有路由,這部分 nuxt 會自動產生路由表 我們只需按照規定生成資料夾與檔案即可 首先路由建立方式,需於項目資料夾中,建立一個 `/pages` 的資料夾 接著要到 `app.vue` 檔案中,修改 `template` 內容,把 `<NuxtWelcome />` 變成 `<NuxtPage />` 標籤 資料夾與檔案規則: - 網址路徑 `/` 指向的首頁頁面,一定會是 `/pages/index.vue` 這個檔案 - 其他頁面可直接建立網址路徑對應的檔案名稱,比如檔案 `/pages/about.vue` 指向 `/about` 的網址路徑 - 如果是有子路由的情況,就會寫成: - `/pages/products/index.vue`(這個檔案就通過網址路徑 `/products` 開啟) - `/pages/products/[id].vue`(這個檔案就通過網址路徑 `/products/:id` 開啟) - 動態路由獲取參數方式: - 以 `/pages/products/[id].vue` 為例,動態 `id` 可在 `[id].vue` 檔案中的 `template` 區域中,通過 `{{ $route.params.id }}` 寫法取得 `id` 的值來顯示在畫面上 - 承上,假設網址是 `/products/DSPA-1928`,通過 `{{ $route.params.id }}` 取得的 `id` 就是 `DSPA-1928` - 假設需要獲取 `id` 到 `script` 中使用,則可寫為: ```htmlmixed <script setup> const router = useRoute() const { id } = router.params console.log(id) </script> ``` - 嵌套路由用法(EX 會員中心頁裏面有其他分頁,路由分別是 /account/profile、/account/orders、/account/wishlist) - 建立同名稱的資料夾與檔案: `/pages/account` 以及 `/pages/account.vue` - 在資料夾裡面建立子路由檔案: `/pages/account/profile.vue` 、 `/pages/account/orders.vue` 、 `/pages/account/wishlist.vue` - 在 `/pages/account.vue` 檔案中添加: `<NuxtChild />` 標籤 ## Nuxt 中常見/常用標籤 - `<NuxtPage />` => 用來生成路由畫面 - `<NuxtLink />` => 用來建立路由連結 - `<NuxtChild />` => 用來生成嵌套路由的子路由畫面 - `<NuxtLayout />` => 用來設定 layout 佈局 ## layout 佈局使用方式 佈局組件,把頁面上共用的內容抓出來處理 >如果整個網站中的佈局都是共享的,可以不用特別建立 layout 資料夾 >直接將各區塊建立成元件後,於 app.vue 中使用即可 首先在項目資料夾中建立一個 `/layouts` 資料夾,接著建立佈局檔案(預設的佈局檔案規定名稱必須是 `default.vue`) 使用佈局: 1. 在 `app.vue` 設定全局通用的佈局組件(沒指定時,預設是使用 `/layouts/default.vue`) ```htmlembedded= <template> <!-- 用 NuxtLayout 把 NuxtPage 包起來 --> <NuxtLayout> <NuxtPage /> </NuxtLayout> </template> ``` 2. 在 `/pages/account.vue` 設定 account 嵌套路由用的新的佈局組件 `/layouts/account.vue` ```htmlembedded= <template> <div> <h1>MY ACCOUNT</h1> <NuxtLayout> <div> <NuxtChild /> </div> </NuxtLayout> </div> </template> <script> definePageMeta({ layout: account, // 傳入 layouts 檔案名稱即可指定 }); </script> ``` 3. 使用 NuxtLayout 標籤時直接用 v-bind 傳入 layouts 檔案名稱 ```htmlembedded= <template> <NuxtLayout :name="layout"> <NuxtPage /> </NuxtLayout> </template> ``` 4. 不使用任何 layout ```htmlembedded= <script> definePageMeta({ layout: false, }); </script> ``` 5. 通過 JS 指定 layout ```htmlembedded= <template> <button @click="EnableCustomLayout"> Update Layout </button> </template> <script setup> const EnableCustomLayout = () => setPageLayout('admin') </script> ``` > 假設整個專案內容都是使用 default.vue 當作 layout > 則官方建議直接將 layout 內容寫在 app.vue 中 > 不要另外建立 layouts 的資料夾使用 ## 設置 head 標籤中的內容 修改 nuxt.config.ts 檔案: ```javascript= export default defineNuxtConfig({ app: { head: { charset: 'utf-8', viewport: 'width=device-width, initial-scale=1', link: [ { rel: 'icon', type: 'image/x-icon', href: '/img/favicon.ico' }, ], script: [ { src: "https://code.jquery.com/jquery-1.12.4.min.js", crossorigin: "anonymous", tagPosition: 'bodyClose' } ], noscript: [ { children: `<iframe src="https://www.googletagmanager.com/ns.html?id=${gtmId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`, tagPosition: 'bodyClose' } ], htmlAttrs: { class: 'overflow-x-hidden mw-100', id: 'html' }, bodyAttrs: { class: 'overflow-x-hidden mw-100', id: 'body' } }, }, }) ``` ## 切換頁面時的過場動畫 當專案中有使用到 layout 時,需要同時設定 layoutTransition 與 pageTransition ,在使用 default layout 的頁面會執行 pageTransition 的設定,而在使用其他 layout 的頁面中則會執行 layoutTransition 的設定。 使用 Transition 的前提是每個頁面都僅有一個根元素,須確保頁面中的 `<template></template>` 裡面僅有一個根元素才有辦法正確執行 Transition 使用方式可以直接修改 nuxt.config.ts 檔案: ```javascript= export default defineNuxtConfig({ app: { pageTransition: { name: 'page', mode: 'out-in' }, // 設定動畫名稱與模式 }, }) ``` 也可以針對頁面進行設定: ```htmlembedded= <script setup lang="ts"> definePageMeta({ layout: 'admin', // 設置 layout layoutTransition: { // 設置過場樣式 name: 'fade', mode: 'out-in' } }) </script> ``` 無論是使用 nuxt.config.ts 或是在頁面中設定 definePageMeta ,最終都須回到 app.vue 檔案中撰寫 style ,建立 .page-enter-active、.page-leave-active、.page-enter-from、.page-leave-to 樣式內容。 >詳細說明可參考官網:https://nuxt.com/docs/getting-started/transitions ## 在頁面中設置 head 標籤內容 規則與在 nuxt.config.ts 檔案中設置一致,可參考:https://unhead.unjs.io/usage/composables/use-head ```htmlembedded <script setup> useHead({ title: 'My App', // <title>My App</title> meta: [ { name: 'description', content: 'My amazing site.' } // <meta name="description" content="My amazing site"> ], link: [ { rel: 'preconnect', href: 'https://fonts.googleapis.com' // <link rel="preconnect" href="https://fonts.googleapis.com"> }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap', crossorigin: '' // <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto&amp;display=swap" crossorigin=""> } ], script: [ { src: 'https://third-party-script.com', body: true // 默認是 false ,如果寫 true 的話表示這個 script 標籤要生成在 </body> 的前一行 } ], }) </script> ``` ## 為頁面設置 SEO 參考:https://unhead.unjs.io/usage/composables/use-seo-meta ```htmlembedded= <script setup> useSeoMeta({ title: 'My App', description: 'My amazing site.', ogTitle: 'My App', ogDescription: 'My amazing site.', ogUrl: 'https://myapp.com/', ogImage: '/img/logo.png', ogImageSecureUrl: 'https://myapp.com/img/logo.png', ogType: 'website', ogSite_name: 'My App', ogLocale: 'zh_TW', }) </script> ``` ## components 元件處理方式 在項目資料夾中建立 `/components` 並在裡面通過大駝峰方式建立 `.vue` 檔案,就可在任意 `.vue` 檔中隨時使用而不需要引入(Nuxt 會自動 import) 比如我們建立 header、footer 元件: - `/components/TheHeader.vue` - `/components/TheFooter.vue` > 不能建立 Head.vue 元件是因為預設 Nuxt 裡就已經有 Head 元件了(即 head 標籤),會衝突 > 可以參考 `.nuxt/components.d.ts` 檔案內容查看當前已有的所有全局元件 再將這兩個檔案放到通用佈局(`/layouts/default.vue`)中: ```htmlembedded= <template> <div> <TheHeader /> <slot /> <TheFooter /> </div> </template> ``` 假設元件很多,想分資料夾管理,比如: 網站有前台與後台,將元件分成前台與後台的資料夾管理 `/components/backend/TheHeader.vue` `/components/backend/TheFooter.vue` `/components/frontend/TheHeader.vue` `/components/frontend/TheFooter.vue` 最後使用時就會變成: ```htmlembedded= <!-- 後台 layout 檔案 --> <template> <div> <BackendTheHeader /> <slot /> <BackendTheFooter /> </div> </template> <!-- 前台 layout 檔案 --> <template> <div> <FrontendTheHeader /> <slot /> <FrontendTheFooter /> </div> </template> ``` ## composables 資料夾 使用 Vue3 的 Composition API 來封裝可重複使用的有狀態邏輯函式。 建立 `/composables/useFetchByBaseURL.js` 做自己的可複用 api ,自動帶入 baseURL : ```javascript= const useFetchByBaseURL = (url, options) => { return useFetch(url, { baseURL: 'https://www.xxx.com/api', ...options }) } export default useFetchByBaseURL ``` 在 `/pages/users.vue` 中呼叫 api: ```javascript= // GET https://www.xxx.com/api/users const { data, pending, refresh, error } = await useApiFetch('/users') ``` ## utils 資料夾 Utils 通常用於存放無狀態的工具函數,比如處理字串、日期格式化等等的功能。 用法同 composable 要先建立資料夾並建立 ts/js 檔案撰寫函數: ```javascript= // utils/formatNumber.js export const formatNumber = (num) => (Math.floor(num * 100) / 100).toFixed(2); ``` 接著在頁面中直接使用即可(Nuxt 會自動 import): ```htmlembedded= <template> {{ formatNumber(product.price) }} </template> ``` > 當 composables 與 utils 中的檔案重名時,Nuxt 預設會先使用 utils 的設置。 ## props & emit 用法 ### props 在使用元件時傳遞參數給元件,比如 `<Nav :action="action" />` 接著在元件中接收與使用參數: ```javascript // /components/Nav.vue const props = defineProps({ action: { type: String } }) watch(() => props.action, (newV, oldV) => { // do something... }); ``` ### emit 在使用元件時,傳遞函數給元件(`/pages/game.vue`): ```htmlembedded <template> <Game @setGameover="setGameover" /> <div class="gameover" v-if="gameover"> GAMEOVER. <button class="btn" @click="reloadNuxtApp()">RESTART</button> </div> </template> <script setup> const gameover = ref(false) const setGameover = (val) => gameover.value = val </script> ``` 接著在元件中接收與調用函數(`/components/Game.vue`): ```htmlembedded <template> <!-- 直接通過 click 傳入 emit 事件,參數一為事件名稱、參數二是傳給該事件的參數 --> <button @click="emit('setGameover', true)"> GAMEOVER </button> </template> <script setup> const emit = defineEmits(['setGameover']); </script> ``` ## 資料獲取方式 建立 `/server/api` 裡面新增資料檔案 比如 `todo.js`: ```javascript= const todos = [ { id: 1, title: 'learn nuxt3', isDone: false }, { id: 2, title: 'write nuxt3 note', isDone: false } ] export default () => { return todos } ``` 到 `/pages/index.vue` 中使用: ```htmlembedded= <template> <div> <h1>HOME</h1> <ul> <li v-for="item in todos" :key="item.id"> <input type="checkbox" v-model="item.isDone"> <p>{{item.title}}</p> </li> </ul> </div> </template> <script setup> // useAsyncData const { data: todos } = await useAsyncData('todos', () => $fetch('/api/todo')) // useFetch const { data: todos } = await useFetch('/api/todo') </script> ``` > $fetch / useFetch / useAsyncData 三者差異可參考:https://ithelp.ithome.com.tw/articles/10326675 ### useFetch / useAsyncData 提供的 options 在發送 useFetch / useAsyncData 請求時,可以在 url 後添加 options 物件 #### pick 用來從對象中挑選要保存的資料,其他資料如在 vue 中使用不到,則不用被保存到資料中 比如 `/server/api/todo.js` 檔案中 return 的資料很雜: ```javascript= const todos = [ { id: 1, title: 'learn nuxt3', isDone: false }, { id: 2, title: 'write nuxt3 note', isDone: false } ] export default () => { return { create_at: '2022-09-29 18:32:45', update_at: '2023-04-29 11:52:04', data: todos, code: 200, success: 'ok' } } ``` 實際取得資料僅需要 todos ,就可以這樣: ```javascript= const { data: todos } = await useFetch("/api/todo", { pick: ['data'] }); ``` #### transform 用來處理資料層級,把資料傳入 transform 函數中進行處理,最終將處理好的內容 return ```javascript= const { data: todos } = await useFetch("/api/todo", { transform(input) { return input.data.map(todo => { todo.title, todo.isCompleted }); }, }); ``` #### lazy 設定是否要等路由載入後才開始執行異步請求函數,預設為 false 若設為 true 則 API 會等到路由頁面載入完成後才開始執行呼叫 ## 狀態共享 首先在 Nuxt 中,可以通過 `useState()` 方法來建立 `ref` 資料, 如下: ```htmlembedded= <button @click="count++">+</button> {{count}} <button @click="count--">-</button> <script setup> // 參數1 資料的唯一名稱、參數2 函式 return 初始值 const count = useState('count', () => 1); </script> ``` 當需要跨檔案使用資料時,最基本需要匯出匯入 那針對匯出匯入這件事, Nuxt 又提供了一個『自動匯入』的設計 可以在項目資料夾中建立一個 `composables` 資料夾 在裡面建立檔案: ```javascript= // useCount.js export const useCount = () => useState('count', () => 1); ``` > export 用來把資料導出,導出的是一個函數 > 所以最終在其他 .vue 檔中使用時會變成 `useCount()` > 使用 => `const count = useCount();` > 且多個檔案同時使用的話,資料是共享的 > 比如我在首頁點擊++,資料變成 5,在其他頁面有使用的話,資料初始值就會直接是 5 ## 插件 項目資料夾中建立 `plugins` 在裡面建立一個測試用的檔案 `test-plugin.js`: ```javascript= export default defineNuxtPlugin(nuxtApp => { return { provide: { hello: () => 'world' } } }) ``` 接著再任意 `.vue` 檔案中 可以通過 `useNuxtApp()` 取用剛才 `provide` 的 `hello` 函式: ```javascript= const { $hello } = useNuxtApp(); console.log($hello()); // world ```