造轮子系列之Button

首先特别感谢方应杭老师,大家可以照着本篇文章操作,初始化项目请看前面的项目仓库代码

attrs的说明

src --> lib 文件夹下新建文件Button.vuecomponents文件下新建ButtonDemo.vue内容如下

vue 复制代码
<script setup lang="ts" name="ButtonDemo">
import Button from '@/lib/Button.vue'
const onClick = () => {
  console.log('点击button按钮')
}
</script>
<template>
  <Button @click="onClick">按钮</Button>
</template>

Button.vue内容如下

vue 复制代码
<template>
  <div class="ban-button">
    <h1>点击我不触发click事件</h1>
    <br>
    <button>
      <slot></slot>
    </button>
  </div>
</template>

问题:给Button组件注册click事件,点击h1也会触发click

默认情况下,父组件传递的,但没有被子组件解析为 props 的 attributes 绑定会被"透传"。这意味着当我们有一个单根节点的子组件时,这些绑定会被作为一个常规的 HTML attribute 应用在子组件的根节点元素上。当你编写的组件想要在一个目标元素或其他组件外面包一层时,可能并不期望这样的行为。我们可以通过设置 inheritAttrsfalse 来禁用这个默认行为。这些 attributes 可以通过 $attrs 这个实例属性来访问($attrs继承所有属性),并且可以通过 v-bind 来显式绑定在一个非根节点的元素上。

vue 复制代码
<script setup lang="ts">
defineOptions({ inheritAttrs: false })
</script>
<template>
  <button v-bind="$attrs">
    <slot></slot>
  </button>
</template>

因为Button.vue组件只有button所以这里我们的代码不需要defineOptions$attrs,这里仅作为知识点

主题theme

ButtonDemo.vue代码

vue 复制代码
<h1>示例1</h1>
<div>
  <Button>你好</Button>
  <Button theme="button">你好</Button>
  <Button theme="link">你好</Button>
  <Button theme="text">你好</Button>
</div>

Button.vue代码

vue 复制代码
<script setup lang="ts">
const props = defineProps({
  theme: {
    type: String,
    default: 'button'
  }
})
const { theme } = props
const classes = computed(() => {
  return {
    [`ran-theme-${theme}`]: theme
  }
})
</script>
<template>
  <button v-bind="$attrs" class="ran-button" :class="classes">
    <slot></slot>
  </button>
</template>

Button.vue样式

scss 复制代码
<style lang="scss">
$h: 32px;
$border-color: #d9d9d9;
$color: #333;
$blue: #40a9ff;
$radius: 4px;
$red: red;
$grey: grey;
.ran-button {
  box-sizing: border-box;
  height: $h;
  padding: 0 12px;
  cursor: pointer;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  white-space: nowrap;
  background: white;
  color: $color;
  border: 1px solid $border-color;
  border-radius: $radius;
  box-shadow: 0 1px 0 fade-out(black, 0.95);
  transition: background 250ms;
  & + & {
    margin-left: 8px;
  }
  &:hover,
  &:focus {
    color: $blue;
    border-color: $blue;
  }
  &:focus {
    outline: none;
  }
  &::-moz-focus-inner {
    border: 0;
  }
  &.ran-theme-link {
    border-color: transparent;
    box-shadow: none;
    color: $blue;
    &:hover,
    &:focus {
      color: lighten($blue, 10%);
    }
  }
  &.ran-theme-text {
    border-color: transparent;
    box-shadow: none;
    color: inherit;
    &:hover,
    &:focus {
      background: darken(white, 5%);
    }
  }
}
</style>

知识点:darken lighten fade_out

lighten()和darken()两个函数都是围绕颜色的亮度值做调整的,其中lighten()函数会让颜色变得更亮,与之相反的darken()函数会让颜色变得更暗。这个亮度值可以是0~1之间,所有的颜色都可以使用不仅仅是background

scss 复制代码
&:hover,
&:focus {
    background: darken($blue, 10%);
    border-color: darken($blue, 10%);
}

scss也有两个调节颜色的

fade-in() 降低颜色的透明度,取值在 0-1 之。
fade-out() 提升颜色的透明度,取值在 0-1 之间。

fade_out background-color: fade-out(black, 0.5);效果是0.1是不透明,1是全透明,0.5是黑色半透明

fade_in没有get到用法

尺寸size

