十分钟就能写好一个button组件,它不香吗

130 阅读3分钟

前端开发在写界面的时候往往都是使用组件库,因为这样很多功能界面不需要我们自己书写,但太依赖组件库对我们前端反而不是一件好事,所以对一些公司业务需求进行封装属于自己的组件,我们可以去使用别人造好的轮子,但我们也要具备自己造轮子的能力。笔者第一次接触组件库就是有赞的移动端Vant,里面封装大多数经常使用的组件,覆盖率绝大多数应用场景。今天手撕Vant组件中最简单最基础的组件也就是我们的Button组件。

我们封装组件之前,建议把你要封装的功能以及实现的样式,也就是先把props先列出来,切记不要一上来就开始进行封装,这样很容易丢三落四。所以这里推荐大家一个小工具进行自动生成props。工具名字:uTools 搜索vue-prop即可。

image.png

如果你不知道具体实现哪些功能你可以打开各大组件库官网,比如有赞vant ,京东nutui,腾讯Td等,先借鉴,完成的时候差不多了可以考虑进入自己的功能,封装组件就是为了以后怎么方便怎么来。所以要在这方面多考虑,如果公司有ui那就按照ui设计的来。

先贴效果图 满满的vant(chaoxi)感

这里录屏软件为screen to GIF 推荐好用的软件不贴下载链接同样是耍流氓 下载链接www.downza.cn/soft/197922…

动画.gif

props: {
  //常用渐变
  // background: linear-gradient(to right, #5faff8, #0068ff) !important;
  gradual: {
    type: Boolean,
  },
  width: {
    type: Number
  },
  height: {
    type: Number
  },
  text: {
    type: String,
    default: ''
  },
  //文字颜色
  color: {
    type: String,
    default: ''
  },
  textSize: {
    type: Number,
  },
  //按钮颜色
  bgColor: {
    type: String,
    default: ''
  },
  //边框颜色
  borderColor: {
    type: String,
    default: ''
  },
  type: {
    type: String,
    default: 'default',
    validator(val) {
      return types.includes(val);
    },
  },

  size: {
    type: String,
    default: 'normal',
    validator(val) {
      return sizes.includes(val);
    },
  },

  //形状
  plain: {
    type: Boolean
  },
  //50%圆角
  round: {
    type: Boolean
  },
  //自定义圆角
  radius: {
    type: Number
  },
  square: {
    type: Boolean
  },
  //圆形按钮  默认 w-h  40px  可以自定义
  circle: {
    type: Boolean,
  },

  //无边框
  noBorder: {
    type: Boolean
  },
  // 细边框
  hairline: {
    type: Boolean
  },
  textBtn: {
    type: Boolean
  },

  disabled: {
    type: Boolean
  },
  loading: {
    type: Boolean

  },
  loadingText: {
    type: String,
    default: ''
  },
  //loading样式
  loadingType: {
    type: String,
    default: "one",
    validator(val) {
      return ["one", "two"].includes(val);
    },
  },

  icon: {
    type: String,
    default: ''
  },

  iconPosition: {
    type: String,
    default: 'start',
    validator(val) {
      return iconPositions.includes(val);
    },

  },
  //路由跳转  直接传入url地址就可以
  url: {
    type: String,
    default: ""
  },
  to: {
    type: String,
    default: ""
  },
  //主要应用返回按钮,为一个返回函数
  goBack: {
    type: Boolean,
  }


},

我这里很多props都是借(chao)鉴(xi)有赞的vant的属性,不一定全部按照来,对一些冷门属性可以进行舍弃以及加入自己的考虑。把这些列出之后再一步步将去搭结构,然后根据不同的功能属性添加相应的样式。

实际编写一个button组件不难,但是里面也有许多细节:

细节一

如何实现0.5px边框,这个问题可能在面试的时候也会遇到:

方法一

定位+缩放

    利用的是 transform 缩放功能,将 1px 缩放一半,同时利用定位,将伪元素覆盖整个 div 元素,从而达到伪元素与本身元素的合并效果。只说方法不给代码的这种事我做不出来;

`.button {   ``position``: ``relative``;}`
.button::before {
    content: "";
       position: absolute;
       top: 0;
       left: 0;
       min-width: 200%;
       height: 200%;
       border: 1px solid $color-black54;
       transform-origin: 0 0;
       transform: scale(.5, .5);
 }

方法二

