首页 前端知识 Vue3全家桶 - Vue3 - 【6】组件(注册组件 组件通信 透传属性和事件 插槽 单文件CSS 依赖注入)

Vue3全家桶 - Vue3 - 【6】组件(注册组件 组件通信 透传属性和事件 插槽 单文件CSS 依赖注入)

2024-04-19 21:04:01 前端知识 前端哥 362 120 我要收藏

组件

一、 注册组件

1.1 ❌ 全局注册

  • 目标文件:main.js
  • 语法
    import { createApp } from 'vue'
    import App from './App.vue'
    const app = createApp(App)
    
    // 全局注册
    app.component('组件名字', 需要注册组件)
    
    app.mount('#app')
    
  • 缺陷:
    • 全局注册,但并没有被使用的组件无法生产打包时被自动移除(也叫tree-shaking)。如果,你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的JS文件中;
    • 全局注册在大型项目中使用项目依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性;

1.2 ✅ 局部注册

  • 哪里注册哪里使用;
  • 选项式的局部注册和Vue2的局部注册一样,这里就不过多介绍了;
  • <script setup>的单文件中:
    • 不用注册,导入之后直接使用;
    • 示例展示:
      <script setup>
          import ComponentA from './ComponentA.vue'
      </script>
      
      <template>
          <ComponentA />
      </template>
      
  • 如果没有<script setup>
    • 需要使用 components 选项来显示注册;
    • 对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现;
    • 示例展示:
      import ComponentA from './ComponentA.js'
      
      export default {
          components: {
              // 使用了ES6对象简写语法
              ComponentA
          },
          setup() {
              // ...
          }
      }
      

二、 组件通信

2.1 父传子 - props

  • 需要在子组件中声明 props 来接收传递的数据的属性,有两种声明 props 的方式:
    • ❌ 字符串数组形式;
    • 对象形式

2.1.1 声明与使用

  • 组合式API:
    • 采用 defineProps() 宏来声明接收传递的数据;
    • JS 中可使用 defineProps() 返回的对象来访问声明的自定义属性;
    • template 中,可直接访问 defineProps() 中声明的自定义属性;
  • 选项式API:
    • 可以提供 props 选项来声明接收传递的数据;
    • JS 中可使用 this.$props 来访问声明的自定义的属性;
    • template 中,可直接使用 props 中声明的自定义属性;

2.1.2 ❌ 字符串数组形式

  • 组合式API:
    <script setup>
        // 使用 defineProps 宏来声明
        // 声明接收父组件传递的属性值:自定义属性
        defineProps(['属性名1', '属性名2', ……]) 
    </script>
    
  • 选项式API:
    <script>
        export default {
            // 使用 props 选项
            props: ['属性名1', '属性名2', ……]
        }
    </script>
    

2.1.3 ✅ 对象形式

  • 对象形式声明的 props ,可以对传递的值进行校验,如果传递的值不满足类型要求,会在浏览器控制台抛出警告来提醒使用者;
  • 对象形式声明的 propskeyprop的名称,值则为约束条件;
  • 对象中的属性
  • type:类型,如Number、String、Boolean、Array、Object、Date、Function、Symbol等等
  • default:默认值,对象数组 应当使用 工厂函数返回(就是用箭头函数返回默认值);
  • required:是否必填,布尔值reqired 和 default 二选一);
  • validator:自定义校验,函数类型;

组合式API

  • 示例展示:
    • 父组件:
      <script setup>
        import Son from '@/components/18-组件通信/父传子.vue'
        import { ref, reactive, onMounted } from 'vue'
      
        let name = ref('才浅')
      
        let age = ref(22)
      
        let gender = ref('男')
      
        const obj = reactive({
          title: '目前主要掌握的技术栈',
          class: 'Vue2、Vue3、JS',
          post: '初级前端开发工程师'
        })
      
        let flag = ref(true)
      </script>
      
      <template>
        <h4>组件通信 - 父传子</h4>
        <!-- 1.有flag,不传obj,有默认值 -->
        <Son :name="name" :age="age" :gender="gender" flag></Son>
      
        <!-- 2.无flag,传递obj,无默认值 -->
        <Son :name="name" :age="age" :gender="gender" :obj="obj"></Son>
      
        <!-- 3.无flag,不传obj,有默认值 -->
        <Son :name="name" :age="age" :gender="gender"></Son>
      
        <!-- 4.都不传递 -->
        <Son></Son>
      </template>
      
    • 子组件:
      <script setup>
          import { ref, reactive, onMounted } from 'vue'
      
          // EXPLAIN defineProps() - 声明接收父组件传递的属性值:自定义属性
      
          // EXPLAIN 字符串数组形式
          // let propsData = defineProps(['name', 'age', 'gender', 'obj', 'flag'])
      
          // EXPLAIN 对象形式
          const propsData = defineProps({
              // EXPLAIN 一个类型直接写就可以
              name: String,
              // EXPLAIN 两个类型及以上,就需要用到数组包裹起来
              age: [Number, String], // age 既可以是数组也可以是字符串
              gender: String,
              flag: Boolean,
              obj: {
                  typr: Object, // 指明类型
                  // EXPLAIN required 和 default 二者出现一个(既然是必传,要什么默认值,反之,同理)
                  // required: true,  // 是否必传
                  default: () => ({
                      title: '喜欢的动漫',
                      class: '完美世界、不良人、一念永恒、吞噬星空、……',
                      post: '废柴'
                  })
              }
          })
      
          // EXPLAIN JS中,需要通过 defineProps() 返回的对象来访问声明的自定义属性 
          const print = () => {
              Object.keys(propsData).forEach(item => {
                  console.log(`propsData.${item} = ${propsData[item]}`)
              })
          }
      
          const addTechnology = () => {
      
          }
      
          // EXPLAIN 子组件不能直接修改父组件传递的数据,会在控制台抛出一个警告
          const changeNameProp = () => {
              propsData.name = '废柴'
          }
      </script>
      
      <template>
          <!-- template 中使用 父组件传递的数据,直接使用自定义属性名即可 -->
          <div>
              <p>姓名:{{ name }}</p>
              <p>年龄:{{ age }}</p>
              <p>性别:{{ gender }}</p>
          </div>
          <hr>
      
          <div>
              <h4>{{ obj.title }}</h4>
              <p>{{ obj.class }}</p>
              <p>{{ obj.post }}</p>
          </div>
          <hr>
      
          <button @click="print">打印 propsData 中的值</button>
          <hr>
      
          // 鼠标右键点击,修改值
          <button @click="addTechnology" @mousedown.right="changeNameProp">增加掌握技术栈 - 子传父</button>
      </template>
      
    • 效果展示:
      • flag,不传obj,有默认值:
        • 直接写flag,子组件就显示 true;
          image.png
      • 无flag,传递obj,无默认值:
        • 即使有默认值,显示的数据还是传递的数据;
        • 不写flag,子组件显示 false;
          image.png
      • 无flag,不传obj,有默认值:
        image.png
      • 都不传递,并且obj没设默认值:
        • 除了flag,其他的都是undefined;
          image.png
        • 在控制台,抛出一个关于 obj必传 的警告;
          image.png
      • 子组件中的prop是只读的,不能修改值:
        image.png

