组件库开发:最基础的两个组件yk-icon、yk-button

1,039 阅读8分钟

Icon组件

我们的第一个组件是Icon,因为它最简单,在有些组件中还会用到,也能激起大家学习的兴趣

废话不多说了,直接开干

image.png

首先,创建好我们icon组件的目录结构

我们的iconyike设计的并且已经上传到了iconfont上,yk-design-icon

那既然咱们说到iconfont了,那大家可能也知道了icon的组件开发方式了

我们直接采用symbol用法,也就是我们把两套图标(面性和线性的)的js直接引入进来

放到我们的assets下。并且新建一个font文件夹,专门用来存放图标

image.png

这两个js文件大家直接去仓库里面拿就行啦~(yike-design-dev/packages/yike-design-ui/src/assets/font at monorepo-dev · ecaps1038/yike-design-dev (github.com))

那么我们icon组件的代码就显而易见啦~

icon.vue

<template>
    <svg class="icon" aria-hidden="true">
        <use :xlink:href="'#' + name"></use>
    </svg>
</template>
<script setup lang="ts">
import '../../../assets/font/line/iconfont.js'
import '../../../assets/font/surface/iconfont.js'
import '../style'
import { IconProps } from './icon'

defineOptions({
    name: 'YkIcon',
})

withDefaults(defineProps<IconProps>(), {
    name: '',
})
</script>

aria-hidden="true":这是一个辅助技术属性,用于指示屏幕阅读器忽略此图标,因为它只是用作视觉装饰而不包含任何有意义的文本内容。如果没说明白的话,看看这个:指路

这段代码其实关键就在 :xlink:href="'#' + name":这是动态绑定属性,name 是一个变量,通过将其与'#'拼接,可以指定使用哪个SVG片段作为复用模板。

通俗点说,我只要传个name,那么就知道我要哪个图标了

icon.ts

export type IconProps = {
  name?: string;
};

既然我就需要传一个name,那么我IconProps就设置一个name这一个type就好了

这也是我最开始讲解icon组件的原因,因为它最简单,而且能走遍整个组件的开发流程

然后呢,我们需要给icon设置一个初始样式

index.less

.icon {
  overflow: hidden;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
}

然后我们style文件夹下新建index.ts用于样式的导出

import './index.less'

index.ts

组件写完之后我们需要在index.ts中进行导出

import Icon from './src/icon.vue';
import { withInstall } from '../../utils/index';

export const YkIcon = withInstall(Icon);
export default YkIcon;
export * from './src/icon';

最后呢,我们在yk-design的根目录的index.ts进行导出即可

怕大家迷糊,我指出一下

image.png

import { YkTest } from './components/test/index';
import YkIcon from './components/icon';
import type { Component, App } from 'vue';
import './styles/index.less';

const components: {
    [propName: string]: Component;
} = {
    YkTest,
    YkIcon
};

export {
    YkTest,
    YkIcon
};
// 全局注册
export default {
    install: (app: App) => {
        for (const c in components) {
            app.component(c, components[c]);
        }
    },
};

使用Icon组件

我们直接回到demo工程下,使用我们的YkIcon组件

image.png

我们看效果

image.png

OK,显示没有问题,那么我们的icon组件就开发完成啦

Button组件

我们这一章来开发一个组件库中较为基础的组件:Button

首先,把我们组件文件结构搭建出来

image.png

我们先思考button.vue中应该怎么去写

  • 首先:我们要有一个最基础的button
  • 其次:我们按钮会有加载状态,那我们可以用svg去实现
  • 再有:按钮的class应该是动态的,那么我们是不是应该使用computed去实现

那我们现在基于这几点思考,我们得出了一个基本的结构

<template>
  <button :class="ykButtonClass" :disabled="disabled || loading">
    <svg v-if="loading" viewBox="25 25 50 50">
      <circle r="20" cy="50" cx="50"></circle>
    </svg>
    <slot name="icon"></slot>
    <slot></slot>
  </button>
</template>

其中:disabled="disabled || loading" 绑定了一个动态属性 disabled,如果disabled或者 loading 为真,则按钮会被禁用。

