Code前端首页关于Code前端联系我们

Vue3组件该怎么学?从基础到实战的通关思路有哪些?

terry 2周前 (09-09) 阅读数 37 #Vue

想学Vue3组件却不知道从哪发力?组件作为Vue框架的核心复用单元,Vue3在语法、性能、开发体验上都有不少更新,不少开发者刚上手时会纠结“组合式API怎么和组件结合?”“新特性怎么用在实际封装里?”“大型项目组件拆分有啥规律?” 这篇文章把Vue3组件学习拆成「基础认知→核心能力→实战技巧→生态联动」四个模块,用白话问答帮你把知识点串成能用的思路。

先搞懂Vue3组件和Vue2的核心区别?

Vue3组件和Vue2组件最大的区别,得从「代码组织逻辑」和「底层性能」两个维度看。

在Vue2里,我们用选项式APIdatamethodscomputed这些选项)写组件,代码按“功能分类”——数据放data,方法放methods,计算属性放computed,这种写法适合小项目,但组件逻辑复杂后,比如一个组件要处理表单验证、接口请求、定时任务,代码会分散在不同选项里,找个方法得在datamethods之间来回跳,维护起来像“拆盲盒”。

Vue3的组合式APIsetup函数、refreactive这些)是按“逻辑关注点”组织代码——把相关逻辑(获取用户信息+处理用户权限”)集中写成一个函数,甚至抽成独立的composables文件,举个例子,以前Vue2里处理用户信息要写:

// Vue2 选项式API
export default {
  data() { return { user: null } },
  methods: { fetchUser() { /* 请求逻辑 */ } },
  mounted() { this.fetchUser() }
}

Vue3组合式API可以这么写:

// Vue3 组合式API
<script setup>
const user = ref(null)
const fetchUser = async () => {
  user.value = await api.getUser()
}
onMounted(fetchUser)
</script>

你看,请求用户信息的逻辑全堆在一起,不用在不同选项里跳转,这种写法对复杂组件太友好了,逻辑拆分和复用都更顺手。

底层性能上,Vue3的组件渲染基于Proxy做响应式(Vue2用Object.defineProperty),Proxy能直接监听对象、数组的变化,不用像Vue2那样递归遍历+重写数组方法,大数据列表场景下性能提升明显,而且Vue3新增了像Teleport(把组件渲染到任意DOM节点,比如弹窗挂到body)、Suspense(异步组件加载时的占位逻辑)这些特性,组件的灵活性和工程化能力直接起飞。

那要不要彻底放弃选项式API?没必要!Vue3完全兼容选项式写法,如果你维护老项目,或者小项目想快速开发,选项式照样能用;但新项目建议优先学组合式,因为它更适合复杂逻辑拆分,也是Vue生态未来的方向。

Vue3组件核心知识:Props、Emits、插槽、状态管理怎么玩?

写组件绕不开传值、通信、内容分发这些事儿,Vue3把这些能力做了升级,咱们逐个拆解。

Props:传值更严谨,控制更灵活

Vue3里Props的「严谨性」和「灵活性」都变强了。

先看类型约束:以前Vue2里Props类型写String、Number这些字符串,Vue3支持直接写构造函数(比如String、Number,甚至自定义类),还能做更细的验证——用对象形式配置requireddefaultvalidator,举个例子:

const props = defineProps({   {  
    type: String,        // 类型是字符串  
    required: true,     // 必须传  
    validator: (val) => val.length > 3 // 自定义验证:长度必须>3  
  },  
  pageSize: {  
    type: Number,  
    default: 10         // 默认值  
  }  
})  

这样传参出错时,控制台会直接报错,比Vue2的提示更直观,团队协作时减少传参歧义。

