在开发博客系统时,遇到了一个路由不生效的问题:/section 可以访问,但 /section/id 却始终无法匹配。折腾了一番后发现是 Nuxt 文件路由的可选参数语法理解有误。
问题背景
需求很简单:
/blog→ 显示博客列表,默认选中第一篇文章/blog/kubernetes-1-32-release→ 显示博客列表,选中指定文章
最初创建了两个路由文件:
ini
pages/
├── [section].vue # 匹配 /blog
└── [section]/
└── [id].vue # 匹配 /blog/xxx
结果:/blog 正常,/blog/kubernetes-1-32-release 却始终匹配不上。
问题原因
最初采用了两个独立文件的方式:
ini
pages/
├── [section].vue # 匹配 /blog
└── [section]/
└── [id].vue # 匹配 /blog/xxx
这种结构看似合理,实则存在多个问题:
- 代码重复:两个文件 95% 代码相同,维护成本高
- 状态同步:需要额外处理跨页面状态共享
- 路由匹配 :某些情况下 Nuxt 无法正确区分两个路由,导致
/blog/xxx匹配失败
解决方案
Nuxt 提供了可选路由参数 语法 ------ 双括号 [[param]],用一个文件同时处理两种情况:
lua
pages/
└── [section]/
└── [[id]].vue # 同时匹配 /blog 和 /blog/xxx
核心区别
| 语法 | 含义 | 匹配示例 |
|---|---|---|
[id] |
必需参数 | /blog/abc ✅ / /blog ❌ |
[[id]] |
可选参数 | /blog/abc ✅ / /blog ✅ |
[[id]] 是 Nuxt/Vue Router 的特殊语法,表示该参数可以存在也可以不存在。
实现方案
合并后的 [[id]].vue:
vue
<script setup>
const route = useRoute()
const section = route.params.section
const articleId = route.params.id // 可能为 undefined
// 有 id 用 id,没有则用 firstArticleId
const activeArticleId = ref(articleId || firstArticleId)
// 监听路由变化(SPA 导航时更新)
watch(() => route.params.id, (newId) => {
if (newId) activeArticleId.value = newId
})
// 点击文章时更新 URL
const handleSelectArticle = (id) => {
activeArticleId.value = id
navigateTo(`/${section}/${id}`, { replace: true })
}
</script>
关键点
route.params.id可能为undefined:需要提供默认值- 添加路由监听:SPA 内导航时 URL 变化不会重新执行 setup,需要 watch
navigateTo更新 URL:选中文章时同步 URL,支持分享和书签
调试技巧
在排查过程中,发现 Nuxt 4 的 console.log 在 SSR 阶段可能被过滤。一个实用的做法是在 composable 中添加显眼前缀:
ts
export function useContentArticles(section: string) {
console.log('>>> [useContentArticles] section:', section)
console.log('>>> [useContentArticles] cache keys:', Object.keys(sectionDataCache))
// ...
}
终端输出:
less
>>> [useContentArticles] section: blog
>>> [useContentArticles] cache keys: [ 'blog', 'interview', 'nuxt4' ]
总结
| 场景 | 推荐方案 |
|---|---|
| 单一页面 + 可选子路径 | [[id]].vue |
| 完全不同的两个页面 | 分开两个文件 |
| 参数必需 | [id].vue |
可选参数 [[param]] 是 Nuxt 文件路由的利器,用好了可以大幅减少代码重复。但要注意处理 undefined 的情况和路由变化的监听。
延伸阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