选项式API

  • 父组件:
    <script>
    import ButtonVue from './components/Button.vue';
    export default {
        components: { ButtonVue },
        data: () => ({
            isError: false, // 主题
            isFlat: false, // 阴影
            btnText: '普通按钮'// 按钮文本
        })
    }
    </script>
    
    <template>
        主题:<input type="checkbox" v-model="isError">
        阴影:<input type="checkbox" v-model="isFlat">
        按钮文本:<input type="text" v-model="btnText">
        <hr>
        <!-- 父向子传值,可采用属性的方式赋值 -->
        <ButtonVue :title="btnText" :error="isError" :flat="isFlat" />
    </template>
    
  • 子组件:
    <script>
    export default {
        // 自定义属性选项
        props: {
            title: {
                type: String,
                required: true
            },
            error: Boolean,
            flat: Boolean,
            tips: {
                type: String,
                default: '我是一个普通的按钮'
            }
        },
        methods: {
            showPropsData() {
                // 在选项式 API JS 中,可以通过 this.$props 来访问 props 中的内容
                console.log(this.$props)
                console.log(this.$props.title)
                console.log(this.$props.error)
                console.log(this.$props.flat)
            },
            changeErrorProps() {
                // 不能直接修改 props 的数据,因为是只读的
                this.$props.error = !this.$props.error
            }
        }
    }
    </script>
    
    <template>
        <!-- 在视图模板上,可直接使用 props 中的属性 -->
        <button :title="tips" :class="{ error, flat }" @click="showPropsData" @mousedown.right="changeErrorProps">
            {{ title }}
        </button>
    </template>
    
    <style>
    button {
        border: none;
        padding: 12px 25px;
    }
    
    .error {
        background-color: rgb(197, 75, 75);
        color: white;
    }
    
    .flat {
        box-shadow: 0 0 10px grey;
    }
    </style>
    
  • 🔺 注意
    • 所有prop默认都是可选的(可传可不传),除非声明了required: true(必传);
    • Boolean 外的为传递的可选 prop 将会有一个默认值 undefined
    • Boolean 类型的为传递 prop 将被转换为 false
    • 🔺 子组件中 props 中的数据是 只读 的,只能通过 父组件 进行 更改
    • props 的检验失败后,Vue会抛出一个控制台警告(在开发模式下);
    • prop 的校验是在组件实例被创建之前:
      • 在选项式 API 中,实例的属性(比如datacomputed等) 将在defaultvalidator函数中不可用;
      • 在组合式 API 中,defineProps宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中;

特别提醒:

  • 关于Boolean类型转换:
  • 为了更贴近原生boolean attributes的行为,声明为Boolean类型的props有特别的类型转换规则:
    • 如声明时:
      • defineProps({ error: Boolean })
    • 传递数据时:
      • <MyComponent error /> :相当于 <MyComponent :error="true" />
      • <MyComponent /> :相当于 <MyComponent :error="false" />