利用的是 box-shadow 的扩散半径可以设置为 0.5px 原理,本组件就是使用的这个,第一兼容性还不错,第二就是代码少

box-shadow: 0px 0px 0px 0.5px #f00;

方法三

线性渐变 linear-gradient

.box {
     height: 1px;
     background: linear-gradient(#f00 50%, transparent 50%);

}

方法四

使用svg

.line{
  background: repeat-x top left url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect fill='red' x='0' y='0' width='1' height='0.5'/></svg>");
  height: 1px;
  width: 100%;
}

细节二

按钮loading怎么画,这个问题问得好,实际这个就是你要对css动画有一定的了解。最简单的loading画法,实际实现这个loading也有很多方法,使用svg也可以,vant使用的就是svg方式,而我反其道行之用来下面最简单,但是今天我们也是用svg来写一个loading,实现方式差不多,都要对css动画有一定的了解。svg实现方式也贴在下面

.vux-loading-two {
  width: 14px;
  height: 14px;
  border: 2px solid #C0C4CC;
  border-radius: 50%;
  display: inline-block;
  vertical-align: middle;
  clip-path: polygon(0% 0%, 100% 0%, 100% 30%, 0% 30%);
  animation: loadingTwo 1s linear infinite;
}

@-webkit-keyframes loadingTwo {
  from {
    transform: rotatez(0deg);
  }

  to {
    transform: rotatez(360deg);
  }
}



<svg

    width="20" height="20"

    viewBox="0 0 50 50">

  <circle

      class="circle"

      cx="25"

      cy="25"

      r="20">

    <animateTransform

        attributeName="transform"

        type="rotate"

        from="0"

        to="360"

        dur="1s"

        repeatCount="indefinite" />

  </circle>
  
  
  
.circle {

stroke-dasharray: 125.6px;

animation: move linear 1s infinite;

transform-origin: center;

stroke-dashoffset: 30px;

fill: none;

stroke: red;

stroke-width: 5px;

}

@keyframes move {

0% {

transform: rotate(0deg);

}

100% {

transform: rotate(360deg);

}

}

</style>

俗话讲的好,贴代码不讲解也是耍流氓的👨‍💻,stroke-dasharray是整个circle的周长,对就是2PIr;stroke是圆弧的宽度,可以理解为border;fill: none;就是不填充背景。可以理解为backGround。stroke-dashoffset: 30px;圆弧的偏移量;

最后

更多的细节等你们自己挖掘,最后贴出源码粘贴就能运行的那种。以上有讲的不好的,或者下面代码写的不好的地方,欢迎大家积极指出,笔者积极学习与你们一起进步!

<template>
  <!--  background
  backgroundColor 不支持渐变-->
  <button class="vux-botton ripple"
          :disabled="disabled"
          :class="[type,size,iconPosition,
          disabled?'disabled':'',
          loading?'loading':'',
          plain?'plain':'',
          round?'round':'',
          square?'square':'',
          noBorder?'noBorder':'',
          hairline?'hairline':'',
          textBtn?'textBtn':'',
          gradual?'gradual':'',
         circle?'circle':'',
          ]"
          @click="handleBtnClick($event)"
          :style="{color: color,
          background:bgColor,
          borderRadius:radiusStyle,
          fontSize:textSizeStyle,
          borderColor: borderColor,
          width:widthStyle,
          height:heightStyle,
          }"
          :loading="loading"

  >

    <span v-if="loading">
      <i :class="[loading?`vux-loading-${loadingType}`:'']">

      </i>
      {{ loadingText }}
    </span>
    <span v-if="icon&&iconPosition==='start'">
    <i :class="icon"></i>
    </span>
    <slot></slot>
    <span v-if="icon&&iconPosition==='end'">
    <i :class="icon"></i>
    </span>
  </button>
</template>

<script>
//基本用法
const types = ["default", "primary", "success", "warning", "danger"];
const sizes = ["large", "small", "mini", "normal"];
const iconPositions = ["start", "end"];
const shapes = ["plain", "round", "square"];
// 0 0.5 1
const borders = ["noBorder", "hairline",];


