乐趣区

第十集-从零开始实现一套pc端vue的ui组件库-计数器组件

第十集: 从零开始实现(计数器组件)

本集定位:
听到计数器这个名字很多人是不是一瞬间没有什么印象, 毕竟这个组件用的比较少, 就是那种左边一个 ’-‘ 右边一个 ’+’, 控制某些数量的时候才会用到, 比如我之前做的商城小程序只有 ’ 下单 ’ 页面的规格弹出框里面才有他的身影, 如果是涉及到处理商品数量很频繁的业务场景应该会很常见吧, 但是不要看这个组件小, 编写它的时候坑还不少, 本次我们就来做一个计数器, 目标就是尽可能小, 尽可能的省性能.

1: 需求分析

  1. 每次 +1 - 1 是常态, 但是如果搞活动, 每次最少为 +- 2 个或三个, 就要兼容一下了,(举一个实际遇到的坑, 我们之前把用户限制为每次活动, 每个用户只能买 2 个, 但是没有做好防备, 导致用户可能这次只买一个, 而下次他再次购买的时候会提示每次只能买两个, 但显示他只点击了买一个, 因为他已经买过一个, 为了兼容这个问题, 搞得还要加莫名其妙的补救代码)
  2. 中间的显示区应该可输入的, 用户想买 1000 个不可能让他 +1+1+1…, 某些组件采用的是, 平时其为 div, 点击之后变为 input, 个人感觉完全没必要, 一个元素就够了, 何必搞两个元素, input 状态下把他的默认样式去掉就好了.
  3. 左右两边要有限制, 很多时候会有限购一说, 比如我做的商城, 库存只有 10 个 或者单个用户最多购买 3 个, 最少买两个等等限制.
  4. 小数位数的显示一说 … 这个其实我还真遇到过, 有一种需求叫做, 只要涉及数字就必须精确到后两位, 这种需求会导致后台同学对数据库做一定的限制, 从而我们传给后台的数据也就存在限制了.

2: 基本结构:

先展示一章普通状态的图, 让我们更直观的去完成它, 造型比较别致, 是本套组件的一个特点, 哈哈做的与别人一样会导致思想的禁锢, 自己写代码多尝试新的东西, 但是工作中一定要中规中矩, 以公司条款为准则.

vue-cc-ui/src/components/InputNumber/index.js

import inputNumber from './main/input-number.vue'

inputNumber.install = function(Vue) {Vue.component(inputNumber.name, inputNumber);
};

export default inputNumber

vue-cc-ui/src/components/InputNumber/main/input-number.vue

<template>
  <div class="cc-input-number">
    // 左侧的 '➖' 符号
    <div class="cc-input-number__reduce">
      // 自己封装的 icon 组件, 出镜率还挺高????.
        <cc-icon name='cc-reduce2'/>
    </div>
    // 中间的显示与输入部分, 让人又爱又恨的 number 属性
    // 下面的属性就能干掉凡人的上下按钮
    // input::-webkit-outer-spin-button,
    // input::-webkit-inner-spin-button {
    // -webkit-appearance: none;
    // }
    <input ref="input"
           type="number"
           class="cc-input-number__input">
    <div class="cc-input-number__add">
        <cc-icon name='cc-add2'/>
    </div>
  </div>
</template>

这里我们选择吧 input 与 button 放在一个 div 里面, 且同级别这种方式, 与其他的不太一样, 因为这样更直观, 而且也足够实现我想要的功能.

3: 事件的绑定

// 减少
 <div class="cc-input-number__reduce"
         @click='reduce'>
 // 增加
<div class="cc-input-number__add"
         @click="add">
// 输入框的监控
<input ref="input"
       type="number"
       class="cc-input-number__input"
       @input="inputChange($event)">

这里我们有个问题, 就是本组件采用的是 v -model 的形式编写, v-model 有一些弊端, 在测试的时候我发现, 比如说用户为多个组件绑定了相同的 v -model 会导致无限渲染的 bug, 下面会解读解决这类 bug 的相关代码.

prpos

props: {max: { type: Number}, // 数字不传默认是 undefined
    min: {type: Number},
    step: { // 每次计算的单位
      type: Number,
      default: 1
    },
    value: { 
    // 绑定的数值, 这里允许两种 type, 为了方便用户书写, 具体判断下面我们自己写
      type: [String, Number],
      required: true
    },
    precision: { // 显示小数点后几位数
      type: Number,
      validator(value) {if (value < 1 || value === undefined) {return 1;} else {return parseInt(value);
        }
      }
    }
  },

add 方法的实现

 add() {
// 很可能用户就输入了一个 string 属性, 
// 1: 比如后台返回的就是字符串;
// 2: input 框输入的就是字符串类型;
// 3: 用 v -model 绑定了同样的值的其他组件赋予了这个值 string 类型;
  let num = Number(this.value) + this.step; // 加上固定的长度
// 这里我们抽象出一个专门负责数值的变化的函数
  this.emitVal(num);
},

reduce 方法的实现

reduce() {let num = Number(this.value) - this.step;
      this.emitVal(num);
    },

监听 input 框的输入事件

inputChange(e) {
// 这里就有可能出现 string 类型的了
      this.emitVal(Number(e.target.value));
    },

