兼容问题笔记

本文是记录一些自己在工作中实际遇到和解决的一些兼容性问题。

【safari】window.open无效

window.open被广告商滥用,严重影响用户的使用,Safari安全机制将其默认拦截。

  解决方案

  • window.location.assign() 新开页面(add一个 history)
  • window.location.replace(或改变 href) 替换当前页

【Android】物理返回键 H5监听拦截

拦截场景

网页本身有返回按钮,有特殊返回逻辑。
用户为了方便,会使用安卓手机自带的物理返回键,页面就会按照你浏览器history栈存储的路径来一层层返回。
网页本身设计期望的返回逻辑没有执行,被破坏。

pushState方法

window.history.back():移动到上一个访问页面,等同于浏览器的后退键。
window.history.forward():移动到下一个访问页面,等同于浏览器的前进键。
window.history.go(num):接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back()。
window.history.pushState():HTML5新增,在页面中创建一个 history 实体。直接添加到历史记录中。
window.history.replaceState():HTML5新增,用来在浏览历史中修改记录。

window.history.pushState(state, title, utl)
state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
注:pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应。

popstate事件

  1. 当活动历史记录条目更改时,将触发popstate事件。
  2. 调用history.pushState()history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在JS代码中调用history.back()或者history.forward()方法)。
  3. 不同的浏览器在加载页面时处理popstate事件的形式存在差异。页面加载时ChromeSafari通常会触发(emit)popstate事件,但Firefox则不会。

拦截原理:

利用pushState方法和他不会触发popstate事件的特点(安卓物理返回键会)

  1. 页面A,先调用pushState(),创建一个历史(B1),页面不会刷新。
  2. 监听popstate事件。
  3. 物理返回,回到A(拦截目的达到)。再次返回就会到A的上一个页面,所以需要4如下。
  4. 此时触发popstate事件,再处理方法中再次调用pushState(),创建一个历史(B2)
  5. 下次物理返回,总是在A页面,如此循环
  window.history.pushState(null, null, "#");
  window.addEventListener("popstate", function(e) {
    window.history.pushState(null, null, "#");
    // 拦截了物理返回 ,同时也拦截了浏览器返回,history(back forward go)返回
  })

缺陷

  1. 如果项目本身使用了pushState,则历史记录会有瑕疵(多了一个历史)
  2. 浏览器的后退按钮点击以及调用history.back()也会被当成按下了返回键

【iOS】页面返回不刷新

场景-转盘抽奖

  1. 页面A点击抽奖,转盘转动,同时调用抽奖接口。
  2. 接口返回401,跳页面B登录,登录成功,或点击返回按钮,返回页面A
  3. 页面A转盘依然在转动,期望是已经停止转动。

解决方案一

页面在非激活状态(hidden)的时候,触发visibilitychange事件,注入停转逻辑。

  const hiddenProperty = 'hidden' in document ? 'hidden' :    
      'webkitHidden' in document ? 'webkitHidden' :    
      'mozHidden' in document ? 'mozHidden' :    
      null;
  const visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange');
  const onVisibilityChange = function(){
    if (document[hiddenProperty]) {    
      console.log('页面非激活');
      // 转盘停止
      if (that.turning) {
        that.stopTurning()
      }
    }
  }
  document.addEventListener(visibilityChangeEvent, onVisibilityChange);

解决方案二(来自网友,未亲自验证)

window.onpageshow事件 :在每次加载页面时都会触发,类似于 onload 事件,但是onload 事件在页面第一次加载时触发,在页面从浏览器缓存中读取时不触发。

...
isIOS &&
window.onpageshow = function(event) {
  if (event.persisted) {
      window.location.reload()
  }
};

【Wkwebview】虚拟键盘将页面顶出视窗,收起后页面未下移

  • 页面无滚动条的情况存在该问题,有滚动条正常
  • input/textarea触发软键盘,输入完成失焦后出现

解决方案:基类或者AppContainer中监听全局blur事件