export default {
  name: "Button",
  props: {
    //常用渐变
    // background: linear-gradient(to right, #5faff8, #0068ff) !important;
    gradual: {
      type: Boolean,
    },
    width: {
      type: Number
    },
    height: {
      type: Number
    },
    text: {
      type: String,
      default: ''
    },
    //文字颜色
    color: {
      type: String,
      default: ''
    },
    textSize: {
      type: Number,
    },
    //按钮颜色
    bgColor: {
      type: String,
      default: ''
    },
    //边框颜色
    borderColor: {
      type: String,
      default: ''
    },
    type: {
      type: String,
      default: 'default',
      validator(val) {
        return types.includes(val);
      },
    },

    size: {
      type: String,
      default: 'normal',
      validator(val) {
        return sizes.includes(val);
      },
    },

    //形状
    plain: {
      type: Boolean
    },
    //50%圆角
    round: {
      type: Boolean
    },
    //自定义圆角
    radius: {
      type: Number
    },
    square: {
      type: Boolean
    },
    //圆形按钮  默认 w-h  40px  可以自定义
    circle: {
      type: Boolean,
    },

    //无边框
    noBorder: {
      type: Boolean
    },
    // 细边框
    hairline: {
      type: Boolean
    },
    textBtn: {
      type: Boolean
    },

    disabled: {
      type: Boolean
    },
    loading: {
      type: Boolean

    },
    loadingText: {
      type: String,
      default: ''
    },
    //loading样式
    loadingType: {
      type: String,
      default: "one",
      validator(val) {
        return ["one", "two"].includes(val);
      },
    },

    icon: {
      type: String,
      default: ''
    },

    iconPosition: {
      type: String,
      default: 'start',
      validator(val) {
        return iconPositions.includes(val);
      },

    },
    //路由跳转  直接传入url地址就可以
    url: {
      type: String,
      default: ""
    },
    to: {
      type: String,
      default: ""
    },
    //主要应用返回按钮,为一个返回函数
    goBack: {
      type: Boolean,
    }


  },
  computed: {
    radiusStyle() {
      if (this.radius) {
        return this.radius + 'px'
      }
    },
    textSizeStyle() {
      if (this.textSize) {
        return this.textSize + 'px'
      }
    },
    widthStyle() {
      if (this.width) {
        return this.width + 'px'

      }

    },
    heightStyle() {
      if (this.height) {
        return this.height + 'px'
      }

    }
  },
  methods: {
    handleBtnClick() {

      if (this.disabled) {
        return
      }
      this.$emit('click')

      //优先级,默认只做一件事
      if (this.to) {
        this.$router.push(this.to)
        return;
      }
      if (this.url) {
        window.open(this.url)
        return;
      }
      if (this.goBack) {
        this.$router.go(-1);

      }


    },


  }
}
</script>

<style scoped lang="less">

