前言

IOS的坑向来就不会少,这不,最近在重构组件库的时候,就发现了两个坑,然后也找到解决办法去把坑给填上,这里记录一下,以备后面回忆。

坑一:Input输入框配合系统输入法在实时搜索的应用中会出现错误的行为

该坑的示例代码如下:

特意录制了以下ios手机的效果:

从视频我们看到在我们不断的从键盘中输入一直会触发onChange事件,导致Toast一直在变化。

想要填坑,需要先了解一下:compositionstartcompositionend事件

compositionstart事件:

compositionstart 事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)

compositionend事件:

compositionend事件触发于一段文字的输入完成或者取消()

如果不使用上述两个事件,那么每次输入的时候,都会触发onChange事件,从而出现了上面gif图片的那种错误的行为。

现在我们使用这两个事件来修复这个问题:

给input标签加入下面的监听事件:(react版本)

this._isCompositing = false
changeHandler = (event: any) => {
  event.persist()
  const {
    onChange,
  } = this.props

  let value = event.target.value
  this.setState({ value }, () => {

    // composition 的事件触发顺序一般如下:
    // compositionStart -> input * n -> compositionEnd
    // 但是在 firefox 中变成这样子:
    // compositionStart -> input * n -> compositionEnd -> input
    // 这里我们在 compositionEnd 之后统一触发一次
    // 此处留个坑,在 firefox 下会连续触发两次onChange, 不过可以用去抖来处理,配合Input组件的debounce属性

    // 如果需要在汉字或者语音等连续输入的时候不触发onChange
    // 这时候如果还在连续输入的话,不作任何处理
    if (this._isCompositing) {
      return
    }

    if (debounce) {
      this._debounceChangeHandler(event)
    } else if (onChange) {
      onChange(event)
    }
  })
}
compositionStartHandler = (event: any) => {
  this._isCompositing = true
  const { onCompositionStart } = this.props
  if (onCompositionStart) {
    onCompositionStart(event)
  }
}
compositionEndHandler = (event: any) => {
  const { onCompositionEnd } = this.props
  this._isCompositing = false
  this.changeHandler(event)
  if (onCompositionEnd) {
    onCompositionEnd(event)
  }
}
render() {
  return (
    <input
      ref={n => this._input = n}
      onChange={this.changeHandler}
      onCompositionStart={this.compositionStartHandler}
      onCompositionEnd={this.compositionEndHandler}
    />
  )
}

效果如下:

从视频可以看到使用了composition事件之后,只有等我们输入完整的中文点击确定后才会触发onChange事件。

坑二:Input输入框focus之后,点击输入框外部的任何区域都没法blur掉

这个行为在安卓是没问题的,但是牛逼的ios就是这么设计的,无奈的我们就只能去给这种设计做一个workaround的解决办法。因为safari不会主动blur,所以就需要我们去主动blur。

解决代码如下:(react版本, 非完整,仅供参考)

this._input = null
componentDidMount() {
  this._isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
  if (this._isIOS) {
    document.addEventListener('touchend', this.documentClickHandler)
  }
}
documentClickHandler = (e: any) => {
  if (this._input) {
    const activeElement = document.activeElement
    if (activeElement.tagName === 'INPUT' && this._input === activeElement && e.target !== this._input) {
      activeElement.blur()
    }
  }
}
render() {
  return (
    <input
      ref={n => this._input = n}
    />)
}

上述的解决办法用到了document.activeElement,该元素的定义如下:

activeElementDocumentShadownRoot接口中是只读的属性,它返回那些在DOM或者Shadow树中被聚焦的节点。大部分时候如果输入框有文本输入的时候activeElement会返回一个<input>或者<textarea>节点,如果是的话我们可以使用selectionStartselectionEnd属性获取更多的详情。另外一些情况可能返回一个<select>元素(在menu中)或者是buttoncheckboxradio类型的input元素。

如果没有选中任何节点,那么返回的是body或者null

另外提到的ShadowRootShadow DOM的根节点,Shadwon DOM是一颗DOM树,一般附着在正常页面DOM节点中,正常页面DOM附着的节点叫做shadow host,其层次结构是:

shadow DOM中,所有的标签和CSS样式都是在shadown host节点内部生效,也就是说定义在Shadow Root的CSS样式不会影响到它的父文档,定义在Shadow Root之外的CSS样式不会影响到主页面。

举个例子:

从示例图上可以看到video标签内部其实实现了一套自己的shadow dom

打开查看shadow dom的配置如下:

参考:

  1. shadow-dom
  2. compositionend
  3. activeElement