ButtonDemo.vue新增代码

vue 复制代码
<h1>示例2</h1>
<br>
<div>
    <div>
        <Button size="big">大大大</Button>
        <Button>普普通</Button>
        <Button size="small">小小小</Button>
    </div>
    <div>
        <Button theme="link" size="big">大大大</Button>
        <Button theme="link">普普通</Button>
        <Button size="small" theme="link">小小小</Button>
    </div>
    <div>
        <Button size="big" theme="text">大大大</Button>
        <Button theme="text">普普通</Button>
        <Button size="small" theme="text">小小小</Button>
    </div>
</div>

Button.vue代码

vue 复制代码
<script setup lang="ts">
const props = defineProps({
  theme: {
    type: String,
    default: 'button'
  },
  size: {
    type: String,
    default: 'normal'
  }
})
const { theme, size } = props
const classes = computed(() => {
  return {
    [`ran-theme-${theme}`]: theme,
    [`ran-size-${size}`]: size
  }
})
</script>

Button.vue新增样式

scss 复制代码
.ran-button {
  &.ran-size-big {
    font-size: 24px;
    height: 48px;
    padding: 0 16px;
  }
  &.ran-size-small {
    font-size: 12px;
    height: 20px;
    padding: 0 4px;
  }
}

类型level

ButtonDemo.vue新增代码

vue 复制代码
<h1>示例3</h1>
<br>
<div>
    <div>
        <Button level="main">主要按钮</Button>
        <Button>普通按钮</Button>
        <Button level="danger">危险按钮</Button>
    </div>
    <div>
        <Button theme="link" level="main">主要链接按钮</Button>
        <Button theme="link">普通链接按钮</Button>
        <Button theme="link" level="danger">危险链接按钮</Button>
    </div>
    <div>
        <Button theme="text" level="main">主要文字按钮</Button>
        <Button theme="text">普通文字按钮</Button>
        <Button theme="text" level="danger">危险文字按钮</Button>
    </div>
</div>

Button.vue代码

vue 复制代码
<script setup lang="ts">
const props = defineProps({
  theme: {
    type: String,
    default: 'button'
  },
  size: {
    type: String,
    default: 'normal'
  },
  level: {
    type: String,
    default: 'normal'
  }
})
const { theme, size, level } = props
const classes = computed(() => {
  return {
    [`ran-theme-${theme}`]: theme,
    [`ran-size-${size}`]: size,
    [`ran-level-${level}`]: level
  }
})
</script>

Button.vue新增样式

scss 复制代码
.ran-button {
    &.ran-theme-button {
        &.ran-level-main {
            background: $blue;
            color: white;
            border-color: $blue;
            &:hover,
            &:focus {
                background: darken($blue, 10%);
                border-color: darken($blue, 10%);
            }
        }
        &.ran-level-danger {
            background: $red;
            border-color: $red;
            color: white;
            &:hover,
            &:focus {
                background: darken($red, 10%);
                border-color: darken($red, 10%);
            }
        }
    }
    &.ran-theme-link {
        &.ran-level-danger {
            color: $red;
            &:hover,
            &:focus {
                color: darken($red, 10%);
            }
        }
    }
    &.ran-theme-text {
        &.ran-level-main {
            color: $blue;
            &:hover,
            &:focus {
                color: darken($blue, 10%);
            }
        }
        &.ran-level-danger {
            color: $red;
            &:hover,
            &:focus {
                color: darken($red, 10%);
            }
        }
    }
}

支持disabled

ButtonDemo.vue新增代码

vue 复制代码
<h1>示例4</h1>
<br>
<div>
    <Button disabled>禁用按钮</Button>
    <Button theme="link" disabled>禁用链接按钮</Button>
    <Button theme="text" disabled>禁用按钮</Button>
</div>

Button.vue代码

vue 复制代码
<script setup lang="ts">
const props = defineProps({
  theme: {
    type: String,
    default: 'button'
  },
  size: {
    type: String,
    default: 'normal'
  },
  level: {
    type: String,
    default: 'normal'
  },
  disabled: {
    type: Boolean,
    default: false
  }
})
const { theme, size, level } = props
const classes = computed(() => {
  return {
    [`ran-theme-${theme}`]: theme,
    [`ran-size-${size}`]: size,
    [`ran-level-${level}`]: level
  }
})
</script>
<template>
  <button class="ran-button" :class="classes" :disabled="disabled">
    <slot></slot>
  </button>