.vux-botton {
  position: relative;
  display: inline-block;
  outline: none;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  padding: 0;
  transition: .2s;
  font-weight: 500;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  font-size: 14px;
  border-radius: 2px;
  height: 44px;

  &.primary {
    color: #fff;
    background-color: #0068ff;
    border-color: #0068ff;

    &.plain {
      color: #0068ff;
      background-color: #fff;
      border-color: #0068ff;
    }

    &.textBtn {
      border: 0 !important;
      border: none !important;
      height: 20px;
      color: #0068ff;
    }

    //实现0.5px边框

    &.hairline {
      border: none !important;
      border: 0 !important;
      box-shadow: 0 0 0 0.5px #0068ff;
    }

    //传入 type+gradual
    &.gradual {
      background: linear-gradient(to right, #5faff8, #0068ff) !important;
      //border-image: linear-gradient(to right, #5faff8, #0068ff) !important;
      border: none !important;
      border: 0 !important;
    }

  }

  &.success {
    color: #fff;
    background-color: #07c160;
    border-color: #07c160;

    &.plain {
      color: #07c160;
      background-color: #fff;
      border-color: #07c160;
    }

    &.textBtn {
      border: 0 !important;
      border: none !important;
      height: 20px;
      color: #07c160;
    }

    &.hairline {
      border: none !important;
      border: 0 !important;
      box-shadow: 0 0 0 0.5px #07c160;

    }

    //传入 type+gradual
    &.gradual {
      background: linear-gradient(to right, #36db84, #07c160) !important;
      //border-image: linear-gradient(to right, #36db84, #07c160) !important;
      border: none !important;
      border: 0 !important;
    }


  }

  &.warning {
    color: #fff;
    background-color: #ff976a;
    border-color: #ff976a;

    &.textBtn {
      border: 0 !important;
      border: none !important;
      height: 20px;
      color: #ff976a;
    }

    &.plain {
      color: #ff976a;
      background-color: #fff;
      border-color: #ff976a;
    }

    &.hairline {
      border: none !important;
      border: 0 !important;
      box-shadow: 0 0 0 0.5px #ff976a;

    }

    //传入 type+gradual
    &.gradual {
      background: linear-gradient(to right, #ff976a, #fa6419) !important;
      //border-image: linear-gradient(to right, #ff976a, #fa6419) !important;
      border: none !important;
      border: 0 !important;
    }


  }

  &.danger {
    color: #fff;
    background-color: #fa2c19;
    border-color: #fa2c19;

    &.textBtn {
      border: 0 !important;
      border: none !important;
      height: 20px;
      color: #fa2c19;
    }

    &.plain {
      color: #fa2c19;
      background-color: #fff;
      border-color: #fa2c19;
    }

    &.hairline {
      border: none !important;
      border: 0 !important;
      box-shadow: 0 0 0 0.5px #fa2c19;

    }

    &.gradual {
      background: linear-gradient(to right, #f15142, #fa2c19) !important;
      //border-image: linear-gradient(to right, #f15142, #fa2c19) !important;
      border: none !important;
      border: 0 !important;
    }


  }

  &.default {
    color: #333;
    background-color: #fff;
    border-color: #ebedf0;

    &.textBtn {
      border: 0 !important;
      border: none !important;
      height: 20px;
      color: #333;
    }

    &.plain {
      color: #333;
      background-color: #fff;
      border-color: #ebedf0;
    }

    &.hairline {
      border: none !important;
      border: 0 !important;
      box-shadow: 0 0 0 0.5px #ebedf0;

    }


  }

  &.large {
    //todo
    display: block;
    width: 100%;
    font-size: 16px;
    height: 44px;
    line-height: 44px;
  }

  &.normal {
    padding: 0 20px;
    font-size: 14px;
    height: 44px;
    line-height: 44px;
  }

  &.small {
    padding: 0 8px;
    font-size: 13px;
    height: 32px;
    line-height: 32px;
  }

  &.mini {
    padding: 0 4px;
    font-size: 12px;
    height: 24px;

  }


  &.square {
    border-radius: 0 !important;
  }

  &.round {
    border-radius: 44px !important;
  }

  &.noBorder {
    border: none !important;

  }


  &.disabled {
    opacity: 0.6 !important;
    cursor: not-allowed !important;
  }

  &.textBtn {
    border: 0 !important;
    border: none !important;
    height: 20px;
    color: #0068ff;
  }


  &.circle {
    width: 40px !important;
    height: 40px !important;
    padding: 0 !important;
    border-radius: 50%;
    line-height: 40px !important;

  }

  .vux-loading-one {
    width: 18px;
    height: 18px;
    display: inline-block;
    vertical-align: middle;
    -webkit-animation: loadingOne 1s steps(12) infinite;
    animation: loadingOne 1s steps(12) infinite;
    background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
    background-size: 100%;
  }

  @-webkit-keyframes loadingOne {
    0% {
      -webkit-transform: rotate(0deg);
      transform: rotate(0deg);
    }

    to {
      -webkit-transform: rotate(1turn);
      transform: rotate(1turn);
    }
  }

  .vux-loading-two {
    width: 14px;
    height: 14px;
    border: 2px solid #C0C4CC;
    border-radius: 50%;
    display: inline-block;
    vertical-align: middle;
    clip-path: polygon(0% 0%, 100% 0%, 100% 30%, 0% 30%);
    animation: loadingTwo 1s linear infinite;
  }

  @-webkit-keyframes loadingTwo {
    from {
      transform: rotatez(0deg);
    }

    to {
      transform: rotatez(360deg);
    }
  }

  @keyframes loadingTwo {
    from {
      transform: rotatez(0deg);
    }

    to {
      transform: rotatez(360deg);
    }
  }

  .ripple {
    position: relative;
    overflow: hidden;

    &::after {
      content: "";
      display: block;
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      pointer-events: none;
      background-image: radial-gradient(circle, #fff 10%, transparent 11%);
      background-repeat: no-repeat;
      background-position: 50%;
      transform: scale(12, 12);
      opacity: 0;
      transition: transform .6s, opacity .6s;
    }

    &:active::after {
      transform: scale(0, 0);
      opacity: .3;
      //设置初始状态
      transition: 0s;
    }
  }


}
</style>