微信 H5 适配 iPhone X 的正确姿势

3,118 阅读9分钟

FBI Warning:以下方案基于 flexible.js 屏幕适配方案讨论

文章来自 我的小专栏,里面会记录我的学习笔记与总结,欢迎订阅。

问题

先来看看 iPhone X, iPhone XS, iPhone XR, iPhone XS Max 的尺寸:

上图截自:www.paintcodeapp.com/news/ultima…

可以看到,iPhone XiPhone XS 的尺寸是一样的,都是 375 * 812,且都是 3 倍屏,那么对应的物理像素即:1125 * 2436

虽然 iPhone XS MaxiPhone XR 的尺寸是一样的,都是 414 * 896 ,但 iPhone XS Max 是 3 倍屏,而 iPhone XR 只是 2 倍屏。所以呢,iPhone XS Max 对应的物理像素是 1242 * 2688, iPhone XR 对应的物理像素是 828 * 1792

假设 H5 页面可以全屏打开,那么 window.innerWidthwindow.innerHeight 对应的就是物理像素。比如 iPhone XS Maxwindow.innerWidthwindow.innerHeight 分别是 1242px2688px(需要注意的是,viewport 是经过 flexible.js 处理的,把 viewportwidth 设置为 device-width 以及与 scale 相关的值也需要处理为 window.devicePixelRatio 的倒数,才能使用 window.innerWidthwindow.innerHeight与物理像素对应)。 如下图:

否则:

下面的所有讨论都是基于 flexible.js 的适配方案的。

言归正传,为什么要说 window.innerHeight 呢,因为等一下介绍用 low low 的方案来适配微信 H5 的时候会用到。为什么要单独说 微信 H5 呢,因为这货会有底部导航栏:

可以看到我手画的那两个 < >,就是微信的导航栏。此时,留给 H5 的展示的大小只剩下如图所示的红框部分。

问题就在于,这个底部的导航栏它不是一定会出现的,总结一下:

  1. 一级页面不会出现。意思就是使用微信内部浏览器打开的第一个页面,导航栏不会显示。就好像用 Chrome 打开一个新的页面,导航栏的 箭头是灰的:

    只不过微信选择不显示导航栏而已。
  2. 当页面跳转到下一级路由的时候,导航栏就出现了。
  3. 但是出现了,也有可能会消失哦。向下滚动页面的时候,会隐藏。再向上滚动页面的时候,导航栏又回来了。 我们来看两张效果图,对比一下:
  • 有导航栏:
  • 向下滚动碳,就没有导航栏了:

可以看到,就算是 iPhone X,如果微信底部的导航栏出来后,其实是不用适配的。但是导航栏没有出来的时候,又是要做适配。 所以,这就是微信导航栏坑的地方。以下基于 device-widthdevice-heightSCSS 适配不再起作用:

@mixin iPhoneXScreenFit() {
  @media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation : portrait) {
    @content;
  }

  @media only screen and (device-width: 812px) and (device-height: 375px) and (-webkit-device-pixel-ratio: 3) and (orientation : landscape) {
    @content;
  }
}

比较 low 的解决方案

我们以 iPhone X 为例,来说说如何去解决。

首先,虽然判断机型是否为 iPhone X,如果是的话,还要判断导航栏是否显示,伪代码如下:

if(isIPhoneX == true && wxNavShow == true) {
    // 做适配
}

好,判断机型是否为 iPhone X 这个很简单,但是怎么判断导航栏是否显示呢?

根据上面两张 有导航栏没有导航栏 的两张图,观察发现导航栏出现和消失都会改变 window.innerHeight。那我们就可以通过监听 windowresize 事件来知道 window.innerHeight ,从而知道导航栏是否显示。

那到底 window.innerHeight 是多少的时候,才认为导航栏是显示的呢?不过我们换个思路,如果知道 window.innerHeight 等于某个值,则认为导航栏没有显示。

通过上图,我们可以计算出 window.innerHeight = (812 - 44 - 44) * 3 = 2172px,不过别高兴太早,我们还有横屏的情况:
通过上图,我们可以计算出 window.innerHeight = (375 - 32) * 3 = 1029px,好了,完美。

只要认为 window.innerHeight2172px 或者 1029px 的时候,则认为底部导航栏没有显示。

实现

1.首先判断是否为 iPhone X:

function isIPhoneXPortrait () {
  return window.screen.height === 812 && window.screen.width === 375;
}
function isIPhoneXLandscape () {
  return window.screen.height === 375 && window.screen.width === 812;
}

function isIPhoneX() {
  let flag = false;
  // getOSName 基于 ua-parser-js 封装
  if(getOsName() === 'ios') { 
    flag = isIPhoneXLandscape() || isIPhoneXPortrait();
  }
  return flag;
}

2.然后,当我们的网页被打开的时候,还要知道 3 个事情:

  1. 是横屏还是竖屏;isPortrait
  2. 导航栏是否有显示;wxNavShow
  3. 导航栏是否从来没有显示过,这个有什么用,后面后讲。neverShowWxNav

判断是否为横屏:

this.isPortrait = window.innerWidth < window.innerHeight;

判断导航栏是否有显示:

function isWxBottomNavNotShow() {
  const windowInnerHeight = window.innerHeight;
  return windowInnerHeight === 2172 || windowInnerHeight === 1029;
}

this.wxNavShow = isWxBottomNavNotShow() ? false : true;

判断导航栏是否从来没有显示过:

this.neverShowWxNav = window.history.length === 1;

3.监听 windowresize 事件:

const handleWindowResize = () => {
   const newIsPortraint = window.innerWidth < window.innerHeight;
    // 保持横屏 或者 保持竖屏,则认为是 
    // 1. 上下滚动页面(如果导航栏已经显示过),
    // 2. 跳转到下一个页面;
    // 
    // 由 1 或者 2 导致的微信底部导航栏 显示或者隐藏, 
    // 进而 导致 了 window resize
    if(this.isPortrait === newIsPortraint) { 
      this.wxNavShow = !this.wxNavShow;
      this.neverShowWxNav = false; // 微信底部导航栏从来没有出现过,出现过一次,改成 false
    } else { // 旋转屏幕 导致了 window resize
      
      if(!this.neverShowWxNav) { // 如果微信底部导航栏 出现过一次,在旋转屏幕时,会再次出现。
        this.wxNavShow = true;
      }
      this.isPortrait = newIsPortraint;
    }
  };

通过上面的步骤,就可以实现以下伪代码,就可以做适配了:

let className = 'container';
if(isIPhoneX == true && wxNavShow == true) {
    // 做适配
    className += ' iPhoneX';
}
// 再根据 className 来写不同的样式去做适配。

下面是完整的代码,包括了 iPhoneXRiPhoneXS Max 的适配:

import { getOsName } from '../../ua/index';
import { debounce, partial } from 'lodash';
/**
 * 判断是否为 iPhoneX
 * 
 * iPhoneX iPhoneXS 812 * 375  3 倍
 * iPHoneX iPhoneXS 竖屏浏览器开始位置到屏幕顶部的高度:(44 + 44) * 3 = 264
 * iPhoneX iPhoneXS 竖屏不显示微信底部导航栏 window.innerHeight: (812 * 3) - 264 = 2172,
 * iPHoneX iPhoneXS 横屏浏览器开始位置到屏幕顶部的高度: 32 * 3 = 96
 * iPhoneX iPhoneXS 横屏不显示微信底部导航栏 window.innerHeight: (375 * 3) - 96 = 1029,

 * iPhoneXR 896 * 414  2 倍
 * iPhoneXR 竖屏浏览器开始位置到屏幕顶部的高度: (44 + 44) * 2 = 176
 * iPhoneXR 竖屏不显示微信底部导航栏 window.innerHeight:  (896 * 2) - 176 = 1616,
 * iPhoneXR 横屏浏览器开始位置到屏幕顶部的高度: 96
 * iPhoneXR 横屏不显示微信底部导航栏 window.innerHeight: (414 * 2) - 96 = 732,

 * iPhoneXS Max 896 * 414  3 倍
 * iPhoneXS Max 竖屏浏览器开始位置到屏幕顶部的高度: (44 + 44) * 3 = 264
 * iPhoneXS Max 竖屏不显示微信底部导航栏 window.innerHeight:  (896 * 3) - 264 = 2424,
 * iPhoneXS Max 横屏浏览器开始位置到屏幕顶部的高度: 96
 * iPhoneXS Max 横屏不显示微信底部导航栏 window.innerHeight:  (414 * 3) - 96 = 1146,
 * @returns {boolean}
 */
export function isIPhoneX() {
  let flag = false;
  if(getOsName() === 'ios') { 
    flag = isIPhoneXRLandscape() || isIPhoneXRPortrait() || isIPhoneXLandscape() || isIPhoneXPortrait();
  }
  return flag;
}

export function isWxBottomNavNotShow() {
  const windowInnerHeight = window.innerHeight;
  return windowInnerHeight === 2172 || windowInnerHeight === 1029 // iPhoneX iPhoneXS
      || windowInnerHeight === 1616 || windowInnerHeight === 732 // iPhoneXR
      || windowInnerHeight === 2424 || windowInnerHeight === 1146; // iPhoneXS Max
}

// https://juejin.cn/post/6844903679263244302
function isIPhoneXPortrait () {
  return window.screen.height === 812 && window.screen.width === 375;
}
function isIPhoneXLandscape () {
  return window.screen.height === 375 && window.screen.width === 812;
}
function isIPhoneXRPortrait () {
  return window.screen.height === 896 && window.screen.width === 414;
}
function isIPhoneXRLandscape () {
  return window.screen.height === 414 && window.screen.width === 896;
}

class IPhoneXAdapterInWechat {
  private wxNavShow: boolean;  // 微信底部导航栏是否显示
  private isPortrait: boolean; // 是否为竖屏
  private neverShowWxNav: boolean; // 是否重来没有显示过导航栏

