第一个页面
为什么从页面开始?
在 Nuxt 中,页面是用户看到的每个"网址"。Nuxt 最强大的功能之一就是"文件路由"——你创建一个 .vue 文件,Nuxt 就自动帮你生成对应的路由。这是你学习 Nuxt 后最先接触、也最常使用的功能。
创建页面
在 Nuxt 4 中,页面放在 app/pages/ 目录下。创建一个文件,Nuxt 自动为其生成路由。
创建首页
如果你还没有 app/pages/ 目录,先创建它:
mkdir -p app/pages创建 app/pages/index.vue:
<template>
<div>
<h1>{{ message }}</h1>
<p>这是我的第一个 Nuxt 页面</p>
</div>
</template>
<script setup lang="ts">
// 页面逻辑写在这里
const message = ref('Hello Nuxt 4!')
</script>
<style scoped>
h1 {
color: #00DC82;
}
</style>INFO
️ 常见 lint 错误:如果定义了变量但模板中未使用(如 const message = ref(...) 但模板里写的是静态文本) ESLint 会报 'message' is assigned a value but never used。确保模板中使用 {{ message }} 而不是硬编码文本
这段代码逐行解释
| 部分 | 代码 | 作用 |
|---|---|---|
<template> | HTML 模板 | 页面显示的内容 |
<script setup> | 逻辑代码 | setup 表示使用 Vue 3 组合式 API,不需要 export default |
lang="ts" | 使用 TypeScript | 让你获得类型提示和检查 |
ref('Hello Nuxt 4!') | 创建响应式变量 | 数据变化时页面自动更新 |
<style scoped> | 局部样式 | scoped 表示样式只对当前组件生效,不会影响其他页面 |
为什么不需要 import { ref } from 'vue'?
因为 Nuxt 自动导入了所有 Vue 和 Nuxt 的 API!这是 Nuxt "自动导入"特性的体现。
访问 http://localhost:3000 即可看到首页。
创建其他页面
# 关于页面 → 访问 /about 时显示
touch app/pages/about.vue
# 联系页面 → 访问 /contact 时显示
touch app/pages/contact.vueapp/pages/about.vue:
<template>
<div>
<h1>关于我们</h1>
<p>这是一个关于页面</p>
</div>
</template>文件名和路由的关系
about.vue→ 路由是/aboutcontact.vue→ 路由是/contact- 你不需要像 Vue Router 那样手动配置路由表,创建文件就是创建路由
路由映射规则
文件路径与路由的映射关系:
| 文件路径 | 路由 | 说明 |
|---|---|---|
app/pages/index.vue | / | index.vue 是特殊文件名,代表根路径 |
app/pages/about.vue | /about | 文件名就是路由路径 |
app/pages/contact.vue | /contact | 同上 |
app/pages/blog/index.vue | /blog | 目录下的 index.vue 代表该目录的根路径 |
app/pages/blog/[id].vue | /blog/:id | 方括号表示动态参数,如 /blog/123 |
app/pages/user/[...slug].vue | /user/:slug(.*)* | [...] 表示匹配任意多级路径 |
记忆技巧
index.vue= 这个目录的"首页"[参数名].vue= 动态路由(如博客文章 ID)[...slug].vue= 通配路由(如文档系统的任意路径)
页面间导航
使用 <NuxtLink> 组件进行页面跳转:
<template>
<nav>
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/about">关于</NuxtLink>
<NuxtLink to="/contact">联系</NuxtLink>
</nav>
</template>为什么用 <NuxtLink> 而不是 <a> 标签?
| 导航方式 | 行为 | 页面状态 |
|---|---|---|
<a href="/about"> | 整页刷新,重新加载所有资源 | 丢失(表单数据、滚动位置等) |
<NuxtLink to="/about"> | 局部更新,只替换变化的部分 | 保留 |
<NuxtLink> 本质是 Vue Router 的 <RouterLink>,它实现了客户端路由——不向服务器请求完整页面,而是在浏览器内切换组件。这样更快、更流畅。
INFO
️ 唯一的例外:如果跳转到外部网站(如 https://google.com) 必须用 <a> 标签,因为 <NuxtLink> 只处理应用内部的路由
根组件 app.vue
app/app.vue 是应用的根组件,必须包含 <NuxtPage /> 来渲染页面:
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template><NuxtRouteAnnouncer /> 是什么?
它是无障碍(A11y)组件,在路由切换时通过屏幕阅读器播报页面变化,帮助视障用户感知导航。视觉上完全不可见,不影响正常用户。建议始终保留。详见 14-内置组件速查/06-无障碍。
这段代码的每一层都有意义
| 组件 | 作用 | 可以没有吗? |
|---|---|---|
<NuxtLayout> | 渲染布局(导航栏、页脚等公共部分) | 可以,去掉则不使用布局 |
<NuxtPage /> | 渲染当前路由对应的页面组件 | ❌ 不能,没有它页面不会显示 |
最简的 app.vue 只需要
<template>
<NuxtPage />
</template>但通常我们加上 <NuxtLayout> 来支持布局系统。
INFO
️ 常见新手错误:在 app.vue 中写了具体内容(如导航栏) 然后发现每个页面都有这些内容。正确的做法是:公共内容放在布局(app/layouts/)中,app.vue 只做容器
添加 API
Nuxt 的全栈能力让你可以在同一个项目里写后端 API:
创建 server/api/hello.ts:
export default defineEventHandler(() => {
return { message: 'Hello from API!' }
})这段代码解释
server/api/目录下的文件自动注册为 API 路由- 文件名
hello.ts→ 访问路径/api/hello defineEventHandler是 Nuxt 服务端的处理函数包装器- 直接返回对象,Nuxt 自动序列化为 JSON
在页面中调用 API:
<script setup lang="ts">
const { data } = await useFetch('/api/hello')
// 在 <script> 中访问数据:data.value.message
// 在 <template> 中访问数据:data.message(Vue 自动解包 ref)
</script>
<template>
<div>
<h1>{{ data?.message }}</h1>
</div>
</template>useFetch 做了什么?
- 在服务端:发起 HTTP 请求获取数据,将数据渲染到 HTML 中
- 在客户端:使用服务端已获取的数据(不会重复请求)
- 后续导航:在客户端发起新请求
这就是 Nuxt "SSR 友好"的数据获取方式——同样的代码,在服务端和客户端都能正确工作。
INFO
️ data 是 Ref 注意 script 和 template 中的访问方式不同
| 位置 | 写法 | 说明 |
|---|---|---|
<template> | {{ data?.message }} | Vue 自动解包 ref,不需要 .value |
<script> | data.value.message | 必须通过 .value 访问 |
这是最常见的初学者错误之一——在 <script> 中写 data.message 会得到 undefined,因为 data 是一个 Ref 对象,真实数据在 data.value 中。
完整的页面结构
<template>
<!-- ① 页面模板:用户看到的 HTML 结构 -->
</template>
<script setup lang="ts">
// ② 组合式函数:获取路由、数据等
const route = useRoute()
const { data } = await useFetch('/api/data')
// ③ 响应式状态:页面内部的数据
const count = ref(0)
// ④ 方法:用户交互触发的函数
function increment() {
count.value++
}
// ⑤ 异步操作:顶层 await 会阻止页面渲染直到完成
await someAsyncFunction()
</script>
<style scoped>
/* ⑥ 组件样式:scoped 确保不影响其他页面 */
</style>推荐的组织顺序
组合式函数 → 响应式状态 → 计算属性 → 方法 → 生命周期。这样代码更清晰、可读性更好。
INFO
️ 关于顶层 await
- 在
<script setup>中使用await会自动创建一个异步组件 - 页面会在
await完成后才开始渲染 - 如果你不想阻塞页面渲染(比如加载数据时先显示骨架屏),使用
useLazyFetch代替useFetch
常见新手问题
创建了页面但访问 404
- 确认文件在
app/pages/目录下(不是项目根目录的pages/) - 确认文件名是
index.vue(首页)或其他正确的名称 - 确认
app.vue中有<NuxtPage /> - 重启开发服务器
修改页面后没有变化
- 检查终端是否有报错
- 确认修改了正确的文件
- 手动刷新浏览器(有时 HMR 不会自动更新页面结构变化)
ref is not defined 报错
运行 nuxt prepare 重新生成类型声明,或重启开发服务器。
知识脉络
开发服务器 → 你在这里:第一个页面
│
├─→ 下一步:项目配置
│
├─→ 深入学习:文件路由(04-路由与导航)
│
├─→ 深入学习:布局系统(05-视图与布局)
│
└─→ 深入学习:数据获取(06-数据获取)