在不少項目中需要針對input的輸入數據做相應的處理,比如講用戶數據的數據轉換成對應的大寫字母,那麼問題就來了。當用戶拿起手機,點中你的input組件後開始輸入時,會發現IOS的手機在中文輸入下會發生重複輸入的情況。
舉例來說,當用戶在中文輸入法下輸入ABC這幾個字母是,input的實際的value值(如果你把input的onChange事件打印出來或是設置成value的話),不是ABC而是AABABC,也就是說用戶輸入的會有緩存,而這在Android手機上沒有出現。
那如何處理這個問題並使得針對IOS也能兼容呢?默認的流程都一樣,預先google一下,會發現主流的方法是讓我們監聽compositionEvent,這是做什麼的呢,可以從字面意思來理解,composition有組成和部分的意思,那麼這個事件的觸發其實就是用戶切換了非英語的輸入法模式後,數據由部分字母比如拼音組合而成時會觸發的事件。
按照MDN上的簡單介紹(確實夠簡單的了)和React官網的事件介紹,compositionEvent在react中共有三個具體的事件:onCompositionEnd onCompositionStart onCompositionUpdate。
接着在本地試了下,發現每次輸入中文都會觸發onCompositionStart和onCompositionUpdate,不過它們都沒有傳遞相關的參數,所以說打印console.log(arguments)都是空的。
方案1:追加中文輸入緩存的末尾字符
針對以上搜集及整理,針對input的兼容可以這樣來實現:
class example extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
this.inputLock = false; // 標記是否處於composition的狀態
}
// input事件調用
onChange(e){
let value = e.target.value;
if(this.inputLock){
// 處於中文輸入狀態,只添加最後一位字符
this.state.value += value.substr(value.length - 1).toUpperCase();
this.inputLock = false; // 由於每次輸入中文字符都會觸發onCompositionStart,所以在這裏將其設置爲false,以備下次觸發
}else {
// 非中文輸入,則正常操作,這裏的需求是將字符轉換成大寫
this.state.value = value.toUpperCase();
}
}
render(){
return (
<input
value={this.state.value}
onChange={this.onChange.bind(this)}
onCompositionStart={()=>{this.inputLock = true}}
/>
)
}
}
方案1中只用到了onCompositionStart,主要發現在IOS中無法觸發onCompositionEnd事件(還沒經過全面測試,瞭解的同學請賜教),雖然能夠解決中午輸入問題,但是存在一個很明顯的bug,就是用戶每次只能在後面添加內容,如果用戶將指針移到文本中間,則無法在中間添加字符,會發現仍舊在字符串末尾添加,而且當用戶點擊刪除鍵,會發現又自動添加了中文緩存中的內容,所以方案1在實際中還無法使用。
方案2:onKeyDown
class example extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
this.inputLock = false; // 標記是否處於composition的狀態
}
onKeyDown(e){
const {value} = this.state;
let length = value.length;
let cursorPos = this.inputRef.selectionEnd; // 獲取當前指針的位置
// 正常字符按鍵
if(e.keyCode >= 48 && e.keyCode <= 90){
if( cursorPos < length){
this.state.value = [
value.substr(0, cursorPos),
String.fromCharCode(e.keyCode),
value.substr(cursorPos),
].join('');
}else {
value += String.fromCharCode(e.keyCode);
}
this.forceUpdate();
}
// 回退backspace
if(e.keyCode === 8){
if(length){
value = value.substr(0, length - 1);
this.forceUpdate();
}
}
}
render(){
return (
<input
ref={ref=>this.inputRef=ref}
value={this.state.value}
onKeyDown={this.onKeyDown.bind(this)}
onCompositionStart={()=>{this.inputLock = true}}
/>
)
}
爲了解決方案1中的問題,方案2中去掉onChange方案,該用onKeyDown或類似的onKeyPress來重寫回退鍵的邏輯,通過添加input reference 引用來獲取要插入的文本位置,實現後的結果已經可以正常輸入不會有影響,唯一的瑕疵是每次在前面的文本中插入了一個字符,則光標未在插入字符的後面停留,而是直接回到了字符串末尾。
方案3:input reset光標復原
方案2中存在的主要問題是每次插入後,input的輸入光標自動回到文本的末尾,這是一個受控input的組件的通病。因爲每次input的value由state更新後都會reset,然後react diff完後調用dom的相應操作更新,這裏面是一個異步且無法控制的過程。現在比較一種勉強的做法就是通過設置setTimeout等一段時間後實現光標的設置,也就是在dom渲染完了以後,這可能跟不同瀏覽器的渲染性能有關。筆者測試下來最低的更新時間約爲30ms,同時爲了代碼清晰,又對input的類做了重新的組件封裝,以下是代碼Input.js:
/**
* Input:兼容IOS中文輸入,只支持大寫字母和數字輸入
*/
import React from 'react';
class Input extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
}
/** 這裏使用了的onKeyDown事件,因爲onChange,
* 或類似的onInput等事件它會將IOS中文輸入法的緩存字符都放進去,這就導致這樣一個結果:
* 有趣的是,百度的輸入法就沒有這個問題,Android也沒有,看來很多App針對自家定製化鍵盤還是非常有必要的。
*/
onKeyDown(e) {
const {value} = this.state;
let cursorPos = this.inputRef.selectionEnd
let length = value.length;
let valueString = ''; // 最終合成的字符串
let setCursor = false; // 判斷是否要設置光標,如果默認在末尾添加的話就爲false
let cursorStep = 1; // 添加字符:1,刪除字符:-1
// 正常字符按鍵
if (e.keyCode >= 48 && e.keyCode <= 90) {
if (cursorPos < length) {
valueString = [
value.substr(0, cursorPos),
String.fromCharCode(e.keyCode),
value.substr(cursorPos),
].join('')
setCursor = true;
} else {
valueString = value + String.fromCharCode(e.keyCode);
}
}
// 回退backspace
if (e.keyCode === 8) {
if (length && cursorPos === length) {
valueString = value.substr(0, length - 1);
}else {
valueString = [
value.substr(0, cursorPos - 1),
value.substr(cursorPos),
].join('')
setCursor = true;
cursorStep = -1;
}
}
this.setState({
value: valueString,
}, () => {
if(setCursor){
// 設置光標回覆位置,input 指針在設置value後reset至行尾。dom操作異步,因此光標設置需預估一個時間(30ms),否則失效
setTimeout(() => {
this.inputRef.setSelectionRange(cursorPos + cursorStep, cursorPos + cursorStep)
}, 30)
}
this.props.updateParentState &&
this.props.updateParentState(this.state);
})
}
render(){
const { innerRef, ...restOfProps } = this.props;
return (
<input
{...restOfProps}
ref={ref => {this.inputRef = ref;innerRef(ref)}}
value={this.state.value}
onKeyDown={this.onKeyDown.bind(this)}
/>
)
}
}
module.exports = Input;
方案3中基本上解決了所有的問題,字符的增加,插入,刪除,選中光標後的刪除等。唯一的遺憾是因爲設置了setTimeout,所以會看到光標在行末尾的閃爍,希望未來有一個徹底的解決方案。
方案4:放棄onKeyDown,使用onInput
方案3還是有光標的閃爍,對於強迫症來說還是不能忍,而且用戶體驗上也大打折扣,人家還以爲你這系統出問題了呢。所以在經過不斷的嘗試後,發現IOS的input當設置完onInput後竟然可以將原本隱藏的中文拼音緩存直接寫到input框中,這樣就提供了一種新的可能:
/**
* Input:兼容IOS中文輸入,只支持大寫字母和數字輸入
*/
import React from 'react';
import PropTypes from 'prop-types';
class Input extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
this.inputLock = false;
}
onKeyDown(e) {
switch(e.keyCode){
case 13:
this.setState({
value: this.inputRef.value.toUpperCase()
})
break;
}
}
onInput(){
this.setState(
prevState => ({ value: this.inputRef.value}),
() => {
this.props.updateParentState &&
this.props.updateParentState(this.state);
}
);
}
render(){
const { innerRef, updateParentState, ...restOfProps } = this.props;
return (
<input
{...restOfProps}
ref={ref => {this.inputRef = ref;innerRef(ref)}}
value={this.state.value}
onKeyDown={this.onKeyDown.bind(this)}
onInput={this.onInput.bind(this)}
onCompositionStart={()=>{this.inputLock=true}}
onCompositionEnd={()=>{
this.inputLock=false;
this.setState({
value: this.inputRef.value.toUpperCase()
})
}}
/>
)
}
}
Input.propTypes = {
innerRef: PropTypes.func,
updateParentState: PropTypes.func,
}
Input.defaultProps = {
innerRef: ()=>{},
updateParentState: ()=>{},
}
module.exports = Input;
Input組件的目標是在無論中英文的輸入法中,都能輸入大寫的字母或數字,而實現的方法則是:
1. 針對中文輸入法:onInput事件配合compositionEvent,用戶點擊確定時(也就是onCompositionEnd)將文本統一轉換,不然在onInput中設置大寫會導致光標的回滾;
2. 針對英文輸入法,默認如果不操作的話會輸出第一個字母大寫後面小寫的字符串,例如Abcdefg。在哪個時機實現大寫的轉換呢?答案是通過監聽用戶輸入return鍵(keyCode=13)來做大寫的轉換。
總的來說,方案4能基本實現目標需求,但期待原生更好的實現和大家更好更流暢的方案。