  constructor(
    storeWhetherIsIPhoneX: (isIPhoneX: boolean) => {},
    storeWxBottomNavVisible: (isVisible: boolean) => {}
  ) {

    const iPhoneXFlag = isIPhoneX();

    if(!iPhoneXFlag) {
      return ;
    }

    this.handleWindowResize = debounce(this.handleWindowResize, 50);

    this.wxNavShow = isWxBottomNavNotShow() ? false : true;
    this.isPortrait = window.innerWidth < window.innerHeight;
    this.neverShowWxNav = window.history.length === 1; // 微信底部导航栏从来没有出现过

    this.setWhetherIsIPhoneX(iPhoneXFlag, storeWhetherIsIPhoneX, storeWxBottomNavVisible);
  }

  setWhetherIsIPhoneX = (
    isIPhoneX: boolean,
    storeWhetherIsIPhoneX: (isIPhoneX: boolean) => {},
    storeWxBottomNavVisible: (isVisible: boolean) => {}
  ) => {
    if(isIPhoneX) {
      storeWhetherIsIPhoneX(true);
      if(this.wxNavShow) {
        storeWxBottomNavVisible(this.wxNavShow);
      }
      window.addEventListener('resize', partial(this.handleWindowResize, storeWxBottomNavVisible));
    }
  }

  handleWindowResize = (
    storeWxBottomNavVisible: (isVisible: boolean) => {} 
  ) => {
    const newIsPortraint = window.innerWidth < window.innerHeight;
    // 保持横屏 或者 保持竖屏,则认为是 
    // 1. 上下滚动页面(如果导航栏已经显示过),
    // 2. 跳转到下一个页面;
    // 
    // 由 1 或者 2 导致的微信底部导航栏 显示或者隐藏, 
    // 进而 导致 了 window resize
    if(this.isPortrait === newIsPortraint) { 
      this.wxNavShow = !this.wxNavShow;
      this.neverShowWxNav = false; // 微信底部导航栏从来没有出现过,出现过一次,改成 false
    } else { // 旋转屏幕 导致了 window resize
      
      if(!this.neverShowWxNav) { // 如果微信底部导航栏 出现过一次,在旋转屏幕时,会再次出现。
        this.wxNavShow = true;
      }
      this.isPortrait = newIsPortraint;
    }
    storeWxBottomNavVisible(this.wxNavShow);
  }
}

export default IPhoneXAdapterInWechat;

稍微好点的解决方案,还是用 SCSS

我们不是知道了导航栏不出现时的 window.innerHeight 吗?那我们还是直接用 @media 就可以啦,干嘛还做那么多 JS 操作。只要把 device-width 改成 width ,把 device-height 改成 height 即可,看代码:

@mixin iPhoneXAndIPhoneXSScreenFit() {
  @media only screen and (width: 1125px) and (height: 2172px) and (-webkit-device-pixel-ratio: 3) and (orientation : portrait) {
    @content;
  }

  @media only screen and (width: 2436px) and (height: 1029px) and (-webkit-device-pixel-ratio: 3) and (orientation : landscape) {
    @content;
  }
}

@mixin iPhoneXRScreenFit() {
  @media only screen and (width: 828px) and (height: 1616px) and (-webkit-device-pixel-ratio: 2) and (orientation : portrait) {
    @content;
  }

  @media only screen and (width: 1792px) and (height: 732px) and (-webkit-device-pixel-ratio: 2) and (orientation : landscape) {
    @content;
  }
}


@mixin iPhoneXSMaxScreenFit() {

  @media only screen and (width: 1242px) and (height: 2424px) and (-webkit-device-pixel-ratio: 3) and (orientation : portrait) {
    @content;
  }

  @media only screen and (width: 2688px) and (height: 1146px) and (-webkit-device-pixel-ratio: 3) and (orientation : landscape) {
    @content;
  }
}

@mixin iPhoneDevicesScreenFit() {
  @include iPhoneXAndIPhoneXSScreenFit() {
    @content;
  }

  @include iPhoneXRScreenFit() {
    @content;
  }

  @include iPhoneXSMaxScreenFit() {
    @content;
  }
}

使用的时候:

@mixin afterStyle {
  content: ' ';
  width: 100vw;
  height: 68px;
  position: absolute;
  bottom: 0;
  left: 0;
  transform: translate(0, 100%);
  background-color: #fff;
}

.fix-bottom-wrapper {
  width: 100vw;
  position: fixed;
  z-index: 999;
  bottom: 0;

  @include iPhoneDevicesScreenFit() {
    bottom: 68px;

    &::after {
      @include afterStyle();
    }
  }

}

完事!

相信大家都会使用 SCSS 的方案了,但这个是有风险的,哪天微信把底部导航栏的高度改一改,那 window.innerHeight 就不符合预期了。

JS 的方案的话,可以用来判断微信底部导航栏是否显示吧。

好了,写得有点多,恭喜你看完了!有问题的话欢迎留言。