Vue组件封装重要知识点

一、什么是组件?

Vue.js的一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件,每个组件依赖的CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以相互嵌套。组件使得开发者能够将不同的功能模块封装起来,形成一个更为复杂的页面,极大地提高了代码的可读性、可维护性和开发效率。

组件可以封装HTML、CSS和JavaScript代码,实现特定的功能,并在需要的时候进行复用。组件之间也可以进行组合,形成更复杂的页面结构。

二、组件拆分

组件拆分是将一个大的组件拆分成多个小的、更易于管理和维护的子组件的过程。组件拆分的目的是提高代码的可读性、可维护性和可复用性。拆分时可以根据功能、业务逻辑、数据流向等因素进行划分。

例如,一张复杂的卡片可以拆分为头部、底部等多个组件,每个组件负责处理特定的功能和数据。

例如,一个编辑弹框可以拆分为多个不同功能的组件,最后再整合起来。同时,编辑弹框和新增弹框可公用一个组件,通过配置参数来判断新增或编辑。

三、如何编写组件?

**1、定义组件结构和功能。**明确组件的用途和功能。这包括确定组件需要接收哪些props(属性)、需要触发哪些events(事件),以及组件内部需要维护哪些data(数据)。

**2、创建组件文件。**一般通过一个包含template、script和style部分的.vue文件来完成,这被叫做单文件组件(简称SFC),这个文件包含了组件的模板、逻辑和样式。

  • template:定义组件的HTML结构。
  • script:包含组件的JavaScript逻辑,如数据、方法、生命周期钩子等。
  • style:定义组件的样式。使用scoped属性确保样式只应用于当前组件。

**3、引入并注册组件。**组件定义好后,需要将其注册到 Vue 实例或者另一个组件中,以便在模板中使用。

  • 局部注册:在父组件中局部注册子组件,使其仅在父组件的模板中可用。
  • 全局注册:在main.js或其他入口文件中全局注册组件,使其在整个应用中都可用。

**4、使用组件。**注册之后,就可以在父组件的模板中使用这个组件了。

**5、组件通信。**Vue组件之间有多种通信方式,包括props向下传递数据、events向上传递信息、使用Vuex进行状态管理,以及通过provide和inject进行跨层级通信。

  • 传递属性。通过props,父组件可以向子组件传递数据。
  • 事件通信。组件可以使用$emit 发送事件,父组件通过监听这些事件来响应子组件的行为。
  • 插槽。使用插槽,父组件可以向子组件的模板中插入内容。

**6、组件优化和重用。**确保组件逻辑清晰、易于理解,并尽可能保持组件的独立性,以便重用。使用计算属性(computed)和侦听器(watch)来优化性能。如果需要,可以将复杂的组件拆分为更小的子组件。

**7、测试和调试。**使用Vue开发者工具进行调试,查看组件的状态和事件,确保其功能的正确性。

**8、文档和注释。**为组件编写清晰的文档和注释,解释其用途、属性和事件。这将有助于其他开发者理解和使用你的组件。

四、组件通信类型

Vue2 组件通信

props

父组件会通过props向下传数据给子组件,当子组件有事情要告诉父组件时,会通过$emit事件告诉父组件。以确保数据流的单向传递,从而使数据改动来源更加明显,方便我们定位问题。

  • 所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。防止从子组件意外改变父级组件的状态,从而导致数据流向难以理解。

①所以Vue推荐需要改动的时候,通过++改变父组件的值++从而触发props的响应。

②或者可以在接收++非引用类型++的值时,使用子组件自身的 data 做一次接收。++text: this.title++

③如果是++数组或是对象++,需要一次深拷贝。因为JavaScript中,引用类型的赋值实际是内存地址的传递,会指向同一个内存地址。++let obj = JSON.parse(JSON.stringify(obj));++

  • 在组件上绑定的属性,如果没有在组件内部用props声明,会默认绑定到组件的根元素上去。这是Vue默认处理的,而且除了 class 和 style 采用合并策略,其它特性会替换掉原来根元素上的属性值。也可以显式的在组件内部关闭掉这个特性:
javascript 复制代码
inheritAttrs: false,//利用inheritAttrs,还可方便的把组件绑定的其它特性,转移到指定元素上

emit、on、$off

emit和on都是组件自身的方法,on可以监听emit派发的事件,$off则用来取消事件监听。

html 复制代码
<!-- Parent Component -->
<template>
  <!-- 在父组件利用 v-on 监听 -->
  <button-component @clickButton="clickButton"></button-component>
</template>
<script>
export default {
  methods: {
    clickButton () { ··· }
  }
}
</script>

<!-- child Component -->
<template>
  <button @click="handleClick"></button>
</template>
<script>
export default {
  methods: {
    handleClick () { // 触发 $emit
      this.$emit('clickButton');
    }
  },
  mounted() {
    this.$on('clickButton', (...arr) => { //也可以自己监听$emit,虽然没啥用
        console.log(...arr);
    })
  }
}
</script>

children 和 parent

children会找到当前组件的子组件,parent会找到当前组件的父组件。如果有多个子组件,需要依赖组件实例的name属性。

  • $parent:父实例,如果当前实例有的话。通过访问父实例也能进行数据之间的交互,但极小情况下会直接修改父组件中的数据。这个属性值不是响应式的。
  • $children:子实例,如果当前实例有的话。通过访问子实例也能进行数据之间的交互。这个属性值是数组类型的,且并不保证顺序,也不是响应式的。

**注意:**尽量避免直接操作children和parent,因为这可能会导致组件之间的耦合过紧,使代码难以维护和测试。

listeners 和 attrs

假如有一串组件相互包含,A包含B,B包含C,C包含D,通过attrs 和 listeners 就能实现直接让组件A传递数据或方法给组件C甚至组件D。

  • $attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(class和style除外)。
    • 如果一个父组件传递了一些属性给子组件,但子组件并没有在props中声明这些属性,那么这些属性就会出现在attrs对象中。子组件可通过v-bind="attrs"将这些属性传递给其内部的子组件。
    • 当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定属性(class和style除外),并且可以通过【v-bind="attrs"】传入内部组件。通过attrs指定给元素的属性,不会与该元素原有属性发生合并或替换,而是以原有属性为准。
  • **listeners**:包含父作用域中的所有v-on事件监听器(包括自定义事件、不包括.native修饰的事件)的对象。可通过v-on="listeners"传入内部组件。
    • 如果一个父组件给子组件绑定了一些事件监听器,那么这些监听器就会出现在listeners对象中。子组件可以通过【v-on="listeners"】将这些事件监听器传递给其内部的子组件。

.sync

一个语法糖,用于简化父子组件之间的双向绑定通信。使用.sync修饰符,子组件可以触发一个特殊的更新事件,父组件监听这个事件并据此更新相应的数据。

通常,父组件通过props向下传递数据给子组件,而子组件则通过事件$emit向上发送消息给父组件.有时,我们可能希望子组件能直接修改某些由父组件传递下来的props,而不需要显式地触发一个事件来通知父组件更新这些值。.sync就能帮我们实现这样的功能。

假设有父组件传递了一个count prop给子组件,并允许子组件修改这个count。由于父组件使用了.sync 修饰符,这个事件会被自动监听,当收到update:count事件时,count的值会更新,同时,父组件中的num的值也会更新。

html 复制代码
<!-- Parent Component -->
<child :count.sync="num" />
<script>
  export default {
    data() {
      return {
        num: 0,
      };
    },
  };
</script>
<!-- child Component -->
<div @click="handleAdd">Add</div>
<script>
  export default {
    data() {
      return {
        counter: this.count,
      };
    },
    props: ['count'],
    methods: {
      handleAdd() {
        this.$emit('update:count', ++this.counter);
      },
    },
  };
