Web前端-Vue2+Vue3基础入门到实战项目-Day4(组件的三大组成部分, 组件通信, 案例-组件版小黑记事本, 进阶语法)

Web前端-Vue2+Vue3基础入门到实战项目-Day4

  • 组件的三大组成部分(结构/样式/逻辑)
    • scoped样式冲突
    • data是一个函数
  • 组件通信
    • 组件通信语法
    • 父传子
    • 子传父
    • props详解
      • 什么是props
      • props检验
      • props与data的区别
    • 非父子(扩展)
      • 事件总线 (event bus)
      • provide - inject
  • 案例 - 小黑记事本(组件版)
    • App.vue
    • TodoHeader.vue
    • TodoMain.vue
    • TodoFooter.vue
  • 进阶语法
    • v-model详解
      • v-model原理
      • 表单类组件封装
      • v-model简化代码
    • sync修饰符
    • ref和$refs
    • $nextTick
  • 来源

组件的三大组成部分(结构/样式/逻辑)

scoped样式冲突

  • 全局样式: 默认的style样式, 会作用于全局
  • 局部样式: 加上scoped属性的style样式, 只会作用于当前组件
  • scoped原理:
    1. 给当前组件模板的所有元素, 添加一个自定义属性
      data-v-hash值: 根据hash值区分不同的组件
    2. css选择器后面, 被自动处理, 添加上了属性选择器
      div[data-v-hash]
<template>
  <div class="base-one">
    BaseOne
  div>
template>

<script>
export default {

}
script>

<style scoped>
div {
  border: 3px solid blue;
  margin: 30px;
}
style>

data是一个函数

  • data必须是一个函数 -> 保证每个组件实例, 维护独立的一个数据对象
  • 每次创建新的组件实例, 都会新执行一次data函数, 得到一个新对象
<template>
  <div class="base-count">
    <button @click="count--">-button>
    <span>{{ count }}span>
    <button @click="count++">+button>
  div>
template>

<script>
export default {
  data() {
    return {
      count: 100,
    }
  },
}
script>

<style>
.base-count {
  margin: 20px;
}
style>

组件通信

组件通信语法

  • 组件关系和对应的通信方案
    • 父子关系: props, $emit
    • 非父子关系: provide, injecteventbus
    • 通用方案: vuex
  • 父子通信方案的核心流程
    • 父传子props:
      1. 父中给子添加属性传值
      2. 子props接收
      3. 使用
    • 子传父$emit
      1. 子$emit发送消息
      2. 父中给子添加消息监听
      3. 父中实现处理函数

父传子

<template>
  <div class="app" style="border: 3px solid #000; margin: 10px">
    我是APP组件
    
    <Son :title="myTitle">Son>
  div>
template>

<script>
import Son from "./components/Son.vue"
export default {
  name: "App",
  components: {
    Son,
  },
  data() {
    return {
      myTitle: "学前端,就来黑马程序员",
    }
  },
}
script>

<style>
style>


<template>
  <div class="son" style="border:3px solid #000;margin:10px">
    
    我是Son组件 {{title}}
  div>
template>

<script>
export default {
  name: 'Son-Child',
  // 2.通过props来接受
  props: ['title']
}
script>

<style>

style>

子传父

<template>
  <div class="app" style="border: 3px solid #000; margin: 10px">
    我是APP组件
    
    <Son :title="myTitle" @changeTitle="handleChange">Son>
  div>
template>

<script>
import Son from "./components/Son.vue"
export default {
  name: "App",
  components: {
    Son,
  },
  data() {
    return {
      myTitle: "学前端,就来黑马程序员",
    }
  },
  methods: {
    // 3. 提供处理函数, 提供逻辑
    handleChange(newTitle){
      this.myTitle = newTitle
    }
  }
}
script>

<style>
style>


<template>
  <div class="son" style="border:3px solid #000;margin:10px">
    我是Son组件 {{title}}
    <button @click="changeFn">修改titlebutton>
  div>
template>

<script>
export default {
  name: 'Son-Child',
  props: ['title'],
  methods: {
    changeFn(){
      // 1. 通过$emit, 向父组件发送消息通知
      this.$emit('changeTitle', "传智教育")
    }
  }
}
script>

<style>

