第 3 章:组件与布局

未匹配的标注

第 3 章:组件与布局(复用 UI 与统一结构)

本章讲解 Nuxt 4 组件机制与布局系统,结合完整代码 + 实战演练,完成导航栏、内容卡片、多布局等通用模块封装,适配掘金小册平台页面复用需求。

3.1 组件自动导入:components 目录与全局注册

Nuxt 4 基于约定式目录,app/components/ 下的组件无需手动 import,全局自动导入,直接在模板中使用。

目录规则

  • 1,一级组件:components/BookCard.vue → 直接使用
  • 2,子目录组件:components/Header/Nav.vue → 自动转为 (目录名 + 组件名)

    实战代码

    新建全局组件 app/components/Hello.vue
    <template>
    <div class="demo-component">
      <h3>Nuxt 4 自动导入组件 Hello</h3>
    </div>
    </template>
    在任意页面直接使用,无需引入:
    app/pages/index.vue
    <template>
    <div>
      <h1>网站首页</h1>
      <!-- 直接使用组件 -->
      <Hello />
    </div>
    </template>

    实战演练

  • 1,在新建 Tips.vue 组件,编写简单提示文本
  • 2,在首页、小册列表页分别引用该组件
  • 3,运行项目,验证无需 import 即可正常渲染

3.2 组合式组件:defineProps / defineEmits / defineExpose

在 Vue3 + Nuxt4 的 <script setup> 语法中,使用内置 API 实现父子组件传值、事件派发、实例暴露,是业务组件开发基础。

1. defineProps 父传子

子组件 app/components/BookItem.vue

<template>
  <div class="book-item">
    <p>小册名称:{{ title }}</p>
    <p>阅读量:{{ viewCount }}</p>
  </div>
</template>

<script setup>
// 定义接收父组件参数
const props = defineProps({
  title: String,
  viewCount: Number
})
</script>

父组件使用并传参:

<template>
  <div>
    <BookItem title="Nuxt4 实战教程" :view-count="1200" />
  </div>
</template>

2. defineEmits 子传父(触发自定义事件)

改造子组件,添加点击派发事件:
子组件 app/components/BookItem2.vue

<template>
  <div class="book-item2" @click="handleClick">
    <p>小册名称:{{ title }}</p>
  </div>
</template>

<script setup>
const props = defineProps(['title'])
// 定义自定义事件
const emit = defineEmits(['select'])

const handleClick = () => {
  // 向父组件派发事件并传参
  emit('select', props.title)
}
</script>

父组件监听事件:

<template>
  <BookItem2 title="Vue3 入门1" @select="onBookSelect" />
  <BookItem2 title="Vue2 KK" @select="onBookSelect" />
</template>

<script setup>
const onBookSelect = (name) => {
  console.log('选中小册:', name)
}
</script>

3. defineExpose 暴露组件实例与方法

子组件向外暴露内部属性 / 方法,供父组件通过 ref 调用:
子组件增加 app/components/BookItem.vue

<script setup>
const msg = '组件内部数据'
const showInfo = () => {
  alert('组件方法被调用')
}

// 对外暴露
defineExpose({
  msg,
  showInfo
})
</script>

父组件调用:
父组件 app/pages/index.vue 追加如下内容

<template>
  <BookItem title="对外暴露数据和方法" :viewCount="1000000" ref="bookRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
const bookRef = ref(null)

const callChildMethod = () => {
  bookRef.value.showInfo()
  console.log(bookRef.value.msg)
}
</script>

实战演练

  • 1,封装小册条目组件,使用 defineProps 接收名称、封面、价格
  • 2,绑定点击事件,通过 defineEmits 向上传递选中事件
  • 3,使用 defineExpose 暴露一个重置方法,父组件点击触发

3.3 布局系统:layouts 目录与默认布局 default.vue

Nuxt4 布局用于统一页面公共头部、底部、侧边栏,所有页面默认继承 layouts/default.vue。

规则说明

  • 目录:app/layouts/
  • default.vue:全局默认布局,所有页面默认使用
  • 布局出口:必须使用 <slot /> 承载页面主体内容

实战代码

创建默认布局
app/layouts/default.vue

<template>
  <!-- 公共导航栏 -->
  <header style="padding: 1rem; background: #f5f5f5;">
    <h2>diibook小册平台 - 公共导航</h2>
  </header>

  <!-- 页面内容插槽 -->
  <main style="padding: 2rem;">
    <slot />
  </main>

  <!-- 公共底部 -->
  <footer style="text-align: center; padding: 1rem; border-top: 1px solid #eee;">
    版权所有 © 小册实战项目
  </footer>
