我们在键盘上留下的余温, 也将随时代传递到更远的将来


:D 获取中...

最近在做项目的时候遇到 input 只能输入整数, 不能输入小数的需求. 想到以前同事封装过这个组件, 可以拿来一用. 结果发现组件只支持正整数, 不支持负数, 看来还需要修改一下了

国际惯例: 源码奉上

同事封装的组件

1
2
3
4
5
<template>
<el-input v-model="model" v-bind="$attrs" @input="_input" v-on="listeners">
<slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
</el-input>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<script>
export default {
inheritAttrs: false,
name: 'NumberInput',
props: {
value: [Number, String],
type: {
// integer 整数, decimal 小数
type: String,
default: 'decimal'
},
places: {
//保留小数位数
type: [Number, String],
default: 2 //默认两位小数
},
Event: {
type: String,
default: 'input'
}
},
data() {
return {
model: this.value
}
},
watch: {
value() {
this.model = this.value
}
},
methods: {
_input(val) {
const numReg = /^[0-9]$/
let v
if (this.type === 'integer') {
// 验证 整数
v = val.replace(/./g, function(e) {
if (numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})
return
}
if (this.type === 'decimal') {
// 验证 只留小数点和数字
v = val.replace(/./g, function(e) {
if (e === '.' || numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
}
//小数位数
if (v.indexOf('.') > -1) {
let l, r
// 阻止输入两个小数点
if (v.split('.').length > 2) {
v = v.slice(0, v.length - 1)
}
// 截取小数点
l = v.split('.')[0]
r = v.split('.')[1] || ''
if (r.length > +this.places) {
r = r.slice(0, +this.places)
}
v = l + '.' + r
}
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})
}
},
computed: {
listeners() {
return Object.assign({}, this.$listeners, { [this.Event]: this._input })
}
}
}
</script>

分析组件

首先 html 部分没啥好说的, 都是基本操作, 这里就不再赘述了

js 部分主要看一下 _input 中的代码:

首先定义了一个验证数字的正则, 其实 /^\d+$/ 就可以了.

先看验证整数的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (this.type === 'integer') {
// 验证 整数
// 依次替换 val 中的每个值
v = val.replace(/./g, function(e) {
// 如果是数字, 就返回本身
if (numReg.test(parseInt(e))) {
return e
// 否则返回空
} else {
return ''
}
})
this.$nextTick(() => {
// 修改绑定值
this.model = v
// emit 值
this.$emit('input', v)
})
return
}

这里一切正常, 就是无法判断负整数, 所以我们需要修改一下正则, 并且还需要增加一个参数来判断是正数还是负数

再看验证小数的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
if (this.type === 'decimal') {
// 验证 只留小数点和数字
// 依次替换 val 中的每个值
v = val.replace(/./g, function(e) {
// 如果是小数点和数字, 就返回本身
// 所以只输入一个小数点, 而不输入数字也会验证通过的
// 注: 这里会返回多个小数点
if (e === '.' || numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
}
//小数位数
// 判断是否有小数点
if (v.indexOf('.') > -1) {
let l, r
// 阻止输入两个小数点
if (v.split('.').length > 2) {
// 如果输入两个小数点, 就截断最后一个字符
// 注: 这里有一个问题
// 我们正常输入都是从左到右, 当输入第二个小数点后, 截断第二个是正常的
// 但用户有可能会移动光标到第一个小数点前面输入小数点
// 这样截断最后一个字符其实是数字
// 而且现在有两个小数点了
v = v.slice(0, v.length - 1)
}
// 截取小数点
// 用小数点分隔, 判断小数位数是否超过预定值
// 其实这里变相的处理了两个小数点的问题
// 因为分隔后, 其实数组中有 3 个值, 这里只取了前两个
// 那必然会导致数据丢失
l = v.split('.')[0]
r = v.split('.')[1] || ''
if (r.length > +this.places) {
r = r.slice(0, +this.places)
}
v = l + '.' + r
}
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})

小数验证一下就出来这么多问题了:

  1. 小数点后没有数字也会验证通过
  2. 在第一个小数点前面再输入一个小数点会导致数据丢失

关于数据丢失的例子:

输入 处理 输出
123.45 验证通过 123.45
12.3.45 进入阻止输入两个小数点的逻辑, 截断最后一个字符 5 12.3.4
继续进入截取小数点的逻辑, 只取数组前两位, 并验证小数位数是否超过预定值 12.3

改造组件

增加一个参数, 来判断正负数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
props: {
type: {
// integer 整数, decimal 小数
type: String,
default: 'decimal'
},
signed: {
// plus 正数, minus: 负数, all: 全部
type: String,
default: 'plus'
},
places: {
// 保留小数位数
type: [Number, String],
default: 2 // 默认两位小数
}
},
computed: {
regs() {
return {
// 注:
// 如果是负数的话, 用户首先会输入 -, 这个要验证通过, 所以数字后面的量词为 *
// 这也就有一个问题了, 用户只输入一个 - 就提交, 也能验证通过, 目前暂时没有好办法处理
'integer-plus': /^\d+$/,
'integer-minus': /^-\d*$/,
'integer-all': /^-?\d*$/,
// 这里是在正则中使用变量的方式
// \ 要写两次
'decimal-plus': new RegExp(`^\\d+(\\.\\d{0,${this.places}})?$`),
'decimal-minus': new RegExp(`^-(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`),
'decimal-all': new RegExp(`^-?(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`)
}
},
regName() {
return `${this.type}-${this.signed}`
},
reg() {
return this.regs[this.regName]
}
}

验证输入

思路: 感觉同事验证整数和小数的逻辑太麻烦, 其实可以根据不同情况生成不同正则, 如果验证通过就返回, 否则就截断最后一个字符再返回. 但这样也犯了同事处理阻止输入两个小数点的问题: 如果在字符串中间输入非法字符, 会截断最后一个合法字符, 而留下这个非法字符.

其实, 如果本次输入验证不通过, 那我就返回上一次输入的. 这就需要记录本次输入和上次输入的值, 需要请出 watch 大神了, 并且需要一个变量记录当前的输入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
data() {
return {
model: this.value,
temporary: this.value // 这里给临时变量赋值为 value 初始值, 防止用户首次输入非法字符后, old 为空, 清空输入
}
},
watch: {
value(n) {
this.temporary = n
},
temporary(n, o) {
// 注:
// n 有值才判断, 防止删除的时候无法删除最后一个字符
if (n.length && !this.reg.test(n)) {
// 如果验证未通过
// 当前输入赋值为上一次的合法输入
n = o
// 临时变量也赋值为上一次
this.temporary = o
// 由于这里临时变量改变了, 会再进入 watch, 并且会验证通过, 所以这里不需要 赋值和 emit 操作
return
}
this.model = n
this.$emit('input', n)
}
},
methods: {
_input(val) {
this.temporary = val
}
}

至此就算正式改造完成了

问题

-. 问题(未解决)

可以输入 - 的时候, 只输入一个 - 也可以验证通过

可以输入 . 的时候, . 后面没有数字也可以验证通过

这两种情况暂时没有好的解决方案.

初始值为不合法字符串(已解决)

如果初始值就是一个不合法的字符串, 会进入无限循环中

可以在 createdvalue 判断一下, 如果不合法就给 temporarymodel 赋值为空.

1
2
3
4
5
if (!this.reg.test(this.value)) {
// 这里要同时修改这两个值, 防止 temporary 的 n 和 o 与实际不符
this.model = ''
this.temporary = ''
}

这种虽然会改变输入值, 但你传入的就是一个非法值, 总比陷入无限循环而卡死页面强吧

默认值为 undefined 会造成页面卡顿甚至卡死(已解决)

当在表格中大量使用该组件并且默认值为 undefined 时, 渲染需要很长时间, 甚至卡死浏览器

如图:

number-input in el-table

这个表格默认查最近 4 天的数据, 由于是动态表格, 所以由后端返回, prop 是当日的时间戳.

表头数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data: [
{
children: [
{
prop: '1588953600000',
label: '09日',
},
{
prop: '1589040000000',
label: '10日',
},
{
prop: '1589126400000',
label: '11日',
},
{
prop: '1589212800000',
label: '12日',
},
],
prop: '1589262808249',
label: '2020年05月',
}
]

表格数据:

后端目前的做法是当日如果有数据就返回当日 keyvalue, 否则就没有当日这个字段

1
2
3
4
5
6
7
8
9
10
11
data: [
{
id: '1611934516379702',
'1588953600000': 10,
name: '11'
},
{
id: '1611934516379703',
name: '22'
},
]

第一条 09 日有数据, 就返回了, 其余日都没数据, 就没有返回; 第二条都没有数据, 所以都没有返回

这样取 10 - 12 日的数据就是 undefined, 当页面中有大约 150 个该组件并且都是 undefined 的时候, 渲染就非常卡顿了, 数据量再大就会卡死页面了

为什么发现是 undefined 引起的呢? 因为还有一个类似的页面并不卡顿, 差别就在那个页面后端默认返回的是 0, 而不是 undefined.

一开始想的是让后端也默认返回 0, 但以后出现类似情况都需要后端处理, 作为一个成熟的组件, 咱要从自身解决这个问题

说说我的看法: 当前的问题是 undefined 引起的, 为什么会这样呢? 其实是因为咱们接收 value 值规定了只接收 [Number, String], 但传入的是 undefined, 但 vue 却没有报类型错误, 应该是内部对这种情况做了处理, 导致占用了大量资源, 导致渲染卡顿. 所以咱们只要给一个类型是 NumberString 的默认值, 问题就解决了

1
2
3
4
5
6
props: {
value: {
type: [Number, String],
default: '' // '' 或者 0 看需求吧
}
}

传入的 value 为 Number 类型时无法验证(已解决)

复现路径: 不通过输入框修改 value, 而是通过代码将 value 值改变为 Number 类型

以前都是通过输入框来验证组件是否可以控制输入, 这种方式会将 Number 转化为 String, 导致一直没有测出这个 bug

由于 Number 没有 length 属性, 并且 RegExp 只能验证字符串(验证数值有问题), 所以 n.length && !this.reg.test(n) 这个判断就进不去了, 从而导致无法验证

所以我们需要先转化为 String 再验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
watch: {
temporary(n, o) {
n = n + ''
// 判断 n.length 和 n 是一样的效果, 这里就判断 n 了
if (n && !this.reg.test(n)) {
n = o
this.temporary = o
return
}
this.model = n
this.$emit('input', n)
}
},
created() {
const val = this.value + ''
if (val && !this.reg.test(val)) {
// 这里 temporary 改变后会触发 watch, 那里会给 model 赋值, 所以这里不用赋值了
this.temporary = ''
}
}

扩展

既然现在是通过传入参数来生成正则表达式, 那何不封装一个接收正则表达式的组件, 这样不就可以让使用者自定义 input 值了吗?

RegInput.vue

1
2
3
4
5
<template>
<el-input v-model="model" v-bind="$attrs" @input="_input" v-on="$listeners">
<slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
</el-input>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<script>
export default {
inheritAttrs: false,
name: 'RegInput',
props: {
value: {
type: [Number, String],
default: ''
},
reg: {
type: RegExp,
default: /./
}
},
data() {
return {
model: this.value,
temporary: this.value
}
},
watch: {
value(n) {
this.temporary = n
},
temporary(n, o) {
n = n + ''
if (this.test(n)) {
n = o
this.temporary = o
return
}
this.model = n
this.$emit('input', n)
}
},
methods: {
_input(val) {
this.temporary = val
},
test(val) {
return val && !this.reg.test(val)
}
},
created() {
const val = this.value + ''
if (this.test(val)) {
this.temporary = ''
}
}
}
</script>

验证发现中文(/^[\u4E00-\u9FA5]*$/)和数字(/^-?(\d*|\d+(\.\d*)?)$/)可以正常输入, 但手机号(/^1[3456789]\d{9}$/)等却无法输入

原因是因为这一类有最少位数, 而我们输入只能一个一个输入, 第一次输入一个, 验证不通过, 返回上一个结果(空值), 如此反复, 永远也无法输入了

对于这一类正则, 目前想到两种方法:

  1. 不让用户输入, 只能粘贴.
  2. 修改正则表达式, 一位也需要验证通过.

两种办法其实都不算好办法, 但目前没有找到合适的解决方案, 等以后找到了再说吧

先把源码奉上


 评论

 无法加载Disqus评论系统,请确保您的网络能够正常访问。