解決 React 中的 input 輸入框在中文輸入法下的 bug

如下會涉及到的技術點:react mobx compositionstart compositionupdate compositionendreact

問題描述

在使用 input 時,一般會對輸入的內容作校驗,校驗的方式無非兩種:git

  1. 容許用戶輸入,而且作錯誤提示;
  2. 不容許用戶輸入正則或者函數匹配到的字符。

現有以下需求:「僅容許輸入英文、數字和漢字,不容許輸入其餘特殊字符和符號」。顯然這種場景須要使用第二種校驗方式。github

而後我自覺得很機智的寫了下面的代碼(引入了組件庫 cloud-react),在輸入值變化的時候(onChange 事件),處理綁定到 input 上的 value,將除了英文、數字、和漢字以外的字符都替換成空字符串。瀏覽器

export default class CompositionDemo extends Component {
  constructor() {
     this.state = {
       value: ''
     };
  }
  
  onChange(evt) {
     this.setState({
       value: evt.target.value.replace(/[^a-zA-Z0-9\u4E00-\u9FA5]/g, '')
     });
  };
  
  render() {
    return <Input
        onChange={this.onChange.bind(this)}
          value={this.state.value}
       />
  }
}

平日常常,普普統統,一切看起來都是正常的操做,結果,當我輸入拼音的時候,神奇的事情發生了:連拼的時候除了最後一個字,前面的都變成了字符。app

what??? 小問號,你是否有不少朋友?函數

因而,我踏上了一條不歸路,呸呸呸,是打開了新世界的大門,就是這個門對於我來講可能有點沉,推了兩天才看到新世界。測試

糾其緣由:拼音輸入是一個過程,確切的說,在這個過程當中,你輸入的每個字母都觸發了 onChange 事件,而你輸入過程當中的這個產物在校驗中被吃掉了,留下了一坨空字符串,因此就發生了上面那個神奇的現象。優化

解決方案

這裏須要用到兩個屬性:this

compositionstartcode

compositionend

簡單點來講,就是當你開始使用輸入法進行新的輸入的時候,會觸發 compositionstart ,中間過程其實也有一個函數 compositionupdate,顧名思義,輸入更新時會觸發它;當結束輸入法輸入的時候,會觸發 compositionend。

下面進入正題:

首先,咱們先看一下 Input 組件的一個很正常的實現:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo1 extends Component {

    constructor(props) {
        super(props);
        this.state = {
            value: '',
        };
    }
    static getDerivedStateFromProps({ value }, { value: preValue }) {
        if (value !== preValue) {
            return { value };
        }
        return null;
    }

    onChange = evt => {
        this.props.onChange(evt);
    };

    render() {
        return <input
            value={this.state.value}
            type="text"
            onChange={this.onChange}
        />
    }
}

Input 組件有兩種應用場景:

  1. 不受控的輸入框:業務方不給組件傳入 value,沒法控制輸入框的值;
  2. 受控的輸入框:業務方能夠經過給組件傳入 value,從而能夠在外部控制輸入框的值。

不受控的輸入框在我使用過程當中並無什麼 bug,此處不作贅述,此處只談受控的輸入框,也就是咱們需求(僅容許輸入英文、數字和漢字,不容許輸入其餘特殊字符和符號)中須要使用的場景。

前面提到的 compositionstart 和 compositionend 該出場了:利用這兩個屬性的特色,在輸入拼音的「過程當中」不讓 input 觸發 onChange 事件,天然就不會觸發校驗,好了,既然有了思路,開始碼代碼。

咱們定義一個變量 isOnComposition 來判斷是否在「過程當中」

isOnComposition = false;

handleComposition = evt => {
  if (evt.type === 'compositionend') {
    this.isOnComposition = false;
    return;
  }

  this.isOnComposition = true;
};

 onChange = evt => {
   if (!this.isOnComposition) {
     this.props.onChange(evt);
   }
 };

render() {
  const commonProps = {
    onChange: this.onChange,
    onCompositionStart: this.handleComposition,
    onCompositionUpdate: this.handleComposition,
    onCompositionEnd: this.handleComposition,
  };
  return <input
    value={this.state.value}
    type="text"
      {...commonProps}
  />
}

你覺得就這麼輕鬆解決了麼?

呵,你想多了!