再看Props透传(inheritAttrs:如果父组件给子组件传了没在defineProps里声明的属性,默认会加到子组件根元素上,但有时候我们想自己控制(比如组件根元素是个label,不想让class、style自动继承,而是加到内部的input上),就可以这么玩:

<script setup>  
import { useAttrs } from 'vue'  
defineOptions({ inheritAttrs: false }) // 关闭自动继承  
const attrs = useAttrs() // 拿到父组件传的未声明属性  
</script>  
<template>  
  <label>  
    <input v-bind="attrs" /> <!-- 手动把属性绑到input上 -->  
  </label>  
</template>  

父组件用<CustomInput class="red-border" data-testid="input" />时,classdata-testid就会精准作用到input上,不会乱加到根节点label,这种细粒度控制在封装表单组件时特别实用。

Emits:事件通信更规范

Vue3强制要求用defineEmits声明自定义事件,好处是「代码即文档」+「参数验证」,比如做一个分页组件<Pagination>,需要触发'change'事件传当前页码,就可以:

const emit = defineEmits({  
  change: (page: number) => {  
    if (typeof page !== 'number' || page < 1) {  
      console.warn('页码必须是大于0的数字')  
      return false // 验证失败,控制台报错  
    }  
    return true  
  }  
})  
// 调用事件
emit('change', 5) // 合法  
emit('change', 0) // 触发验证,控制台报错  

父组件用<Pagination @change="handleChange" />时,再也不用猜“change事件传什么参数?”,代码可读性和鲁棒性直接拉满。

分发玩出花

分发的关键,Vue3里插槽的玩法更灵活,分三类:

  • 默认插槽:子组件用<slot></slot>占位,父组件直接写内容;
  • 具名插槽:子组件用<slot name="header"></slot>,父组件用<template #header>,适合一个组件里插多个区域(比如弹窗的头部、主体、底部);
  • 作用域插槽:子组件把数据传给父组件的插槽模板,实现「子传父」的反向传值。

举个作用域插槽的复杂例子:写一个<TableWithExpand>组件,支持点击行展开详情,子组件结构:

<template>  
  <table>  
    <tr v-for="item in list" @click="item.isExpanded = !item.isExpanded">  
      <td>{{ item.title }}</td>  
      <td>  
        <slot name="expand" :item="item" v-if="item.isExpanded"></slot>  
      </td>  
    </tr>  
  </table>  
</template>  
<script setup>  
defineProps({ list: Array })  
</script>  

父组件用的时候,自定义展开区域的内容:

<TableWithExpand :list="tableData">  
  <template #expand="{ item }">  
    <div class="expand-content">  
      <p>详情:{{ item.detail }}</p>  
      <Button @click="handleDetail(item.id)">查看更多</Button>  
    </div>  
  </template>  
</TableWithExpand>  

子组件负责管理展开状态,父组件负责渲染展开内容,这种「状态内聚+内容外抛」的设计,让组件既可控又灵活,很多开源UI库的复杂组件都是这么玩的。

组合式API:状态管理=逻辑乐高

组合式API的核心是把重复逻辑抽成composables(可以理解为“逻辑函数”),比如有个获取用户信息+权限判断的逻辑,在多个组件里要用,就写个usePermission.js

// usePermission.js  
import { ref, onMounted } from 'vue'  
import { useUserStore } from './stores/user' // 假设用Pinia存用户信息  
export function usePermission() {  
  const user = useUserStore().user  
  const hasPermission = (key) => {  
    return user.roles.some(role => role.permissions.includes(key))  
  }  
  // 异步获取最新权限  
  const fetchPermission = async () => {  
    const res = await api.getPermission()  
    useUserStore().setPermissions(res.data)  
  }  
  onMounted(fetchPermission)  
  return { hasPermission, fetchPermission }  
}  

然后在组件里复用:

<script setup>  
import { usePermission } from './usePermission'  
const { hasPermission } = usePermission()  
</script>  
<template>  
  <Button v-if="hasPermission('delete')">删除</Button>  
</template>  

把权限判断逻辑从组件里抽走,多个组件复用usePermission,既减少重复代码,又让组件只关注渲染,维护起来轻松太多,这就是Vue3「逻辑复用」的灵魂——像搭乐高一样组合逻辑。

实战阶段:怎么封装高质量Vue3组件?

封装组件不是堆代码,得先想清楚「谁用这个组件?用它解决啥问题?」,咱们从UI组件业务组件性能优化三个角度拆解。

UI组件封装:以Button为例

UI组件(比如Button、Dialog)的核心是「通用性+扩展性」,以Button为例,要考虑这些点:

  • 功能分层:基础样式(大小、颜色、圆角)用Props控制,比如size(small/medium/large)、type(primary/danger);交互逻辑(点击防抖、加载状态)用组合式API处理。
  • 扩展性:留插槽给自定义内容(比如按钮里加图标 <Button><Icon /></Button>);用透传attrs处理classstyle,让用户能覆盖样式。
  • 可访问性:加aria-label、role这些属性,适配屏幕阅读器(比如按钮加aria-label="提交表单",盲人用户用屏幕阅读器能听到描述)。
  • 样式可控:用CSS变量+Scoped CSS+深度选择器,让用户能自定义主题又不破坏组件结构。

代码示例(加载状态+样式自定义):

<script setup>  
const props = defineProps({  
  loading: Boolean,  
  type: { type: String, default: 'primary' },  
  size: { type: String, default: 'medium' }  
})  
const emit = defineEmits(['click'])  
const handleClick = () => {  
  if (props.loading) return  
  emit('click')  
}  
</script>  
<template>  
  <button  
    :class="[  
      'button',  
      `button--${props.type}`,  
      `button--${props.size}`  
    ]"  
    :disabled="props.loading"  
    @click="handleClick"  
  >  
    <slot></slot>  
    <Spinner v-if="props.loading" /> <!-- 假设Spinner是加载组件 -->  
  </button>  