</template>

修改入口文件
app/app.vue

<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

任意页面无需额外配置,自动套用该布局。

实战演练

  • 1,编写默认布局,添加导航、主体插槽、页脚
  • 2,启动项目,访问首页、列表页,查看所有页面统一套用布局

3.4 多布局切换:definePageMeta({ layout: ‘xxx’ })

项目中常存在前台布局、后台布局、登录页独立布局,通过 definePageMeta 快速切换布局。

1. 新建自定义布局

创建后台布局
app/layouts/admin.vue

<template>
  <div style="display: flex;">
    <!-- 后台侧边栏 -->
    <aside style="width: 200px; background: #222; color: #fff; padding: 1rem;">
      <p>创作者后台菜单</p>
    </aside>
    <!-- 后台内容区 -->
    <div style="flex: 1; padding: 2rem;">
      <slot />
    </div>
  </div>
</template>

2. 页面指定布局

后台页面 app/pages/admin/index.vue,通过路由元信息切换布局:

<template>
  <div>创作者控制台</div>
</template>

<script setup>
// 指定使用 admin 布局
definePageMeta({
  layout: 'admin'
})
</script>

3. 禁用布局(独立页面)

登录页、空白页可关闭所有布局,设置 layout: false

<script setup>
definePageMeta({
  layout: false
})
</script>

实战演练

  • 1,新建 admin 后台布局、login 登录布局
  • 2,分别给后台页、登录页绑定对应布局
  • 3,访问不同页面,验证布局切换效果

3.5 全局公共组件封装(导航栏、侧边栏、卡片)

结合自动导入特性,封装掘金小册平台高频复用组件。

1. 导航栏组件 app/components/GlobalNav.vue

<template>
  <nav style="background: #1677ff; padding: 0.8rem; color: #fff;">
    <NuxtLink to="/" style="margin-right: 1rem; color: #fff;">首页</NuxtLink>
    <NuxtLink to="/books" style="margin-right: 1rem; color: #fff;">小册列表</NuxtLink>
    <NuxtLink to="/admin" style="color: #fff;">创作者中心</NuxtLink>
  </nav>
</template>

2. 小册卡片组件 app/components/BookCard.vue

<template>
  <div style="border: 1px solid #eee; padding: 1rem; border-radius: 6px; width: 240px;">
    <h4>{{ bookName }}</h4>
    <p>作者:{{ author }}</p>
    <p>阅读量:{{ views }}</p>
    <NuxtLink :to="`/books/${bookId}`">查看详情</NuxtLink>
  </div>
</template>

<script setup>
defineProps({
  bookId: [String, Number],
  bookName: String,
  author: String,
  views: Number
})
</script>

3. 在默认布局中引入公共组件

修改 layouts/default.vue,嵌入全局导航:

<template>
  <!-- 全局导航 -->
  <GlobalNav />
  <main style="padding: 2rem;">
    <slot />
  </main>
  <footer>版权所有 © 小册实战项目</footer>
</template>

4. 页面批量使用卡片组件

app/pages/books/index.vue

<template>
  <div style="display: flex; gap: 1rem; flex-wrap: wrap;">
    <BookCard book-id="1" book-name="Nuxt4 全栈实战" author="技术作者" :views="3600" />
    <BookCard book-id="2" book-name="Vue3 进阶指南" author="前端达人" :views="2800" />
  </div>
</template>

实战演练

  • 1,封装全局导航、小册卡片两个通用组件
  • 2,将导航嵌入默认布局,实现全站统一导航
  • 3,在列表页循环使用卡片组件展示多条小册数据

3.6 组件懒加载与性能优化

对于大体积组件、弹窗、非首屏组件,使用懒加载减少首屏资源体积,提升页面加载速度。
Nuxt4 已经把组件懒加载、懒水合、数据懒加载做成内置能力,不需要手动写 defineAsyncComponent 也能实现 “点击再加载弹窗” 这类需求Nuxt。

1. 组件动态懒加载(局部懒加载)

Nuxt4 自动扫描 components/ 下的组件,名字加 Lazy 就自动变成异步懒加载组件 Nuxt
弹窗场景最佳实践:
app/components/Modal.vue

<template>
  <div class="mask" @click.self="$emit('close')">
    <div class="box">
      <slot />
      <button @click="$emit('close')">关闭</button>
    </div>
  </div>
</template>