</script>
  • .sync修饰符本质上只是语法糖,它会被Vue转换为update:count形式的事件监听。
  • 使用.sync时应谨慎,因为它可能会让数据流变得不那么清晰。在大型或复杂的应用中,可能更倾向于使用Vuex或其他状态管理库来管理共享状态。

v-model

它创建了一个双向数据绑定,即视图层(DOM)与Vue实例中的数据保持同步。当数据发生变化时,视图会自动更新;反之,用户在视图中进行的修改也会反映到数据中。实际上利用了Vue的响应式系统,它自动监听数据的变化,并在变化发生时更新DOM。

v-model还可以配合修饰符使用,如.lazy、.number和.trim,以改变其默认行为:

  • .lazy修饰符会使得v-model在change事件而不是input事件之后同步数据。
  • .number修饰符会自动将用户的输入转换为数值类型。
  • .trim修饰符会自动去除用户输入的首尾空白字符。
  • v-model="value":用于单选框、复选框和选择框的数据绑定,绑定的是选择的值。
    • 区别:单选框和复选框绑定的是选中状态,而选择框绑定的是选中的值。
    • 注意事项:当多个单选框或复选框绑定同一个数据时,需要为每个元素添加不同的value属性。
  • v-model="value":用于输入框等表单元素的双向数据绑定,绑定的是输入框的值。
    • 区别:v-model与value属性的实现方式不同,v-model实现了双向绑定的效果。
    • 注意事项:需要为输入框添加type属性,并且该元素必须支持input事件或change事件。

ref

简单来说就是获取元素的Dom对象和子组件实例。如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例。要在Dom加载完成后使用,否则可能获取不到,可以用this.$nextTick。

ref、parent、children 它们几个的一个缺点就是无法处理跨级组件和兄弟组件。

EventBus中央事件总线

对于任意组件间的数据通信,可以采用Vuex和EventBus进行数据传输。就是任意组件之间打交道,没有多余的业务逻辑,只需通过新建一个Vue事件bus对象,然后通过 bus.emit 触发事件, bus.on 监听触发的事件。

Vue中可使用EventBus来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但若使用不慎就会造成难以维护的灾难,这边一个emit 那边一个on的,而且无法共享相关数据。因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。

Vue3 组件通信

props

props以单向数据流的形式可以很好的完成父子组件的通信。props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。一个组件需要显式声明它所接受的 props,这样Vue才能知道外部传入的哪些是props,哪些是透传attribute。

  1. prop被用于传入初始值,而子组件想在之后将其作为一个局部数据属性:最好是新定义一个局部数据属性,从 props 上获取初始值即可。++const counter = ref(props.initialCounter)++
  2. 需要对传入的prop值做进一步的转换:最好是基于该prop值定义一个计算属性。++const normalizedSize = computed(() => props.size.trim().toLowerCase())++

所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但反过来则不行。这样能++防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。++额外的,每次父级组件发生更新时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器的控制台中发出警告。

  • 在使用<script setup>的单文件组件中,props可以使用++defineprops()++宏来声明:
javascript 复制代码
<script setup>
const props = defineprops(['foo'])
</script>
  • 在没有使用<script setup>的组件中,prop可以使用++props选项++来声明:
javascript 复制代码
export default {
  props: ['foo'], // 如果这行不写,下面就接收不到
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

注意:

  • 传递给defineprops()的参数和提供给props选项的值是相同的,两种声明方式背后其实使用的都是prop选项。
  • defineprops()宏中的参数不能访问<script setup>中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
  • 除了使用字符串数组来声明prop外,还可以使用++对象的形式++:
javascript 复制代码
// 使用 <script setup>
defineprops({
  title: String,
  likes: Number
})
// 非 <script setup>
export default {
  props: {
    title: String,
    likes: Number
  }
}
  • 搭配TypeScript使用<script setup>,也可以使用++类型标注++来声明props:
javascript 复制代码
<script setup lang="ts">
defineprops<{
  title?: string
  likes?: number
}>()
</script>
  • Prop校验:Vue组件可以更细致地声明对传入的props的校验要求。要声明对props的校验,可以向defineprops()宏提供一个带有props校验选项的对象。
javascript 复制代码
defineprops({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawprops) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

使用一个对象绑定多个 prop​

如果想要将一个对象的所有属性都当作props传入,可以使用没有参数的v-bind,即只使用v-bind而非:prop-name。例如,这里有一个post对象:

javascript 复制代码
const post = {
  id: 1,
  title: 'My Journey with Vue'
}
//使用v-bind绑定多个prop:
<BlogPost v-bind="post" />
//而这实际上等价于:
<BlogPost :id="post.id" :title="post.title" />

$emit

  • 组件可以显式地通过 defineEmits() 宏来声明它要触发的事件:
javascript 复制代码
<script setup>
 defineEmits(['inFocus', 'submit'])
</script>
  • 在<template>中使用的$emit方法不能在组件的<script setup>部分中使用,但defineEmits()会返回一个相同作用的函数供我们使用:
javascript 复制代码
<script setup>
//defineEmits()不能在子函数中使用,它必须直接放置在<script setup>的顶级作用域下
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
  emit('submit')
}
const handleClick = ()=> {
  emit("submit", "这是发送给父组件的信息")
}
</script>
  • 如果显式地使用了setup函数而不是<script setup>,则事件需要通过emits选项来定义,emit函数也被暴露在setup()的上下文对象上:
javascript 复制代码
export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}
//与setup()上下文对象中的其他属性一样,emit可以安全地被解构
export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}
  • emits选项和defineEmits()宏还支持对象语法。通过TypeScript为参数指定类型,它允许我们对触发事件的参数进行验证:
javascript 复制代码
<script setup lang="ts">
const emit = defineEmits({
  submit(payload: { email: string, password: string }) {
    // 通过返回值为 true 还是为 false 来判断
    // 验证是否通过
  }
})
</script>
  • 如果正在搭配TypeScript使用<script setup>,也可以使用纯类型标注来声明触发的事件:
javascript 复制代码
<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>
  • 事件校验:所有触发的事件可以使用对象形式来描述,要为事件添加校验,那么事件可被赋值为一个函数,接受的参数就是抛出事件时传入emit的内容,返回一个布尔值来表明事件是否合法。
javascript 复制代码
<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})
function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

expose / ref

父组件获取子组件的属性或者调用子组件方法。

javascript 复制代码
<!-- Child.vue -->
<script setup>
defineExpose({
  childName: "这是子组件的属性",
  someMethod(){
    console.log("这是子组件的方法")
  }
})
</script>
<!-- Parent.vue -->
<template>
  <child ref="comp"></child>
  <button @click="handlerClick">按钮</button>
</template>
<script setup>
import child from "./child.vue"
import { ref } from "vue"
const comp = ref(null)
const handlerClick = () => {
  console.log(comp.value.childName) // 获取子组件对外暴露的属性
  comp.value.someMethod() // 调用子组件对外暴露的方法
}
</script>

attrs

包含父作用域里除class和style以外的非props属性集合。没有参数的v-bind会将一个对象的所有属性都作为attribute应用到目标元素上。

如果不想要一个组件自动地继承attribute,可以在组件选项中设置inheritAttrs:false。也可以直接在<script setup>中使用defineOptions:

javascript 复制代码
<script setup>
defineOptions({
  inheritAttrs: false
})
</script>

v-model

v-model可以在组件上使用以实现双向绑定。Vue3.4推荐的实现方式是使用 defineModel()宏:

javascript 复制代码
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
  model.value++
}
</script>
<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>
<!-- 父组件可以用 v-model 绑定一个值 -->
<!-- Parent.vue -->
<Child v-model="count" />

