页面
app/pages/ 目录下的 Vue 组件会根据文件路径自动注册为路由页面。
页面 vs 组件的区别
| 特性 | 页面(pages/) | 组件(components/) |
|---|---|---|
| 位置 | app/pages/ | app/components/ |
| 是否有路由 | ✅ 自动生成 | ❌ 没有 |
| 是否需要手动导入 | 不需要 | 不需要 |
是否能用 definePageMeta | ✅ | ❌ |
| 是否是独立页面 | ✅ 是 | ❌ 是页面的一部分 |
简单判断
用户能通过 URL 直接访问的 → 页面;嵌入在页面中复用的 → 组件。
基本页面
<!-- app/pages/index.vue -->
<template>
<div>
<h1>首页</h1>
<p>欢迎来到我的网站</p>
</div>
</template>页面组件结构
<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>推荐的代码组织顺序
- 数据获取(
useFetch等) - 响应式状态(
ref、useState等) - 计算属性(
computed) - 方法和事件处理
definePageMetauseHead/ SEO- 生命周期钩子
为什么这个顺序?
先数据后逻辑,先定义后使用,阅读起来更自然。
动态参数页面
<!-- 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 会要求你处理数组情况。
页面间传参
通过路由参数
<!-- 列表页 -->
<template>
<NuxtLink :to="`/user/${user.id}`">查看详情</NuxtLink>
</template>通过查询参数
<!-- 发送方 -->
<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
通过状态管理
<!-- 发送方 -->
<script setup>
const selectedUser = useState('selectedUser')
selectedUser.value = user
</script>
<!-- 接收方 -->
<script setup>
const selectedUser = useState('selectedUser')
</script>三种传参方式的对比
| 方式 | 数据持久性 | 刷新后还在吗? | 适用场景 |
|---|---|---|---|
| 路由参数 | URL 中 | ✅ 在 | 资源标识(用户 ID) |
| 查询参数 | URL 中 | ✅ 在 | 过滤条件、搜索词 |
| 状态管理 | 内存中 | ❌ 不在 | 临时数据、表单草稿 |
选择建议
如果刷新后数据还需要存在,用 URL 参数;如果只是临时传递,用状态管理。
404 页面
创建一个捕获所有路由的页面作为 404:
<!-- 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 />:
<!-- app/app.vue -->
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>如果没有 pages/ 目录
Nuxt 不会包含 vue-router,app.vue 就是唯一的页面。这种情况适合:
- 单页着陆页
- 不需要路由的应用
删除 pages/ 目录可以减小打包体积(因为不需要 vue-router)。
页面的 key
默认情况下,Nuxt 会根据路由名称为页面设置 key。如果需要在同一路由参数变化时完全重新渲染:
<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 状态:
<script setup>
// 这个 await 会触发 Suspense
const { data } = await useFetch('/api/data')
</script>Suspense 做了什么?
- 顶层
await的页面会被标记为异步组件 - 在异步操作完成前,显示 fallback 内容
- 异步操作完成后,显示页面内容
配合 NuxtLoadingIndicator 使用
在数据加载时显示顶部进度条:
<!-- app/app.vue -->
<template>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>知识脉络
页面元信息 → 你在这里:页面
│
├─→ 下一步:布局
│
└─→ 相关:文件路由(04-路由与导航)