2.2 子传父 - 事件

  • 有时候,父组件在使用子组件时,子组件如何给父组件传值呢?
    1. 子组件声明自定义事件;
    2. 子组件触发自定义事件并进行传值;
    3. 父组件使用子组件时监听对应的自定义事件,并执行父组件中的函数(获取子组件传递值);

  • 1️⃣ 子组件 - 声明组件事件
    • 选项式API 中,子组件 可通过 emits 选项来 声明自定义事件
    • 组合式API 中,子组件 可通过 defineEmits() 宏来 声明自定义事件
    • 声明自定义事件的方式:
      • 字符串数组式声明自定义事件
      • 对象式声明自定义事件
    • 语法
      • 组合式API
        • 字符串数组:
          <script setup>
              // 为了方便,此处的变量名,在后面语法演示就用emit代替
              const 变量名 = defineEmits(['事件1', '事件2', '事件3', ……])
          </script>
          
        • 对象形式:
          <script setup>
              // 为了方便,此处的变量名,在后面语法演示就用emit代替
              const 变量名 = defineEmits({
                  事件1: null,
                  事件2: (val) => {
                      // return true 表示传递的数据通过校验
                      // return false 表示传递的参数没有通过校验,控制台会有警告语句
                      // 也可以自己写一个警告语句
                      // console.warn('xxx')
                      // 不管通没通过校验,val都会传递给父组件
                  }
              })
          </script>
          
      • 选项式API
        • 字符串数组:
          export default {
              emits: ['事件1', '事件2', '事件3', ……]
          }
          
        • 对象形式:
          export default {
              emits: {
                  事件1: null,
                  事件2: (val) => {
                      // return true 表示传递的数据通过校验
                      // return false 表示传递的参数没有通过校验,控制台会有警告语句
                      // 也可以自己写一个警告语句
                      // console.warn('xxx')
                      // 不管通没通过校验,val都会传递给父组件
                  }
              }
          
    • 采用对象形式的好处:
      • 采用对象式声明自定义事件,还可以进行校验传递的参数是否符合预期要求;
      • 对象式声明自定义事件中,属性名自定义事件名属性值 则是 是否验证传递的参数
        • 属性值为 null 则不需要验证;
        • 属性值为 函数 ,参数为传递的数据,函数返回 true 则验证通过,返回 false 则验证失败,验证失败可以用警告语句提示开发者;
      • 🔺 注意
        • 无论是 true 还是 false 都会继续执行下去,父组件都会获取到传递的值

  • 2️⃣ 子组件 - 触发组件事件
    • 组合式API:
      • 可调用 defineEmits() 宏返回的 emit(event, ...args) 函数来触发当前组件自定义的事件;
      • event:触发事件名,字符串类型;
      • ...args:传递参数,可没有,可多个;
    • 选项式API:
      • 可通过组件的当前实例 this.$emit(event, ...args) 来触发当前组件自定义的事件;
    • 语法
      • 选项式API
        // 触发自定义的某个事件
        this.$emit('在emits选项中定义的事件名', 向父组件传递的值)
        // 在 template 中写时,this 可以省略
        
      • 组合式API
        const emit = defineEmits({
            自定义事件1: null,
            自定义事件2: (val) => {
                // 对向父组件传递的数据进行校验
            }
        })
        
        const 子组件事件名 = () => {
            emit('自定义事件', 向父组件传递的数据)
        }
        

  • 3️⃣ 父祖件 - 监听子组件自定义事件
    • 使用 v-on:event="callback" 或者 @event="callback" 来监听子组件是否触发了该事件;
      • event:事件名字(camelCase 形式命名的事件,在父组件中可以使用camel-case形式来监听);
      • callback:回调函数,如果子组件触发该事件,那么在父组件中执行对应的回调函数,回调函数声明参数可自动接收到触发事件传来的值;
    • 语法
      • 组合式API
        <script setup>
            const 父组件触发事件的事件名 = (val) => {
                // val 就是子组件向父组件传递的数据
                // 父组件就可以进行相关操作了
            }
        </script>
        <template>
            <子组件标签 @子组件中自定义事件名="父组件中要触发的事件" />
        </template>
        
      • 选项式API
        <script>
            export default {
                methods: {
                    父组件触发事件的事件名(val) {
                        // val 就是子组件向父组件传递的数据
                        // 父组件就可以进行相关操作了
                    }
                }
            }
        </script>
        <template>
            <子组件标签 @子组件中自定义事件名="父组件中要触发的事件" />
        </template>
        

  • 基于上一小节的代码的示例展示:
    • 父组件:
      <script setup>
          import Son from '@/components/18-组件通信/父传子.vue'
          import {ref, reactive } from 'vue'
      
          let name = ref('才浅')
      
          let age = ref(22)
      
          let gender = ref('男')
      
          const obj = reactive({
              title: '目前主要掌握的技术栈',
              species: 'Vue2、Vue3、JS',
              post: '初级前端开发工程师'
          })
      
          const addSpecies = (val) => {
              obj.species += val
          }
      
          let flag = ref(true)
      </script>
      
      <template>
          <h4>组件通信 - 父传子</h4>
          <!-- 1.有flag -->
          <!-- <Son :name="name" :age="age" :gender="gender" flag></Son> -->
      
          <!-- 2.无flag,传递obj,无默认值 -->
          <!-- <Son :name="name" :age="age" :gender="gender" :obj="obj"></Son>-->
      
          <!-- 3.无flag,不传obj,有默认值 -->
          <!-- <Son :name="name" :age="age" :gender="gender"></Son> -->
      
          <!-- 4.都不传递 -->
          <!-- <Son></Son> -->
      
          <!-- 5.子组件向父组件传值 -->
          <Son :name="name" :age="age" :gender="gender" :obj="obj" @change-obj="addSpecies"></Son>
      </template>
      
    • 子组件:
      <script setup>
          import { ref, reactive } from 'vue'
      
          // EXPLAIN defineProps() - 声明接收父组件传递的属性值:自定义属性
      
          // EXPLAIN 字符串数组形式
          // let propsData = defineProps(['name', 'age', 'gender', 'obj', 'flag'])
      
          // EXPLAIN 对象形式
          const propsData = defineProps({
              // EXPLAIN 一个类型直接写就可以
              name: String,
              // EXPLAIN 两个类型及以上,就需要用到数组包裹起来
              age: [Number, String], // age 既可以是数组也可以是字符串
              gender: String,
              flag: Boolean,
              obj: {
                  type: Object, // 指明类型
                  // EXPLAIN required 和 default 二者只能出现一个(既然是必传,要什么默认值;反之同理)
                  required: true,  // 是否必传
                  // default: () => ({
                  //   title: '喜欢的动漫',
                  //   class: '完美世界、不良人、一念永恒、吞噬星空、……',
                  //   post: '废柴'
                  // })
              }
          })
      
          // TODO 自定义事件
          // EXPLAIN 字符串数组形式
          // const emit = defineEmits(['changeObj'])
      
          // EXPLAIN 对象形式
          const emit = defineEmits({
              // 不需要对参数进行验证,属性值写 null 即可
              // changObj: null,
              // 对传递的参数进行验证,函数返回true表示验证通过,反之则不通过,不管通不通过,该值都会传给父组件
              changeObj: (val) => val.constructor === String
          })
      
          // EXPLAIN JS中,需要通过 defineProps() 返回的对象来访问声明的自定义属性
          const print = () => {
              Object.keys(propsData).forEach(item => {
                  console.log(`propsData.${item} = ${propsData[item]}`)
              })
          }
      
          // TODO 向父组件传递数据
          const addTechnology = () => {
              emit('changeObj', 'TypeScript, React')
          }
      
          // EXPLAIN 子组件不能直接修改父组件传递的数据,会在控制台抛出一个警告
          const changeNameProp = () => {
              propsData.species = '废柴'
          }
      </script>
      
      <template>
          <!-- template 中使用 父组件传递的数据,直接使用自定义属性名即可 -->
          <div>
              <p>姓名:{{ name }}</p>
              <p>年龄:{{ age }}</p>
              <p>性别:{{ gender }}</p>
          </div>
          <hr>
      
          <div>
              <h4>{{ obj.title }}</h4>
              <p>{{ obj.species }}</p>
              <p>{{ obj.post }}</p>
          </div>
          <hr>
      
          <button @click="print">打印 propsData 中的值</button>
          <hr>
      
          <button @click="addTechnology" @mousedown.right="changeNameProp">增加掌握技术栈 - 子传父</button>
      </template>
      

  • 子传父案例:
    • 选项式API
      • 父组件:
        <script>
            import StudentVue from './components/Student.vue';
        
            export default {
                components: { StudentVue },
                data: () => ({
                    student: {
                        name: 'Jack',
                        age: 18,
                        sex: '男'
                    }
                }),
                methods: {
                // 获取子组件传递值
                    getNewAge(newAge) {
                        console.log('年龄的新值:' + newAge)
                        this.student.age = newAge
                    },
                    getNewAgeAndName(newAge, newName) {
                        console.log('年龄的新值:' + newAge)
                        console.log('名字的新值:' + newName)
                        this.student.age = newAge
                        this.student.name = newName
                    },
                    getNewStudent(stu){
                        console.log('学生新值:');
                        console.log(stu);
                        this.student.age = stu.age
                        this.student.name = stu.name
                        this.student.sex = stu.sex
                    }
                }
            }
        </script>
        
        <template>
            {{ student }}
            <hr>
            <StudentVue 
                @change-student="getNewStudent"
                @change-age-and-name="getNewAgeAndName" 
                @change-age="getNewAge" />
        </template>
        
      • 子组件:
        <script>
            export default {
                // 自定义事件选项 - 字符串数组(不能对传递的参数进行校验)
                // emits: ['changeAge', 'changeAgeAndName', 'changeStudent']
                
                // 自定义事件选项 - 对象形式
                emits: {
                    changeAge: null,  // 无需验证
                    changeAgeAndName: null,  // 无需验证
                    changeStudent: stu => {
                        if (stu.age <= 0) {
                            console.warn('年龄不得小于等于0')
                            // false:验证不通过,会有警告语句,父组件依旧可以获取该值
                            return false
                        }
                        // true:验证通过
                        return true
                    }
                },
                methods: {
                    emitEventAge() {
                        // 选项式通过 this.$emit 触发自定义事件,并传值
                        this.$emit('changeAge', 30)
                    }
                }
            }
        </script>
        
        <template>
            <button @click="emitEventAge">更改年龄</button>
            <br>
            <br>
            <button @click="$emit('changeAgeAndName', 10, 'Annie')">
                更改年龄和名字
            </button>
            <br>
            <br>
            <button @click="$emit('changeStudent', { age: 40, name: 'Drew', sex: '男' })">
                更改学生(验证通过)
            </button>
            <br>
            <br>
            <button @click="$emit('changeStudent', { age: -10, name: 'Tom', sex: '男' })">
                更改学生(验证失败)
            </button>
        </template>
        
    • 组合式API
      • 父组件:
        <script setup>
            import { reactive } from 'vue';
            import StudentVue from './components/Student.vue';
        
            let student = reactive({
                name: 'Jack',
                age: 18,
                sex: '男'
            })
        
            // 获取子组件传递值
            function getNewAge(newAge) {
                console.log('年龄的新值:' + newAge)
                student.age = newAge
            }
            function getNewAgeAndName(newAge, newName) {
                console.log('年龄的新值:' + newAge)
                console.log('名字的新值:' + newName)
                student.age = newAge
                student.name = newName
            }
            function getNewStudent(stu){
                console.log('学生新值:');
                console.log(stu);
                student.age = stu.age
                student.name = stu.name
                student.sex = stu.sex
            }
        </script>
        
        <template>
            {{ student }}
            <hr>
            <StudentVue 
                @change-student="getNewStudent"
                @change-age-and-name="getNewAgeAndName" 
                @change-age="getNewAge" />
        </template>
        
      • 子组件:
        <script setup>
            // 自定义事件 - 字符串数组形式
            const emit = defineEmits(['changeAge', 'changeAgeAndName', 'changeStudent'])
            
            // 自定义事件 - 对象形式
            let emit = defineEmits({
                changeAge: null, // 无需验证
                changeAgeAndName: null, // 无需验证
                changeStudent: stu => {
                    if (stu.age <= 0) {
                        console.warn('年龄不得小于等于0')
                        // false:验证不通过,会有警告语句,父组件依旧可以获取该值
                        return false
                    }
                    // true:验证通过
                    return true
                }
            })
        
            function emitEventAge() {
                // 选项式通过 this.$emit 触发自定义事件,并传值
                emit('changeAge', 30)
            }
        </script>
        
        <template>
            <button @click="emitEventAge">更改年龄</button>
            <br>
            <br>
            <button @click="emit('changeAgeAndName', 10, 'Annie')">
                更改年龄和名字
            </button>
            <br>
            <br>
            <button @click="emit('changeStudent', { age: 40, name: 'Drew', sex: '男' })">
                更改学生(验证通过)
            </button>
            <br>
            <br>
            <button @click="emit('changeStudent', { age: -10, name: 'Tom', sex: '男' })">
                更改学生(验证失败)
            </button>
        </template>
        

2.3 组件通信 - 购物车综合案例

三、 透传属性和事件

  • Vue3官网-透传;

3.1 如何透传属性和事件

  • 父组件在使用子组件的时候,如何“透传属性和事件”给子组件呢?
    1. 透传属性和事件并没有在子组件中用 propsemits 声明;
    2. 透传属性和事件最常见的如 click、class、id、style
    3. 子组件 只有 一个根元素 时,透传属性事件自动添加该根元素 上;如果根元素已有 classstyle 属性,它会自动合并
  • 示例展示:
    • 父组件:
      <script setup>
          import { ref, reactive } from 'vue'
          import Son1 from '@/components/20-透传属性和事件/子1.vue'
          const test = () => console.log('透传属性和事件')
      </script>
      
      <template>
          <!--
              透传的属性 (class、style)并没有在子组件中的 props 中声明
              透传的事件 (click) 并没有在子组件中的 emits 中声明
          -->
          <Son1
              class="son1"
              style="background-color: rgb(0, 255, 243)"
              @click="test"
          ></Son1>
      </template>
      
    • 子组件:
      <script setup>
      </script>
      
      <template>
          <div 
              class="son-content"
              :style="{
                  border: '4px solid purple',
                  boxShadow: '0 0px 8px 2px grey'
          }">
              <h3>子组件</h3>
          </div>
      </template>
      
    • 效果展示:
      • 可以看到父组件子组件标签上绑定的属性和事件会出现在子组件中;
        image.png

