MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Fragment 实际项目中的应用场景与案例分享

2023-07-293.4k 阅读

Vue Fragment 基础概念

在 Vue 组件开发中,Vue Fragment 是一个很实用的特性。传统的 Vue 组件模板要求只能有一个根节点,如果组件结构复杂,可能会导致模板中包裹过多不必要的父元素,而这时候 Vue Fragment 就派上用场了。它允许我们在组件模板中返回多个根节点,而无需额外的父元素包裹。

在 Vue 3 中,Fragment 可以通过使用 <template> 标签来实现。例如:

<template>
  <div>第一个元素</div>
  <div>第二个元素</div>
</template>

这里的 <template> 标签就充当了 Fragment 的角色,它不会渲染成实际的 DOM 元素,使得组件模板结构更加简洁,同时也避免了因过多嵌套而导致的 CSS 样式和布局问题。

实际项目中的应用场景

列表渲染场景

在实际项目中,列表渲染是非常常见的需求。比如,我们要展示一个商品列表,每个商品项可能包含图片、标题、价格等信息,而且在某些情况下,列表项的结构可能比较复杂,需要多个根节点来组织。

假设我们有一个商品列表组件 ProductList,每个商品项需要同时展示图片、标题和价格,并且可能还需要一些操作按钮。传统方式下,如果没有 Vue Fragment,可能会这样写:

<template>
  <ul>
    <li v-for="product in products" :key="product.id">
      <div class="product-image">
        <img :src="product.imageUrl" alt="product">
      </div>
      <div class="product-info">
        <h3 class="product-title">{{ product.title }}</h3>
        <p class="product-price">{{ product.price }}</p>
        <button @click="addToCart(product)">加入购物车</button>
      </div>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      products: [
        { id: 1, title: '商品1', price: 100, imageUrl: 'img1.jpg' },
        { id: 2, title: '商品2', price: 200, imageUrl: 'img2.jpg' }
      ]
    }
  },
  methods: {
    addToCart(product) {
      // 加入购物车逻辑
      console.log(`将 ${product.title} 加入购物车`);
    }
  }
}
</script>

<style scoped>
.product-image {
  width: 100px;
  height: 100px;
}

.product-info {
  margin-left: 10px;
}

.product-title {
  font-size: 16px;
}

.product-price {
  color: red;
}
</style>

这种写法虽然可行,但 <li> 标签实际上是为了满足 Vue 模板单一根节点的要求而添加的,在语义上并不一定完全符合需求。比如,从样式布局角度看,图片和信息部分可能更适合作为平级元素来处理,而不是嵌套在 <li> 里面。

使用 Vue Fragment 后,代码可以优化为:

<template>
  <ul>
    <template v-for="product in products" :key="product.id">
      <div class="product-image">
        <img :src="product.imageUrl" alt="product">
      </div>
      <div class="product-info">
        <h3 class="product-title">{{ product.title }}</h3>
        <p class="product-price">{{ product.price }}</p>
        <button @click="addToCart(product)">加入购物车</button>
      </div>
    </template>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      products: [
        { id: 1, title: '商品1', price: 100, imageUrl: 'img1.jpg' },
        { id: 2, title: '商品2', price: 200, imageUrl: 'img2.jpg' }
      ]
    }
  },
  methods: {
    addToCart(product) {
      // 加入购物车逻辑
      console.log(`将 ${product.title} 加入购物车`);
    }
  }
}
</script>

<style scoped>
.product-image {
  width: 100px;
  height: 100px;
  display: inline-block;
}

.product-info {
  display: inline-block;
  margin-left: 10px;
}

.product-title {
  font-size: 16px;
}

.product-price {
  color: red;
}
</style>

这里使用 <template> 作为 Vue Fragment,使得图片和信息部分可以作为平级元素,在样式布局上更加灵活,同时也保持了代码结构的清晰。

表单布局场景

在表单开发中,也经常会遇到需要多个根节点的情况。比如,一个用户注册表单,可能包含个人信息部分、联系方式部分和提交按钮。

传统写法可能如下:

<template>
  <form>
    <div class="personal-info">
      <label for="name">姓名:</label>
      <input type="text" id="name" v-model="user.name">
    </div>
    <div class="contact-info">
      <label for="email">邮箱:</label>
      <input type="email" id="email" v-model="user.email">
    </div>
    <button type="submit" @click="submitForm">提交</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '',
        email: ''
      }
    }
  },
  methods: {
    submitForm() {
      // 提交表单逻辑
      console.log('提交用户信息:', this.user);
    }
  }
}
</script>

<style scoped>
.personal-info,
.contact-info {
  margin-bottom: 10px;
}

label {
  display: inline-block;
  width: 80px;
}
</style>

这种写法虽然实现了功能,但从语义和布局角度看,<div> 元素只是为了满足单一根节点要求,可能会影响 CSS 选择器的编写和样式复用。

