Nuxt 3 專案建立筆記:CLI、目錄結構、路由與 Layout
從 nuxi 建立 Nuxt 3 專案開始的完整筆記,包含 TypeScript 型別檢查與 ESLint 設定、預設目錄結構每個資料夾的用途、檔案系統路由的動態與 catch-all 寫法、Layout 與插槽的用法,以及 Pinia、Sass、Tailwind 的安裝與踩到的 overrides 問題。
W
安裝 Nuxt CLI
nuxi 全名是 Nuxt CLI(Nuxt Command Line Interface),是 Nuxt 官方提供的標準工具。它之於 Nuxt,就像 Vue CLI 之於 Vue——用來建立與管理專案。
環境準備
為了確保 nuxi 不會拿到快取的舊版本,可以先清一次 npx 快取:
npx clear-npx-cache
Node.js 需要 v16.10.0 以上。
建立專案
npx nuxi@latest init <project-name>
執行後會問幾個問題,一路照預設走就好:
✔ Which package manager would you like to use?
› npm / pnpm / yarn / bun
✔ Initialize git repository?
› Yes / No
裝完的專案長這樣(只有最基本的幾個檔案,其他資料夾要自己開):
nuxt-app/
├── .nuxt/
├── node_modules/
├── public/
├── server/
├── app.vue
├── nuxt.config.ts
├── package.json
├── tsconfig.json
└── README.md
啟動:
npm run dev
TypeScript
Nuxt 3 內建支援 TypeScript,設定放在專案根目錄的 tsconfig.json。我習慣在開發時就開型別檢查,出錯當下就看到,不要等到 build。
安裝 Vue 的型別檢查工具:
npm install -D vue-tsc
在 nuxt.config.ts 打開 typeCheck:
export default defineNuxtConfig({
typescript: {
typeCheck: true
}
})
ESLint
ESLint 是 JavaScript 的 linter,用來檢查程式碼風格(縮排、單雙引號⋯⋯)並找出語法問題。在團隊裡它的價值更明顯:大家寫出來的東西會長得一樣,code review 就不用花時間在排版上吵架。
它可以直接套用大公司的規則配置(Google、Airbnb),也可以自己客製。這裡選 Nuxt 官方提供的那組。
1. 安裝套件
npm install -D eslint @nuxtjs/eslint-config-typescript eslint-plugin-vue
2. 建立設定檔
專案根目錄新增 .eslintrc.js:
module.exports = {
env: {
browser: true,
es2023: true
},
extends: ['@nuxtjs/eslint-config-typescript'],
parserOptions: {
ecmaVersion: 2023,
sourceType: 'module'
},
rules: {
'no-undef': 'off'
}
}
extends 是陣列,放要擴展的規則配置。@nuxtjs/eslint-config-typescript 是官方針對 Nuxt + TypeScript 的規則;如果專案沒用 TypeScript,改用 @nuxtjs/eslint-config 即可。
no-undef 要關掉,是因為 Nuxt 有大量自動匯入的 API(useRoute、defineNuxtConfig⋯⋯),ESLint 看不到它們的宣告,開著會滿江紅。
預設目錄結構
Nuxt 3 是「約定優於設定」,資料夾名稱本身就是設定,所以要先知道每個資料夾在幹嘛。
nuxt-app/
├── .nuxt/
├── .output/
├── assets/
├── components/
├── composables/
├── content/
├── layouts/
├── middleware/
├── node_modules/
├── pages/
├── plugins/
├── public/
└── server/
├── api/
├── routes/
└── middleware/
├── .gitignore
├── .nuxtignore
├── app.config.ts
├── app.vue
├── nuxt.config.ts
├── package.json
└── tsconfig.json
| 目錄/檔案 | 用途 |
|---|---|
.nuxt/ | 開發環境下自動產生的 Vue 網站,不要手動改 |
.output/ | build 的產出,每次建置都會重新產生,不要手動改 |
assets/ | 需要被 Vite 處理的靜態資源(CSS/SASS、字型、圖片) |
components/ | Vue 元件,Nuxt 會自動載入,不用 import |
composables/ | 組合式函數,同樣自動載入 |
content/ | 給 Nuxt Content 用,放 .md/.yml/.csv/.json,做檔案式 CMS |
layouts/ | 版面模板,提供跨頁面重複使用的外框 |
middleware/ | 路由中間件,導航到下一頁之前執行(例如權限驗證) |
pages/ | 頁面,檔案結構直接決定路由 |
plugins/ | 外掛,會自動載入 |
public/ | 直接放在網站根目錄的檔案(robots.txt、favicon.ico) |
server/ | 後端邏輯(API、routes、middleware),不自動載入但支援 HMR |
幾個檔案:
.nuxtignore:建置時要忽略的檔案app.config.ts:執行期會暴露給客戶端的設定,不要放任何機密app.vue:Nuxt 3 網站的入口元件nuxt.config.ts:專案設定檔tsconfig.json:Nuxt 3 會在.nuxt/底下自動產一份含路徑別名的設定,根目錄這份用來擴展或覆蓋它
assets/ 與 public/ 最容易搞混:assets/ 裡的東西要在 JS/CSS 裡 import 才會被處理,public/ 則是原封不動搬到根目錄,用絕對路徑 /xxx.png 引用。
目錄名稱也可以改,在 nuxt.config.ts 的 dir 參數調整,但不是每個都能改,能改的只有官方列出的那幾個。
檔案系統路由
Nuxt 的核心特性是「檔案系統路由器(file system router)」——pages/ 底下每一個 Vue 檔案都會產生一個對應的 URL。底層還是 vue-router,只是路由表由檔名自動推出來,不用自己維護。
建立第一個頁面
新增 pages/index.vue,然後把 app.vue 改成:
<template>
<div>
<NuxtPage />
</div>
</template>
<NuxtPage /> 就是路由頁面被塞進來的位置。一旦建立了 pages/ 目錄,就一定要有這個元件,否則什麼都不會顯示。
查看自動產生的路由
想看 Nuxt 實際產出的路由表長什麼樣,可以 build 之後翻產物:
npm run build
打開 .output/server/chunks/app/server.mjs,搜尋 const _routes =,或直接搜尋你剛建立的檔名(例如 about),就會看到類似這樣的結構:
const _routes = [
{
name: "about",
path: "/about",
component: () => import('./_nuxt/about-xxxxxxxx.mjs')
},
...
]
路由連結
用 <NuxtLink> 而不是 <a>,才會走前端導航不重新載入整頁:
<NuxtLink to="/about">前往 About</NuxtLink>
<NuxtLink to="/contact">前往 Contact</NuxtLink>
動態路由
檔名用中括號包起來就是動態參數:
./pages/
└── users/
└── [id].vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-3xl text-gray-600">這裡是 Users 動態路由頁面</h1>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Id:
<span class="text-5xl font-semibold text-blue-600">{{ id }}</span>
</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { id } = route.params
</script>
匹配所有層級
三個點加參數名,可以吃掉後面所有層級:
./pages/
└── catch-all/
└── [...slug].vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-4xl text-gray-800">這是 catch-all/... 下的頁面</h1>
<p class="mt-8 text-3xl text-gray-600">匹配到的 Params:</p>
<p class="my-4 text-5xl font-semibold text-violet-500">{{ $route.params.slug }}</p>
<span class="text-xl text-gray-400">每個陣列元素對應一個層級</span>
</div>
</div>
</template>
輸入 /catch-all/hello 與 /catch-all/hello/world 都會進到這頁,差別在 slug 是一個陣列,每個元素對應一個層級。
404 頁面
把 catch-all 放在 pages/ 根目錄,所有沒被匹配到的路由都會交給它:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-8xl font-semibold text-red-500">404</h1>
<p class="my-8 text-3xl text-gray-800">Not Found</p>
</div>
</div>
</template>
<script setup>
setResponseStatus(404)
</script>
setResponseStatus(404) 不能省。少了它畫面上雖然寫著 404,HTTP 狀態碼卻是 200,搜尋引擎會把這些不存在的頁面通通收錄進去。
多層目錄與巢狀路由
目錄結構直接對應路徑層級:
./pages/
└── posts/
├── [postId]/
│ ├── comments/
│ │ └── [commentId].vue
│ └── index.vue
├── index.vue
└── top-[number].vue
巢狀路由則是同名的資料夾與 .vue 檔並存:
./pages/
├── docs/
│ ├── doc-1.vue
│ └── doc-2.vue
└── docs.vue
docs.vue 是外層容器(裡面要放 <NuxtPage />),docs/ 底下的檔案會被渲染進去。
靠目錄結構與中括號就能涵蓋大部分情境,實務上很少需要手動定義路由規則。
Layout
版面模板放在 layouts/,同樣會自動載入。
預設模板
新增 layouts/default.vue:
<template>
<div class="bg-sky-100 py-2">
<p class="px-6 py-4 text-2xl text-gray-700">這是預設的布局,全部頁面都會使用到</p>
<slot name="header" />
<slot />
<slot name="footer" />
</div>
</template>
模板裡一定要有一個未命名的 <slot />,那是採用這個模板的頁面內容會出現的位置。
在 app.vue 掛上 NuxtLayout
<template>
<div>
<p class="pb-4 text-2xl text-slate-600">這裡是最外層 app.vue</p>
<NuxtLayout name="default">
<template #header>
<p class="px-6 pt-4 text-xl text-green-500">這段會放置在 header 插槽</p>
</template>
<template #default>
<NuxtPage />
</template>
<template #footer>
<p class="px-6 pt-4 text-xl text-blue-500">這段會放置在 footer 插槽</p>
</template>
</NuxtLayout>
</div>
</template>
name 預設就是 default,這裡明寫出來只是避免誤會。
指定其他模板
新增 layouts/custom.vue:
<template>
<div class="bg-rose-100 py-2">
<p class="px-6 py-4 text-2xl text-gray-700">
使用 <span class="font-bold text-rose-500">Custom</span> 布局
</p>
<slot />
</div>
</template>
在要套用的頁面用 definePageMeta:
<script setup>
definePageMeta({
layout: 'custom'
})
</script>
模板名稱一律是 kebab-case。檔名寫 customLayout.vue 的話,要傳給 layout 的值是 custom-layout,不是 customLayout。
元件
components/ 底下的元件會被自動載入,不需要 import。命名規則是用路徑推出元件名稱:
./components/
├── AppHeader.vue → <AppHeader />
└── base/
└── Button.vue → <BaseButton />
子資料夾名稱會變成元件名稱的前綴,所以 base/Button.vue 是 <BaseButton /> 而不是 <Button />。想關掉這個行為,在 nuxt.config.ts 設 components: { pathPrefix: false }。
檔名加上 .client 或 .server 後綴可以決定它只在哪一端渲染,例如 Chart.client.vue 只在瀏覽器渲染——用到 window 的元件很需要這個。
Pinia
Pinia 是取代 Vuex 的全域狀態管理函式庫,比較輕量,適合中小型專案。
npm install pinia @pinia/nuxt --save-dev
nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'@pinia/nuxt'
]
})
踩坑:Pinia 或相依套件裝不起來
裝的時候如果一直卡在相依衝突,在 package.json 加上:
{
"overrides": {
"vue": "latest"
}
}
原因是 Pinia 與 Nuxt 各自宣告的 vue 版本範圍對不上,npm 解不出一組共同的答案。overrides 直接指定用哪一版,繞過解析。
Sass
npm install node-sass sass-loader sass -D
export default defineNuxtConfig({
css: ['~/assets/scss/app.scss']
})
Tailwind CSS
npm install -D @nuxtjs/tailwindcss
nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss']
})
tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./components/**/*.{vue,js,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./composables/**/*.{js,ts}',
'./plugins/**/*.{js,ts}',
'./app.{js,ts,vue}'
],
theme: {
extend: {}
},
plugins: []
}
content 這個陣列很關鍵——Tailwind 只會產出這些檔案裡真的出現過的 class。漏掉某個資料夾,那裡的樣式在正式環境就會整組消失(開發環境常常看不出來)。
搭配 Prettier 自動排序
class 一多就很難一眼確認有沒有重複,官方的 Prettier 外掛可以自動照推薦順序排列:
npm install -D prettier-plugin-tailwindcss
附錄:Nuxt 2 的 create-nuxt-app
Nuxt 2 用的是另一支 CLI,會問一長串問題:
npx create-nuxt-app my-nuxt-project
逐題的選項大致如下:
? Project name: my-nuxt-project
? Programming language: (Use arrow keys)
❯ JavaScript
TypeScript
? Package manager:
❯ Yarn
Npm
? UI framework:
❯ None
Ant Design Vue
BalmUI
Bootstrap Vue
Buefy
Chakra UI
Element
Framevuerk
Oruga
Tachyons
Tailwind CSS
Windi CSS
Vant
View UI
Vuetify.js
? Template engine:
❯ HTML
Pug
? Nuxt.js modules: (Press <space> to select)
❯ ◯ Axios - Promise based HTTP client
◯ Progressive Web App (PWA)
◯ Content - Git-based headless CMS
? Linting tools: (Press <space> to select)
❯ ◯ ESLint
◯ Prettier
◯ Lint staged files
◯ StyleLint
◯ Commitlint
? Testing framework:
❯ None
Jest
AVA
WebdriverIO
Nightwatch
? Rendering mode:
❯ Universal (SSR / SSG)
Single Page App
? Deployment target:
❯ Server (Node.js hosting)
Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select)
❯ ◯ jsconfig.json
◯ Semantic Pull Requests
◯ Dependabot
ESLint 與 Prettier 兩個都會出現在 linting 那題,它們的分工是:
- ESLint:靜態分析工具,找出程式碼裡的問題,也管風格
- Prettier:純格式化工具,只管排版,不管品質
通常兩個一起用——ESLint 負責分析,Prettier 負責排版。
Nuxt 2 裝 Pinia
npm install pinia
Nuxt 2 裝 Sass
npm install node-sass sass-loader --save-dev
在 nuxt.config.js 的 build 加上:
build: {
loaders: {
scss: { implementation: require('sass') }
}
}
TypeScript 版本:
import { Configuration } from '@nuxt/types'
const config: Configuration = {
build: {
loaders: {
scss: { implementation: require('sass') }
}
}
}
export default config
後記
這篇整理完沒多久,Nuxt 4 就成了預設版本,上面有好幾段已經不能照抄了——最明顯的是整個 srcDir 搬進 app/,還有 .eslintrc.js 這種 legacy config、@nuxtjs/tailwindcss 與 tailwind.config.js 都換掉了。老實說 ESLint 那段在寫的時候就已經該換成 @nuxt/eslint 了,這裡留著是當時的實況。
新的做法另外寫在 Nuxt 4 現在怎麼開專案。