制作 Button 组件

Button

[toc]


需求分析

  • 借鉴 AntD/ Bulma/Eleme/iView/Vuetify 等
  • 需求
    • 可以有不同的等级
    • 可以是链接,可以是文字
    • 可以 click/focus/鼠标悬浮
    • 可以设置size
    • 可以设置状态为禁用
    • 可以设置状态为加载中

API 设计

1
2
3
4
5
6
7
8
<Button @click=?
        @focus=?
        @mouseover=?
        theme="button/link/text"
        level="big/normal/small"
        disabled
        loading>
</Button>

创建 Button 组件

Button 基础样式

Button.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
  <button>
    <slot></slot>
  </button>
</template>

<script lang="ts">
export default {
  name: 'Button'
};
</script>

<style lang="scss" scoped>

</style>

ButtonDemo.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import Button from '@/lib/Button.vue';
</script>

<template>
  <h1>Button 示例</h1>
  <h2>示例一</h2>
  <div>
    <Button>按钮</Button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ButtonDemo'
};
</script>
  • 使用<slot></slot>来让UI库用户自定义button内部的结构

让Button支持事件

Button.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import Button from '@/lib/Button.vue';

const onClick = () => {
  console.log('hi');
};
</script>

<template>
  <h1>Button 示例</h1>
  <h2>示例一</h2>
  <div>
    <Button @click="onClick">
      按钮
    </Button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ButtonDemo'
};
</script>
  • vue自动将绑定的事件传到子组件中根节点上
  • 不用再处理事件代理逻辑

这样会带来一个问题:点击外层元素,而未点击到button元素上也会触发事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Button.vue
<template>
  <div>
    <button>
      <slot></slot>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'Button'
};
</script>

<style lang="scss" scoped>
div {
  border: 1px solid red;
}
</style>

让div不继承属性

  • 使用inheritAttrs: false来禁用 Attribute 继承,。偶人为true
  • 此时事件没有绑在子组件任何一个元素上

Button.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <div>
    <button>
      <slot></slot>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'Button',
  inheritAttrs: false
};
</script>

<style lang="scss" scoped>
div {
  border: 1px solid red;
}
</style>

Vue 3 属性绑定细节

让div里的button显式地绑定$attrs

  • 在父组件/元素上写$attrs,实现所有属性的祖传孙元素
  • $attrs会以对象的形式展示所有祖元素/组件上的属性
  • 使用v-bind="$attrs",此处不可缩写

Button.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <div>
    <button v-bind="$attrs">
      <slot></slot>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'Button',
  inheritAttrs: false
};
</script>

<style lang="scss" scoped>
div {
  border: 1px solid red;
}
</style>
  • 小结:让组件内部某一个节点拥有外部(使用该组件时)传入的(写在标签上的)所有属性,而其他节点不同时拥有的情况

让div继承一部分属性,让button继承另一部分属性

  • setup中使用context.attrs将属性结构出来
  • script setup语法糖中,使用useAttrs()获取$attrs来处理
    • 解构赋值取出属性const {size, ...rest} = attrs;
    • 在外层div上绑定<div :size="size">...</div>
    • 在button上绑定其他剩余属性<button v-bind="rest"><slot></slot></button>

Button.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup lang="ts">
import {useAttrs} from 'vue';

const attrs = useAttrs();
const {size, ...rest} = attrs;

</script>

<template>
  <div :size="size">
    <button v-bind="rest">
      <slot></slot>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'Button',
  inheritAttrs: false
};
</script>

<style lang="scss" scoped>
div {
  border: 1px solid red;
}
</style>

小结

  • Vue3属性绑定
    • 默认所有属性都绑定到根元素
    • 使用inheritAttrs: false可以取消默认绑定
    • 使用$attrs或者context.attrs获取使用该组件时绑定的所有属性
    • 单文件组件