vue组件开发和组件二次封装(一)

394 阅读1分钟

一、基础组件开发

参考官方文档:配合v-model指令使用

1、方式一

原生元素上的用法(表单输入绑定)

vue文档介绍,如下v-model指令等价于手动连接值绑定和更改事件监听

<input v-model="text">
<input :value="text" @input="event => text = event.target.value"/>
<input :value="text" @input="text = $event.target.value"/>

使用在组件上,v-model 会被展开为如下的形式:

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

组件内实现:

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

2、方式二

另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 gettersetter 的计算属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

3、自定义组件样例(方式一)

本文例子是基于vue3的,体验组件开发的简单样例。

图像-2 (1).gif

使用GInputNumber组件

<GInputNumber v-model="num" :min="0" :max="100" @change="handleChange" />

GInputNumber.vue

<template>
    <div class="g-input-number" ref="gInputNumber" @mouseover="addHoverClass" @mouseout="removeHoverClass">
        <input ref="input" class="g-input-number__inner" type="text" :value="modelValue" @input="handleInput" />
        <div class="g-input-number__ft">
            <span @click="add">+</span>
            <span @click="minus">-</span>
        </div>
    </div>
</template>
<script>
    export default ({
        name: 'GInputNumber',
        props: {
            modelValue: String,
            min: Number,
            max: Number
        },
        watch: {
            modelValue(newVal) {
                console.log('watch modelValue', newVal)
            }
        },
        emits: ["update:modelValue"],
        methods: {
            setNativeInputValue(val) {
                this.$refs.input.value = val;
            },
            handleInput(event) {
                console.log('handleInput', event.target.value);

                let newVal = event.target.value;

                // 校验,如果不符合则恢复原值
                if (new Number(newVal).toString() == 'NaN') {
                    newVal = this.modelValue;
                } //new Number将带有字符的数字转换为只有数字的类型

                if (isNaN(newVal) || newVal == '') newVal = 0;

                if (this.min != undefined && newVal < this.min) newVal = this.min;
                if (this.max != undefined && newVal > this.max) newVal = this.max;
                console.log('newVal', newVal);

                if (newVal == new Number(this.modelValue)) { //新值和旧值一样时,不会触发modelValue的watch,所以组件显示没有更新,需要单独处理
                    this.setNativeInputValue(
                        newVal); // !!! 一开始不太理解为什么没加这一句,有时回显的还是原来的数值,比如100的时候加多一位数1003时newVal是100,但组件显示还是1003
                }
                this.$emit("update:modelValue", new Number(newVal) + '');
            },
            addHoverClass() {
                this.$refs.gInputNumber.classList.add('hover');
            },
            removeHoverClass() {
                this.$refs.gInputNumber.classList.remove('hover');
            },
            add() {
                let newVal = new Number(this.modelValue) + 1;
                if (this.min != undefined && newVal < this.min) newVal = this.min;
                if (this.max != undefined && newVal > this.max) newVal = this.max;
                this.$emit("update:modelValue", new Number(newVal) + '');
            },
            minus() {
                let newVal = new Number(this.modelValue) - 1;
                if (this.min != undefined && newVal < this.min) newVal = this.min;
                if (this.max != undefined && newVal > this.max) newVal = this.max;
                this.$emit("update:modelValue", new Number(newVal) + '');
            }
        },
    });
</script>
<style scoped lang="less">
    .g-input-number {
        position: relative;
        display: inline-block;
        width: 100px;

        .g-input-number__inner {
            width: 100px;
            line-height: 30px;
            padding: 1px 30px 1px 30px;
            box-sizing: border-box;
            text-align: center;
        }

        .g-input-number__ft {
            display: none;
            position: absolute;
            right: 1px;
            top: 2px;
            width: 24px;
            cursor: pointer;
        }

        &.hover {
            .g-input-number__ft {
                display: initial;

                span {
                    display: block;
                    line-height: 16px;
                    border-left: 1px solid #ccc;

                    &:first-child {
                        border-bottom: 1px solid #ccc;
                    }
                }
            }
        }
    }
</style>

二、封装第三方组件

参考官网文档 透传 Attributes

知识点

  • attributes透传:

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。

如果不希望自动继承,想指定元素,则可设定inheritAttrs: false,把v-bind="$attrs"加到指定元素上

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>
  • 由于propreadonly的,组件内需要用计算属性nummodelValue中转一下;(同上“方式二”中所述)

二次封装组件样例

下面基于element-uiel-input-number,封装的带单位(%)格式的GPercentInputNumber组件

GPercentInputNumber.gif

此组件只是做二次封装组件的例子,细节有待完善的。 该组件用到了el-input-number、el-input,el-input只是做显示格式化作用。

使用GPercentInputNumber组件

<GPercentInputNumber v-model="numP" :min="0" :max="100" @change="handleChange2" />

GPercentInputNumber.vue

<template>
    <div class="g-percent-input-number">
        <el-input-number ref="button" v-bind="filteredAttrs" v-model="num" />
        <el-input class="label-input" ref="inputText" v-model="text" @input="onInputText" :formatter="formatter"
            :parser="parser" />
    </div>
</template>

<script>
    export default {
        name: 'GInputNumber',
        inheritAttrs: false,
        props: {
            modelValue: String,
            addonAfter: {
                type: String,
                default: '%'
            }
        },
        data() {
            return {}
        },
        watch: {},
        computed: {
            filteredAttrs: function () {
                return {
                    ...this.$attrs,
                    // modelValue: undefined,
                };
            },
            num: {
                get() {
                    return this.modelValue
                },
                set(value) {
                    this.$emit('update:modelValue', value)
                }
            },
            text: {
                get() {
                    return this.formatter(this.modelValue)
                },
                set(value) {
                    console.log('text set ', value)
                    this.$emit('update:modelValue', this.parser(value))
                }
            }
        },
        mounted() {
            console.log('attrs', this.$attrs);
        },
        methods: {
            onInputText(value) {
                console.log('onInputText', value)
            },
            formatter(value) {
                console.log('formatter before', value);
                let text = '';
                if (new Number(value).toString() == 'NaN') {
                    text = this.modelValue + `${this.addonAfter}`; //遗留问题未处理,如果输入了字母如2a%
                } else text = `${value}${this.addonAfter}`;
                console.log('formatter after', text);
                return text;
            },
            parser(value) {
                console.log('parser', value);
                try {
                    return value.replace(this.addonAfter, '');
                } catch (e) {
                    return 0;
                }
            }
        }
    }
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
    .g-percent-input-number {
        position: relative;

        .label-input {
            position: absolute;
            display: inline-block;
            width: 66px;
            margin-left: -107px;
            overflow: hidden;
            line-height: 28px;
            height: 30px;
            margin-top: 1px;

            ::v-deep .el-input__wrapper {
                border: none;
                box-shadow: none;

                .el-input__inner {
                    text-align: center;
                }
            }
        }
    }
</style>