3.2 如何禁止透传属性和事件

  • 当只有 一个根元素 的时候,透传属性和事件自动添加该根元素上
  • 禁止透传属性和事件:
    • 添加给子组件
    • 组合式API:
      • <script setup> 中,需要一个额外的<script>默认导出一个对象,来写inheritAttrs: false 选项来禁止;
    • 选项式API:
      • 可以在组件选项中设置 inheritAttrs: false来阻止;
  • 示例展示:
    • 基于上一小结的代码;
    • 子组件(组合式API):
      // TODO 禁止透传属性和事件
      <script>
          export default {
              // 阻止自动透传给唯一的根组件
              inheritAttrs: false
          }
      </script>
      <script setup>
          import { ref, reactive, onMounted } from 'vue'
          onMounted(() => {});
      </script>
      
      <template>
          <div 
              class="son-content"
              :style="{
                  border: '4px solid purple',
                  boxShadow: '0 0px 8px 2px grey'
          }">
              <h3>子组件</h3>
          </div>
      </template>
      
    • 子组件(选项式API):
      // TODO 禁止透传属性和事件
      <script>
          export default {
              // 阻止自动透传给唯一的根组件
              inheritAttrs: false
          }
      </script>
      
      <template>
          <div 
              class="son-content"
              :style="{
                  border: '4px solid purple',
                  boxShadow: '0 0px 8px 2px grey'
          }">
              <h3>子组件</h3>
          </div>
      </template>
      
  • 效果展示:
    image.png