isIOS && 
document.addEventListener('blur', event => {
  // 当页面没出现滚动条时才执行,因为有滚动条时,不会出现这问题
  // input textarea 标签才执行,因为 a 等标签也会触发 blur 事件
  if (
    document.documentElement.offsetHeight <= document.documentElement.clientHeight 
    && ['input', 'textarea'].includes(event.target.localName)
  ) {
    document.body.scrollIntoView 
    ? document.body.scrollIntoView() 
    : window.scrollTo(0,0) // 回顶部
  }
}, true ) // blur事件不冒泡,切记在捕获阶段执行

【Wkwebview】软键盘遮挡input

出现场景

弹出Dialog(fixed在页面底部),input密码输入框自动聚焦,调出软键盘,Dialog未上移被软件盘遮挡。
历史代码,UIwebview正常。APP升级WKwebview出现的兼容性问题。

Dialog 核心精简代码:

  // 封装的滑入动画ui组件,css3(animation transform)
  import Transform from '../Transform/Transform'
  import Mask from '../Mask/Mask' // 遮罩蒙层ui组件
  ...
  return (
    <div
      className={classNames('ActionDialog', className)}
    >
      <Mask className={this.state.maskStatus} />
      <Transform {...this.props}>
        <div className='body'>
          <div className='title line-bottom'>
            <i className='iconfont iconClose stat_closeTransform' onClick={this.onClose} />
            {title}
          </div>
          <div className='content'>
            <NumberInput
              labelName="please input password "
              showBotton={true}
              className="password-input"
              inputChangeCallback={(text, flag) => {
                ...
              }}
            />
          </div>
        </div>
      </Transform>
    </div>
  )

NumberInput 核心精简代码:

  ...
  componentDidMount() {
    this.tradingPwdHideInput.focus()
  }
  onFocus() {
    this.setState({
      focus: true
    })  
  }
  onBlur() {
    this.setState({
      focus: false
    })
  }
  ...
  render() {
    const { focus } = this.state
    const { labelName, errMsg } = this.props
    let arryDigits = [...'123456']

    return (
      <div className='NumberInput'>
        {labelName}
        <input
          type='tel'
          id='tradingPwdHideInput'
          ref={ref => {
            this.tradingPwdHideInput = ref
          }}
          onClick={() => {
            this.tradingPwdHideInput.focus()
          }}
          onChange={this.tradingPwdChange.bind(this)}
          onBlur={() => {
            this.onBlur()
          }}
          onFocus={() => {
            this.onFocus()
          }}
        />
        <ul className={classNames('numberbox', focus ? 'focus' : null)}>
          {arryDigits.map((value, index) => {
            return (
              <li className={classNames({ border: focus })} key={index}>
                <i className='passWord'>{this.tradingpwd.charAt(index)}</i> 
              </li>
            )
          })}
        </ul>
        <div className="input-bottom">
          <div className="err-msg">{errMsg}</div>
        </div>
      </div>
    )
  }

主要css:

/*--------ActionDialog--------------*/
.ActionDialog {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100vw; 
  height: 100vh; /*注意这里*/
  z-index: 11;

  .animationEnd{
    position: absolute;
    width: 100%;
    bottom: 0;
    height: 60%;
    z-index: 12;
  }
  .content {
    width: 100%;
    height: 100%;
  }
  .body {
    position: absolute;
    z-index: 99;
    width: 100%;
    height: 100%;
  }
}
/*--------Transform--------------*/
@keyframes down-in {
  from {transform: translateY(-100%);}
  to {transform: translateY(0%);}
}
@keyframes down-out {
  from {transform: translateY(0%);}
  to {transform: translateY(100%);}
}
.down-in {
   animation:down-in .4s
}
.down-out {
   animation:down-out .4s;
}
/*---------------Mask--------------------*/
.Mask {
  position: fixed;
  z-index: 11;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}