使用 Vue Fragment 优化后:

<template>
  <form>
    <template>
      <div class="personal-info">
        <label for="name">姓名:</label>
        <input type="text" id="name" v-model="user.name">
      </div>
      <div class="contact-info">
        <label for="email">邮箱:</label>
        <input type="email" id="email" v-model="user.email">
      </div>
      <button type="submit" @click="submitForm">提交</button>
    </template>
  </form>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '',
        email: ''
      }
    }
  },
  methods: {
    submitForm() {
      // 提交表单逻辑
      console.log('提交用户信息:', this.user);
    }
  }
}
</script>

<style scoped>
.personal-info,
.contact-info {
  margin-bottom: 10px;
}

label {
  display: inline-block;
  width: 80px;
}
</style>

通过 Vue Fragment,使得表单各部分的结构更加清晰,在样式编写上也可以更加直接地针对各个部分进行操作,而不会受到多余父元素的干扰。

组件组合场景

在大型项目中,组件组合是非常常见的。有时候,我们需要将多个组件组合在一起,而这些组件可能有各自独立的根节点需求。

例如,我们有一个 PageHeader 组件用于展示页面标题和一些操作按钮,还有一个 PageContent 组件用于展示页面主体内容。我们想在一个父组件 MainPage 中组合使用这两个组件。

PageHeader.vue 组件:

<template>
  <div class="page-header">
    <h1>{{ pageTitle }}</h1>
    <button @click="doSomething">操作按钮</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      pageTitle: '页面标题'
    }
  },
  methods: {
    doSomething() {
      console.log('执行操作');
    }
  }
}
</script>

<style scoped>
.page-header {
  background-color: lightgray;
  padding: 10px;
}
</style>

PageContent.vue 组件:

<template>
  <div class="page-content">
    <p>这里是页面主体内容。</p>
  </div>
</template>

<script>
export default {
  data() {
    return {}
  }
}
</script>

<style scoped>
.page-content {
  padding: 20px;
}
</style>

如果不使用 Vue Fragment,在 MainPage.vue 组件中可能会这样写:

<template>
  <div class="main-page">
    <PageHeader></PageHeader>
    <PageContent></PageContent>
  </div>
</template>

<script>
import PageHeader from './PageHeader.vue';
import PageContent from './PageContent.vue';

export default {
  components: {
    PageHeader,
    PageContent
  }
}
</script>

<style scoped>
.main-page {
  background-color: white;
}
</style>

这里的 <div class="main - page"> 是为了满足单一根节点要求添加的。但如果我们使用 Vue Fragment,MainPage.vue 组件可以优化为:

<template>
  <template>
    <PageHeader></PageHeader>
    <PageContent></PageContent>
  </template>
</template>

<script>
import PageHeader from './PageHeader.vue';
import PageContent from './PageContent.vue';

export default {
  components: {
    PageHeader,
    PageContent
  }
}
</script>

<style scoped>
/* 这里可以直接对 PageHeader 和 PageContent 的样式进行操作,无需通过中间父元素 */
</style>

这样使得组件组合更加简洁,避免了不必要的父元素嵌套,在样式管理和代码维护上都更加方便。

案例分享

电商项目中的商品详情页

在一个电商项目的商品详情页开发中,我们遇到了需要复杂结构展示的情况。商品详情页包含商品图片展示区域、商品基本信息区域、商品描述区域以及相关推荐商品区域。

商品图片展示区域需要展示主图和多张副图,并且可能有一些图片切换和放大缩小的交互。商品基本信息区域包含商品名称、价格、库存等信息。商品描述区域是一段较长的文本描述。相关推荐商品区域则是一个类似商品列表的展示。

如果按照传统方式,可能会在每个区域外面包裹一层 <div> 元素,以满足 Vue 模板单一根节点的要求。但这样会导致 HTML 结构变得复杂,而且在 CSS 样式编写和维护上会增加难度。

使用 Vue Fragment 后,代码结构如下:

<template>
  <template>
    <div class="product-image-section">
      <img :src="product.mainImageUrl" alt="product">
      <div v-for="image in product.otherImageUrls" :key="image.id" class="product - sub - image">
        <img :src="image.url" alt="product">
      </div>
    </div>
    <div class="product - basic - info - section">
      <h1 class="product - name">{{ product.name }}</h1>
      <p class="product - price">{{ product.price }}</p>
      <p class="product - stock">库存: {{ product.stock }}</p>
    </div>
    <div class="product - description - section">
      <p>{{ product.description }}</p>
    </div>
    <div class="related - products - section">
      <h2>相关推荐商品</h2>
      <ul>
        <li v-for="relatedProduct in relatedProducts" :key="relatedProduct.id">
          <img :src="relatedProduct.imageUrl" alt="related product">
          <p>{{ relatedProduct.name }}</p>
          <p>{{ relatedProduct.price }}</p>
        </li>
      </ul>
    </div>
  </template>