<svg> 通过 v-if,当 loading 为真时,会渲染一个圆形进度条SVG 动画

  • <slot name="icon"></slot> 是具名插槽,用于插入按钮的图标。
  • <slot></slot> 是默认插槽,用于插入按钮的文本或其他内容

通过这样书写,我们可以在需要的地方插入按钮的图标和内容,并根据需要设置按钮的样式、禁用状态和加载状态。

好的,那我们现在应该思考button.ts怎么写了,我们props都需要传什么值

  • 按钮类型 type
  • 按钮尺寸 size
  • 按钮形状 shape
  • 图标按钮 icon
  • 按钮状态 status
  • 禁用状态 disabled
  • 加载中按钮 loading
  • 长按钮 long

好的,这是我们预想的几种情况,那么我们是不是可以书写button.ts

button.ts

import { Shape, Size, Status, Type } from '../../../utils/constant';

export type ButtonProps = {
  type?: Type;
  status?: Status;
  size?: Size;
  shape?: Shape;
  long?: boolean;
  loading?: boolean;
  disabled?: boolean;
};

诶,这个Shape, Size, Status, Type,我在最开始组件库准备工作中是不是有提到,我们将一些常用的类型,状态封装在了constant.ts中,这回是不是就用上啦!

image.png

好的,那我们props的类型定义好了,我们可以继续书写我们的组件啦

button.vue

我们现在引入buttonProps,并设置默认值

import { ButtonProps } from './button'

const props = withDefaults(defineProps<ButtonProps>(), {
    type: 'primary',
    size: 'l',
    shape: 'default',
    long: false,
    loading: false,
    disabled: false,
})

好的,现在我们是不是就差通过computed来动态渲染类名了

const ykButtonClass = computed(() => {
    return {
        'yk-button': true,
        'yk-button--loading': props.loading,
        'yk-button--long': props.long,
        'yk-button--disabled': props.disabled || props.loading,
        [`yk-button--${props.status}`]: props.status,
        [`yk-button--${props.type}`]: props.type,
        [`yk-button--${props.size}`]: props.size,
        [`yk-button--${props.shape}`]: props.shape,
    }
})

在计算属性的回调函数中,返回一个对象,该对象描述了按钮可能具有的各种 CSS 类

通过使用这个计算属性,你可以根据传入的属性值动态设置按钮的样式类。这样,你就可以很方便地根据不同的属性配置自定义按钮的外观和行为

我相信,这不仅会教你怎么开发组件库,也会让你真正了解computed的作用,不要面试一问computedwatch区别就死记硬背啦~

好的,我们组件代码完成了,现在是这样的

<template>
    <button :class="ykButtonClass" :disabled="disabled || loading">
        <svg v-if="loading" viewBox="25 25 50 50">
            <circle r="20" cy="50" cx="50"></circle>
        </svg>
        <slot name="icon"></slot>
        <slot></slot>
    </button>
</template>

<script lang="ts">
export default {
    name: 'YKButton',
}
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { ButtonProps } from './button'
import '../style'

const props = withDefaults(defineProps<ButtonProps>(), {
    type: 'primary',
    size: 'l',
    shape: 'default',
    long: false,
    loading: false,
    disabled: false,
})

const ykButtonClass = computed(() => {
    return {
        'yk-button': true,
        'yk-button--loading': props.loading,
        'yk-button--long': props.long,
        'yk-button--disabled': props.disabled || props.loading,
        [`yk-button--${props.status}`]: props.status,
        [`yk-button--${props.type}`]: props.type,
        [`yk-button--${props.size}`]: props.size,
        [`yk-button--${props.shape}`]: props.shape,
    }
})
</script>

然后我们需要书写样式啦

style

我们在style下定义个variables.less

我们在这里面设置不同类型按钮的颜色样式

