前端开发在写界面的时候往往都是使用组件库,因为这样很多功能界面不需要我们自己书写,但太依赖组件库对我们前端反而不是一件好事,所以对一些公司业务需求进行封装属于自己的组件,我们可以去使用别人造好的轮子,但我们也要具备自己造轮子的能力。笔者第一次接触组件库就是有赞的移动端Vant,里面封装大多数经常使用的组件,覆盖率绝大多数应用场景。今天手撕Vant组件中最简单最基础的组件也就是我们的Button组件。
我们封装组件之前,建议把你要封装的功能以及实现的样式,也就是先把props先列出来,切记不要一上来就开始进行封装,这样很容易丢三落四。所以这里推荐大家一个小工具进行自动生成props。工具名字:uTools 搜索vue-prop即可。
如果你不知道具体实现哪些功能你可以打开各大组件库官网,比如有赞vant ,京东nutui,腾讯Td等,先借鉴,完成的时候差不多了可以考虑进入自己的功能,封装组件就是为了以后怎么方便怎么来。所以要在这方面多考虑,如果公司有ui那就按照ui设计的来。
先贴效果图 满满的vant(chaoxi)感
这里录屏软件为screen to GIF 推荐好用的软件不贴下载链接同样是耍流氓 下载链接www.downza.cn/soft/197922…
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>