defineModel()返回的值是一个ref,它可以像其他ref一样被访问和修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:

  • 它的.value和父组件的v-model的值同步;
  • 当它被子组件变更了,会触发父组件绑定的值一起更新。

provide / inject依赖注入

● **provide:**可以让我们指定想要提供给后代组件的数据或方法。一个组件可以多次调用provide(),使用不同的注入名,注入不同的依赖值。在应用级别提供的数据在该应用内的所有组件中都可以注入,这在编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。

● **inject:**在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用。

javascript 复制代码
// Parent.vue
<script setup>
  import { provide } from "vue"
  provide(/*注入名*/"name", /*值*/"小陈")//也可指定为this,子代便能访问父代所有的数据和方法
</script>
// Child.vue
<script setup>
  import { inject } from "vue"
  const name = inject("name")//inject接收父组件的数据和方法
  console.log(name) //小陈
</script>

Vuex

Vuex是一个专为Vue.js应用程序开发的状态管理模式。

javascript 复制代码
// 方法一:直接使用
<template>
  <div>{{ $store.state.count }}</div>
  <button @click="$store.commit('add')">按钮</button>
</template>
 
// 方法二:获取
<script setup>
import { useStore, computed } from "vuex"
const store = useStore()
console.log(store.state.count) // 1
const count = computed(()=>store.state.count) // 响应式,会随着vuex数据改变而改变
console.log(count) // 1 
</script>

mitt

Vue3中没有了EventBus跨组件通信,但是现在有了一个替代的方案mitt.js,原理还是EventBus。

javascript 复制代码
// 组件 A
<script setup>
import mitt from './mitt'
const handleClick = () => {
  mitt.emit('handleChange')
}
</script>
// 组件 B 
<script setup>
import mitt from './mitt'
import { onUnmounted } from 'vue'
const someMethed = () => { ... }
mitt.on('handleChange',someMethed)
onUnmounted(()=>{
  mitt.off('handleChange',someMethed)
})
</script>

六、插槽

插槽是一种强大的工具,它允许我们构建更加灵活和可扩展的组件。通过使用插槽,可以将组件的内容分发到指定位置,从而实现组件的自定义和复用。可以让父组件插入内容到子组件的特定位置,并且可以传递数据以支持复杂的交互场景。

插槽的分类和使用