@import '../../../styles/index.less';
@btn-primary-color-text: #fff;
@btn-primary-color-text_primary: #fff;
@btn-primary-color-text_success: #fff;
@btn-primary-color-text_warning: #fff;
@btn-primary-color-text_danger: #fff;
@btn-primary-color-bg: @pcolor;
@btn-primary-color-bg_primary: @pcolor;
@btn-primary-color-bg_success: @scolor;
@btn-primary-color-bg_warning: @wcolor;
@btn-primary-color-bg_danger: @ecolor;
@btn-primary-color-border: @pcolor;
@btn-primary-color-hover: @pcolor-6;
@btn-primary-color-border_primary: @pcolor;
@btn-primary-color-border_success: @scolor;
@btn-primary-color-border_warning: @wcolor;
@btn-primary-color-border_danger: @ecolor;
@btn-primary-color-hover_primary: @pcolor-6;
@btn-primary-color-hover_success: @scolor-6;
@btn-primary-color-hover_warning: @wcolor-6;
@btn-primary-color-hover_danger: @ecolor-6;

@btn-secondary-color-text: @font-color-l;
@btn-secondary-color-text_primary: @pcolor;
@btn-secondary-color-text_success: @scolor;
@btn-secondary-color-text_warning: @wcolor;
@btn-secondary-color-text_danger: @ecolor;
@btn-secondary-color-bg: @gray-1;
@btn-secondary-color-hover: @gray-2;
@btn-secondary-color-bg_primary: @pcolor-1;
@btn-secondary-color-bg_success: @scolor-1;
@btn-secondary-color-bg_warning: @wcolor-1;
@btn-secondary-color-bg_danger: @ecolor-1;
@btn-secondary-color-hover_primary: @pcolor-2;
@btn-secondary-color-hover_success: @scolor-2;
@btn-secondary-color-hover_warning: @wcolor-2;
@btn-secondary-color-hover_danger: @ecolor-2;
@btn-secondary-color-border: transparent;
@btn-secondary-color-border_primary: transparent;
@btn-secondary-color-border_success: transparent;
@btn-secondary-color-border_warning: transparent;
@btn-secondary-color-border_danger: transparent;

@btn-outline-color-text: @font-color-l;
@btn-outline-color-text_primary: @pcolor;
@btn-outline-color-text_success: @scolor;
@btn-outline-color-text_warning: @wcolor;
@btn-outline-color-text_danger: @ecolor;
@btn-outline-color-bg: transparent;
@btn-outline-color-hover: @gray-1;
@btn-outline-color-bg_primary: transparent;
@btn-outline-color-bg_success: transparent;
@btn-outline-color-bg_warning: transparent;
@btn-outline-color-bg_danger: transparent;
@btn-outline-color-border: @gray-3;
@btn-outline-color-border_primary: @pcolor-3;
@btn-outline-color-border_success: @scolor-3;
@btn-outline-color-border_warning: @wcolor-3;
@btn-outline-color-border_danger: @ecolor-3;
@btn-outline-color-hover_primary: @pcolor-1;
@btn-outline-color-hover_success: @scolor-1;
@btn-outline-color-hover_warning: @wcolor-1;
@btn-outline-color-hover_danger: @ecolor-1;

这里面都定义了什么呢,我们拿primary举例即可

  • 主要按钮的文本颜色
  • 主要按钮的背景颜色
  • 主要按钮的边框颜色和鼠标悬停状态下的背景颜色

当然,其中的一些变量是在根目录下的styles下定义的

image.png

这在准备工作的那节文章也说过了~

secondaryoutline同理

index.less

@import './variables.less';

/* stylelint-disable */
.yk-button--loading {
    opacity: 0.7 !important;

    svg {
        margin-right: 4px;
        width: 16px;
        transform-origin: center;
        animation: rotate4 2s linear infinite;
    }

    circle {
        fill: none;
        stroke: hsl(227, 16%, 89%);
        stroke-width: 3;
        stroke-dasharray: 1, 200;
        stroke-dashoffset: 0;
        stroke-linecap: round;
        animation: dash4 1.5s ease-in-out infinite;
    }

    @keyframes rotate4 {
        100% {
            transform: rotate(360deg);
        }
    }

    @keyframes dash4 {
        0% {
            stroke-dasharray: 1, 200;
            stroke-dashoffset: 0;
        }

        50% {
            stroke-dasharray: 90, 200;
            stroke-dashoffset: -35px;
        }

        100% {
            stroke-dashoffset: -125px;
        }
    }
}

