Skip to content

第一个页面

为什么从页面开始?

在 Nuxt 中,页面是用户看到的每个"网址"。Nuxt 最强大的功能之一就是"文件路由"——你创建一个 .vue 文件,Nuxt 就自动帮你生成对应的路由。这是你学习 Nuxt 后最先接触、也最常使用的功能。

创建页面

在 Nuxt 4 中,页面放在 app/pages/ 目录下。创建一个文件,Nuxt 自动为其生成路由。

创建首页

如果你还没有 app/pages/ 目录,先创建它:

bash
mkdir -p app/pages

创建 app/pages/index.vue

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 即可看到首页。

创建其他页面

bash
# 关于页面 → 访问 /about 时显示
touch app/pages/about.vue

# 联系页面 → 访问 /contact 时显示
touch app/pages/contact.vue

app/pages/about.vue

vue
<template>
  <div>
    <h1>关于我们</h1>
    <p>这是一个关于页面</p>
  </div>
</template>

文件名和路由的关系

  • about.vue → 路由是 /about
  • contact.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> 组件进行页面跳转:

vue
<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 /> 来渲染页面:

vue
<template>
  <div>
    <NuxtRouteAnnouncer />
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

<NuxtRouteAnnouncer /> 是什么?

它是无障碍(A11y)组件,在路由切换时通过屏幕阅读器播报页面变化,帮助视障用户感知导航。视觉上完全不可见,不影响正常用户。建议始终保留。详见 14-内置组件速查/06-无障碍

这段代码的每一层都有意义

组件作用可以没有吗?
<NuxtLayout>渲染布局(导航栏、页脚等公共部分)可以,去掉则不使用布局
<NuxtPage />渲染当前路由对应的页面组件❌ 不能,没有它页面不会显示

最简的 app.vue 只需要

vue
<template>
<NuxtPage />
</template>

但通常我们加上 <NuxtLayout> 来支持布局系统。

INFO

常见新手错误:在 app.vue 中写了具体内容(如导航栏) 然后发现每个页面都有这些内容。正确的做法是:公共内容放在布局app/layouts/)中,app.vue 只做容器

添加 API

Nuxt 的全栈能力让你可以在同一个项目里写后端 API:

创建 server/api/hello.ts

ts
export default defineEventHandler(() => {
  return { message: 'Hello from API!' }
})

这段代码解释

  • server/api/ 目录下的文件自动注册为 API 路由
  • 文件名 hello.ts → 访问路径 /api/hello
  • defineEventHandler 是 Nuxt 服务端的处理函数包装器
  • 直接返回对象,Nuxt 自动序列化为 JSON

在页面中调用 API:

vue
<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 做了什么?

  1. 在服务端:发起 HTTP 请求获取数据,将数据渲染到 HTML 中
  2. 在客户端:使用服务端已获取的数据(不会重复请求)
  3. 后续导航:在客户端发起新请求

这就是 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 中。

完整的页面结构

vue
<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

  1. 确认文件在 app/pages/ 目录下(不是项目根目录的 pages/
  2. 确认文件名是 index.vue(首页)或其他正确的名称
  3. 确认 app.vue 中有 <NuxtPage />
  4. 重启开发服务器

修改页面后没有变化

  1. 检查终端是否有报错
  2. 确认修改了正确的文件
  3. 手动刷新浏览器(有时 HMR 不会自动更新页面结构变化)

ref is not defined 报错

运行 nuxt prepare 重新生成类型声明,或重启开发服务器。

知识脉络

text
开发服务器 → 你在这里:第一个页面

               ├─→ 下一步:项目配置

               ├─→ 深入学习:文件路由(04-路由与导航)

               ├─→ 深入学习:布局系统(05-视图与布局)

               └─→ 深入学习:数据获取(06-数据获取)

基于 Nuxt 4 官方文档整理编写