/*---------------NumberInput--------------------*/
.NumberInput {
  overflow: hidden;
  position: relative;
  padding-top: 40px;
  padding-bottom: 1px; /*no*/
  width: 100%;
  background: #fff;

  input {
    position: absolute;
    left: 0; /*no*/
    z-index: 1;
    width: 80%;
    display: block;
    overflow: hidden;
    padding: 0 !important;
    background-clip: padding-box;
    font-family: Courier, monospace;
    opacity: 0.01;
    border: 0 none !important;
    box-sizing: content-box !important;
    outline: none;
    -webkit-appearance: none;
    /*解决ios光标问题 */
    color: transparent;
    text-indent: -100px;
    -webkit-transform: scale(2);
  }
  .numberbox {
    display: -webkit-box !important;
    display: -ms-flexbox !important;
    display: flex !important;
    padding: 0 !important;
    box-sizing: border-box;
    display: block;
    width: 100%; /*no*/
    height: 50px;/*no*/
    font-size: 24px; /*no*/
    border: 1px solid $color-input-border; /*no*/
    background-color: $color-bg;
    background-clip: padding-box;
    overflow: hidden;
    li {
      -webkit-box-flex: 1;
      -ms-flex: 1;
      box-flex: 1;
      position: relative;
      width: 17%;
      line-height: 50px;/*no*/
      margin-right: -1px; /*no*/
      border-right: 1px solid $color-input-border; /*no*/
      overflow: hidden;
      text-align: center;
    }
    .tradingInputshow {
      visibility: visible;
    }
    .tradingInputhide {
      visibility: hidden;
    }
    .numberboxItem {
      display: inline-block;
      width: 14px; /*no*/
      line-height: 50px;/*no*/
      font-style: normal;
      text-align: center;
      overflow: hidden;
    }
    .passWord {
      line-height: 44px; /*no*/
      font-style: normal;
    }
    .numberboxItem:empty {
      width: 12px; /*no*/
      height: 12px; /*no*/
      border-radius: 12px; /*no*/
      background-clip: padding-box;
      background-color: $color-input-text;
    }
  }
  .focus {
    border: 1px solid $color-input-border-focus; /*no*/
  }
  .border {
    border-right: 1px solid $color-input-border-focus !important;/*no*/
  }
}

开始怀疑是Dialog弹出后自动聚焦,虚拟键盘弹出,由于Dialog有一个CSS3滑入动画(0.4s)导致。但发现手动点击input触发也有同样的问题,此时Dialog已经完全展示。
后续解决办法如下:

解决办法一

  • Iphone7 iOS 13.1.2 表现为Dialog不上移被遮挡
  • Iphone8 iOS 11.1.0 iphone xs 12.4.1 表现为Dialog上移250+,超出可视区域。
    onFocus() {
      if (Platform.getOS().name === 'iOS') {
        // 200其实是虚拟键盘的高度,网上建议用ScrollHeight,但是Dialog上移会太厉害
        document.scrollingElement.scrollTop = 200;
      }
      ...
    }
    onBlur() {
      if (Platform.getOS().name === 'iOS') {
        // 失焦以后必须设为0,否则input会停留在上移以后的位置下不来
        // 副作用:Dialog关闭以后,页面也回到顶部
        // 如果要保留原来滚动位置,需要在Dialog前后增加scrollTop存储赋值逻辑
        document.scrollingElement.scrollTop = 0;
      }
      ...
    }
    why scrollingElement

解决办法二(iphoneX ios11.1.1)

  1. iphoneX ios11.1.1机器,上述解决办法一无效,发现根本无法设置document.documentElement.scrollTop
  2. 发现原来的Dialog顶层DIV设置了 height:100vh 样式, 删除后正常。

[IOS] 页面重定向 哈希丢失

  • 场景:iOS手机( 包括微信,safari浏览器.chrome浏览器) 都出现。安卓手机都正常。
  • 表象:访问公司短链,重定向到对应页面的长链的时候,丢失哈希。应该到详情页,结果到列表页(默认路由)。
  • 原因:生成短链的长链URl使用了http,短链是https。页面重定向的时候会出现二次重定向(短链 > http 长链 > https 长链)。第二次重定向,哈希会丢失。
  • 解决办法:长链URl使用https重新生成短链。网上看到别人解决该办法是通过哈希前面加/。自己测试,不成功。
  • 根本原因:iosHSTS安全机制?
http://{domain}/path/knowledge/#/detail?id=101 🔴 
http://{domain}/path/knowledge?locale=en_us/#/detail?id=101 🔴 
https://{domain}/path/knowledge?locale=en_us#/detail?id=101 ✅ 

参考资料,上述场景验证无效

[Wkwebview] a链接无法跳转

  • 原因:target='_blank'
  • 解决:删除 target
  • window.open(url) 同样无法跳转,改用 window.location.assign(url)

参考资料