这一节了解一下Vue3中的组件基础,组件是Vue中强大的功能之一,通过组件,可以封装出复用性强,扩展性强的HTML元素,并且通过组件可以将复杂的页面元素拆分多个独立的内部组件,方便代码的逻辑分离和管理,简单总结如下:
API
- 组件注册(局部)
含义:在当前页面 / 组件内注册子组件,只在当前生效 作用:模块化、拆分页面
javascript
<template>
<Child />
</template>
<script setup>
// 定义子组件
const Child = { template: `<view>我是子组件</view>` }
</script>
- 组件注册(全局)
含义:在 main.js 注册,全项目可用 作用:高频使用的公共组件
javascript
// main.js
import { createSSRApp } from 'vue'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 全局注册
app.component('GlobalChild', { template: `<view>全局组件</view>` })
return { app }
}
- props 父传子
含义:父组件 → 子组件传数据 作用:组件传参、配置化
javascript
<template>
<Child title="来自父组件" />
</template>
<script setup>
const Child = {
props: ['title'],
template: `<view>标题:{{ title }}</view>`
}
</script>
- props 类型校验
含义:限制传入数据类型(String/Number/Boolean/Object/Array) 作用:防止传错、更健壮
javascript
<script setup>
const Child = {
props: {
num: {
type: Number,
required: true
}
},
template: `<view>数字:{{ num }}</view>`
}
</script>
- props 默认值
含义:没传参时自动用默认值 作用:避免页面报错
javascript
<script setup>
const Child = {
props: {
title: {
type: String,
default: '默认标题'
}
},
template: `<view>{{ title }}</view>`
}
</script>
- props 自定义校验 validator
含义:自定义规则校验参数 作用:强约束传参
javascript
<script setup>
const Child = {
props: {
score: {
validator: (v) => v >= 0 && v <= 100
}
},
template: `<view>分数:{{ score }}</view>`
}
</script>
- $emit 子传父(事件通信)
含义:子组件 → 父组件发送消息 / 数据 作用:子通知父做操作
javascript
<template>
<Child @send="getMsg" />
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('')
const getMsg = (data) => msg.value = data
const Child = {
methods: {
send() { this.$emit('send', '子组件数据') }
},
template: `<button @click="send">点我传值</button>`
}
</script>
- 组件 v-model 双向绑定
含义:让自定义组件支持 v-model 作用:表单类组件封装
javascript
<template>
<MyInput v-model="val" />
</template>
<script setup>
import { ref } from 'vue'
const val = ref('')
const MyInput = {
props: ['modelValue'],
methods: {
input(e) { this.$emit('update:modelValue', e.target.value) }
},
template: `<input :value="modelValue" @input="input" />`
}
</script>
- 匿名插槽 slot
含义:父组件往子组件插入内容 作用:容器组件、布局组件
javascript
<template>
<Child>我是插入的内容</Child>
</template>
<script setup>
const Child = {
template: `
<view style="border:1px solid #eee">
<slot />
</view>
`
}
</script>
- 具名插槽
含义:多个插槽按名字分发作用:头部、主体、底部区分
javascript
<template>
<Child>
<template #header>头部</template>
<template #default>主体</template>
</Child>
</template>
<script setup>
const Child = {
template: `
<view>
<slot name="header" />
<slot />
</view>
`
}
</script>
- 插槽默认内容
含义:父组件没插内容,显示默认 作用:增强容错
javascript
<script setup>
const Child = {
template: `<slot>默认内容</slot>`
}
</script>
- 动态组件 component :is
含义:根据变量切换显示哪个组件 作用:选项卡、多页面切换
javascript
<template>
<button @click="com='A'">A</button>
<button @click="com='B'">B</button>
<component :is="com" />
</template>
<script setup>
import { ref } from 'vue'
const com = ref('A')
const A = { template: `<view>组件A</view>` }
const B = { template: `<view>组件B</view>` }
</script>
- 组件 data 必须是函数
含义:每个组件实例独立数据 作用:避免多实例互相污染
javascript
<script setup>
const Child = {
data() {
return { count: 0 }
},
template: `<button @click="count++">{{ count }}</button>`
}
</script>
- 组件 methods
含义:组件内部方法 作用:事件处理、逻辑封装
javascript
<script setup>
const Child = {
methods: { add() { alert('点击') } },
template: `<button @click="add">点击</button>`
}
</script>
栗子:
javascript
<template>
<view class="container">
<!-- 父传子 -->
<Card title="订单列表" :count="3" @refresh="onRefresh" />
<!-- v-model 组件 -->
<view class="m-10">输入:{{ value }}</view>
<MyInput v-model="value" />
<!-- 具名插槽 -->
<Dialog>
<template #title>提示</template>
确认执行操作?
<template #footer>
<button>确认</button>
</template>
</Dialog>
<!-- 动态组件 -->
<view class="m-10">
<button @click="current='User'">用户</button>
<button @click="current='Order'">订单</button>
<component :is="current" />
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const value = ref('')
const current = ref('User')
const onRefresh = () => uni.showToast({ title: '刷新成功' })
// 1. props + $emit
const Card = {
props: { title: String, count: Number },
methods: { refresh() { this.$emit('refresh') } },
template: `
<view class="card">
<text>{{ title }} ({{ count }})</text>
<button @click="refresh" size="mini">刷新</button>
</view>
`
}
// 2. 组件v-model
const MyInput = {
props: ['modelValue'],
methods: { input(e) { this.$emit('update:modelValue', e.target.value) } },
template: `<input :value="modelValue" @input="input" class="input" />`
}
// 3. 具名插槽
const Dialog = {
template: `
<view class="dialog">
<view class="title"><slot name="title">提示</slot></view>
<view class="content"><slot></slot></view>
<view class="footer"><slot name="footer"></slot></view>
</view>
`
}
// 4. 动态组件
const User = { template: `<view>用户组件</view>` }
const Order = { template: `<view>订单组件</view>` }
</script>
<style scoped>
.container { padding: 30rpx; }
.card { padding: 20rpx; border: 1px solid #eee; margin: 10rpx 0; }
.input { border: 1px solid #eee; padding: 10rpx; margin: 10rpx 0; }
.dialog { border: 1px solid #ccc; padding: 20rpx; margin: 10rpx 0; }
.title { font-weight: bold; }
.footer { display: flex; justify-content: flex-end; margin-top: 10rpx; }
.m-10 { margin: 10rpx 0; }
</style>
javascript
<template>
<view class="container">
<view class="title">Demo</view>
<good-card
title="Vue3实战课程"
price="199"
:isHot="true"
@buy="handleBuy"
@add-cart="handleAddCart"
>
<view class="slot-tip">限时特价</view>
</good-card>
<view class="section-title">自定义 v-model 组件</view>
<custom-input v-model="userName" placeholder="请输入姓名" />
<view class="tip">你输入的是:{{ userName }}</view>
<view class="section-title">动态组件切换</view>
<button @click="currentCom = 'com1'">组件1</button>
<button @click="currentCom = 'com2'">组件2</button>
<component :is="currentCom" class="dynamic-box" />
</view>
</template>
<script setup>
import { ref } from 'vue'
const userName = ref('')
const currentCom = ref('com1')
const handleBuy = (data) => {
uni.showToast({
title: '购买成功:' + data.title,
icon: 'success'
})
}
const handleAddCart = () => {
uni.showToast({
title: '加入购物车',
icon: 'success'
})
}
const goodCard = {
// Props 父传子
props: {
title: {
type: String,
required: true
},
price: {
type: [String, Number],
required: true
},
isHot: {
type: Boolean,
default: false
}
},
// 组件方法
methods: {
// 子传父:$emit 发送事件
buy() {
this.$emit('buy', { title: this.title, price: this.price })
},
addCart() {
this.$emit('add-cart')
}
},
// 模板 + 插槽
template: `
<view class="card">
<view class="card-title">{{ title }}</view>
<view class="price">¥{{ price }}</view>
<view class="tag" v-if="isHot">热卖</view>
<!-- 插槽:父组件内容插入这里 -->
<slot></slot>
<view class="btns">
<button @click="buy" type="primary" size="mini">立即购买</button>
<button @click="addCart" size="mini" style="margin-left:10rpx">加入购物车</button>
</view>
</view>
`
}
const customInput = {
// v-model 固定接收参数
props: ['modelValue', 'placeholder'],
methods: {
// v-model 固定触发事件
onInput(e) {
this.$emit('update:modelValue', e.target.value)
}
},
template: `
<input
:value="modelValue"
:placeholder="placeholder"
@input="onInput"
class="custom-input"
/>
`
}
const com1 = {
template: `<view class="com-box">组件1 - 用户信息</view>`
}
const com2 = {
template: `<view class="com-box">组件2 - 订单列表</view>`
}
</script>
<style scoped>
.container {
padding: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
margin: 40rpx 0 15rpx;
}
/* 商品卡片样式 */
.card {
border: 1rpx solid #eee;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.price {
color: #f30;
font-size: 30rpx;
margin-bottom: 10rpx;
}
.tag {
background: #f30;
color: #fff;
padding: 4rpx 10rpx;
border-radius: 6rpx;
font-size: 22rpx;
display: inline-block;
margin-bottom: 10rpx;
}
.slot-tip {
color: #ff6600;
margin: 10rpx 0;
}
.btns {
margin-top: 20rpx;
display: flex;
}
/* 自定义输入框 */
.custom-input {
border: 1rpx solid #eee;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
}
.tip {
margin-top: 10rpx;
color: #666;
}
/* 动态组件 */
.dynamic-box {
margin-top: 20rpx;
}
.com-box {
padding: 30rpx;
background: #f9f9f9;
border-radius: 12rpx;
}
</style>