|-----------------|---------------------------------------------|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| | 默认插槽 | 具名插槽 | 作用域插槽 |
| <slot> 标签 | 没有任何 attribute | name | 绑定数据,通过slot标签的props,可以绑定多个 |
| 使用方式 | 1. 直接将内容放入组件标签内部 2. v-slot:default或#default | 1. slot:要对应组件中slot 标签上有name attribute 2. v-slot:作为指令参数,如v-slot:header | 1. <slot-scope>:接收对象,可以用slotprops.<xxx>引用slot绑定的数据 2. v-slot:接收对象,作为指令值,如 v-slot:header="headerSlotprops" |
| 拓展 | - | 插槽名可以是动态值 | 可通过解构直接获取属性使用,也可重命名和定义默认值 |

  • 插槽Slot:在组件模板中定义一个或多个位置,这些位置可以被父组件或其他使用者插入内容。
  • 作用域插槽Scoped Slots:子组件在作用域上绑定属性,将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上,父组件在使用时通过v-slot:(简写:#)获取子组件信息,在内容中使用。只有在提供参数时才能使用简写形式,就是说,想使用简写语法,务必指定插值的名字。
  • 子组件定义了一个默认插槽:
javascript 复制代码
<!-- child Component(定义了一个默认插槽)-->
<span>
  <slot :user="user">
    {{ user.lastName }}
  </slot>
</span>
  • 父组件通过slot-scope来访问,作用于<template>上:
javascript 复制代码
<!-- Parent Component -->
<!---------------------- 1.通过slot-scope ---------------------->
<current-user>
  <template slot-scope="props">
    {{ props.user.firstName }}
  </template>
</current-user>
<!---------------------- 2.通过v-slot ---------------------->
<current-user>
  <template v-slot:default="slotprops">
    {{ slotprops.user.firstName }}
  </template>
</current-user>
  • 父组件通过v-slot来访问,直接作用(当且仅当提供了默认插槽时可用)于组件<current-user>上
javascript 复制代码
<!---------------------- 显式调用默认插槽名字 -------------------->
<current-user v-slot:default="props">
  {{ props.user.firstName }}
</current-user>
<!---------------------- 省略默认插槽名字(简化默认插槽写法) ----------------------->
<current-user v-slot="props">
  {{ props.user.firstName }}
</current-user>
  • 为了避免作用域模糊,默认插槽的缩写语法不能与具名插槽混用,当有其它具名插槽时,默认插槽也应当使用<template>模板语法。

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非<template>节点都被隐式地视为默认插槽的内容。

html 复制代码
<current-user>
  <!----------------- 两种写法均可 ----------------->
  <!--<template v-slot="props">
    {{ props.user.firstName }}
  </template>-->
  <template v-slot:default="props">
    {{ props.user.firstName }}
  </template>
  <!----------------- 两种写法均可 ----------------->
  <!--<template v-slot:other="otherSlotprops">-->
  <template slot="other" slot-scope="otherSlotprops">
    ...
  </template>
</current-user>
  • **slot=""**可以用在父组件的<template>元素上或直接用在一个普通的元素上。
  • **v-slot:**只能添加到<template>或自定义组件上。当且仅当提供了默认插槽时,可以使用v-slot:直接作用在组件上,但是当有其它具名插槽时,默认插槽也应用<template>模板语法。
  • 动态插槽名称可以根据组件的状态或其他条件动态地改变插槽的名称。动态插槽名称要求子组件中存在与动态名称相对应的插槽。
html 复制代码
<template>  
  <ChildComponent>  
    <template v-slot:[dynamicSlotName]>  
      <p>这是动态插槽的内容</p>  
    </template>  
    <!-- 缩写为
    <template #[dynamicSlotName]>
      ...
    </template> -->
  </ChildComponent>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      dynamicSlotName: 'header' // 初始插槽名称为'header'  
    };  
  },  
  methods: {  
    changeSlotName() {  
      this.dynamicSlotName = 'footer'; // 改变插槽名称为'footer'  
    }  
  }  
}  
</script>

插槽的作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。插槽内容无法访问子组件的数据。

Vue模板中的表达式只能访问其定义时所处的作用域,和JavaScript的词法作用域规则一致。换言之:父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

插槽的内容可以访问组件的数据和方法,这是通过插槽的作用域实现的。在具名插槽中,可以使用<template v-slot:name="props">的形式来接收来自组件的数据。

插槽的数据传递

我们已经知道,插槽的内容无法访问到子组件的状态。

然而在某些场景下,插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们可以像对组件传递props那样,向一个插槽的出口上传递attributes。

场景:当前,有一个多处使用的弹窗,里面的左半部分是动态变化的,右半部分是统一的,我们需要动态配置左边,所以可以把左边区域的整块内容挖空,在使用到dialog的地方通过插槽去配置。

++这就涉及到了插槽的传递:从子组件一层层往上传到使用的那一层,传递过程中,可以保持同一个插槽名,也可以选用不同的插槽名,但要注意每一层传递过程中,相应的插槽名要对得上。++

下面,记录一下我使用到的两种情况。

  • **第一种:保持插槽同名。**在这里,笔者从上到下每一层都保持名为DragContentDialogMsg。
  • **第二种:不保持插槽同名。**在这里,笔者从上到下每一层都改名了,依次为:DragContentMsgTop→DragContentItems→DragContentMsg。

遍历时的插槽使用