3.3 多根元素的透传属性和事件

  • 多根节点的组件没有自动“透传属性和事件”,由于 vue 不确定要将“透传属性和事件”绑定到哪里【也就是说,透传属性和事件默认会绑定到唯一的根节点上】,所以我们需要 v-bind="$attrs" 来显示绑定,否则将会抛出一个运行警告;
    image.png
  • 注意
    • 绑定对象:子组件中接受透传属性和事件 的元素;
    • 绑定元素数量不受限制;
    • 绑定该属性之后,阻止透传就不会生效;
  • 代码展示:
    • 还是基于 7.3.1 中的代码;
    • 父组件代码没变;
    • 子组件:
      <script setup>
          import { ref, reactive, onMounted } from 'vue'
          onMounted(() => {});
      </script>
      
      <template>
          <button class="chip" :="$attrs"> 普通纸片1 </button>
          <button class="chip" :="$attrs"> 普通纸片2 </button>
          <button class="chip"> 普通纸片3 </button>
          <div class="son-content">
              <h3>子组件1</h3>
          </div>
      </template>
      
      <style scoped>
          .son-content {
              margin-top: 10px;
              border: 4px solid purple;
              box-shadow: 0 0px 8px 2px grey;
          }
      </style>
      
  • 效果展示:
    image.png

3.4 访问 透传属性和事件

  • 组合式API
    • <script setup> 中引入 useAttra() 来访问一个组件的 “透传属性和事件”;
  • 选项式API
    • 通过 this.$attrs 来访问 “透传的属性和事件”
  • 🔺 注意
    • 虽然这里的 attrs 对象总是反映为最新的 “透传属性和事件”,但它并 不是响应式 的(考虑到性能因素),你不能通过侦听器去监听它的变化;
    • 如果 需要响应式,可以使用 prop 或者也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用;
  • 示例展示:
    • 还是基于 7.3.1 的代码,父组件代码没有变;
    • 子组件:
      • 组合式API:
        <script setup>
            import { ref, reactive, onMounted, useAttrs } from 'vue'
            // TODO 使用 变量 接收 透传的属性和事件
            const attrs = useAttrs()
        
            const attrsTest = () => {
                console.log(attrs)
                console.log(attrs.value)
                // attrs 不是响应式的数据,可以进行解构
                const { class: className, style, onClick } = attrs
                console.log(className, style, onClick)
            }
        
            // TODO 执行透传的事件
            const transferEvent = attrs.onClick
        </script>
        
        <template>
            <button class="chip" :="$attrs" @mousedown.right="attrsTest"> 普通纸片1 </button>
            <button class="chip" :="$attrs"> 普通纸片2 </button>
            <button class="chip" @click="attrs.onClick"> 执行透传事件 - 1 </button>
            <button class="chip" @click="transferEvent"> 执行透传事件 - 2 </button>
            <div class="son-content">
                <h3>子组件1</h3>
            </div>
        </template>
        
        <style scoped>
            .son-content {
                margin-top: 10px;
                border: 4px solid purple;
                box-shadow: 0 0px 8px 2px grey;
            }
        </style>
        
        image.png
        • 如果没有使用 <script setup>attrs 会作为 setup() 上下文对象的一个属性暴露:
        export default {
            setup(props, ctx) {
                // 透传 attribute 被暴露为 ctx.attrs
                console.log(ctx.attrs)
            }
        }
        