<style scoped>
.mask { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
.box { background: white; padding: 24px; border-radius: 8px; }
</style>

页面使用(内置懒加载)
app/pages/books/index.vue

<template>
  <button @click="showModal = true">打开弹窗</button>
  <LazyModal v-if="showModal" @close="showModal = false"> 
    我是懒加载弹窗内容
  </LazyModal>
</template>

<script setup>
const showModal = ref(false)
</script>

2. 懒加载组件懒加载 + 四种水合策略 + 图片懒加载 + 数据懒加载

基础组件
app/components/HeavyTable.vue

<!-- 重型表格组件 -->
<template>
  <div class="table-box">
    <h3>大数据表格(懒加载组件)</h3>
    <table border="1" cellpadding="6">
        <tbody>
            <tr>
                <td>序号</td>
                <td>名称</td>
                <td>状态</td>
            </tr>
            <tr v-for="i in 10" :key="i">
                <td>{{ i }}</td>
                <td>测试数据 {{ i }}</td>
                <td>正常</td>
            </tr>
        </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
// 模拟复杂逻辑
console.log('HeavyTable 组件已加载 & 水合 ')
</script>

components/Modal.vue

<template>
  <div class="mask" @click.self="$emit('close')">
    <div class="box">
      <slot />
      <p>点击后才完成加载与水合</p>
      <button @click="$emit('close')">关闭</button>
    </div>
  </div>
</template>
<script setup lang="ts">
alert('Modal 已水合')
</script>
<style scoped>
.mask { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
.box { background: white; padding: 24px; border-radius: 8px; }
</style>

components/FooterInfo.vue

<!-- 页脚(浏览器空闲时水合) -->
<template>
  <div class="footer">
    网站底部信息 · 懒加载+空闲水合
  </div>
</template>

<script setup lang="ts">
console.log('FooterInfo 空闲时完成水合')
</script>

<style scoped>
.footer {
  margin-top: 800px;
  padding: 20px;
  background: #eee;
}
</style>

加入nuxt/image组件

yarn add @nuxt/image

启用(nuxt.config.ts)

import { defineConfig } from 'nuxt/app'

export default defineConfig({
modules: [
‘@nuxt/image’
]
})

页面调用(内置懒加载)
app/pages/index.vue
```js
<template>
  <div>
    <h2>Nuxt4 内置懒加载 实战演示</h2>

    <!-- 1. 可见时水合:进入视口才加载+激活(长列表/表格首选) -->
    <div style="margin-top: 600px;">
      <LazyHeavyTable hydrate-on-visible />
    </div>

    <!-- 2. 交互时水合:点击/hover 才加载激活(弹窗/表单) -->
    <button @click="showModal = true">打开弹窗</button>
    <LazyModal v-if="showModal" @close="showModal = false">
      我是懒加载弹窗内容
    </LazyModal>

    <!-- 3. 浏览器空闲时水合:非关键组件 -->
    <LazyFooterInfo hydrate-on-idle />

    <!-- 4. 图片懒加载(Nuxt 内置 <NuxtImg> 自动懒加载) -->
    <div style="margin-top: 300px;">
      <h3>图片懒加载</h3>
      <NuxtImg
        src="images/demo.png"
        width="600"
        height="300"
        alt="示例图"
      />
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
// 控制弹窗显示
const showModal = ref(false)
</script>

3. 基础优化建议

  • 首屏非必需组件全部使用懒加载
  • 大图片、富文本编辑器、表格等重型组件优先异步引入
  • 拆分巨型组件,按功能拆分为多个小组件

4. 实战演练

  • 1,封装一个弹窗组件,实现点击后懒加载
  • 2,查看浏览器网络面板,验证组件仅在触发后才请求资源
  • 3,对后台复杂组件配置全局懒加载

本章综合实战任务

  • 1,完善全局导航、页脚,整合进默认布局,前台页面统一样式结构
  • 2,封装可复用的小册卡片组件,实现参数传值与跳转
  • 3,搭建独立后台布局,区分前台 / 后台页面样式
  • 4,对弹窗、富文本等重型组件实现懒加载,优化首屏性能
  • 5,熟练使用 defineProps / defineEmits 完成组件交互

本章总结

  • 1,Nuxt4 components 目录组件自动全局导入,简化开发;
  • 2,掌握 defineProps / defineEmits / defineExpose 实现组件通信;
  • 3, layouts 布局实现全站结构统一,default.vue 为默认布局;
  • 4,通过 definePageMeta 灵活切换多套布局,适配前台 / 后台 / 登录页;
  • 5,封装导航、卡片等公共组件,提升代码复用率;
  • 6,使用异步组件实现懒加载,完成页面性能基础优化。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
Vckin
讨论数量: 0
发起讨论 查看所有版本


暂无话题~