Nuxt.js 的布局系统是如何工作的?如何创建和使用自定义布局?
布局系统的核心机制
Nuxt.js 的布局系统本质上是一个页面级别的"外壳组件"。每个页面都会被包裹在某个布局中,布局负责渲染导航栏、侧边栏、页脚等公共结构,页面内容通过插槽注入。
在 Nuxt 3 中,布局文件放在 layouts/ 目录下,使用 <slot /> 渲染页面内容(Nuxt 2 使用 <Nuxt />,已废弃)。页面通过 definePageMeta 指定布局名称,框架自动完成匹配。整个流程是:app.vue 中的 <NuxtLayout> 读取当前页面的 layout 元信息,加载对应的布局组件,再通过 <slot /> 把页面内容插入布局的指定位置。
默认布局的工作方式
layouts/default.vue 是所有页面的默认外壳。只要这个文件存在,未被显式指定其他布局的页面都会自动使用它。
vue<!-- layouts/default.vue --> <template> <div> <nav> <NuxtLink to="/">首页</NuxtLink> <NuxtLink to="/dashboard">控制台</NuxtLink> </nav> <main> <slot /> </main> <footer>© 2026 MyApp</footer> </div> </template>
app.vue 中需要用 <NuxtLayout> 包裹 <NuxtPage />,布局系统才会生效:
vue<!-- app.vue --> <template> <NuxtLayout> <NuxtPage /> </NuxtLayout> </template>
如果 app.vue 中直接写 <NuxtPage /> 而没有 <NuxtLayout> 包裹,即使页面声明了 layout: 'auth',布局也不会渲染——这是初学者最容易踩的坑。
创建自定义布局
在 layouts/ 下新建 .vue 文件即创建了一个布局。文件名(去掉扩展名)就是布局名称,支持嵌套目录,名称会自动转为 kebab-case。例如 layouts/admin-panel.vue 对应布局名 admin-panel。
以一个认证页面布局为例:
vue<!-- layouts/auth.vue --> <template> <div class="auth-wrapper"> <div class="auth-card"> <slot /> </div> </div> </template> <style scoped> .auth-wrapper { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f5f5f5; } .auth-card { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); width: 400px; } </style>
在页面中指定布局
Nuxt 3 使用 definePageMeta 编译器宏来声明布局,它在编译时处理,没有运行时开销:
vue<!-- pages/login.vue --> <script setup> definePageMeta({ layout: 'auth' }) </script> <template> <form @submit.prevent="handleLogin"> <input v-model="email" type="email" placeholder="邮箱" /> <input v-model="password" type="password" placeholder="密码" /> <button type="submit">登录</button> </form> </template>
Nuxt 2 的写法是 export default { layout: 'auth' },Nuxt 3 已不推荐。definePageMeta 的好处是它不参与响应式系统,不会出现在最终的客户端 bundle 中。
动态切换布局
某些场景下需要根据运行时条件切换布局,比如管理员和普通用户看到不同外壳。有两种方式:
方式一:在页面模板中使用 NuxtLayout 动态 name
vue<!-- pages/dashboard.vue --> <script setup> const route = useRoute() const layoutName = computed(() => route.path.startsWith('/admin') ? 'admin' : 'default' ) </script> <template> <NuxtLayout :name="layoutName"> <div>控制台内容</div> </NuxtLayout> </template>
方式二:在路由中间件中修改 to.meta.layout
javascript// middleware/layout.js export default defineNuxtRouteMiddleware((to) => { if (to.path.startsWith('/admin')) { to.meta.layout = 'admin' } })
方式一适合页面内部的条件判断,方式二适合全局性的布局策略(比如在 nuxt.config.ts 中配置全局中间件)。注意:方式一在页面中又嵌了一层 NuxtLayout,实际上会产生双重布局包裹,需要同时在 definePageMeta 中设置 layout: false 来禁用自动布局。
具名插槽传递区域内容
布局支持 Vue 的具名插槽,页面可以向布局的特定区域注入内容。这在需要灵活控制头部或侧边栏时特别有用:
vue<!-- layouts/custom.vue --> <template> <div> <header> <slot name="header"> <h1>默认标题</h1> </slot> </header> <div class="content-wrapper"> <aside> <slot name="sidebar" /> </aside> <main> <slot /> </main> </div> </div> </template>
页面中使用 template #slotName 传入内容:
vue<!-- pages/profile.vue --> <script setup> definePageMeta({ layout: 'custom' }) </script> <template> <template #header> <h1>用户中心</h1> </template> <template #sidebar> <ul> <li>基本信息</li> <li>安全设置</li> </ul> </template> <div>个人资料编辑区域</div> </template>
具名插槽的 <slot name="header"> 中可以写默认内容,页面不传 #header 时自动展示默认值,传了则覆盖——这比在布局里用 v-if 判断灵活得多。
禁用布局与自定义页面过渡
某些页面(如全屏展示页、打印页)不需要任何布局外壳,用 layout: false 关闭:
vue<script setup> definePageMeta({ layout: false }) </script> <template> <div class="fullscreen">全屏内容,不受任何布局包裹</div> </template>
布局切换时还可以配置过渡动画。在 nuxt.config.ts 中设置 layoutTransition:
javascript// nuxt.config.ts export default defineNuxtConfig({ app: { layoutTransition: { name: 'layout', mode: 'out-in' } } })
css/* assets/css/main.css */ .layout-enter-active, .layout-leave-active { transition: opacity 0.3s ease; } .layout-enter-from, .layout-leave-to { opacity: 0; }
布局与页面的生命周期顺序
了解执行顺序对调试很重要。Nuxt 3 中布局和页面的生命周期执行顺序为:
- 布局的
setup()执行 - 布局的
onMounted注册(此时 DOM 未挂载) - 页面的
setup()执行 - 页面的
onMounted注册 - 页面的
onMounted回调触发 - 布局的
onMounted回调触发
布局的 onMounted 反而在页面之后触发,因为布局要等插槽内容(即页面)渲染完毕才算挂载完成。如果需要在布局中访问插槽内容的 DOM,要等 onMounted 而非 setup 阶段。
常见坑与排查
布局不生效? 检查 app.vue 是否用 <NuxtLayout> 包裹了 <NuxtPage />。没有这一层,布局系统不会启动,页面声明的 layout 会被忽略。
页面内容空白? 布局文件中必须包含 <slot />(Nuxt 3)或 <Nuxt />(Nuxt 2),否则页面内容无处渲染。
布局名称不匹配? layouts/custom-layout.vue 的布局名是 custom-layout,不是 customLayout。definePageMeta 中的 layout 值要与 kebab-case 名称一致。
Nuxt 2 项目迁移? 三件事:把布局中的 <Nuxt /> 替换为 <slot />;把页面中的 layout 选项改为 definePageMeta({ layout: 'xxx' });把错误页面从 layouts/error.vue 移到项目根目录的 error.vue。
布局中获取页面数据? 布局无法直接调用 useAsyncData 或 useFetch 获取数据(它不是路由组件),但可以通过 useRoute 获取路由参数,或通过 provide/inject 与页面通信。