四、 插槽

  • Vue3官网-插槽;
  • 插槽这块和Vue2没有什么区别,更详细的笔记或例子请移步Vue2 - 插槽;

4.1 什么是插槽

  • 在封装组件时,可以使用 <slot> 元素把不确定的、希望由用户指定的部分定义为插槽;可以理解为给预留的内容提供占位符;
  • 插槽也可以提供默认内容,如果组件的使用者没有为插槽提供任何内容,则插槽内的默认内容会生效;
  • 如果在封装组件时没有预留任何<slot>插槽,用户提供传递一些模板片段内容会失效;
  • image.png

4.2 具名插槽

  • 如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot> 插槽指定具体的 name 名称,这种带有具体名称的插槽叫做 具名插槽
  • 没有指定 name 名称的插槽,会有隐含的名称叫做 default(匿名插槽);
  • <template>元素上使用 v-slot:slotName#slotName 向指定的具名插槽提供内容;

4.3 作用域插槽

  • 如何在向插槽提供内容的同时获得子组件域内的数据:
    • 在声明插槽时使用属性值的方式来传递子组件的数据,这种带有数据的插槽称为作用域插槽
    • template 元素上使用 v-slot:slotNmae="slotProps"#slotNmae="slotPros" 的方式来访问插槽传递的数据;
    • 如果没有使用 template 元素,而是直接在使用子组件中直接给默认插槽提供内容,我们可以在使用该子组件时用 v-slot="slotProps" 来接收该插槽传递的数据对象;
  • 注意
    • slot 插槽上的 name 是一个 Vue 特别保留的属性,不会在作用域插槽中访问到;

五、 单文件组件CSS功能

  • 默认情况下,写在.vue组件中的样式会全局生效,很容易造成多个组件之间的样式冲突问题;
  • 导致组件之间样式冲突的根本原因:
    • 单页面应用过程中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的;

5.1 组件作用域CSS

  • <style>标签带有scoped属性后:
    • 它的CSS只会影响当前组件的元素,父组件的样式将不会渗透到子组件中;
    • 该组件的所有元素编译后会自带一个特定的属性;
      • data-v- + 8位随机哈希值
        image.png
    • <style scoped> 内的选择器,在编译后会自动添加特定的属性选择器;
    • 子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响,主要是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式;
      image.png
      <!-- 让下方选择器的样式只作用在该组件中,或者子组件的根元素上 -->
      <!-- 该组件的所有元素及子组件中的根元素会加上固定的属性 -->
      <!-- 该CSS选择器都会自动添加固定的属选择器 -->
      <style scoped>
       /* 选择器 */
      </style>
      

5.2 深度选择器

  • 处于 scoped 样式中的选择器如果想要做更【深度】的选择,既,能影响到子组件,可以使用 :deep() 这个 伪类
  • :deep() 解释(我自己这么理解的):
    .box:deep(h3) {}
    /* 影响 .box 下的 所有h3标签 */
    /* 如果这个h3是直接写在template下面(没有父级容器),则不会受影响 */
    
    :deep(.h3) {}
    /* 影响当前组件所有类名为 .h3 的元素和子组件中的所有类名为 .h3 元素 */
    

5.3 CSS中的v-bind()

  • 单文件组件的 <style> 标签支持 v-bind() CSS 函数将 CSS 的值链接到动态的组件状态(变量);
  • 这个语法同样也适用于 <script setup> ,且支持JavaScript表达式(需要用引号包裹起来);
  • 实际的值会被编译成 哈希化CSS自定义属性,因此CSS本身仍是静态的;
  • 自定义属性 会通过 内联样式 的方式应用到 组件的某个元素上,并且在 源值变更 的时候 响应式地更新
  • 示例展示:
    <script setup>
        import { ref, reactive, onMounted } from 'vue'
        const buttonTheme =reactive({
            backColor: '#6fbbff',
            textColor: '#fff'
        })
    </script>
    
    <template>
        <button>普通按钮</button>
    </template>
    
    <style scoped>
        button {
            background-color: v-bind('buttonTheme.backColor');
            color: v-bind('buttonTheme.textColor');
        }
    </style>
    
  • 效果展示:
    image.png

六、 依赖注入

  • Vue3官网-依赖注入;
  • 业务场景:
    • 有一个深层的子组件,需要一个较远的祖先组件的部分数据;
  • 解决方案:
    • 使用props沿着组件链逐级传递下去;
      image.png
    • 可以在祖先组件中使用provide提供数据,后代组件使用inject注入数据;
      image.png