</template>

<script>
export default {
  data() {
    return {
      product: {
        mainImageUrl: 'main.jpg',
        otherImageUrls: [
          { id: 1, url: 'sub1.jpg' },
          { id: 2, url: 'sub2.jpg' }
        ],
        name: '商品名称',
        price: 500,
        stock: 100,
        description: '这是一个商品描述...'
      },
      relatedProducts: [
        { id: 1, name: '相关商品1', price: 300, imageUrl: 'rel1.jpg' },
        { id: 2, name: '相关商品2', price: 400, imageUrl: 'rel2.jpg' }
      ]
    }
  }
}
</script>

<style scoped>
.product - image - section {
  width: 100%;
  display: flex;
  justify - content: space - around;
}

.product - sub - image {
  width: 80px;
  height: 80px;
  margin: 10px;
}

.product - basic - info - section {
  margin: 20px;
}

.product - name {
  font - size: 24px;
}

.product - price {
  color: red;
  font - size: 20px;
}

.product - description - section {
  margin: 20px;
}

.related - products - section {
  margin: 20px;
}
</style>

通过 Vue Fragment,商品详情页各部分结构清晰,CSS 样式可以直接针对各个区域进行编写,提高了代码的可读性和维护性。

后台管理系统的用户列表页

在后台管理系统的用户列表页开发中,我们需要展示用户列表,同时在页面顶部有搜索框和一些筛选条件按钮,底部有分页导航。

用户列表部分需要展示用户头像、用户名、用户角色等信息,并且可以对用户进行编辑、删除等操作。搜索框用于根据用户名或其他条件搜索用户,筛选条件按钮可以根据用户角色等条件进行筛选,分页导航用于分页展示用户列表。

传统方式下,为了满足单一根节点要求,可能会在整个页面结构外面包裹一个大的 <div> 元素,将搜索框、用户列表和分页导航都放在里面。但这样在样式布局和组件复用方面会存在一些问题。

使用 Vue Fragment 后,代码如下:

<template>
  <template>
    <div class="search - and - filter - section">
      <input type="text" placeholder="搜索用户" v - model="searchText">
      <button v - for="filter in filters" :key="filter.id" @click="applyFilter(filter)">{{ filter.name }}</button>
    </div>
    <div class="user - list - section">
      <ul>
        <li v - for="user in filteredUsers" :key="user.id">
          <img :src="user.avatarUrl" alt="user">
          <p>{{ user.username }}</p>
          <p>{{ user.role }}</p>
          <button @click="editUser(user)">编辑</button>
          <button @click="deleteUser(user)">删除</button>
        </li>
      </ul>
    </div>
    <div class="pagination - section">
      <button @click="prevPage">上一页</button>
      <span>{{ currentPage }}</span>
      <button @click="nextPage">下一页</button>
    </div>
  </template>
</template>

<script>
export default {
  data() {
    return {
      searchText: '',
      filters: [
        { id: 1, name: '管理员' },
        { id: 2, name: '普通用户' }
      ],
      users: [
        { id: 1, username: 'user1', role: '管理员', avatarUrl: 'avatar1.jpg' },
        { id: 2, username: 'user2', role: '普通用户', avatarUrl: 'avatar2.jpg' }
      ],
      currentPage: 1,
      itemsPerPage: 10
    }
  },
  computed: {
    filteredUsers() {
      let filtered = this.users;
      if (this.searchText) {
        filtered = filtered.filter(user => user.username.includes(this.searchText));
      }
      // 这里省略根据筛选条件进一步过滤的逻辑
      return filtered;
    }
  },
  methods: {
    applyFilter(filter) {
      // 应用筛选条件逻辑
      console.log(`应用筛选条件: ${filter.name}`);
    },
    editUser(user) {
      // 编辑用户逻辑
      console.log(`编辑用户: ${user.username}`);
    },
    deleteUser(user) {
      // 删除用户逻辑
      console.log(`删除用户: ${user.username}`);
    },
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
      }
    },
    nextPage() {
      // 这里省略根据总页数判断是否能下一页的逻辑
      this.currentPage++;
    }
  }
}
</script>

<style scoped>
.search - and - filter - section {
  padding: 20px;
  background - color: lightblue;
}

.user - list - section {
  padding: 20px;
}

.pagination - section {
  padding: 20px;
  text - align: center;
}
</style>

通过 Vue Fragment,页面各部分功能模块清晰,样式和逻辑的维护更加方便,不同部分的组件复用也更加容易实现。

在实际项目开发中,Vue Fragment 能够有效地解决许多因单一根节点要求而带来的结构和样式问题,合理地运用它可以提升代码的质量和开发效率。无论是小型项目还是大型复杂项目,都可以根据具体需求充分发挥 Vue Fragment 的优势。同时,在使用过程中要注意与其他 Vue 特性的结合,确保项目的整体架构清晰、易于维护。