关键性的赋值函数
emitVal

 emitVal(newVal) {let { max, min} = this;
      // 不传参数的时候默认值就是 undefined
      // 对这个值的限制就是, max 之内, min 以上
      if (max !== undefined && newVal > max) newVal = max;
      if (min !== undefined && newVal < min) newVal = min;
      // 这里兼容一下位数控制
      let value = Number(newVal).toFixed(this.precision);
      // 这个 oldVal 下面会解释????
      if (value === this.oldVal) return;
      this.oldVal = ls;
      // 发出两个事件, 一个负责改变 value, 一个负责返回给用户
      // 毕竟用户不可能监听 input 事件然后再把值附上去, 太麻烦
      this.$emit("input", value);
      this.$emit("change", value);
      // 这一步很重要
      // 下面会详细说
      this.$refs.input.value = value;
    }

上面遗留的问题, 这里解释一下.

  1. oldVal: 能防止很多多余的改变, 比如说用户复制粘贴了一组数进来, 这个数大于 max, 但是当时显示的数值就是 max, 所以就不用渲染了, 或者 v -model 不止绑定了这个组件, 还绑定了其他各种组件, 导致值超出范围, 这边也会进行相应的限制, 而这个 oldVal 就是上一个合法的值, 所以在做完检测之后, 检测通过的数值要赋值给他.
  2. this.$refs.input.value = value; 这一步看似很没用, 因为输入框里面的是 value, value 改变 input 里面的值自然会改变, 但是实际测试并不是这样, 问题也是出现在 v -model 上, 绑定很多的时候会出现值的不改变, 可能是 vue 的机制问题, 而且他要放在 this.$emit(….); 下面操作, 如果放在上面会导致多次执行, 因为他的执行会循环触发 input 的监听事件, 多次试验之后, 还是放在这里没有 bug.

上面的两个问题都是涉及到 v -model 的问题, 下面还有一个同类的问题, 我们来看看.
对 value 进行的监控
因为 value 的变化, 不一定全是 通过 +- 输入这三种方式, 还有第三方通过 v -model 的方式, 还有用户手动乱填的方式.

  watch: {
    value: {handler() {
        // 为了解决, 多组件共同 v -model 采用的这个方法, 也算是另辟蹊径了
        let {value, time} = this;
        clearTimeout(time);
        // 毕竟把它放入宏任务 Macrotasks 可以躲过很多无限循环.
        time = setTimeout(() => {if (value !== undefined) this.emitVal(value);
        });
      },
      // 这个是开启进页面的瞬间就出发一次的意思, 很有用, 但是数据稍大会消耗性能, 慎用
      // watch 还有一个 deep 属性, 更是吃性能吃的厉害, 可以深度监控里面的数据
      immediate: true
    }
  }

上面的问题都是基于 v -model 的, 所以很早就有人剔除双向绑定的坏处, 封装越多的组件感觉就越明显.

4: 关于样式的判定

在计算属性里面我们队当前值进行了监控, 返回的是置灰的颜色, 这个让用户自定的意义不大, 所以直接写了.

  computed: {valueMin() {if (this.value === this.min) return "#bbbbbb";
      return "";
    },
    valueMax() {if (this.value === this.max) return "#bbbbbb";
      return "";
    }
  },

dom, 点击到了最大值的话就会置灰, 我们上面已经阻止了继续点击的渲染

<cc-icon size='25px'
         name='cc-add2'
         :color="valueMax" />

做点有意思的事
slot 是个自由度很高的标签
把左右按钮都包上, 让用户可以自己定义显示的标签是什么样子的

 <div class="cc-input-number__reduce"
         @click='reduce'>
      <slot name='left'>
        <cc-icon size='25px'
                 name='cc-reduce2'
                 :color='valueMin' />
      </slot>
    </div>

vue-cc-ui/src/style/inputNumber.scss

@import './common/var.scss';
@import './common/extend.scss';
@import './common/mixin.scss';
@import './config/index.scss';

@include b(input-number) {
// 友好的小手
    cursor: pointer;
    // 有个放大动画, 看过我文章的同学都知道, 操作类的组件, 我喜欢有一个悬停放大效果.
    transition:all .1s;
    align-items: center;
    display: inline-flex;
    background-color: white;
    &:hover {
    // 放大被其他组件挡住就划不来了
      z-index: 6;
      transform: scale(1.2);
    }
    // 招牌阴影
    @include commonShadow($--color-black);
    @include e(add) {@include flexCenter();
        padding: 4px 6px;
    }
    @include e(reduce) {@include flexCenter();
        padding: 4px 6px;
    }
    @include e(input) {
    // 去掉输入框的默认样式
        border: none;
        outline:none;
        display: block;
        text-align: center;
        width:60px;
        height: 20px;
    }
}

效果展示


end
总的来说是这些组件中比较简单的一个了, 有些坑能够让我更好的学习 vue 以及前端的思想, 总的来说挺有趣的.
大家继续一起学习, 一起进步, 早日实现自我价值!!
下一集准备聊聊 tab 切换组件的相关知识;
github: 链接描述
个人博客: 链接描述

退出移动版