</template>  
<style scoped>  
.button {  
  --btn-primary-color: #42b983; /* 定义CSS变量,用户可覆盖 */  
  --btn-danger-color: #ff4d4f;  
  background-color: var(--btn-primary-color);  
  /* 其他基础样式 */  
}  
.button--primary { background-color: var(--btn-primary-color); }  
.button--danger { background-color: var(--btn-danger-color); }  
/* 深度选择器,让用户能覆盖样式 */  
:deep(.custom-class) {  
  border-radius: 0;  
}  
</style>  

用户用的时候,既能传loading控制状态,又能通过CSS变量换主题,还能加custom-class覆盖样式,组件的扩展性拉满。

业务组件封装:以订单列表为例

业务组件(比如订单列表、表单)更考验「场景抽象」能力,以电商的「订单列表」组件为例,要处理商品展示、数量修改、删除、筛选等逻辑,拆分思路是:

  1. 拆分模块:把「单个订单项」拆成<OrderItem>子组件,「筛选条件」拆成<OrderFilter>子组件;
  2. 逻辑内聚:把筛选逻辑、请求参数处理抽成useOrderFilter.js composable;
  3. 事件驱动:子组件<OrderFilter>触发'filter-change'事件,父组件拿到新条件后请求数据,再传给<OrderList>
  4. 状态管理:用Pinia存储订单数据(如果多个组件共享),或在组件内用ref管理。

代码结构示例:

<!-- OrderList.vue 主组件 -->  
<script setup>  
import { ref } from 'vue'  
import OrderItem from './OrderItem.vue'  
import OrderFilter from './OrderFilter.vue'  
import { useOrderFilter } from './useOrderFilter'  
const props = defineProps({  
  orders: Array  
})  
const { filterConditions, fetchOrders } = useOrderFilter()  
// 父组件监听筛选变化,重新请求数据  
function handleFilterChange(newConditions) {  
  fetchOrders(newConditions).then(res => {  
    // 更新orders逻辑...  
  })  
}  
</script>  
<template>  
  <OrderFilter @filter-change="handleFilterChange" />  
  <div class="order-list">  
    <OrderItem v-for="order in orders" :order="order" :key="order.id" />  
  </div>  
</template>  
<!-- OrderFilter.vue 子组件 -->  
<script setup>  
const emit = defineEmits(['filter-change'])  
const handleSubmit = () => {  
  // 收集筛选条件  
  const conditions = { /* 筛选参数 */ }  
  emit('filter-change', conditions)  
}  
</script>  
<template>  
  <div class="filter-bar">  
    <!-- 筛选表单 -->  
    <Button @click="handleSubmit">筛选</Button>  
  </div>  
</template>  
// useOrderFilter.js 逻辑抽离  
export function useOrderFilter() {  
  const filterConditions = ref({})  
  const fetchOrders = async (conditions) => {  
    return await api.getOrders(conditions)  
  }  
  return { filterConditions, fetchOrders }  
}  

这样拆分后,每个模块各司其职:OrderFilter只负责UI和触发事件,useOrderFilter处理逻辑,OrderList做整合,后期要加「导出订单」功能,只需要在useOrderFilter里加exportOrders函数,组件里调用就行,不用动其他模块,维护成本直线下降。

性能优化:让组件飞起来

组件性能差?这些Vue3特性能救场:

  • defineOptions + inheritAttrs:关闭不必要的属性继承(inheritAttrs: false),减少DOM属性更新开销;
  • shallowRef / markRaw:对不依赖响应式的数据,用shallowRef(只监听第一层变化)、markRaw(标记为非响应式),减少响应式追踪的性能损耗;
  • <KeepAlive>:包裹动态组件,配合max属性限制缓存数量,避免重复渲染;
  • defineSlots:显式声明插槽,让Vue编译器提前优化渲染(比如<script setup> defineSlots({ default: () => {} }) </script>);
  • 计算属性+缓存:用computed处理依赖变化的计算逻辑,避免重复计算;对复杂列表,用v-for + :key确保Diff算法高效。

举个shallowRef的例子:处理大对象(比如图表配置项),只需要在替换整个对象时触发更新,内部属性变化不需要响应式:

<script setup>  
import { shallowRef } from 'vue'  
const chartOptions = shallowRef({  
  xAxis: { type: 'category' },  
  series: []  
})  
// 替换整个对象时触发更新(适合大数据场景)  
chartOptions.value = { ...chartOptions.value, series: newSeries }  
// 内部属性变化不触发(减少响应式开销)  
chartOptions.value.series.push(newData) // 这行不会触发更新  
</script>  

这些优化技巧,在处理复杂页面、大数据列表时,能让组件渲染速度翻倍。

Vue3组件怎么和生态工具联动?

Vue3组件不是孤立的,和生态工具结合才能发挥最大威力,咱们看几个关键联动场景:

和Vite:开发体验拉满

Vite的按需导入(配合unplugin-auto-import插件)让组件代码更简洁——不用每次import { ref, reactive } from 'vue',Vite自动帮

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门