场景:有一个组件接收一个列表作为数据,还需要为列表中的每一项提供自定义的内容或模板。这通常通过作用域插槽(scoped slots)来实现,作用域插槽允许父组件根据子组件传递的数据来动态地定义插槽的内容,从而实现更加灵活和可复用的组件交互。

  • 子组件:接收一个list数组作为属性,并定义一个名为DragContentMsg的作用域插槽。
html 复制代码
<ul>  
  <li v-for="(element, index) in list" :key="index">  
    <slot name="DragContentMsg" :element="element"></slot>  
  </li>  
</ul> 
  • 父组件:使用子组件并为DragContentMsg插槽提供内容,该内容是基于从子组件传递过来的element数据。在<ListComponent>中,我们可以多次渲染<slot>并每次都提供不同的数据。
html 复制代码
<ListComponent :list="dataList">  
  <template v-slot:DragContentMsg="props">  
    <span>{{ props.element.plan_time }}-{{ props.element.complete_time }}</span>  
   </template>  
</ListComponent>  

七、配置组件封装

配置组件封装是++将组件的某些行为或属性通过外部配置来定义++,而不是硬编码在组件内部。这种封装方式允许开发者根据不同的需求或场景,通过配置参数来调整组件的行为或外观。

使用这种方式,组件内所渲染的内容和字段都是动态配置的,可以根据需要自由调整。

配置化组件封装的步骤:

  • ①定义组件可配置的配置项。这些配置项可以是组件的属性、方法、插槽名称等。
  • ②创建组件。在组件中,需要根据配置动态地绑定属性、调用方法或渲染插槽。
  • ③封装配置化组件。封装一个工厂函数或高阶组件,它接收配置对象并返回配置化后的组件。
  • ④使用配置化组件。使用工厂函数来创建配置化组件的实例,并在父组件中注册和使用它。

举个例子:

首先,定义了一个底部信息统计+按钮组件,根据外部传进来的配置信息和数据,显示不同的图标、字段、按钮名称与方法。

html 复制代码
<template>
<div class="bottom-content-wrap">
  <!-- 左侧列表 -->
  <div v-for="(item,index) in statisticItems" :key="index">
    <span class="content-items">
      <i :class="item.icon">
        <span class="items-text">{{messages[item.props]}}</span>
      </i> 
    </span>
  </div>
  <!-- 右下角按钮 -->
  <div>
    <el-dropdown>
      <i class="el-icon-more"></i>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item v-for="(but,index) in statisticButtons" :key="index">
          <i :class="but.icon" @click="handleButClick(but.event,messages)">{{but.title}}</i>
        </el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</div>
</template>

<script>
export default {
  name: 'BottomContent',
  components: {
  },
  props: {
    // 统计项配置
    statisticItems:{
      type:Array,
      default:()=>[]
    },
    // 统计项数据
    messages: {
      type: Object,
      default: () => {}
    },
    // 按钮配置
    statisticButtons:{
      type:Array,
      default:() => [
        {icon:'',title:'',event:''},
      ]
    }
  },
  methods: {
    // 点击调用的方法
    handleButClick(event,element){
      this.$emit(event,element)
    },
  }
}
</script>

然后,在父组件中使用并传入数据和配置项。

html 复制代码
 <div 
  v-for="element in list" 
  :key="element.id">
  <TopContent :element="element" />
  <BottomContent
    :messages="element" 
    :statisticItems="statisticItems"
    :statisticButtons="statisticButtons"
  />
</div>
javascript 复制代码
// 卡片统计配置
statisticItems(){
  return [
    {icon:'el-icon-user',props:'responsible_name'},
    {icon:'el-icon-set-up',props:'subtask_num'},
    {icon:'el-icon-connection',props:'attachment_num'},
    {icon:'el-icon-chat-line-square',props:'comment_num'}
  ]
},
// 卡片右下角按钮配置
statisticButtons(){
  return [
    {icon:'el-icon-document-copy',title:'复制任务',event:'digitalTaskCopy'},
    {icon:'el-icon-delete',title:'删除任务',event:'digitalTaskDelete'},
  ]
},