</template>

Button.vue新增样式

scss 复制代码
.ran-button {
    &.ran-theme-button {
        &[disabled] {
            cursor: not-allowed;
            color: $grey;
            &:hover {
                border-color: $grey;
            }
        }
    }
    &.ran-theme-link, &.ran-theme-text {
        &[disabled] {
            cursor: not-allowed;
            color: $grey;
        }
    }
}

支持loading

ButtonDemo.vue新增代码

vue 复制代码
<h1>示例5</h1>
<br>
<div>
    <Button loading>加载中</Button>
    <Button>加载完毕</Button>
</div>

Button.vue代码

vue 复制代码
<script setup lang="ts">
const props = defineProps({
  theme: {
    type: String,
    default: 'button'
  },
  size: {
    type: String,
    default: 'normal'
  },
  level: {
    type: String,
    default: 'normal'
  },
  disabled: {
    type: Boolean,
    default: false
  },
  loading: {
    type: Boolean,
    default: false
  }
})
const { theme, size, level } = props
const classes = computed(() => {
  return {
    [`ran-theme-${theme}`]: theme,
    [`ran-size-${size}`]: size,
    [`ran-level-${level}`]: level
  }
})
</script>
<template>
  <button class="ran-button" :class="classes" :disabled="disabled">
    <span v-if="loading" class="ran-loadingIndicator"></span>
    <slot></slot>
  </button>
</template>

Button.vue新增样式

scss 复制代码
.ran-button {
    > .ran-loadingIndicator{
        width: 14px;
        height: 14px;
        display: inline-block;
        margin-right: 4px;
        border-radius: 8px;
        border-color: $blue $blue $blue transparent;
        border-style: solid;
        border-width: 2px;
        animation: ran-spin 1s infinite linear;
    }
}
@keyframes ran-spin {
    0%{transform: rotate(0deg)}
    100%{transform: rotate(360deg)}
}

组件库中的CSS

不能使用scoped

因为data-v-xxx中的xxx每次运行可能不同

必须输出稳定不变的class选择器,方便使用者覆盖(避免使用者使用v:deep)

必须加前缀

所有组件库的class类必须加上组件前缀,比如.button变成.ran-button

css最小影响原则

css绝不能影响库的使用者,那怎么才能不影响使用者呢?

global.scss这个是main.ts引入的,不能保证初始化样式别人也会引入怎么办?

写一个random.scss,这个要告诉使用者,一定要引入这个scss,然后再引入自己的公共样式,那这样的话,不管使用者的样式怎么样,我们的样式都不会受到影响!

src -> lib新建文件random.scss

scss 复制代码
[class^="ran-"], [class*=" ran-"] {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-size: 16px;
  // 为什么这样写 font-family
  // 答案见 https://github.com/zenozeng/fonts.css/
  font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica,
    "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB",
    "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN",
    "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti",
    SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
}

main.ts中引入random.scss

typescript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import '@/lib/random.scss'
import '@/assets/styles/global.scss'

import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

[class^="b-"], [class*=" b-"] 的意思是以b-开头的class和包含空格 b-的class类

知识点:[ ^$*~= ]

[attribute] 用于选取带有指定属性的元素。

[attribute=value] 用于选取带有指定属性和值的元素。

[attribute~=value] 用于选取属性值中包含指定词汇的元素。

[attribute|=value] 用于选取带有以指定值开头的属性值的元素,该值必须是整个单词。

[attribute^=value] 匹配属性值以指定值开头的每个元素。

[attribute$=value] 匹配属性值以指定值结尾的每个元素。

[attribute*=value] 匹配属性值中包含 指定值的每个元素。

下面的例子为包含指定值的 title 属性的所有元素设置样式。适用于由空格分隔的属性值: [title~=hello] { color:red; }

下面的例子为带有包含指定值的 lang 属性的所有元素设置样式。适用于由连字符分隔的属性值: [lang|=en] { color:red; }

~=其中的value必须是一个独立的单词,例如 [title~=test]test a可以被选中testa不能被选中. *=其中的value只要是值的子串就可以,例如test-atest atesta均可以被选中. |=~=的特性一样,^=*=的特性一样.因此平时还是用^=*=较好

相关推荐
喵叔哟10 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django