6.1 provide(提供数据)

  • 在应用层方面:
    • 可通过 app.provide() 为后代提供数据;
    • 语法
      app.provide('属性名', '传递的数据')
      
    • 示例展示:
      • 目标文件:main.js
        // NOTE 从 vue 中导入 createApp 函数
        import { createApp } from 'vue'
        
        import './style.css'
        
        // NOTE 导入根组件 App.vue
        import App from './App.vue'
        
        // NOTE 通过 createApp 函数创建应用实例
        let app = createApp(App)
        
        // NOTE 应用层main注册,为所有的组件提供一个数据
        app.provide('userName', '禁止摆烂_才浅')
        
        // NOTE mount 函数,将应用实例渲染在容器元素里面
        app.mount('#app')
        
      • App.vue组件:
        <script setup>
            import Vue1 from '@/components/22-依赖注入/1.vue'
            import { ref, reactive, onMounted } from 'vue'
        </script>
        
        <template>
            <div class="area" style="background-color: purple;">
                <h3>App 组件</h3>
                <Vue1></Vue1>
            </div>
        </template>
        
        <style scoped>
            .area {
                padding: 10px;
                color: #fff;
            }
        </style>
        
      • 1.vue组件(App.vue的子组件):
        <script setup>
            import Vue2 from './2.vue'
            import { ref, reactive, onMounted } from 'vue'
        </script>
        
        <template>
            <div class="area-1" style="background-color: yellow;">
                <h3>App的子组件</h3>
                <Vue2></Vue2>
            </div>
        </template>
        
        <style scoped>
            .area-1 {
                padding: 10px;
                color: #000;
            }
        </style>
        
      • 2.vue组件(App.vue的孙子组件):
        <script setup>
            import { ref, reactive, onMounted, inject } from 'vue'
            let userName = inject('userName')
        </script>
        
        <template>
            <div class="area-2" style="background-color: #00ffe3;">
                <h3>App的孙子组件</h3>
                <h5>应用层【main.js】提供给的数据: <span>{{ userName }}</span></h5>
            </div>
        </template>
        
        <style scoped>
            .area-2 {
                padding: 10px;
                color: #000;
            }
            
            span {
                padding: 4px;
                border: 3px solid red;
            }
        </style>
        
    • 效果展示:
      image.png
  • 在组件中如何提供:
    • 组合式API
      • <script setup>中,可通过provide()函数来为后代组件提供数据;
      • 语法
        import { provide } from 'vue'
        
        provide('注入数据时的名字', 要提供的数据)
        
      • 示例展示:
        • 目标文件:App.vue
          • App.vue
            <script setup>
                import Vue1 from '@/components/22-依赖注入-组合式API/1.vue'
                import { ref, computed, provide } from 'vue'
            
                // TODO 定义响应式数据
                let title = '博客'
                let subtitle = ref('每天进步一点点')
            
                // TODO 定义方法
                const changeSubtitle =  (val) => {
                    subtitle.value = val
                }
            
                // TODO 使用 provide() 提供数据
                provide('title', title)
                provide('subtitle', subtitle)
                provide('changeSubtitle', changeSubtitle);
            </script>
            
            <template>
                <div class="area" style="background-color: purple;">
                    <h3>App 组件</h3>
                    标题:<input type="text" v-model="title"><br>
                    副标题:<input type="text" v-model="subtitle">
                    <Vue1></Vue1>
                </div>
            </template>
            
            <style scoped>
                .area {
                    padding: 10px;
                    color: #fff;
                }
            </style>
            
      • 运行展示:见 6.2 组合式API
    • 选项式API:【推荐使用函数的方式】
      • 可以通过provide选项为后代提供数据;
      • 语法
        export default {
            provide: {
                return {
                    // 注入非响应式数据
                    注入数据时的名字: 要提供的数据(this.变量名),
                    
                    // 注入响应式数据
                    注入数据时的名字: computed(() => this.变量名),
                    
                    // 注入函数
                    注入数据时的名字: 当前组件的函数名(this.函数名)
                }
            }
        }
        
      • 如果想访问到组件的实例thisprovide必须采用函数的方式【不能用箭头函数】,为保证注入方和供给方之间的响应性链接,必须借助组合式API中的computed()函数提供计算属性,还可以提供修改响应式数据的函数(响应式数据的修改,尽量放在同一组件中,为了好维护);
      • 🔺 注意
        • provide选项通中通过computed()提供的响应式的数据,需要设置app.config.unwrapInjectedREf = true以保证注入会自动解包这个计算属性。这将会在Vue3.3后成为一个默认行为,而我们暂时在此告知此项配置以避免后续升级对代码的破坏性。在3.3后就不要这样做了;
      • 示例展示:
        • 目标文件:App.vue + 2.vue
          • App.vue:
            <!-- 选项式API -->
            <script>
                import Vue1 from '@/components/22-依赖注入/1.vue'
                import { computed } from 'vue'
                export default {
                    components: { Vue1 },
                    data: () => ({
                        title: '博客',
                        subtitle: '每天进步一点点'
                    }),
                    methods: {
                        changeSubtitle (val) {
                          this.subtitle = val
                        }
                    },
                    // NOTE 选项式API中,使用 provide 选项来给后代组件提供数据
                    // NOTE 但是这种方式无法访问组件的实例 this
                    /* provide: {
                    title: '博客',
                    title: this.title
                    } */
            
                    // NOTE 想要访问组件的实例 this,provide必须是函数的形式,并且不能是箭头函数
                    provide() {
                        return {
                        // NOTE 提供的数据 不是响应式的数据 - 注入方 和 提供方 没有响应式的连接
                        title: this.title,
                        // NOTE 提供的数据是 响应式的数据
                        subtitle: computed(() => this.subtitle),
                        // NOTE 提供 修改响应式数据 的函数
                        changeSubtitle: this.changeSubtitle
                        }
                    }
                }
            </script>
            
            <template>
                <div class="area" style="background-color: purple;">
                    <h3>App 组件</h3>
                    标题:<input type="text" v-model="title"><br>
                    副标题:<input type="text" v-model="subtitle">
                    <Vue1></Vue1>
                </div>
            </template>
            
            <style scoped>
                .area {
                    padding: 10px;
                    color: #fff;
                }
            </style>
            
          • 2.vue:
            <!-- 选项式API -->
            <script>
                export default {
                    inject: ['userName', 'title', 'subtitle', 'changeSubtitle']
                }
            </script>
            
            <template>
                <div class="area-2" style="background-color: #00ffe3;">
                    <h3>App的孙子组件</h3>
                    <h5>应用层【main.js】提供的数据: <span>{{ userName }}</span></h5>
                    <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5>
                    <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> -->
                    <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle }}</span></h5>
                    <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button>
                </div>
            </template>
            
            <style scoped>
                .area-2 {
                    padding: 10px;
                    color: #000;
                }
            
                span {
                    padding: 4px;
                    border: 3px solid red;
                }
            </style>
            
        • 效果展示:
          image.png

6.2 inject(注入)

