共计 4082 个字符,预计需要花费 11 分钟才能阅读完成。
第四集: 从零开始实现(button 组件 2)
本集定位:
之前一直在忙别的事情, 现在终于闲下来, 好好把这个库的文章写一下
本篇目的是承接上文, 把 button 组件的功能全部实现
1: 为 button 添加 icon
按钮的 icon 很重要, 现在一般的按钮都带个图案, 因为这样符合人脑的快捷思维, 方便理解与记忆.
<button class="cc-button" | |
@touchstart='touchstart($event)' | |
:class="[ | |
sizeType, | |
type ? 'cc-button--' + type : '', | |
{ | |
'is-left':left, | |
'is-right':right, | |
'is-centre':centre, | |
'is-disabled':disabled, | |
}]":type="nativeType"@click="click"> | |
<!-- 图标按钮 --> | |
<ccIcon v-if="icon" // 有没有 | |
:name='icon' // 有什么样的 | |
:color="realyIconColor" // 什么颜色的 | |
style="margin-right:2px" /> // 与文字有点距离 | |
<slot /> | |
</button> |
计算 realyIconColor
icon 有默认的颜色, 但是如果按钮式禁用状态, 那么 icon 也要相应的置灰
computed: {realyIconColor() {if (this.disabled) return "#bbbbbb"; | |
else return this.iconColor; | |
} | |
} |
2: icon 的位置
很少遇到需要上下左右布局的 icon, 如果需要的话.
方案一: 移动 <slot> 标签, 或是具名 slot 标签
方案二: 多写几个 icon 组件, 通过判断决定显示谁
3: hover 效果
方案一:
- hover 的时候出现灰色蒙层效果
- 被点下时, 按钮缩小动画
&:not(.is-disabled) { | |
&:active { | |
box-shadow: none; | |
opacity: 0.7; | |
transform: translateX(2px) translateY(2px) scale(0.9); | |
} | |
&:hover {background-color:rgba(0,0,0,0.1) | |
} | |
} |
效果图
方案二:
- hover 出现金属光泽, bulingbuling 的, 金属光泽.
- 被点下时, 按钮缩小动画
综上分析, 金属光泽流过可能成为一个公用属性, 那么我直接写一个公共的样式吧 “cc-bling”
我的思路:
- 尝试使用渐变的背景颜色, 然后用 ackground-position-x 控制背景的右移
此方案实践起来不舒服, 而且并没有做到 ’ 解耦合 ’ 的设计原则, 所以不用了 - 伪元素, 用一个伪元素加上背景色, 然后把这个元素从左至右划过, 同时用动画把它做得倾斜 30 度
这样虽然多了个元素, 但是跟父级解耦了, 值得说的一点是, 倾斜之后高度不够盛满了, 简单粗暴的
方式就是把高度定位多一些的, 这样旋转也不会导致高度不够的状态了.
let’go
<button class="cc-button" | |
:class="[ | |
{ | |
'is-bling':bling, // 加了一个接受 是否金属光泽的属性 | |
'is-left':left, | |
'is-right':right, | |
'is-centre':centre, | |
'is-disabled':disabled, | |
}]" | |
</button> | |
bling:Boolean, // 条纹 |
在 button.scss; 里面添加
@at-root {@include commonType(cc-button--); | |
.is-bling { | |
// 此属性名在 hover 的时候才进行 bling 操作 | |
&:hover {@extend .cc-bling;} | |
} | |
}; |
animation.scss
定义一个从左至右的动画
@keyframes bling{ | |
0% {left: 0;} | |
100% {left: 300%;} | |
} |
extend.scss
定义具体的样式把
.cc-bling { | |
&:after { | |
content: ''; | |
position: absolute; | |
background-image: linear-gradient(to right, rgb(232, 229, 229), white); | |
left: 0; | |
top: -20px; // 避免倾斜的时候头部漏出尖角 | |
width: 15px; | |
height: calc(100% + 30px); // 避免旋转时候出现高度不够的情况 | |
transform: rotate(-30deg); | |
animation-name: bling; | |
animation-duration: 1s; // 总用时 | |
animation-iteration-count: infinite; // 无限循环 | |
animation-timing-function: linear; // 匀速 | |
} | |
} |
效果图, 是动态的, 从左至右划过.
4: 防抖与节流
介绍: 这种节流与防抖都是用户自己做的, 至少按钮这种东西本套组件库就是要组件来做.
使用场景:
- 有一次, 我写注册登录页面, 如果用户在注册的时候, 快速的点击了两下, 虽然跳到登录成功页面, 但是会弹出弹框, “ 该手机已被注册 ”, 原来是由于第一个请求把手机注册了, 所以接下来的点击事件的请求后台当然返回的是已注册, 所以这里就需要这样的处理, 每次点击有效之后的点击 n 秒内无效, 防止连点, (防止变速齿轮, 想起了流星蝴蝶剑);
- 我们公司 19 年搞了个抢购活动, 可能发生这样的场景, 用户守着抢购按钮, 不断地点击, 我们 web 端需要在每次用户点击的时候询问后台 ’ 活动开始了么?’, 没开始就给用户弹 tosat(下一章就做这个组件了), 开始了才进入活动也或是订单页, 但是, 用户如果是个金手指, 疯狂把玩抢购键, 那就会发出大量的请求了,
我来说一下:
第一: 为什么不使用后台返回的活动开始时间与本地的事件进行比对??
原因是用户的本地时间并不是一个值得信赖的量, 平时可以作为一个参考, 但是像抢购这种分秒必争的事情, 就要让用户与服务器时间同步起来了.
第二: 为什么不在第一次请求之后把时间戳记录下来, 本地用计时器模拟计时??
原因是某些浏览器环境下, 用户切出程序之类的一些操作, 他会杀掉计时器, 导致计时不准, 而这种问题暂时无法解决, 也监控不到, 所以为了分秒必争保险起见.
最后只能是每次都请求一下, 那就需要, 比如说 每 600 毫秒内, 只让用户的点击有效一次.
我们就不能单纯的写 click 事件了, 要抽离出来进行蹂躏☺️.
dom
<button class="cc-button" | |
@click="click"> | |
</button> |
接值
props: { | |
..., | |
shake: Number, // 防抖的秒数 | |
throttle: Number, // 节流, 请输入秒数 | |
clickId: [String, Number], // 相同 id 的组件走一套计时. | |
} |
事件
click() { | |
// 根据用户的输入, 来决定怎么计时. | |
// 值得一提的是, clickId 相同的话我们是统一计时的, | |
// 比如说: 三个按钮, 点了其中一个, 其他的几个在规定时间内, 都会不可点击 | |
let clickType, num; | |
if (this.throttle) { | |
clickType = 1; | |
num = this.throttle; | |
} else if (this.shake && this.shake > 0) { | |
clickType = 2; | |
num = this.shake; | |
} else if (this.shake && this.shake < 0) { | |
clickType = 3; | |
num = this.shake * -1; | |
} | |
prevent( | |
this.clickId, | |
() => {this.$emit("click"); | |
}, | |
num, | |
clickType | |
); | |
}, |
在之前的工作中自己写过一个防抖与节流的函数, 这次就直接拿来用了
let preventList = {} | |
const prevent = function(id, obj, time, model = 1) {switch (model) { | |
case 1: | |
model1(id, obj, time) | |
break; | |
case 2: | |
model2(id, obj, time) | |
break; | |
case 3: | |
model3(id, obj, time) | |
break; | |
default: | |
} | |
} | |
// 模式 1 不管点多少下每隔 time 秒, 触发一次 | |
function model1(id, obj, time) {if (preventList['can' + id]) return | |
obj() | |
preventList['can' + id] = true | |
preventList['time' + id] = setTimeout(() => {preventList['can' + id] = false | |
}, time) | |
} | |
// 模式 2 每次动作都有 time 的延时再执行, 也就是所有点击完事的时候执行一个 | |
function model2(id, obj, time) {clearTimeout(preventList['time' + id]) | |
preventList['time' + id] = setTimeout(() => {obj() | |
}, time) | |
} | |
// 默认的模式, 模式 3, 第一下点击触发, 之后时间内不触发 | |
function model3(id, obj, time) {if (preventList['can' + id]) {clearTimeout(preventList['time' + id]) | |
} else {obj() | |
preventList['can' + id] = true | |
} | |
preventList['time' + id] = setTimeout(() => {preventList['can' + id] = false | |
}, time) | |
} | |
export default prevent |
具体的使用
// 后续涉及到防抖与节流的事件也都是走的这套程序;
下一篇
- toast 的封装, 通过封装 toast 让我巩固了好多 ’ 芝士 ’
- 组件的另一种写法
end
这段时间作者辞职, 专心学源码, 练习算法, 重学 js, 重学 node, 重做自己的打包工具, 反正挺忙的, 接下来的时间与精力主要放到这套组件上 同时也会出一些算法啊, 心得之类的文章, 欢迎同学们一起交流, 一起变得更优秀.
github: 链接描述
个人网站: 链接描述