FBI Warning:以下方案基于
flexible.js屏幕适配方案讨论
文章来自 我的小专栏,里面会记录我的学习笔记与总结,欢迎订阅。
问题
先来看看 iPhone X, iPhone XS, iPhone XR, iPhone XS Max 的尺寸:

可以看到,iPhone X 和 iPhone XS 的尺寸是一样的,都是 375 * 812,且都是 3 倍屏,那么对应的物理像素即:1125 * 2436。
虽然 iPhone XS Max 和 iPhone XR 的尺寸是一样的,都是 414 * 896 ,但 iPhone XS Max 是 3 倍屏,而 iPhone XR 只是 2 倍屏。所以呢,iPhone XS Max 对应的物理像素是 1242 * 2688, iPhone XR 对应的物理像素是 828 * 1792。
假设 H5 页面可以全屏打开,那么 window.innerWidth 和 window.innerHeight 对应的就是物理像素。比如 iPhone XS Max 的 window.innerWidth 和 window.innerHeight 分别是 1242px 和 2688px。(需要注意的是,viewport 是经过 flexible.js 处理的,把 viewport 的 width 设置为 device-width 以及与 scale 相关的值也需要处理为 window.devicePixelRatio 的倒数,才能使用 window.innerWidth 和 window.innerHeight与物理像素对应)。 如下图:

否则:

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

< >,就是微信的导航栏。此时,留给 H5 的展示的大小只剩下如图所示的红框部分。
问题就在于,这个底部的导航栏它不是一定会出现的,总结一下:
- 一级页面不会出现。意思就是使用微信内部浏览器打开的第一个页面,导航栏不会显示。就好像用
Chrome打开一个新的页面,导航栏的左右箭头是灰的:
只不过微信选择不显示导航栏而已。 - 当页面跳转到下一级路由的时候,导航栏就出现了。

- 但是出现了,也有可能会消失哦。向下滚动页面的时候,会隐藏。再向上滚动页面的时候,导航栏又回来了。 我们来看两张效果图,对比一下:
- 有导航栏:

- 向下滚动碳,就没有导航栏了:

可以看到,就算是 iPhone X,如果微信底部的导航栏出来后,其实是不用适配的。但是导航栏没有出来的时候,又是要做适配。
所以,这就是微信导航栏坑的地方。以下基于 device-width 和 device-height 的 SCSS 适配不再起作用:
@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。那我们就可以通过监听 window 的 resize 事件来知道 window.innerHeight ,从而知道导航栏是否显示。
那到底 window.innerHeight 是多少的时候,才认为导航栏是显示的呢?不过我们换个思路,如果知道 window.innerHeight 等于某个值,则认为导航栏没有显示。

window.innerHeight = (812 - 44 - 44) * 3 = 2172px,不过别高兴太早,我们还有横屏的情况:

window.innerHeight = (375 - 32) * 3 = 1029px,好了,完美。
只要认为 window.innerHeight 为 2172px 或者 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 个事情:
- 是横屏还是竖屏;
isPortrait - 导航栏是否有显示;
wxNavShow - 导航栏是否从来没有显示过,这个有什么用,后面后讲。
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.监听 window 的 resize 事件:
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 来写不同的样式去做适配。
下面是完整的代码,包括了 iPhoneXR 和 iPhoneXS 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 的方案的话,可以用来判断微信底部导航栏是否显示吧。
好了,写得有点多,恭喜你看完了!有问题的话欢迎留言。