组合式API

  • 访问
    • 可通过inject()的返回值来注入祖先组件提供的数据;
  • 语法
    import{ inject } from 'vue'
    // 有祖先组件提供的数据 / 没有祖先组件提供数据【没有祖先组件提供数据的时候会抛出一个警告】
    const 变量名 = inject('注入的数据名')
    // 没有祖先组件提供数据,在当前组件可以设置默认值
    const 变量名 = inject('注入的数据名', 默认值)
    // 默认值可以是任何类型的数据
    
    请将 语法 和 实例 结合阅读
    
  • 如果提供数据的值是一个 ref,注入进来的会是该 ref对象和提供方保持响应式链接
  • 如果注入的数据并没有在祖先组件中提供,则会抛出一个警告,可在 provide()第二个参数 设置默认值 来解决;
  • 它们可以在 JS视图模板直接访问
  • 示例展示:
    • 2.vue
      <script setup>
          import { inject, onMounted } from 'vue';
      
          // TODO 使用 inject() 函数的返回值来注入祖先组件提供的数据
          const _userName = inject('userName')  // 应用层提供的数据
          const title = inject('title')  // 普通数据 - 非响应式数据
          const _subtitle = inject('subtitle')  // ref响应式数据 - _subtitle 也是 ref 对象
          const changeSubtitle = inject('changeSubtitle')  // 函数
          const likes = inject('likes')  // 祖先组件并没有提供数据,则会抛出一个警告
          const anime = inject('anime', '完美世界')  // 祖先组件没有提供数据,当前组件可以在 inject() 的第二个参数设置默认值
      
          onMounted(() => {
              console.log('应用层提供的数据:', _userName)
              console.log('祖先组件提供的非响应式数据:', title)
              console.log('祖先组件提供的响应式数据:', _subtitle)
              console.log('祖先组件提供的函数:', changeSubtitle)
              console.log('注入的数据,祖先组件并没有提供,就会抛出一个警告:', likes)
              console.log('祖先组件没有提供 anime 这个数据,当前组件设置在注入的时候默认值:', anime)
          });
      </script>
      
      <template>
          <div class="area-2" style="background-color: #00ffe3;">
              <h3>App的孙子组件</h3>
              <h5>应用层【main.js】提供的数据: <span>{{ _userName }}</span></h5>
              <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5>
              <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> -->
              <h5>App.vue 提供的 响应式 的数据: <span>{{ _subtitle }}</span></h5>
              <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button>
              <h5>注入的数据,祖先组件并没有提供,就会抛出一个警告: <span>{{ `${likes}` }}</span></h5>
              <h5>祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值: <span>{{ anime }}</span></h5>
          </div>
      </template>
      
      <style scoped>
          .area-2 {
              padding: 10px;
              color: #000;
          }
      
          span {
              padding: 4px;
              border: 3px solid red;
          }
      
          h5 {
              padding-left: 20px;
              text-align: left;
          }
      </style>
      
  • 运行展示:
    image.png

选项式API

  • 访问
    • 可通过 inject选项 来声明需要注入祖先组件提供的数据,它们可以在 JS中直接通过this来访问,在 视图模板 中也可以 直接访问
  • 语法
    export default {
         // 注入方法一:字符串数组
        inject: ['注入的数据名1', '注入的数据名2', '注入的函数名',……]
    
        // 注入方法二:对象
        inject: {
            新的变量名: { from: '注入的数据名' },
    
            // 当 新的变量名 == 注入的数据名 时,form 选项可以省略
            注入的数据名(新的变量名): {},
    
            // 祖先组件没有提供数据,会抛出一个警告
            // 解决方法:在当前组件,可以使用 default 选项设置默认值
            新的变量名: { from: '注入的数据名', default: 默认值 }
        }
    }
    请将 语法 和 实例 结合阅读
    
  • inject采用对象的形式来注入祖先组件提供的数据有哪些好处?
    • 可用本地属性名注入祖先组件提供的数据【如相同时(本地接收名注入名和接收的数据名),form选项可省略】;
    • 如果注入的数据并没有在祖先组件中提供,则会抛出警告,可采用 default选项设置默认值来解决;
  • 示例展示:(2.vue
    <!-- 选项式API -->
    <script>
        export default {
            // TODO 注入数据选项 - 祖先组件提供的数据
            // inject: ['userName', 'title', 'subtitle', 'changeSubtitle'],
    
            // TODO 采用对象形式注入
            inject: {
                _userName: {
                    from: 'userName'
                },
                title: {},
                _subtitle: {
                    from: "subtitle"
                },
                changeSubtitle: {},
                // EXPLAIN 如果注入的数据,祖先组件并没有提供数据,就会抛出一个警告
                likes: {
                    from: 'xiHa'
                },
                // EXPLAIN 解决:当前组件可以设置默认值
                anime: {
                    // 没有组件提供这个 anime 数据
                    from: 'anime',
                    default: '完美世界'
                }
            },
            mounted() {
                console.log('应用层提供的数据', this._userName)
                console.log('祖先组件提供的数据', this.title, this._subtitle)
                console.log('祖先组件提供的函数', this.changeSubtitle)
                console.log('注入的数据,祖先组件并没有提供,就会抛出一个警告', this.likes)
                console.log('祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值', this.anime)
            },
        }
    </script>
    
    <template>
        <div class="area-2" style="background-color: #00ffe3;">
            <h3>App的孙子组件</h3>
            <h5>应用层【main.js】提供的数据: <span>{{ _userName }}</span></h5>
            <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5>
            <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> -->
            <h5>App.vue 提供的 响应式 的数据: <span>{{ _subtitle }}</span></h5>
            <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button>
            <h5>注入的数据,祖先组件并没有提供,就会抛出一个警告: <span>{{ `${likes}` }}</span></h5>
            <h5>祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值: <span>{{ anime }}</span></h5>
        </div>
    </template>
    
    <style scoped>
        .area-2 {
            padding: 10px;
            color: #000;
        }
    
        span {
            padding: 4px;
            border: 3px solid red;
        }
    
        h5 {
            padding-left: 20px;
            text-align: left;
        }
    </style>
    
  • 运行展示:
    image.png
转载请注明出处或者链接地址:https://www.qianduange.cn//article/5290.html
标签
评论
发布的文章

用js生成小米商城

2024-04-27 21:04:59

网页汇率计算器vue代码

2024-04-26 13:04:44

Python读写Json文件

2024-04-23 22:04:19

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!