style>

props详解

什么是props

  • 定义: 组件上注册的一些自定义属性
  • 作用: 向子组件传递数据
  • 特点:
    • 可以传递任意数量的prop
    • 可以传递任意类型的prop

父组件

<template>
  <div class="app">
    <UserInfo
      :username="username"
      :age="age"
      :isSingle="isSingle"
      :car="car"
      :hobby="hobby"
    >UserInfo>
  div>
template>

<script>
import UserInfo from './components/UserInfo.vue'
export default {
  data() {
    return {
      username: '小帅',
      age: 28,
      isSingle: true,
      car: {
        brand: '宝马',
      },
      hobby: ['篮球', '足球', '羽毛球'],
    }
  },
  components: {
    UserInfo,
  },
}
script>

<style>
style>

子组件

<template>
  <div class="userinfo">
    <h3>我是个人信息组件h3>
    <div>姓名:{{username}} div>
    <div>年龄:{{age}} div>
    <div>是否单身:{{isSingle ? '是' : '否'}} div>
    <div>座驾:{{car.brand}} div>
    <div>兴趣爱好:{{hobby.join(', ')}} div>
  div>
template>

<script>
export default {
  props: ['username', 'age', 'isSingle', 'car', 'hobby']
}
script>

<style>
.userinfo {
  width: 300px;
  border: 3px solid #000;
  padding: 20px;
}
.userinfo > div {
  margin: 20px 10px;
}
style>

props检验

  • 作用: 为组件的prop指定验证要求, 不符合要求, 控制台会有错误提示
  • 语法
    1. 类型检验
    2. 非空检验
    3. 默认值
    4. 自定义检验

父组件

<template>
  <div class="app">
    <BaseProgress :w="width">BaseProgress>
  div>
template>

<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
  data() {
    return {
      width: 23,
    }
  },
  components: {
    BaseProgress,
  },
}
script>

<style>
style>

子组件

<template>
  <div class="base-progress">
    <div class="inner" :style="{ width: w + '%' }">
      <span>{{ w }}%span>
    div>
  div>
template>

<script>
export default {
  // props: ["w"],
  // 1.基础写法(类型校验)
  // props: {
  //   w: Number // Number String Boolean Array Object
  // }
  // 2.完整写法(类型、是否必填、默认值、自定义校验)
  props: {
    w: {
      type: Number,
      // required: true
      default: 0,
      validator (value) {
        if(value >= 0 && value <= 100){
          return true
        }
        console.error('传入的prop w, 必须是0-100的数字')
        return false
      }
    }
  }
}
script>

<style scoped>
.base-progress {
  height: 26px;
  width: 400px;
  border-radius: 15px;
  background-color: #272425;
  border: 3px solid #272425;
  box-sizing: border-box;
  margin-bottom: 30px;
}
.inner {
  position: relative;
  background: #379bff;
  border-radius: 15px;
  height: 25px;
  box-sizing: border-box;
  left: -3px;
  top: -2px;
}
.inner span {
  position: absolute;
  right: 0;
  top: 26px;
}
style>

props与data的区别

  • 共同点: 都可以给组件提供数据
  • 区别:
    • data的数据是自己的 -> 随便改
    • prop的数据是外部的 -> 不能直接改, 要遵循单向数据流
  • 单向数据流: 父级prop的数据更新, 会向下流动, 影响子组件. 这个数据流动是单向的.

父组件

<template>
  <div class="app">
    <BaseCount 
      @changeCount="handleChange"
      :count="count">
    BaseCount>
  div>
template>

<script>
import BaseCount from './components/BaseCount.vue'
export default {
  components:{
    BaseCount
  },
  data(){
    return {
      count:100
    }
  },
  methods:{
    handleChange(value){
      this.count = value
    }
  }
}
script>

<style>

style>

子组件

<template>
  <div class="base-count">
    <button @click="handleSub">-button>
    <span>{{ count }}span>
    <button @click="handleAdd">+button>
  div>
template>

<script>
export default {
  // 1.自己的数据随便修改  (谁的数据 谁负责)
  // data () {
  //   return {
  //     count: 100,
  //   }
  // },
  // 2.外部传过来的数据 不能随便修改

  // 单向数据流: 父组件的prop更新, 会单向向下流动, 影响到子组件.
  props: {
    count: Number
  },
  methods: {
    handleAdd(){
      this.$emit('changeCount', this.count+1)
    },
    handleSub(){
      this.$emit('changeCount', this.count-1)
    }
  }
 
}
script>

<style>
.base-count {
  margin: 20px;
}
style>

非父子(扩展)

事件总线 (event bus)

  • 作用: 非父子组件之间, 进行简易消息传递(复杂场景 -> vuex)
  • 语法:
    1. 创建一个都能访问的事件总线(空vue实例) -> utils/EventBus.js
    import Vue from 'vue'
    const Bus  =  new Vue()
    export default Bus
    
    1. A组件(接受方), 监听Bus实例的事件
    created() {
      Bus.$on('sendMsg', (msg) => {
        // console.log(msg)
        this.msg = msg
      })
    }
    
    1. B组件(发送方), 触发Bus实例的事件
    Bus.$emit('sendMsg', '今天天气不错,适合旅游')
    

provide - inject

  • 作用: 跨层级共享数据
  • 语法:
    1. 父组件provide提供数据
    provide() {
      return {
        // 简单类型 是非响应式的
        color: this.color,
        // 复杂类型 是响应式的
        userInfo: this.userInfo,
      }
    }
    
    1. 子/孙组件 inject 取值使用
    <script>
    export default {
      inject: ['color', 'userInfo'],
    }
    </script>
    

案例 - 小黑记事本(组件版)

核心步骤

  1. 拆分基础组件
    新建组件 -> 拆分存放结构 -> 导入注册使用
  2. 渲染待办任务
    提供数据(公共父组件) -> 父传子传递list -> v-for渲染
  3. 添加任务
    收集数据v-model -> 监听事件 -> 子传父传递任务 -> 父组件unshift
  4. 删除任务
    监听删除id -> 子传父传递id -> 父组件filter删除
  5. 底部合计和清空功能
    底部合计: 父传子list -> 合计展示
    清空功能: 监听点击 -> 子传父通知父组件 -> 父组件清空
  6. 持久化存储: watch监视数据变化, 持久化到本地

App.vue

<template>
  
  <section id="app">
    <TodoHeaderVue @add="handleAdd">TodoHeaderVue>
    <TodoMainVue @del="handleDel" :list="list">TodoMainVue>
    <TodoFooterVue @clear="handleClear" :list="list">TodoFooterVue>
  section>
template>

<script>
import TodoHeaderVue from './components/TodoHeader.vue'
import TodoMainVue from './components/TodoMain.vue'
import TodoFooterVue from './components/TodoFooter.vue'

// 渲染功能:
// 1. 提供数据-> 提供在公共的父组件 App.vue
// 2. 通过父传子, 奖数据传递给 TodoMain
// 3. 利用v-for渲染

// 添加功能
// 1. 收集表单数据 -> v-model
// 2. 监听事件 (回车 + 点击都要进行添加)
// 3. 子传父, 将任务名称传递给父组件App.vue
// 4. 进行添加 unshift

// 删除功能
// 1. 监听事件 (监听删除的点击) 携带id
// 2. 子传父, 将删除的id传递给父组件App.vue
// 3. 进行删除 filter

// 底部合计: 父传子list -> 渲染
// 清空功能: 子传父 通知父组件 -> 父组件进行清空
// 持久化存储: watch深度监视list的变化 -> 往本地存储 -> 进入页面优先读取本地存储
export default {
  data () {
    return {
      list: JSON.parse(localStorage.getItem('list')) || [
        {id: 1, name: '打篮球'},
        {id: 2, name: '看电影'},
        {id: 3, name: '逛街'},
      ]
    }
  },
  methods: {
    handleAdd(todoName){
      this.list.unshift({
        id: +new Date(),
        name: todoName
      })
    },
    handleDel(id){
      this.list = this.list.filter(item => item.id!==id)
    },
    handleClear(){
      this.list = []
    }
  },
  watch: {
    list: {
      deep: true,
      handler(newValue){
        localStorage.setItem('list', JSON.stringify(newValue))
      }
    }
  },
  components: {
    TodoHeaderVue,
    TodoMainVue,
    TodoFooterVue
  }
}
script>

<style>

style>

TodoHeader.vue

<template>
  <div>
    
    <header class="header">
      <h1>小黑记事本h1>
      <input 
        v-model.trim="todoName" 
        @keyup.enter="handleAdd" placeholder="请输入任务" class="new-todo"/>
      <button @click="handleAdd" class="add">添加任务button>
    header>
  div>
template>

<script>
export default {
  data(){
    return {
      todoName: ''
    }
  },
  methods: {
    handleAdd(){
      if(this.todoName.trim() === ''){
        alert('任务名称不能为空')
        return 
      }
      this.$emit('add', this.todoName)
      this.todoName = ''
    }
  }
}
script>

<style>
style>

TodoMain.vue

<template>
  <div>
    
    <section class="main">
      <ul class="todo-list">
        <li class="todo" v-for="(item, index) in list" :key="item.id">
          <div class="view">
            <span class="index"> {{index+1}}. span> 
            <label> {{item.name}} label>
            <button @click="handleDel(item.id)" class="destroy">button>
          div>
        li>
      ul>
    section>
  div>
template>

<script>
export default {
  props: {
    list: Array
  },
  methods: {
    handleDel(id){
      this.$emit('del', id)
    }
  }
}
script>

<style>

style>

TodoFooter.vue

<template>
  <div>
    
    <footer class="footer">
      
      <span class="todo-count">合 计:<strong> {{list.length}} strong>span>
      
      <button @click="clear" class="clear-completed">
        清空任务
      button>
    footer>
  div>
template>

<script>
export default {
  props: {
    list: Array
  },
  methods: {
    clear(){
      this.$emit('clear')
    }
  }
}
script>

<style>

style>

进阶语法

v-model详解

v-model原理

  • 原理: v-model本质上是一个语法糖. 例如应用在输入框上, 就是value属性和input事件的合写.
  • 作用: 提供数据的双向绑定
    1. 数据发生变化, 视图自动变化: value
    2. 视图发生变化, 数据自动变化: @input
  • $event: 用在模板中, 获取事件的形参
<div class="app">
  <input v-model="msg1" type="text" /> <br />
  <input :value="msg2" @input="msg2 = $event.target.value" type="text" >
div>

表单类组件封装

实现子组件和父组件数据的双向绑定

  • 父传子: 数据 由父组件props传递, v-model拆解绑定数据
  • 子传父: 监听输入, 子传父传值给父组件修改

父组件

<template>
  <div class="app">
    <BaseSelect :selectId="selectId" @change="selectId = $event">BaseSelect>
  div>
template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '102',
    }
  },
  components: {
    BaseSelect,
  },
  methods: {
    
  }
}
script>

<style>
style>

子组件

<template>
  <div>
    <select :value="selectId" @change="handleChange">
      <option value="101">北京option>
      <option value="102">上海option>
      <option value="103">武汉option>
      <option value="104">广州option>
      <option value="105">深圳option>
    select>
  div>
template>

<script>
export default {
  props: {
    selectId: String
  },
  methods: {
    handleChange(e){
      this.$emit('change', e.target.value)
    }
  }
}
script>

<style>
style>

v-model简化代码

父组件v-model简化实现子组件和父组件数据双向绑定

  • 子组件: props通过value接收, 事件触发input
  • 父组件: v-model绑定数据 (:value + @input)

父组件

<template>
  <div class="app">
    <BaseSelect v-model="selectId">BaseSelect>
  div>
template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '102',
    }
  },
  components: {
    BaseSelect,
  },
}
script>

<style>
style>

子组件

<template>
  <div>
    <select :value="value" @change="handleChange">
      <option value="101">北京option>
      <option value="102">上海option>
      <option value="103">武汉option>
      <option value="104">广州option>
      <option value="105">深圳option>
    select>
  div>
template>

<script>
export default {
  props: {
    value: String
  },
  methods: {
    handleChange(e){
      this.$emit('input', e.target.value)
    }
  }
}
script>

<style>
style>

sync修饰符

  • 作用: 实现子组件与父组件的数据双向绑定, 简化代码
  • 特点: prop属性名, 可以自定义, 非固定为value
  • 场景: 封装弹框类的基础组件, visible属性 true显示 false隐藏
  • 本质: :属性名 + @update:属性名

父组件

<template>
  <div class="app">
    <button @click="isShow = true">退出按钮button>
    <BaseDialog :visible.sync="isShow">BaseDialog>
  div>
template>

<script>
import BaseDialog from "./components/BaseDialog.vue"
export default {
  data() {
    return {
      isShow: false
    }
  },
  methods: {
    
  },
  components: {
    BaseDialog,
  },
}
script>

<style>
style>

子组件

<template>
  <div v-show="visible" class="base-dialog-wrap">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:h3>
        <button @click="close" class="close">xbutton>
      div>
      <div class="content">
        <p>你确认要退出本系统么?p>
      div>
      <div class="footer">
        <button>确认button>
        <button>取消button>
      div>
    div>
  div>
template>

<script>
export default {
  props: {
    visible: Boolean
  },
  methods: {
    close(){
      this.$emit('update:visible', false)
    }
  }
}
script>

<style scoped>
.base-dialog-wrap {
  width: 300px;
  height: 200px;
  box-shadow: 2px 2px 2px 2px #ccc;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 0 10px;
}
.base-dialog .title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 2px solid #000;
}
.base-dialog .content {
  margin-top: 38px;
}
.base-dialog .title .close {
  width: 20px;
  height: 20px;
  cursor: pointer;
  line-height: 10px;
}
.footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 26px;
}
.footer button {
  width: 80px;
  height: 40px;
}
.footer button:nth-child(1) {
  margin-right: 10px;
  cursor: pointer;
}
style>

ref和$refs

  • 作用: 通过ref$refs可以获取dom元素和组件实例
  • 使用:
    1. 目标组件 - 添加ref属性
    <div ref="test">div>
    
    1. 通过this.$refs.ref属性值获取目标组件
    this.$refs.test
    
  • 获取dom
    <div ref="mychart" class="base-chart-box">子组件</div>
    
    const myChart = echarts.init(this.$refs.mychart)
    
  • 获取组件
    父组件
    <template>
      <div class="app">
        <BaseForm ref="baseFrom">BaseForm>
        <button @click="handleGet">获取数据button>
        <button @click="handleReset">重置数据button>
      div>
      
    template>
    
    <script>
    import BaseForm from './components/BaseForm.vue'
    export default {
      components: {
        BaseForm,
      },
      methods: {
        handleGet(){
          console.log(this.$refs.baseFrom.getValues())
        },
        handleReset(){
          this.$refs.baseFrom.resetValues()
        }
      }
    }
    script>
    
    <style>
    style>
    
    子组件
    <template>
      <div class="app">
        <div>
          账号: <input v-model="username" type="text">
        div>
        <div>
          密码: <input v-model="password" type="text">
        div>
      div>
    template>
    
    <script>
    export default {
      data() {
        return {
          username: 'admin',
          password: '123456',
        }
      },
      methods: {
        getValues() {
          return {
            username: this.username,
            password: this.password
          }
        },
        resetValues() {
          this.username = ''
          this.password = ''
          console.log('重置表单数据成功');
        },
      }
    }
    script>
    
    <style scoped>
    .app {
      border: 2px solid #ccc;
      padding: 10px;
    }
    .app div{
      margin: 10px 0;
    }
    .app div button{
      margin-right: 8px;
    }
    style>
    

$nextTick

  • Vue是异步更新DOM的
  • $nextTick: 在DOM更新完成之后做某件事
<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="inp" />
      <button>确认button>
    div>
    <div v-else>
      <span>{{ title }}span>
      <button @click="handleEdit">编辑button>
    div>
  div>
template>

<script>
export default {
  data() {
    return {
      title: '大标题',
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
    handleEdit(){
      // 1. 显示输入框 (异步dom更新)
      this.isShowEdit = true
      // 2. 让输入框显示焦点
      // console.log(this.$refs.inp) // undefined
      
      this.$nextTick(()=>{
        this.$refs.inp.focus()
      })
        
    }
  },
}
script>

<style>
style>

来源

黑马程序员. Vue2+Vue3基础入门到实战项目

你可能感兴趣的:(Web前端,前端,vue.js,组件通信,javascript,组件组成,nextTick,ref,refs)