我仍然使用開篇那個 demo 來測試這個代碼,發現事情又神奇了一點呢,此次拼音壓根就輸不進去了哇~

我查看了下在輸入拼音時函數的調用:
是的,寧沒有看錯,只觸發了onCompositionstart 和 onCompositionupdate這兩個函數,我起初覺得是邏輯被我寫扣圈了,想了想緣由(其實我想了很久,人略笨,見笑):

罪魁禍首就是綁定在 input 上的那個 value,輸入拼音的過程當中,state.value 一直沒變,input 中天然不會有任何輸入值,沒有輸入值,也就完成不了輸入過程,觸發不了 compositionend,一直處於「過程當中」。

因此此次不是程序邏輯扣圈,是中斷了。

因而我又想如何把中斷的程序接起來(對的,垮掉了咱們就撿起來,哈),完成這個鏈條。

我想了好多辦法,也在網上看了好多辦法,惋惜都解決不了個人困境。

各類心酸不堪回首,幸虧最後找到了一個辦法:其實想一想原來代碼中用 state.value 去控制 input 值的變化,仍是沒有把 input 中什麼時候輸入值的控制權放在本身手裏,「過程當中」這個概念也就失去了意義。只要 state.value 還和 input 綁在一塊兒,就是我本身玩我本身的,人家玩人家的。因而,就有了下面讓控制權回到我手中的代碼。

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo extends Component {

    inputRef = createRef();

    isOnComposition = false;

    componentDidMount() {
        this.setInputValue();
    }

    componentDidUpdate() {
        this.setInputValue();
    }

    setInputValue = () => {
        this.inputRef.current.value = this.props.value || ''
    };

    handleComposition = evt => {
        if (evt.type === 'compositionend') {
            this.isOnComposition = false;
            return;
        }

        this.isOnComposition = true;
    };

    onChange = evt => {
        if (!this.isOnComposition) {
            this.props.onChange(evt);
        }
    };

    render() {
        const commonProps = {
            onChange: this.onChange,
            onCompositionStart: this.handleComposition,
            onCompositionUpdate: this.handleComposition,
            onCompositionEnd: this.handleComposition,
        };
        return <input
            ref={this.inputRef}
            type="text"
            {...commonProps}
        />
    }
}

測了一下,大體上是沒問題了。

還要看一下谷歌瀏覽器和火狐瀏覽器,果真還有坑:

  1. 火狐瀏覽器中的執行順序:compositionstart compositionend onChange
  2. 谷歌瀏覽器中的執行順序:compositionstart onChange compositionend

最後再作一下兼容處理,修改一下 handleComposition 函數

handleComposition = evt => {
   if (evt.type === 'compositionend') {
     this.isOnComposition = false;

     // 谷歌瀏覽器:compositionstart onChange compositionend
     // 火狐瀏覽器:compositionstart compositionend onChange
     if (navigator.userAgent.indexOf('Chrome') > -1) {
       this.onChange(evt);
     }

     return;
   }

   this.isOnComposition = true;
 };

由於無論中間執行了那些函數,最後都是須要執行 onChange 事件的,所以加了判斷,對谷歌瀏覽器作了特殊處理(其它瀏覽器暫時沒作考慮和處理)。

完整代碼 https://github.com/liyuan-meng/my-react-app/tree/master/src/inputAndComposition

後記

到此,正文結束了,我還要說兩個須要注意的地方,其實也是踩了的坑:

  1. 若是 Input 組件的實現使用了 React.PureComponent ,在以上需求中會出現的問題:輸入特殊字符時,外部經過正則將其 replace 掉了,傳入 Input 組件內部的 value 實際上沒有任何變化,也不會觸發組件 render。這是由於 PureComponent 對 shouldComponentUpdate 函數作了優化,若是發現 props 和 state 上的屬性都沒有變化,不會從新渲染組件,所以我暫時的處理是:使用 React.Component ,組件實現中對 shouldComponentUpdate 封裝。
  2. 在外部使用 mobx 的時候,若是使用 observable 監聽 value,會出現和上面蕾絲的狀況—輸入特殊字符時,經過正則將其 replace 掉了,mobx 也發現 value 沒有任何變化,就不會觸發 render,我暫時的處理是使用 state,雖然我以爲這不是最好的辦法,但我目前還想不到其它的處理方式。
相關文章
相關標籤/搜索