一个让人崩溃的 Bug
想象一下这个场景:
你的小程序里有一个「昵称」输入框,设置了 maxlength="16"。用户已经输入了 15 个字,想继续输入「深圳」------当他在键盘上敲出 shenzhen 的时候,到 shenz 就卡住了,后面的 hen 死活打不进去。
不是你写错了代码,这是微信小程序的底层行为。
<input> 的 maxlength 会把输入法正在「上屏」过程中的拼音字符串也算进字符长度。对于双拼、全拼输入法,一个字的拼音可能多达 6 个字母,只要已有内容的长度接近 maxlength 上限,拼音就会被打断。
我踩过这个坑之后翻了翻社区,发现从 2017 年就有人在微信开放社区反馈,至今没有原生修复。这篇文章把我知道的所有解法都写出来。
为什么 maxlength 会截断拼音?
先搞清楚根本原因,后面才好对症下药。
微信小程序的 <input> 组件底层是原生渲染,maxlength 在其原生逻辑中的实现方式是:
- 监听文本变化事件
- 每次文本变化时检查
value.length - 如果
value.length >= maxlength,拒绝后续输入
问题出在第 2 步。 输入法在「未确认」状态下的拼音串,也被算进了 value 的长度。当已有文本接近上限时,拼音的前几个字母填满了剩余额度,后面的字母就被原生截断了。
以全拼输入法为例,输入「深圳」的过程:
| 操作 | value | value.length | maxlength | 结果 |
|---|---|---|---|---|
| 已有 15 个字 | 一二三四五六七八九十十一十二十三十四 |
15 | 16 | 正常 |
| 开始打 s | ...十四s |
16 | 16 | 截断! |
| 期望 | ...十四shenzhen深圳 |
19 | 16 | 已不可能 |
输入法都还没开始选字,拼音就已经被杀了。
解法一:放弃 maxlength,用 JS 手动控制
这是最直接的想法,也是官方推荐的做法之一。
核心思路: 不用 maxlength 属性,通过 bindinput 事件自己截断。
html
<input
value="{{value}}"
bindinput="onInput"
data-maxlength="16"
/>
javascript
onInput(e) {
const maxlength = e.currentTarget.dataset.maxlength || 16
let val = e.detail.value
// 只截断确认后的字符,不碰拼音状态
if (val.length > maxlength) {
val = val.slice(0, maxlength)
}
this.setData({ value: val })
}
效果: 拼音可以正常输入了,但输入框里不会显示输入法正在拼写的拼音串,体验不够好。
解法二:利用 composition 事件区分拼音状态
这是更优雅的方案。关键是利用 compositionstart / compositionend 事件来判断输入法是否处于「拼写中」状态。
核心思路:
- 标记一个
isComposing状态 - 输入法开始拼写(
compositionstart)时设为true - 输入法确认上屏(
compositionend)时设为false - 只有
isComposing === false时才执行截断逻辑
微信小程序从基础库 2.25.0+ 开始,<input> 组件支持 bindcompositionstart / bindcompositionend 事件。
html
<input
value="{{value}}"
maxlength="-1"
bindinput="onInput"
bindcompositionstart="onCompositionStart"
bindcompositionend="onCompositionEnd"
data-maxlength="16"
/>
javascript
Page({
data: {
value: '',
isComposing: false,
},
onCompositionStart() {
this.data.isComposing = true
},
onCompositionEnd(e) {
this.data.isComposing = false
// composition 结束后再做一次截断校验
this.doTruncate(e.detail.value)
},
onInput(e) {
// 正在拼写中,不截断
if (this.data.isComposing) {
this.setData({ value: e.detail.value })
return
}
// 非拼写状态,正常截断
this.doTruncate(e.detail.value)
},
doTruncate(val) {
const maxlength = 16
if (val.length > maxlength) {
val = val.slice(0, maxlength)
}
this.setData({ value: val })
},
})
但有一个坑需要注意: 微信开发者工具上 compositionend 的触发时机和真机不一致。真机上 compositionend 在 input 事件前触发,工具里顺序可能是反的。所以不能依赖 compositionend 来做截断。
解法三:只保留 composition 期间最终一次输入(推荐)
综合真机行为和工具差异,最稳妥的方案是:
compositionstart标记状态compositionend不做截断,只重置状态bindinput中判断:拼写中直接更新 value,拼写结束才截断
javascript
Page({
data: {
value: '',
composing: false,
MAX_LENGTH: 16,
},
onCompositionStart() {
this.composing = true
},
onCompositionEnd(e) {
// 重点:延迟一帧再重置,确保 input 事件已处理
const val = e.detail.value
setTimeout(() => {
this.composing = false
// composition 结束后,有可能一次确认多个字,需要重新截断
if (val.length > this.MAX_LENGTH) {
this.setData({ value: val.slice(0, this.MAX_LENGTH) })
}
}, 0)
},
onInput(e) {
const val = e.detail.value
// 如果正在 composition,不截断
if (this.composing) {
this.setData({ value: val })
return
}
// 普通输入或 composition 已经结束,截断
if (val.length > this.MAX_LENGTH) {
this.setData({ value: val.slice(0, this.MAX_LENGTH) })
} else {
this.setData({ value: val })
}
},
})
微信小程序官方文档中有一段说明:
建议使用
maxlength属性来控制输入长度。如果需要对输入内容进行复杂处理,建议设置maxlength="-1"并通过bindinput事件自行控制。
所以设置 maxlength="-1" 来禁用原生截断,配合 composition 事件,是目前最被推荐的做法。
解法四:封装为自定义组件(一劳永逸)
如果项目里多处用到,可以封装成一个组件 limited-input:
html
<!-- components/limited-input/index.wxml -->
<input
value="{{value}}"
maxlength="-1"
placeholder="{{placeholder}}"
bindinput="onInput"
bindcompositionstart="onCompositionStart"
bindcompositionend="onCompositionEnd"
disabled="{{disabled}}"
/>
<text class="counter">{{currentLength}}/{{maxlength}}</text>
javascript
// components/limited-input/index.js
Component({
properties: {
maxlength: { type: Number, value: 140 },
placeholder: { type: String, value: '' },
disabled: { type: Boolean, value: false },
},
data: {
value: '',
currentLength: 0,
},
composing: false,
methods: {
onCompositionStart() {
this.composing = true
},
onCompositionEnd(e) {
const val = e.detail.value
setTimeout(() => {
this.composing = false
this.validateAndUpdate(val)
}, 0)
},
onInput(e) {
const val = e.detail.value
if (this.composing) {
this.setData({ value: val, currentLength: val.length })
return
}
this.validateAndUpdate(val)
},
validateAndUpdate(val) {
const max = this.properties.maxlength
if (val.length > max) {
val = val.slice(0, max)
}
this.setData({ value: val, currentLength: val.length })
this.triggerEvent('input', { value: val })
},
},
})
使用方式:
html
<limited-input
maxlength="16"
placeholder="请输入昵称"
bindinput="onNicknameInput"
/>
对比总结
| 方案 | 代码量 | 拼音体验 | 字数统计准确性 | 推荐场景 |
|---|---|---|---|---|
| 纯 JS 截断 | 少 | 中等,拼音不显示 | 准确 | 简单场景,快速修复 |
| composition 事件 | 中等 | 好,拼音完整显示 | 准确 | 多数项目 |
| 延迟截断 | 中等 | 最好 | 较准确 | 要求严格的生产项目 |
| 自定义组件 | 较多 | 最好 | 准确 | 多人协作的中大型项目 |
我的建议:
- 新项目直接用「解法四」封装组件,收益大于成本
- 存量项目用「解法三」,改几行代码就能修好
- 如果对基础库版本有顾虑,确保 ≥ 2.25.0
最后一个小提醒:即使你解决了拼音问题,也要考虑 emoji 和特殊字符的 Unicode 长度。一个 emoji 的 length 可能是 2,所以如果要精确限制「用户可见字符数」,需要配合 Array.from() 或 [...str] 来计算。
你的小程序里遇到过这个问题吗?欢迎在评论区分享其他踩坑经历 👇
如果这篇文章对你有帮助,点个赞让更多被拼音截断折磨的开发者看到吧。