八、模板组件封装

模板组件封装是++将可复用的代码片段或功能提取出来++,创建为独立的组件,以便重复使用。这些模板通常包含了一组预定义的布局、样式和行为,开发者可以基于这些模板快速创建新的组件实例。

使用这种方式,组件中显示的内容和字段名是固定的,各种样式也都是固定的,可根据需要,增加一些参数去调整。

模板组件封装的步骤:

  • ①创建组件文件。创建一个新的.vue文件来定义你的组件。
  • ②注册组件。可以全局注册或者局部注册。
  • ③使用组件。一旦组件被注册,就可以在父组件的模板中通过标签的形式使用它。
  • ④传递属性和事件。可以向组件传递属性和监听组件触发的事件。

**举个例子:**​​​​​​​

首先,定义了一个评论区每条评论的布局,根据外部传进来的数据渲染数量不一定的评论数。

html 复制代码
<template>
<div class="info-container">
  <slot name="avater">
    <avatar :username="data.user" :size="18" background-color="#409EFF" color="#fff" />
  </slot>
  <slot name="info">
    <span>{{ data.user }}</span>
    <span>{{ data.time }}</span>
  </slot>
  <el-tag v-if="data.major" effect="dark" type="warning" size="mini">
    {{ data.major }}
  </el-tag>
  <el-tag v-if="data.status && data.status !== 0" :type="getStatusTag(data.status, 'type')" size="mini">
    {{ getStatusTag(data.status, 'label') }}
  </el-tag>
  <slot name="tag-append" />
  <slot name="header-append">
    <div class="header-append">
      <i v-if="showEdit" @click.stop="$emit('edit')" />
    </div>
    <div class="header-append">
      <i v-if="showDelete" @click.stop="$emit('delete')" />
    </div>
  </slot>
</div>
</template>

然后,在父组件中使用并传入数据。

html 复制代码
<comments
  :show-delete="data.is_oper"
  :data="data"
  width="550px"
  @delete="deleteComments(data)"
/>

注意:配置组件和模板组件不能混用!要么都用模板,要么都用配置化。笔者亲测,混用会出问题。

动态组件

有些场景会需要在两个甚至多个组件间来回切换,比如Tab界面,可以通过通过<component>元素和特殊的 is attribute 实现,被传给 :is 的值可以是以下几种:

  • ①被注册的组件名
  • ②导入的组件对象
  • ③一般的 HTML 元素
html 复制代码
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
<!-- 表单控件 - input -->
<component :is="el-input"></component>

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过<KeepAlive>组件强制被切换掉的组件仍然保持存活的状态。

DOM 内模板解析注意事项

如果你想在DOM中直接书写Vue模板,Vue则必须从DOM中获取模板字符串。由于浏览器的原生HTML解析行为限制,有一些需要注意的事项。

大小写区分:HTML标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当使用DOM内的模板时,名称都需要转换为相应等价的短横线连字符形式。

闭合标签:在DOM内模板中,我们必须显式地写出关闭标签。

元素位置限制​:某些HTML元素对于放在其中的元素类型有限制,例如<ul>、<ol>、<table>和<select>,相应的,某些元素仅在放置于特定元素中时才会显示,例如<li>、<tr>和<option>。这将导致在使用带有此类限制元素的组件时出现问题。

html 复制代码
<table>
  <blog-post-row></blog-post-row>
</table>
<!-- 自定义的组件<blog-post-row>将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。
     我们可以使用特殊的 is attribute 作为一种解决方案:-->
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

当使用在原生HTML元素上时,is的值必须加上前缀vue: 才可以被解析为一个Vue组件。这是必要的,为了避免和原生的自定义内置元素相混淆。

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪8 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪8 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试