第 1 步:先搞懂 Nuxt 解决了什么问题
在动手写代码之前,值得花两分钟搞清楚 Nuxt 的定位。
Vue 本身是个客户端渲染(CSR)框架。浏览器拿到一个近乎空的 HTML 壳子,然后下载并执行 JS 代码,才把页面“填”出来。这样做的问题是:
- 首屏慢:用户得等 JS 下载、解析、执行完才能看到内容。
- SEO 差:搜索引擎爬虫抓取到的几乎是空页面。
Nuxt 3 则提供了服务端渲染(SSR)和静态站点生成(SSG)等能力。它能在服务端把页面提前渲染成完整的 HTML 再发给浏览器,首屏秒开,SEO 也直接搞定。
而且它还内置了文件系统路由、自动导入、Vite 构建工具、Nitro 服务引擎……说白了,Vue 项目里你以前要手动配置的那一堆东西,Nuxt 都帮你自动搞定了。
对于 2026 年的 Nuxt 3 来说,最稳定的版本是 v3.11+,Node.js 版本要求 18.x 或更高。
第 2 步:初始化项目,感受“开箱即用”
2.1 创建项目
用官方工具 nuxi 一行命令搞定,不需要任何交互式问答:
bash
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev
浏览器打开 http://localhost:3000,看到欢迎页就说明环境跑通了。
2.2 看一眼核心目录结构
初始化好的项目默认只有几个文件,但我们需要知道往哪里加东西。这是 Nuxt 3 的约定目录结构,记住它你就掌握了 80% 的使用方法:
text
my-nuxt-app/
├── app.vue # 应用入口(根组件)
├── nuxt.config.ts # Nuxt 配置文件
├── pages/ # 页面目录 → 自动生成路由(核心!)
├── components/ # 组件目录 → 自动全局导入
├── composables/ # 组合式函数目录 → 自动导入
├── layouts/ # 布局目录
├── server/ # 服务端 API 和中间件(全栈能力)
├── public/ # 静态资源(不经过构建)
├── assets/ # 需要构建的静态资源(CSS、图片等)
└── stores/ # Pinia 状态管理(需额外安装)
2.3 把欢迎页替换成自己的页面
修改 app.vue,把 <NuxtWelcome /> 替换成 <NuxtPage />,这样它就会去渲染 pages/ 目录下的页面了:
vue
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
然后在 pages/ 目录下新建 index.vue:
vue
<!-- pages/index.vue -->
<template>
<div>
<h1>Hello Nuxt 3</h1>
<p>我的第一个 Nuxt 页面</p>
</div>
</template>
刷新页面,内容就变了。这就是 Nuxt 的文件系统路由——你不用配置任何 router.js,文件放在 pages/ 下,路径就自动映射好了。
第 3 步:吃透 Vue 3 在 Nuxt 里的用法
Nuxt 3 完全继承了 Vue 3 的能力,你在 Vue 项目里怎么写的组件、组合式 API,在 Nuxt 里都一样写。组件结构依然是 <template> + <script setup> + <style> 三段式:
vue
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const increment = () => count.value++
onMounted(() => {
console.log('组件已挂载')
})
</script>
<style scoped>
button {
padding: 8px 16px;
cursor: pointer;
}
</style>
唯一要注意的点:Nuxt 3 在服务端也会执行组件代码(SSR 场景)。如果你的代码里用了 window、document 这些浏览器对象,需要放在 onMounted 里,或者用 <ClientOnly> 组件包裹。
第 4 步:路由系统——约定大于配置
Nuxt 3 最爽的一点就是路由。你只管在 pages/ 下建文件,路由自动生成。
4.1 基础路由
text
pages/
├── index.vue → /
├── about.vue → /about
└── blog/
└── index.vue → /blog
4.2 动态路由
用方括号 [param] 表示接受动态参数:
text
pages/
└── blog/
└── [id].vue → /blog/1, /blog/2, ...
在页面里获取参数:
vue
<script setup>
const route = useRoute()
const id = route.params.id // 类型是 string
</script>
4.3 页面跳转
用 <NuxtLink> 组件代替 <a> 标签,实现 SPA 级别的无刷新跳转:
vue
<template>
<NuxtLink to="/about">关于我们</NuxtLink>
<NuxtLink :to="`/blog/${postId}`">查看文章</NuxtLink>
</template>
4.4 页面元信息(definePageMeta)
在页面组件里用 definePageMeta 配置标题、布局、中间件等:
vue
<script setup>
definePageMeta({
title: '博客详情',
layout: 'custom', // 使用自定义布局
requiresAuth: true // 自定义字段,可在路由守卫中判断
})
</script>
第 5 步:数据获取——SSR 场景下的正确姿势
在 Nuxt 里,数据获取不能用普通的 fetch 或 axios 直接写,而是要用它提供的几个专用函数,确保数据在服务端和客户端都能正确获取。
5.1 useFetch(最常用,推荐)
useFetch 是 useAsyncData + $fetch 的封装,最简洁、最常用:
vue
<script setup>
interface Post {
id: number
title: string
}
const { data, pending, error, refresh } = await useFetch<Post[]>('/api/posts')
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</template>
5.2 useAsyncData(需要更灵活控制时用)
当你的数据获取逻辑比较复杂(比如需要组合多个请求、做数据转换)时,用 useAsyncData + $fetch 的组合:
<script setup>
const { data, refresh } = await useAsyncData('unique-key', async () => {
// 可以写更复杂的逻辑
const res = await $fetch('/api/posts')
return res.filter(post => post.published)
}, {
watch: [], // 监听依赖变化自动刷新
staleTime: 60000 // 缓存 60 秒
})
</script>
5.3 $fetch(纯客户端请求用)
$fetch 是 Nuxt 内置的通用请求工具,但如果直接用在组件顶层,数据不会在服务端获取。一般用在用户交互触发的请求里:
vue
<script setup>
const submitForm = async () => {
const result = await $fetch('/api/submit', {
method: 'POST',
body: { name: '张三' }
})
console.log(result)
}
</script>
💡 一个核心原则:需要 SSR 的数据用
useFetch或useAsyncData,纯客户端交互用$fetch。
第 6 步:组件与组合式函数的自动导入——少写 80% 的 import
这是 Nuxt 3 开发体验提升最大的功能。
6.1 组件自动导入
components/ 目录下的所有 .vue 文件,无需 import,直接在模板里用标签名引用:
text
components/
├── Button.vue → <Button />
├── blog/
│ └── PostCard.vue → <BlogPostCard />
└── ui/
└── Modal.vue → <UiModal />
页面里直接用:
vue
<template>
<div>
<BlogPostCard :post="post" />
<UiModal v-model:open="showModal" />
</div>
</template>
6.2 组合式函数自动导入
composables/ 目录下的函数也会自动导入。创建一个 composables/useCounter.ts:
typescript
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
在组件里直接用,不需要 import:
vue
<script setup>
const { count, increment } = useCounter(10)
</script>
⚠️ 注意:
1、但是在utils文件下面的.ts文件需要导入才能使用
2、只有当文件导出了具名函数时,自动导入才生效。默认导出不会自动导入。
第 7 步:布局系统——统一页面的骨架
布局用来抽离多个页面的公共部分(导航栏、页脚、侧边栏)。
7.1 默认布局
在 layouts/default.vue 里定义全局布局:
vue
<!-- layouts/default.vue -->
<template>
<div class="app-layout">
<header>
<nav>导航栏</nav>
</header>
<main>
<slot /> <!-- 这里是页面的具体内容 -->
</main>
<footer>页脚</footer>
</div>
</template>
所有页面默认套用这个布局。
7.2 自定义布局
在 layouts/admin.vue 定义后台管理布局,然后在页面里指定使用它:
vue
<script setup>
definePageMeta({
layout: 'admin'
})
</script>
7.3 不使用布局
某些页面(如登录页)不需要布局,可以关闭:
vue
<script setup>
definePageMeta({
layout: false
})
</script>
第 8 步:状态管理——Pinia 是首选
虽然 Nuxt 内置了 useState 做轻量级全局状态,但正式项目推荐用 Pinia,生态成熟、DevTools 支持好。
8.1 安装
bash
npm install @pinia/nuxt pinia
在 nuxt.config.ts 中注册模块:
typescript
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
8.2 定义 Store
在 stores/user.ts 中定义用户 store:
typescript
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
id: '',
name: '',
email: '',
token: ''
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
async login(email: string, password: string) {
const res = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
this.$patch({
id: res.user.id,
name: res.user.name,
email: res.user.email,
token: res.token
})
},
logout() {
this.$reset()
}
}
})
8.3 在组件中使用
vue
<script setup>
import { useUserStore } from '~/stores/user'
const userStore = useUserStore()
const handleLogin = async () => {
await userStore.login('user@example.com', 'password')
}
</script>
<template>
<div v-if="userStore.isLoggedIn">
欢迎,{{ userStore.name }}
<button @click="userStore.logout">退出</button>
</div>
<button v-else @click="handleLogin">登录</button>
</template>
第 9 步:服务端 API——全栈开发的“意外收获”
Nuxt 3 内置了 Nitro 服务引擎,你可以在 server/ 目录里直接写后端接口,不需要单独启动一个 Node 服务。
9.1 创建一个 API 接口
在 server/api/posts/index.get.ts 中:
typescript
export default defineEventHandler(async (event) => {
// 模拟从数据库获取数据
const posts = [
{ id: 1, title: 'Nuxt 3 入门指南' },
{ id: 2, title: 'Vue 3 组合式 API 详解' }
]
return posts
})
前端直接 useFetch('/api/posts') 就能拿到数据。
9.2 带参数的 API
server/api/posts/[id].get.ts:
typescript
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// 根据 id 查询数据
return { id, title: `文章 ${id}` }
})
9.3 服务器中间件(如鉴权)
server/middleware/auth.ts:
typescript
export default defineEventHandler((event) => {
const token = getHeader(event, 'authorization')
if (!token) {
setResponseStatus(event, 401)
return { error: '未授权' }
}
// 验证 token...
})
这样,你就在同一个项目里搞定了前后端,部署也只需要一个命令 npm run build,Nitro 会自动打包成适配各种环境的部署文件。
第 10 步:性能优化——让项目跑得更快
10.1 用 computed 代替方法
在模板里调用方法,每次重新渲染都会重新计算。用 computed 会缓存结果:
typescript
// ❌ 坏:每次渲染都重新计算
const getActiveItems = () => items.value.filter(i => i.active)
// ✅ 好:只有 items 变化时才重新计算
const activeItems = computed(() => items.value.filter(i => i.active))
10.2 大列表用 v-memo
对于长列表,用 v-memo 让 Vue 只重新渲染变化的部分:
vue
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.updated]"
>
<ExpensiveComponent :data="item" />
</div>
10.3 组件懒加载
体积大的组件(图表、富文本编辑器等)用 defineAsyncComponent 懒加载:
typescript
const HeavyChart = defineAsyncComponent({
loader: () => import('~/components/HeavyChart.vue'),
loadingComponent: LoadingSpinner,
delay: 200
})
10.4 渲染模式按需配置
在 nuxt.config.ts 里针对不同路由配置不同的渲染模式:
typescript
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // 首页 SSG
'/blog/**': { swr: 3600 }, // 博客页 ISR,缓存 1 小时
'/admin/**': { ssr: false }, // 后台管理纯 CSR
'/api/**': { cors: true } // API 接口允许跨域
}
})
第 11 步:打包与部署——从开发到上线
11.1 构建命令
bash
# SSR 服务端部署(默认)
npm run build
# SSG 静态站点生成
npm run generate
11.2 部署方式
SSR 模式:运行 npm run build 后,.output/ 目录就是部署产物。用 Node.js 启动即可:
bash
node .output/server/index.mjs
SSG 模式:运行 npm run generate 后,dist/ 目录是纯静态文件,可以直接托管到 Nginx、Vercel、Netlify 或阿里云 OSS。
写在最后
从新手到熟练,核心不在于背 API,而在于理解 Nuxt 的核心理念:约定优于配置。你不需要花时间纠结路由怎么配、webpack 怎么改、SSR 怎么搞——Nuxt 已经把最佳实践封装好了,你只要按规矩来,就能写出性能好、可维护性强的 Vue 应用。
最后给你一个可以循序渐进的学习路径:
- 先用
nuxi init搭个项目,把pages/和components/玩熟; - 然后用
useFetch接一个真实 API,感受 SSR 数据获取; - 最后试着在
server/里写几个接口,体验一把“单项目全栈开发”的爽感。