Skip to content

页面

app/pages/ 目录下的 Vue 组件会根据文件路径自动注册为路由页面。

页面 vs 组件的区别

特性页面(pages/)组件(components/)
位置app/pages/app/components/
是否有路由✅ 自动生成❌ 没有
是否需要手动导入不需要不需要
是否能用 definePageMeta
是否是独立页面✅ 是❌ 是页面的一部分

简单判断

用户能通过 URL 直接访问的 → 页面;嵌入在页面中复用的 → 组件。

基本页面

vue
<!-- app/pages/index.vue -->
<template>
  <div>
    <h1>首页</h1>
    <p>欢迎来到我的网站</p>
  </div>
</template>

页面组件结构

vue
<template>
  <!-- 页面模板 -->
</template>

<script setup lang="ts">
// 1. 数据获取(顶层 await,SSR 时在服务端执行)
const { data: users } = await useFetch('/api/users')

// 2. 响应式状态
const searchQuery = ref('')

// 3. 计算属性
const filteredUsers = computed(() => {
  return users.value?.filter(u =>
    u.name.includes(searchQuery.value)
  )
})

// 4. 方法
function handleSearch() {
  // ...
}

// 5. 页面元信息
definePageMeta({
  layout: 'default',
})

// 6. SEO
useHead({
  title: '用户列表',
})

// 7. 生命周期(仅客户端执行)
onMounted(() => {
  console.log('页面已挂载')
})
</script>

<style scoped>
/* 页面样式 */
</style>

推荐的代码组织顺序

  1. 数据获取(useFetch 等)
  2. 响应式状态(refuseState 等)
  3. 计算属性(computed
  4. 方法和事件处理
  5. definePageMeta
  6. useHead / SEO
  7. 生命周期钩子

为什么这个顺序?

先数据后逻辑,先定义后使用,阅读起来更自然。

动态参数页面

vue
<!-- app/pages/user/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const id = route.params.id as string

const { data: user } = await useFetch(`/api/users/${id}`)
</script>

<template>
  <div v-if="user">
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
  </div>
</template>

as string 类型断言

route.params.id 的类型是 string | string[]。因为我们知道这个路由只有一个参数,所以断言为 string。如果不做断言,TypeScript 会要求你处理数组情况。

页面间传参

通过路由参数

vue
<!-- 列表页 -->
<template>
  <NuxtLink :to="`/user/${user.id}`">查看详情</NuxtLink>
</template>

通过查询参数

vue
<!-- 发送方 -->
<NuxtLink :to="{ path: '/search', query: { q: 'nuxt', page: 1 } }">
  搜索
</NuxtLink>

<!-- 接收方 -->
<script setup>
const route = useRoute()
const query = route.query.q   // 'nuxt'(string 类型)
const page = route.query.page // '1'(string 类型!不是 number)
</script>

INFO

查询参数始终是 string!URL 中的 ?page=1route.query.page'1' 不是 1

通过状态管理

vue
<!-- 发送方 -->
<script setup>
const selectedUser = useState('selectedUser')
selectedUser.value = user
</script>

<!-- 接收方 -->
<script setup>
const selectedUser = useState('selectedUser')
</script>

三种传参方式的对比

方式数据持久性刷新后还在吗?适用场景
路由参数URL 中✅ 在资源标识(用户 ID)
查询参数URL 中✅ 在过滤条件、搜索词
状态管理内存中❌ 不在临时数据、表单草稿

选择建议

如果刷新后数据还需要存在,用 URL 参数;如果只是临时传递,用状态管理。

404 页面

创建一个捕获所有路由的页面作为 404:

vue
<!-- app/pages/[...path].vue -->
<script setup>
const { path } = useRoute()

definePageMeta({
  validate: (route) => {
    // 拦截所有未匹配的路由
    return true
  },
})
</script>

<template>
  <div>
    <h1>404</h1>
    <p>页面 {{ path }} 不存在</p>
    <NuxtLink to="/">返回首页</NuxtLink>
  </div>
</template>

为什么用 [...path].vue 做自定义 404?

Nuxt 默认的 404 页面很简陋。用通配路由可以完全自定义 404 页面的外观和功能。

INFO

注意:这个文件要放在 pages/ 的根目录下 且是最后一个被匹配的路由

页面与 app.vue

app.vue 必须包含 <NuxtPage />

vue
<!-- app/app.vue -->
<template>
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

如果没有 pages/ 目录

Nuxt 不会包含 vue-router,app.vue 就是唯一的页面。这种情况适合:

  • 单页着陆页
  • 不需要路由的应用

删除 pages/ 目录可以减小打包体积(因为不需要 vue-router)。

页面的 key

默认情况下,Nuxt 会根据路由名称为页面设置 key。如果需要在同一路由参数变化时完全重新渲染:

vue
<script setup>
definePageMeta({
  key: route => route.fullPath,
})
</script>

key 的作用

Vue 使用 key 来判断是否需要重新创建组件实例。相同的 key = 复用,不同的 key = 重建。

设置效果
默认(路由名称)/users/1/users/2 复用组件
route => route.fullPath/users/1/users/2 重建组件

大多数情况用默认就好

如果遇到参数变化但组件状态没更新的问题,再考虑设置 key。

页面与 Suspense

Nuxt 自动使用 <Suspense> 包裹页面。如果页面有顶层 await,会显示 loading 状态:

vue
<script setup>
// 这个 await 会触发 Suspense
const { data } = await useFetch('/api/data')
</script>

Suspense 做了什么?

  • 顶层 await 的页面会被标记为异步组件
  • 在异步操作完成前,显示 fallback 内容
  • 异步操作完成后,显示页面内容

配合 NuxtLoadingIndicator 使用

在数据加载时显示顶部进度条:

vue
<!-- app/app.vue -->
<template>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

知识脉络

text
页面元信息 → 你在这里:页面

               ├─→ 下一步:布局

               └─→ 相关:文件路由(04-路由与导航)

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