.yk-button {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    padding: 6px 16px;
    white-space: nowrap;
    // 默认按钮状态
    color: @btn-primary-color-text;
    background-color: @btn-primary-color-bg;
    outline: none;
    transition: all @animatb ease-in-out;
    box-sizing: border-box;
    border-width: 1px;
    border-style: solid;
    cursor: pointer;
    user-select: none;
    color: @btn-primary-color-text;

    circle {
        stroke: @btn-primary-color-text;
    }

    .btn-type(@type) {
        .normal() {
            color:~'@{btn-@{type}-color-text}';

            circle {
                stroke:~'@{btn-@{type}-color-text}';
            }

            background-color:~'@{btn-@{type}-color-bg}';
            border-color:~'@{btn-@{type}-color-border}';

            &:not(:disabled):hover {
                background-color:~'@{btn-@{type}-color-hover}';
            }

            &:not(:disabled):active {
                background-color:~'@{btn-@{type}-color-bg}';
            }
        }

        &--@{type} {
            .normal();
        }
    }

    .btn-status(@type: primary, @status: primary) {
        .normal() {
            color:~'@{btn-@{type}-color-text_@{status}}';

            circle {
                stroke:~'@{btn-@{type}-color-text_@{status}}';
            }

            background-color:~'@{btn-@{type}-color-bg_@{status}}';
            border-color:~'@{btn-@{type}-color-border_@{status}}';

            &:not(:disabled):hover {
                background-color:~'@{btn-@{type}-color-hover_@{status}}';
            }

            &:not(:disabled):active {
                background-color:~'@{btn-@{type}-color-bg_@{status}}';
            }
        }

        &.yk-button--@{type}.yk-button--@{status} {
            .normal();
        }
    }

    .btn-type(primary);
    .btn-type(secondary);
    .btn-type(outline);

    .btn-status(primary);
    .btn-status(primary, success);
    .btn-status(primary, warning);
    .btn-status(primary, danger);

    .btn-status(secondary);
    .btn-status(secondary, success);
    .btn-status(secondary, warning);
    .btn-status(secondary, danger);

    .btn-status(outline);
    .btn-status(outline, success);
    .btn-status(outline, warning);
    .btn-status(outline, danger);

    // 尺寸 size
    &--s {
        padding: 0px @space-s;
        min-width: 24px;
        height: 24px;
        font-size: @size-ss;
        border-radius: @radius-s;
    }

    &--m {
        padding: 0px @space-l;
        min-width: 32px;
        height: 32px;
        border-radius: @radius-s;
    }

    &--l {
        padding: 0px @space-l;
        min-width: 36px;
        height: 36px;
        border-radius: @radius-m;
    }

    &--xl {
        padding: 0px @space-xl;
        min-width: 48px;
        height: 48px;
        font-size: @size-m;
        border-radius: @radius-m;
    }

    &--long {
        display: block;
        width: 100%;
    }

    // 圆角样式
    &--round {
        border-radius: @radius-r;
    }

    &--circle {
        padding: 0;
        border-radius: @radius-r;
    }

    &--square {
        padding: 0;
    }

    // 禁用
    &--disabled {
        .disabled();
    }
}

index.ts

import './index.less'

我们在这里面定义了示按钮加载状态的动画效果,SVG 图标的旋转动画和圆圈的动画效果,按钮的基本样式,默认状态下的按钮样式,特定类型和状态下的按钮样式

这里原谅我没法一句一句地讲每一行less是什么意思,因为讲less并不是咱们的主要目的

好的,最后咱们按照之前的方式,进行注册和导出

image.png

image.png

然后我们可以在demo工程中去试验一下啦!

在demo工程中使用yk-button

image.png

image.png

OK,没有问题,我们的button组件也就完成啦~