Vue笔记(四)--组件基础

这一节了解一下Vue3中的组件基础,组件是Vue中强大的功能之一,通过组件,可以封装出复用性强,扩展性强的HTML元素,并且通过组件可以将复杂的页面元素拆分多个独立的内部组件,方便代码的逻辑分离和管理,简单总结如下:

API

  1. 组件注册(局部)

含义:在当前页面 / 组件内注册子组件,只在当前生效 作用:模块化、拆分页面

javascript 复制代码
<template>
  <Child />
</template>
<script setup>
// 定义子组件
const Child = { template: `<view>我是子组件</view>` }
</script>
  1. 组件注册(全局)

含义:在 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 }
}
  1. props 父传子

含义:父组件 → 子组件传数据 作用:组件传参、配置化

javascript 复制代码
<template>
  <Child title="来自父组件" />
</template>
<script setup>
const Child = {
  props: ['title'],
  template: `<view>标题:{{ title }}</view>`
}
</script>
  1. props 类型校验

含义:限制传入数据类型(String/Number/Boolean/Object/Array) 作用:防止传错、更健壮

javascript 复制代码
<script setup>
const Child = {
  props: {
    num: {
      type: Number,
      required: true
    }
  },
  template: `<view>数字:{{ num }}</view>`
}
</script>
  1. props 默认值

含义:没传参时自动用默认值 作用:避免页面报错

javascript 复制代码
<script setup>
const Child = {
  props: {
    title: {
      type: String,
      default: '默认标题'
    }
  },
  template: `<view>{{ title }}</view>`
}
</script>
  1. props 自定义校验 validator

含义:自定义规则校验参数 作用:强约束传参

javascript 复制代码
<script setup>
const Child = {
  props: {
    score: {
      validator: (v) => v >= 0 && v <= 100
    }
  },
  template: `<view>分数:{{ score }}</view>`
}
</script>
  1. $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>
  1. 组件 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>
  1. 匿名插槽 slot

含义:父组件往子组件插入内容 作用:容器组件、布局组件

javascript 复制代码
<template>
  <Child>我是插入的内容</Child>
</template>
<script setup>
const Child = {
  template: `
    <view style="border:1px solid #eee">
      <slot />
    </view>
  `
}
</script>
  1. 具名插槽

含义:多个插槽按名字分发作用:头部、主体、底部区分

javascript 复制代码
<template>
  <Child>
    <template #header>头部</template>
    <template #default>主体</template>
  </Child>
</template>
<script setup>
const Child = {
  template: `
    <view>
      <slot name="header" />
      <slot />
    </view>
  `
}
</script>
  1. 插槽默认内容

含义:父组件没插内容,显示默认 作用:增强容错

javascript 复制代码
<script setup>
const Child = {
  template: `<slot>默认内容</slot>`
}
</script>
  1. 动态组件 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>
  1. 组件 data 必须是函数

含义:每个组件实例独立数据 作用:避免多实例互相污染

javascript 复制代码
<script setup>
const Child = {
  data() {
    return { count: 0 }
  },
  template: `<button @click="count++">{{ count }}</button>`
}
</script>
  1. 组件 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>
相关推荐
哈撒Ki1 小时前
快速入门WebSocket
前端·websocket
张元清1 小时前
React 里不用 setTimeout 的计时器写法:useTimeout、useInterval、useCountDown 和 useRafFn
前端·javascript·面试
李白的天不白1 小时前
HMR模块热替换
前端
2601_958492551 小时前
A Technical Log: Hosting Gravity Dunk - HTML5 Casual game
前端·html·html5
. . . . .1 小时前
css module
前端·css
天渺工作室1 小时前
把一篇老文章内容 Vibecoding 成了 npm 包
前端·vue.js·npm
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2026.05.17 题目:1306. 跳跃游戏 III
笔记·leetcode
南城雨落1 小时前
uni-app开发经验分享-跨端开发经验总结
javascript·vue.js·node.js
会编程的土豆1 小时前
Gin 核心概念 & 前后端交互笔记
笔记·交互·gin