中文输入法与React文本输入框的问题与解决方案
问题来源是来自这个React官方存储库的issue #3926,与这个议题关联的有很多其他的issue,来自许多项目,有些是与React相关,有些则是vue或其它JS套件。也已经有其他的项目是专注于解决这个问题,例如react-composition,不过它是一个使用ES5语法的React组件。在其他的讨论区上也有类似的问题与解答。本文的目的是希望能针对这个问题提供一些说明、现在暂时性的解决方案。 下图为目前解决React中"Controlled"(受控制的)input元件的演示,可以到这里去测试:
问题何来React组件主要使用 举出一个实际的应用情况,一个使用React撰写的搜索计算机书籍的功能,用户可以在文本输入框里输入要搜索的书名,程序中是利用 当然,你也可以用对中文字词检查的修正方式,或是干脆不要用 网页上的DOM元素与"Uncontrolled"(不受控制的)的组件这个问题在浏览器中,早就已经有了可应对的解决方法,DOM事件中有一组额外的CompositionEvent(组成事件)可以辅助开发者,它可以在可编辑的DOM元素上触发,主要是input与textarea上,所以可以用来辅助解决 藉由CompositionEvent的辅助来解决的方式,也就是说在网页上的input元素,可以利用CompositionEvent作为一个信号,如果正在使用IME输入中文时, 在React应用中,如果是一个"Uncontrolled"(不受控制的)的input组件,它与网页上真实DOM中的input元素的事件行为无差异,也就是说,直接使用CompositionEvent的解决方式,就可以解决这个输入法的问题,以下面的代码为例子: // @flow import React from 'react' const Cinput = (props: Object) => { // record if is on Composition let isOnComposition: boolean = false const handleComposition = (e: KeyboardEvent) => { if (e.type === 'compositionend') { // composition is end isOnComposition = false } else { // in composition isOnComposition = true } } const handleChange = (e: KeyboardEvent) => { // only when onComposition===false to fire onChange if (e.target instanceof HTMLInputElement && !isOnComposition) { props.onChange(e) } } return ( <input {...props} onCompositionStart={handleComposition} onCompositionUpdate={handleComposition} onCompositionEnd={handleComposition} onChange={handleChange} /> ) } export default Cinput 上面这是一个典型的"Uncontrolled"(不受控制的)input组件,主要是它不用 这个解决方案在几乎所有能支持CompositionEvent的浏览器(IE9以上)都可以运行得很好,不过在Google Chrome浏览器在2016年的版本53之后,更动了 // detect it is Chrome browser? const isChrome = !!window.chrome && !!window.chrome.webstore const handleComposition = (e: KeyboardEvent) => { if (e.type === 'compositionend') { // composition is end isOnComposition = false // fixed for Chrome v53+ and detect all Chrome // https://chromium.googlesource.com/chromium/src/ // +/afce9d93e76f2ff81baaa088a4ea25f67d1a76b3%5E%21/ if (e.target instanceof HTMLInputElement && !isOnComposition && isChrome) { // fire onChange props.onChange(e) } } else { // in composition isOnComposition = true } } "Uncontrolled"(不受控制的)input或textarea组件,解决方式就是这么简单而已,利用CompositionEvent过滤掉不必要的
"Controlled"(受控制的)的组件在React应用中,使用"Controlled"(受控制的)的input或textarea组件是另一回事,它会开始复杂起来。 "Controlled"(受控制的)的组件并不是只有加上 把这整个流程串接在一起后,我相信事件触发的不连续情况会变得很严重,需要对不同情况下作测试与评估。目前我所作的测试还只是最基本的组件运用而已,复杂的组件情况还没有开始进行。因为state有很多种用途,有时候内部使用,有时候要对外部用户输入介面的事件,或是有时候要对服务器端的数据接收或传送,不论是不是要使用Redux、MobX或Flux之类的state容器函数库或框架,最终要进行重新渲染的工作,还是得调用React中的setState方法才行。 在基本的测试时,我发现"Controlled"(受控制的)的input组件,它不仅事件触发不连续的情况严重,而且有可能在不同浏览器上会有不同的结果。完全不会有问题的只有一个浏览器,就是上面注释中所说的已经实作出IME API的IE11,IE11上可能根本不需要任何解决方案,它的输入法编辑器是独立于浏览器上的文本输入框之外的。 目前已测试的结果是有三种情况,"Chrome,Opera,IE,Edge"为一种,"Firefox"为一种,"Safari"为一种。我为这三种情况分别写了不同的解决方式的代码,但这个事件触发的不连续情况,现在无法有一致性的解决方案,我只能推测这大概可能是React内部设计的问题。 不论是三种的那一种解决方案,有一个重点是你不能像上面的一般性解决方案,阻挡
我使用了另一个内部的state对象中的值,称为 这个解决方案,是一个"挂羊头卖狗肉"的用法,不论用户在input组件如何输入,输入的过程都会改变 大致上说明一下解决方式的代码,首先它有两个在这个模块作用域中的全局变量,一个用来记录是否在输入法的组字过程中,另一个是给专给Safari浏览器用的: // if now is in composition session let isOnComposition = false // for safari use only,innervalue can't setState when compositionend occurred let isInnerChangeFromOnChange = false 在专门处理 后面的代码,是代表在输入法的组字过程中,setState方法使用的差异,在组字过程中( handleChange = (e: Event) => { // console.log('change type ',e.type,',target ',e.target,target.value ',e.target.value) // Flow check if (!(e.target instanceof HTMLInputElement)) return if (isInnerChangeFromOnChange) { this.setState({ inputValue: e.target.value,innerValue: e.target.value }) isInnerChangeFromOnChange = false return } // when is on composition,change inputValue only // when not in composition change inputValue and innerValue both if (!isOnComposition) { this.setState({ inputValue: e.target.value,innerValue: e.target.value,}) } else { this.setState({ inputValue: e.target.value }) } } 在专门处理 handleComposition = (e: Event) => { // console.log('type ',e.target.value,data',e.data) // Flow check if (!(e.target instanceof HTMLInputElement)) return if (e.type === 'compositionend') { // Chrome is ok for only setState innerValue // Opera,IE and Edge is like Chrome if (isChrome || isIE || isEdge || isOpera) { this.setState({ innerValue: e.target.value }) } // Firefox need to setState inputValue again... if (isFirefox) { this.setState({ innerValue: e.target.value,inputValue: e.target.value }) } // Safari think e.target.value in composition event is keyboard char,// but it will fire another change after compositionend if (isSafari) { // do change in the next change event isInnerChangeFromOnChange = true } isOnComposition = false } else { isOnComposition = true } }
总结如果你是使用"Uncontrolled"(不受控制的)的组件,那么解决方法很简单,就如同上面所说的,像一般的网页上的DOM元素的解决方式即可。 但对于"Controlled"(受控制的)的组件来说,目前的解决方案是一种try-and-error(试误法)的暂时性解决方案,我目前只能按照已测试的平台与浏览器去修正,没测过的浏览器与平台,就不得而知了。 关于这个"Controlled"(受控制的)的组件的事件触发,目前看到有在不同浏览器上的事件触发不连续情况,我也有发一个议题(Issue)给React官方。或许比较好的治本方案,是需要从state更动方式的内部代码,或是人造事件触发的顺序,进行一些调整,这超出我的能力范围,就有待开发团队的回应了。 最后,如果你正好有需要到这个功能,或是你认为这个功能有需要,你可以帮忙测试看看或是提供一些建议。我已经把所有的代码、演示、线上测试、解决方案都集中到这个Github库的react-compositionevent中。或许你现在需要一个解决方案,你可以用里面目前的暂时性解决方式试试也可以。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |