移动端h5相关

Posted by Qz on March 6, 2018

“Yeah It’s on. ”

正文

移动端适配

让网页在各终端上的展示效果就像缩放设计稿图片一样,在不同屏幕上等比缩放,每一个元素与整体比例保持不变,真实还原设计稿。

flexible 实现

由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用

https://github.com/amfe/lib-flexible

https://www.toyou.xyz/posts/flexible.js.html

lib-flexible + postcss-pxtorem

设置根字体

设置根字体 = UI设计稿尺寸/100

可是为什么要除以100呢,为什么不是10,50或者其它的数值呢?

  const setRem = () => {
    const deviceWidth = document.documentElement.clientWidth;
    // 获取相对UI稿,屏幕的缩放比例
    const rem = (deviceWidth *100) / 750;
    // 动态设置html的font-size
    document.querySelector('html').style.fontSize =  rem + 'px';
  };

// 也可以使用库 amfe-flexible 

所以:

   postcss: {
        plugins: [
          postCssPxToRem({
            rootValue: 37.5, // 1rem的大小  => 设计稿/10
            propList: ['*'], // 需要转换的属性,这里选择全部都进行转换
          })
        ]
      }

vw 实现

新一代解决方案 => 使用vw来替代以前Flexible中的rem缩放方案。

首要解决的是适配终端。回想一下,以前的Flexible方案是通过JavaScript来模拟vw的特性,那么到今天为止,vw已经得到了众多浏览器的支持,也就是说,可以直接考虑将vw单位运用于我们的适配布局中。

众所周知,vw是基于Viewport视窗的长度单位,这里的视窗(Viewport)指的就是浏览器可视化的区域,而这个可视区域是window.innerWidth/window.innerHeight的大小。用下图简单的来示意一下:

enter description here

在CSS Values and Units Module Level 3中和Viewport相关的单位有四个,分别为vw、vh、vmin和vmax。

vmin和vmax是根据Viewport中长度偏大的那个维度值计算出来的,如果window.innerHeight > window.innerWidth则vmin取百分之一的window.innerWidth,vmax取百分之一的window.innerHeight计算。

目前出视觉设计稿,我们都是使用750px宽度的,从上面的原理来看,那么100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。看到这里,很多同学开始感到崩溃,又要计算,能不能简便一点,能不能再简单一点,其实是可以的,我们可以使用PostCSS的插件 postcss-px-to-viewport,让我们可以直接在代码中写p

在实际使用的时候,你可以对该插件进行相关的参数配置:

"postcss-px-to-viewport": {
    viewportWidth: 750,
    viewportHeight: 1334,
    unitPrecision: 5,
    viewportUnit: 'vw',
    selectorBlackList: [],
    minPixelValue: 1,
    mediaQuery: false
}

假设你的设计稿不是750px而是375px,那么你就可以修改vewportWidth的值

postcss-px-to-viewport的问题

include配置不生效问题

postcss-px-to-viewport插件已弃用,使用postcss-px-to-viewport-8-plugin代替


如果 要 使用 exclude/include option 和 支持 vite 的话 使用 @minko-fe/postcss-pxtoviewport 代替

设备像素

DPR

网页链接

本文所说devicePixelRatio其实指的是window.devicePixelRatio, 被所有WebKit浏览器以及Opera所支持,随着显示器的发展,这个属性也慢慢登上了前端技术的舞台。

物理分辨率:硬件所支持的 逻辑分辨率:软件可以达到的

window.devicePixelRatio是设备上物理像素和设备独立像素(device-independent pixels (dips))的比例。

公式表示就是:window.devicePixelRatio = 物理像素 / dips

CSS像素也就是逻辑像素

而对于视网膜屏幕的iphone,如iphone4s, 纵向显示的时候,屏幕物理像素640像素。同样,当用户设置<meta name="viewport" content="width=device-width">的时候,其视区宽度并不是640像素,而是320像素,这是为了有更好的阅读体验 – 更合适的文字大小。

这样,在视网膜屏幕的iphone上,屏幕物理像素640像素,独立像素还是320像素,因此,window.devicePixelRatio等于2.

PPI

PPI 是英文 Pixels Per Inch 的缩写,意味每寸能容纳多少颗像素,用于描述屏幕的像素密度。我们上面提到的印刷物以无数多的墨点来构成图像,而屏幕同样也是以一定数量的发光点来构成图像。见过街上那些走红字的 LED 显示屏么?上面的那一颗颗的 LED 灯就是这块屏幕的发光点,我们使用的 MacBook 的 Retina 显示屏的原理也跟这些看起来十分粗糙的走红字显示屏是一样的,只不过 Retina 显示屏的发光点密度非常高,人眼已经看不出来颗粒感而已。对于屏幕来说 PPI 是用于描述每英寸发光点数量的,它表明了一块屏幕发光点密度的高低,这些发光点我们更常称之为像素,一块屏幕宽高有几寸是在生产的时候就被定好的,而宽高各能容纳下多少颗像素,也是在生产的时候就被定好的,所以我们所说的 PPI 可以说是一个物理单位。

识别横屏

js识别

window.addEventListener("resize", ()=>{
    if (window.orientation === 180 || window.orientation === 0) { 
      // 正常方向或屏幕旋转180度
        console.log('竖屏');
    };
    if (window.orientation === 90 || window.orientation === -90 ){ 
       // 屏幕顺时钟旋转90度或屏幕逆时针旋转90度
        console.log('横屏');
    }  
});

css识别

@media screen and (orientation: portrait) {
  /*竖屏...*/
} 
@media screen and (orientation: landscape) {
  /*横屏...*/
}

针对不同dpr屏幕采用不同分辨率图片

如:在dpr=2的屏幕上展示两倍图(@2x),在dpr=3的屏幕上展示三倍图(@3x)

使用media查询判断不同的设备像素比来显示不同精度的图片:

.avatar{
            background-image: url(conardLi_1x.png);
        }
        @media only screen and (-webkit-min-device-pixel-ratio:2){
            .avatar{
                background-image: url(conardLi_2x.png);
            }
        }
        @media only screen and (-webkit-min-device-pixel-ratio:3){
            .avatar{
                background-image: url(conardLi_3x.png);
            }
        }

一般用于大图如背景图,小图标没必要用这种方式,不值得

滚动

获取窗口滚动高度

页面需要获取和修改页面的滚动高度,进行窗口移动的定位操作。这里有个安卓手机和 iOS 的兼容性问题。

对于 document.documentElement.scrollTop ,iOS 可以正常读取和设置值,安卓不可以(读到的值为 0,修改值不会真正改变窗口滚动高度);

对于 document.body.scrollTop ,安卓可以正常读取和设置值,iOS 不可以;

对于 window.scrollY ,貌似两者都可以。

获取窗口滚动高度的兼容写法:

const scrollTop = Math.max(window.scrollY, document.documentElement.scrollTop, document.body.scrollTop)

修改窗口滚动高度的兼容写法:

document.body.scrollTop = 100 // 安卓有效
document.documentElement.scrollTop = 100 // iOS有效

快速轻触滚动

https://developer.chrome.com/blog/scrolling-intervention?hl=zh-cn

滚动响应对于用户与移动设备上的网站互动至关重要,但触摸事件监听器经常会导致严重的滚动性能问题。

使用 passive提升滚动性能

https://developer.chrome.com/blog/passive-event-listeners?hl=zh-cn

passive 改善滚屏性能

passive 设为 true 可以启用性能优化,并可大幅改善应用性能

passive 可选 => 一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()

根据规范,addEventListener()passive 默认值始终为 false。然而,这引入了触摸事件和滚轮事件的事件监听器在浏览器尝试滚动页面时阻塞浏览器主线程的可能性——这可能会大大降低浏览器处理页面滚动时的性能。

触摸事件

网页链接

pc上的web页面鼠 标会产生onmousedown、onmouseup、onmouseout、onmouseover、onmousemove的事件,但是在移动终端如 iphone、ipod Touch、ipad上的web页面触屏时会产生ontouchstart、ontouchmove、ontouchend、ontouchcancel 事件,分别对应了触屏开始、拖拽及完成触屏事件和取消。

  • 当按下手指时,触发ontouchstart;
  • 当移动手指时,触发ontouchmove
  • 当移走手指时,触发ontouchend。
  • 当一些更高级别的事件发生的时候(如电话接入或者弹出信息)会取消当前的touch操作,即触发ontouchcancel。一般会在ontouchcancel时暂停游戏、存档等操作。

上面的这些事件都会冒泡,也都可以取消

DOM 0级事件不生效: document.ontouchstart = function(){alert(“a”)} //不起作用

触摸事件还包含下面三个用于跟踪触摸的属性。

  • touches:当前屏幕上所有触摸点的列表
  • targetTouches:当前对象上所有触摸点的列表
  • changeTouches:涉及当前(引发)事件的触摸点的列表
  1. 用一个手指接触屏幕,触发事件,此时这三个属性有相同的值。
  2. 用第二个手指接触屏幕,此时,touches有两个元素,每个手指触摸点为一个值。当两个手指触摸相同元素时, targetTouches和touches的值相同,否则targetTouches 只有一个值。changedTouches此时只有一个值, 为第二个手指的触摸点,因为第二个手指是引发事件的原因
  3. 用两个手指同时接触屏幕,此时changedTouches有两个值,每一个手指的触摸点都有一个值
  4. 手指滑动时,三个值都会发生变化
  5. 一个手指离开屏幕,touches和targetTouches中对应的元素会同时移除,而changedTouches仍然会存在元素。
  6. 手指都离开屏幕之后,touches和targetTouches中将不会再有值,changedTouches还会有一个值, 此值为最后一个离开屏幕的手指的接触点。(常常可以用于touchend事件的逻辑处理)

每个Touch对象包含的属性如下。

  • clientX:触摸目标在视口中的x坐标。
  • clientY:触摸目标在视口中的y坐标。
  • identifier:标识触摸的唯一ID。
  • pageX:触摸目标在页面中的x坐标。
  • pageY:触摸目标在页面中的y坐标。
  • screenX:触摸目标在屏幕中的x坐标。
  • screenY:触摸目标在屏幕中的y坐标。
  • target:触目的DOM节点目标。

触点坐标选取

touchstart和touchmove使用: e.targetTouches[0].pageX 或 (jquery)e.originalEvent.targetTouches[0].pageX

touchend使用: e.changedTouches[0].pageX 或 (jquery)e.originalEvent.changedTouches[0].pageX

判断手指滑动方向

    var startx, starty;
        //获得角度
        function getAngle(angx, angy) {
            return Math.atan2(angy, angx) * 180 / Math.PI;
        };
     
        //根据起点终点返回方向 1向上 2向下 3向左 4向右 0未滑动
        function getDirection(startx, starty, endx, endy) {
            var angx = endx - startx;
            var angy = endy - starty;
            var result = 0;
     
            //如果滑动距离太短
            if (Math.abs(angx) < 2 && Math.abs(angy) < 2) {
                return result;
            }
     
            var angle = getAngle(angx, angy);
            if (angle >= -135 && angle <= -45) {
                result = 1;
            } else if (angle > 45 && angle < 135) {
                result = 2;
            } else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) {
                result = 3;
            } else if (angle >= -45 && angle <= 45) {
                result = 4;
            }
     
            return result;
        }
        //手指接触屏幕
        document.addEventListener("touchstart", function(e) {
            startx = e.touches[0].pageX;
            starty = e.touches[0].pageY;
        }, false);
        //手指离开屏幕
        document.addEventListener("touchend", function(e) {
            var endx, endy;
            endx = e.changedTouches[0].pageX;
            endy = e.changedTouches[0].pageY;
            var direction = getDirection(startx, starty, endx, endy);
            switch (direction) {
                case 0:
                    alert("未滑动!");
                    break;
                case 1:
                    alert("向上!")
                    break;
                case 2:
                    alert("向下!")
                    break;
                case 3:
                    alert("向左!")
                    break;
                case 4:
                    alert("向右!")
                    break;
                default:
            }
        }, false);

audio相关

https://zhuanlan.zhihu.com/p/74566301

audio不触发canplaythrough事件

https://www.cnblogs.com/pingfan1990/p/4595925.html

var audio = new Audio();
audio.addEventListener("canplaythrough",function(){
    console.log("加载完成!");
},false);
audio.addEventListener("error",function(){
    console.log("加载失败!");
},false);
audio.src = src;

看这段代码好像没错,在pc端检测也没有错,但是当我们放到h5上面就会出错,因为手机上面音乐是流媒体加载的,就是说在加载的过程中是可以播放的一边加载一边播放,canplaythrough 事件在移动端,只有允许audio/video文件加载播放完之后才会执行。

var audio = new Audio();
//canplaythrough这个事件在手机上流媒体要一边播放才能监听得到,pc端chrome可以完美支持
audio.addEventListener("canplaythrough",function(){
    //我们发现播放完之后这里执行了
    console.log("加载完成!");
},false);
audio.addEventListener("error",function(){
    console.log("加载失败!");
},false);
audio.src = src;
audio.play();

元素拖动

网页链接

<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
#div1 {width:198px; height:66px;padding:10px;border:1px solid #aaaaaa;}
</style>
<script type="text/javascript">
function allowDrop(ev)
{
ev.preventDefault();
}

function drag(ev)
{
ev.dataTransfer.setData("Text",ev.target.id);
}

function drop(ev)
{
ev.preventDefault();
var data=ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}
</script>
</head>
<body>

<p>请把 W3School 的图片拖放到矩形中:</p>

<div id="div1" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<br />
<img id="drag1" src="/i/eg_dragdrop_w3school.gif" draggable="true" ondragstart="drag(event)" />

</body>
</html>

设置元素为可拖放

首先,为了使元素可拖动,把 draggable 属性设置为 true : <img draggable="true" />

拖动什么 - ondragstart 和 setData()

然后,规定当元素被拖动时,会发生什么。 在上面的例子中,ondragstart 属性调用了一个函数,drag(event),它规定了被拖动的数据。 dataTransfer.setData() 方法设置被拖数据的数据类型和值:

function drag(ev)
{
ev.dataTransfer.setData("Text",ev.target.id);
}

放到何处 - ondragover

ondragover 事件规定在何处放置被拖动的数据。 默认地,无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。 这要通过调用 ondragover 事件的 event.preventDefault() 方法:

event.preventDefault()

进行放置 - ondrop

当放置被拖数据时,会发生 drop 事件。 在上面的例子中,ondrop 属性调用了一个函数,drop(event):

function drop(ev)
{
ev.preventDefault();
var data=ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}

代码解释

  • 调用 preventDefault() 来避免浏览器对数据的默认处理(drop 事件的默认行为是以链接形式打开)
  • 通过 dataTransfer.getData(“Text”) 方法获得被拖的数据。该方法将返回在 setData() 方法中设置为相同类型的任何数据。
  • 被拖数据是被拖元素的 id (“drag1”)
  • 把被拖元素追加到放置元素(目标元素)中

100vh问题

https://juejin.cn/post/6844904017051549703

https://allthingssmitty.com/2020/05/11/css-fix-for-100vh-in-mobile-webkit/

在移动端使用 100vh 时,发现在 Chrome、Safari 浏览器中,因为浏览器栏和一些导航栏、链接栏导致不一样的。

呈现:

  • 你以为的 100vh === 视口高度
  • 实际上 100vh === 视口高度 + 浏览器工具栏(地址栏等等)的高度

结论:

移动端不要使用100vh

解决方案

https://www.youtube.com/watch?v=pOuE9sgK9jY

body {
  // 100% of the viewport height
  /* mobile viewport bug fix */
  height: 100svh;
}

1px问题

因为Retine屏的分辨率始终是普通屏幕的2倍,1px的边框在devicePixelRatio=2的retina屏下会显示成2px,所以在高清屏下看着1px总是感觉变胖了

font-weight:500

https://jelly.jd.com/article/6006b1045b6c6a01506c87bf

https://juejin.cn/post/7056752646283067400

问题:在微信浏览器测试 font-weight:500 和 font-weight:400 效果是一样的

font-weight可取值:100~900和normal、bold、bolder、lighter。

若所指定的字重不存在直接匹配,则会通过字体匹配算法规则匹配使用邻近的可用字重。

viewport

手机浏览器是把页面放在一个虚拟的“窗口”(viewport)中,通常这个虚拟的“窗口”(viewport)比屏幕宽,这样就不用把每个网页挤到很小的窗口中(这样会破坏没有针对手机浏览器优化的网页的布局),用户可以通过平移和缩放来看网页的不同部分。移动版的 Safari 浏览器最新引进了 viewport 这个 meta tag,让网页开发者来控制 viewport 的大小和缩放,其他手机浏览器也基本支持。

一个常用的针对移动网页优化过的页面的 viewport meta 标签大致如下:

<meta name=”viewport” content=”width=device-width, initial-scale=1, maximum-scale=1″>
  • width:控制 viewport 的大小,可以指定的一个值,如果 600,或者特殊的值,如 device-width 为设备的宽度(单位为缩放为 100% 时的 CSS 的像素)。
  • height:和 width 相对应,指定高度。
  • initial-scale:初始缩放比例,也即是当页面第一次 load 的时候缩放比例。
  • maximum-scale:允许用户缩放到的最大比例。
  • minimum-scale:允许用户缩放到的最小比例。
  • user-scalable:用户是否可以手动缩放

viewport并非只是ios上的独有属性,在android、winphone上同样也有viewport。它们要解决的问题是相同的,即无视设备的真实分辨率,直接通过dpi,在物理尺寸和浏览器之间重设分辨率,这个分辨率和设备的分辨率无关。比如,你拿个3.5寸-320 * 480的iphone3 gs、3.5寸-640 960的iphone4或者9.7寸-1024768的ipad2,虽然设备的分辨率不同,物理尺寸也不同,但你可以通过设置viewport让它们在浏览器里有相同的分辨率。比如说,你的网站是800px宽,你可以通过设置viewport的width=800,来让你的网站在这三个不同的设备上都刚好满屏显示你的网站。

唤起APP

https://mp.weixin.qq.com/s/v4EKb3A3QsZMK_-C5cnAVg

顶部隔开状态栏

其实就是app自带tabbar 和 手机自带状态栏

我们做内嵌app的h5要隔离开这两个距离

实现:

  <div class="schedule-wrapper" :style="{paddingTop: headerHeight + 'px'}">
    <div class="schedule-scroll">
        // 内容
    </div>
  </div>  

headerHeight 这里是计算出来的

一般调用移动提供的方法 拿到tabbar和状态栏这两个的高度,headerHeight 就是这两个高度的和。

.schedule-wrapper {		
  position: relative;
  width:100%
  height: 100vh;
  overflow: hidden;
    .schedule-scroll {
        position: relative;
        width: 100%;
        height: 100%;
        overflow: scroll;
    }
 }   

ios问题

ios 滑动不流畅

上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。

为什么 iOS 的 webview 中 滑动不流畅,它是如何定义的?

原来在 iOS 5.0 以及之后的版本,滑动有定义有两个值 autotouch,默认值为 auto

-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */
-webkit-overflow-scrolling: auto; /* 当手指从触摸屏上移开,滚动会立即停止 */

解决方案

-webkit-overflow-scrolling 值设置为 touch

.wrapper {   
	-webkit-overflow-scrolling: touch;
}

设置滚动条隐藏:

.container ::-webkit-scrollbar {
    display: none;
}

设置 overflow

设置外部 overflowhidden,设置内容元素 overflowauto。内部元素超出 body 即产生滚动,超出的部分 body 隐藏。

body {
    overflow-y: hidden;
}
.wrapper {
    overflow-y: auto;
}

两者结合使用更佳!

ios 上拉边界下拉出现白色空白

手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。

产生原因

在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。

解决方案

监听事件禁止滑动

  1. touchstart :手指放在一个DOM元素上。
  2. touchmove :手指拖曳一个DOM元素。
  3. touchend :手指从一个DOM元素上移开。

touchmove 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节

preventDefault 方法,阻止同一触点上所有默认行为,比如滚动。

由此我们找到解决方案,通过监听 touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。

值得注意的是我们要过滤掉具有滚动容器的元素。

实现如下:

document.body.addEventListener('touchmove', function(e) {  
if(e._isScroller) return;   
    // 阻止默认事件   
    e.preventDefault();
}, { passive: false
});

滚动妥协填充空白,装饰成其他功能

在很多时候,我们可以不去解决这个问题,换一直思路。根据场景,我们可以将下拉作为一个功能性的操作

比如:下拉后刷新页面

页面放大或缩小不确定性行为

HTML 本身会产生放大或缩小的行为,比如在 PC 浏览器上,可以自由控制页面的放大缩小。但是在移动端,我们是不需要这个行为的。所以,我们需要禁止该不确定性行为,来提升用户体验。

<meta name=viewport content="width=device-width, initial-scale=1.0, minimum-scale=1.0 maximum-scale=1.0, user-scalable=no">

click 点击事件延时与穿透

监听元素 click 事件,点击元素触发时间延迟约 300ms

点击蒙层,蒙层消失后,下层元素点击触发。

为什么会产生 click 延时?

iOS 中的 safari,为了实现双击缩放操作,在单击 300ms 之后,如果未进行第二次点击,则执行 click 单击操作。也就是说来判断用户行为是否为双击产生的。但是,在 App 中,无论是否需要双击缩放这种行为,click 单击都会产生 300ms 延迟。

为什么会产生 click 点击穿透?

双层元素叠加时,在上层元素上绑定 touch 事件,下层元素绑定 click 事件。由于 click 发生在 touch 之后,点击上层元素,元素消失,下层元素会触发 click 事件,由此产生了点击穿透的效果。


解决方案一:使用 touchstart 替换 click

前面已经介绍了,移动设备不仅支持点击,还支持几个触摸事件。那么我们现在基本思路就是用 touch 事件代替click 事件。

click 替换成 touchstart 不仅解决了 click 事件都延时问题,还解决了穿透问题。因为穿透问题是在 touchclick 混用时产生。

开源解决方案中,也是既提供了 click 事件,又提供了touchstart 事件。如 vant 中的 button 组件

那么,是否可以将 click 事件全部替换成 touchstart 呢?为什么开源框架还会给出 click 事件呢?

我们想象一种情景,同时需要点击和滑动的场景下。如果将 click 替换成 touchstart 会怎样?

事件触发顺序: touchstart, touchmove, touchend, click

很容易想象,在我需要touchmove滑动时候,优先触发了touchstart的点击事件,是不是已经产生了冲突呢?

所以呢,在具有滚动的情况下,还是建议使用 click 处理。

解决方案二:使用 fastclick 库

使用 npm/yarn 安装后使用

import FastClick from 'fastclick';FastClick.attach(document.body, options);

同样,使用fastclick库后,click 延时和穿透问题都没了

iPhone X系列安全区域适配问题

头部刘海两侧区域或者底部区域,出现刘海遮挡文字,或者呈现黑底或白底空白区域。

产生原因

iPhone X 以及它以上的系列,都采用刘海屏设计全面屏手势。头部、底部、侧边都需要做特殊处理。才能适配 iPhone X 的特殊情况。

解决方案

设置安全区域,填充危险区域,危险区域不做操作和内容展示。

危险区域指头部不规则区域,底部横条区域,左右触发区域。

具体操作为:viewport-fit meta 标签设置为 cover,获取所有区域填充。判断设备是否属于 iPhone X,给头部底部增加适配层

viewport-fit 有 3 个值分别为:

  • auto:此值不影响初始布局视图端口,并且整个web页面都是可查看的。
  • contain:视图端口按比例缩放,以适合显示内嵌的最大矩形。
  • cover:视图端口被缩放以填充设备显示。强烈建议使用 safe area inset 变量,以确保重要内容不会出现在显示之外。

设置 viewport-fit 为 cover

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, viewport-fit=cover">

增加适配层

使用 safe area inset 变量

/* 适配 iPhone X 顶部填充*/@supports (top: env(safe-area-inset-top)){  body,  .header{      padding-top: constant(safe-area-inset-top, 40px);      padding-top: env(safe-area-inset-top, 40px);      padding-top: var(safe-area-inset-top, 40px);  }}/* 判断iPhoneX 将 footer 的 padding-bottom 填充到最底部 */@supports (bottom: env(safe-area-inset-bottom)){    body,    .footer{        padding-bottom: constant(safe-area-inset-bottom, 20px);        padding-bottom: env(safe-area-inset-bottom, 20px);        padding-top: var(safe-area-inset-bottom, 20px);    }

软键盘将页面顶起来、收起未回落问题

Android 手机中,点击 input 框时,键盘弹出,将页面顶起来,导致页面样式错乱。

移开焦点时,键盘收起,键盘区域空白,未回落。

产生原因

我们在app 布局中会有个固定的底部。安卓一些版本中,输入弹窗出来,会将解压 absolutefixed 定位的元素。导致可视区域变小,布局错乱。

原理与解决方案

软键盘将页面顶起来的解决方案,主要是通过监听页面高度变化,强制恢复成弹出前的高度。

// 记录原有的视口高度
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight;

window.onresize = function(){
  var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
  if(resizeHeight < originalHeight ){
    // 恢复内容区域高度
    // const container = document.getElementById("container")
    // 例如 container.style.height = originalHeight;
  }
}

键盘不能回落问题出现在 iOS 12+ 和 wechat 6.7.4+ 中,而在微信 H5 开发中是比较常见的 Bug。

ios弹出键盘再收起点击事件无效

https://juejin.im/post/5c07442f51882528c4469769

我们都知道在H5端是没法监控键盘的弹出与收起的,resize事件触发的机型极其有限,何况我在ios中实测没有触发,安卓反而可以。因为安卓弹起键盘时会修改视窗的大小,但是ios并不会,如果你在ios上设置一个100%高度的body,弹起键盘后你会发现这个body是可以上下滚动的,即100%高度的body超出了视窗。

ios上一直有个很🐂的优化,弹出键盘时会自动把当前输入框滚动到可视区域,在安卓中会出现键盘遮挡输入框的问题,需要手动调整


ios弹出键盘再收起点击事件无效,Why?

推测是body没有正确重新渲染,导致点击事件不处于body内而无法触发。

那么怎么解决呢,是不是只要把body‘推’会来就行了? 方向有了,现在是如何‘推’的问题。

上文有说过,ios下弹出/收起键盘是没有触发resize事件的,那么在什么节点触发‘推’的操作就成了问题。


最终解决方案

    onBlur = (e) => {
        const { onBlur } = this.props;
        document.body && (document.body.scrollTop = document.body.scrollTop);
        onBlur && onBlur(e);
    }

在input输入框失去焦点的钩子中设置滚动到原有位置(document.body.scrollTop = document.body.scrollTop),触发浏览器的重绘,使的错误的渲染回复正常,滚动位置也不会有改变,没有影响体验。

更为通用的方法

/**
 * 处理iOS 微信客户端6.7.4 键盘收起页面未下移bug
 */
;(/iphone|ipod|ipad/i.test(navigator.appVersion)) && document.addEventListener('blur', (e) => {
    // 这里加了个类型判断,因为a等元素也会触发blur事件
    ['input', 'textarea'].includes(e.target.localName) && document.body.scrollIntoView(false)
}, true)

要在包个定时器才能彻底解决,点击页面其他区域关闭输入法的情况还是会错位

setTimeout(function(){         
    document.body.scrollIntoView(false);    
},200); 

ios的fixed定位问题

通常我们理解的fixed元素的相对于视窗来定位的,但是在ios里,fixed元素是相对于最先的,tranfrom属性不为none(或者will-change不为none)的祖宗元素来定位的。

苹果异形屏适配方案

@supports ((height: constant(safe-area-inset-top)) or (height: env(safe-area-inse body {
/* */
padding-top: constant(safe-area-inset-top);
/* */
padding-bottom: constant(safe-area-inset-bottom); }
}

微信h5问题

html被微信强制缓存

html被微信强制缓存在本地

据我们多次试验和观察,微信对整个H5页面缓存了,而不是其中的图片,css等资源,所以对图片,css加上版本控制可能对该问题无效。

解决方案

  • 通过url加时间戳或其他参数避免缓存
  • get请求加上时间戳参数
  • 通过服务端(nginx)解决 (直接将nginx的缓存设置成{expires-1;},设置成永远不缓存)

补充

Flutter 和 RN

https://ask.dcloud.net.cn/article/36083

React Native使用的JavaScript语言开发, Flutte使用的是Dart语言

v2-375798a9424bf72b0e60156702ec5ed5_1440w

  • Flutter完全独立于平台层的渲染管线的优势
  • RN映射实体组件的方式
  • React Native 带有较强的平台关联性,而 Flutter UI 的平台关联性十分薄弱。

React Native 是一套 UI 框架,默认情况下 React Native 会在 Activity 下加载 JS 文件,然后运行在 JavaScriptCore 中解析 Bundle 文件布局,最终堆叠出一系列的原生控件进行渲染。

简单来说就是 通过写 JS 代码配置页面布局,然后 React Native 最终会解析渲染成原生控件,如 <View> 标签对应 ViewGroup/UIView<ScrollView> 标签对应 ScrollView/UIScrollView<Image> 标签对应 ImageView/UIImageView 等。


如果说 React Native 是为开发者做了平台兼容,那 Flutter 则更像是为开发者屏蔽平台的概念。

Flutter 中只需平台提供一个 Surface 和一个 Canvas ,剩下的 Flutter 说:“你可以躺下了,我们来自己动”。

Flutter 中绝大部分的 Widget 都与平台无关, 开发者基于 Framework 开发 App ,而 Framework 运行在 Engine 之上,由 Engine 进行适配和跨平台支持。这个跨平台的支持过程,其实就是将 Flutter UI 中的 Widget “数据化” ,然后通过 Engine 上的 Skia 直接绘制到屏幕上 。

Flutter 则是让你忘掉平台,专注于 Flutter UI 就好了。

Flutter 的整体渲染脱离了原生层面,直接和 GPU 交互

动态性

webview、rn/weex,都有一个特点,可以远程动态载入js代码,可以更新本地的js代码。前端开发者认为动态性是天经地义的,但其实flutter并不支持。

flutter是有编译优化概念的,如果它提供动态性支持,会影响它的性能。

除了flutter,rn/weex/uni-app都可以动态热更新。

Flutter的优势

  • 性能上优于React Native

    • RN 所使用的JSCore,原本用在浏览器中,用于解释执行网页中的JavaScriptd代码,为了兼容Web标准留下的历史包袱, 无法针对移动端进行性能优化
    • Flutter 一开始就抛弃了这种包袱,使用全新的Dart语言进行编写,同时支持AOT和JIT两种编译方式, 而没有采用HTML/CSS/JavaScriptz组合方式开发, 在执行效率上明显高于JSCore
  • 开发效率体验优越

    • 凭借热重载这种急速调试技术,极大的提升了开发效率
    • Flutter因为重新实现了UI框架,可以不依赖IOS和Android平台的原生控件,所以无需专门去处理平台上的差异,在开发体验上实现了真正的统一

    因为引擎定制问题,flutter包的大小是一个问题,从长远来看,App store对包的大小的限制只会越来越小,所以说这个问题一定不会成为卡点

    动态化能力默认不支持在ios上面热更

uni-app

对于国外的开发者,rn、flutter的生态肯定比uni-app好,比如facebook登陆分享、Google地图等。

但对于国内的开发者,那是反过来的,中国开发者需要的全端推送(UniPush集成了iOS、华为、小米、OPPO等众多原厂推送)、各种国内登陆、支付、分享SDK、各种国内地图、各种ui库、以及Echart图表等,都是在uni-app体系里,这方面生态可比rn、flutter丰富多了。uni-app的插件市场有数千款插件,不能说应有尽有,但确实是最丰富的跨端开发框架生态了。

另外,uni-app的生态还比其他竞品强在如下方面:

  • App和H5提供了renderjs技术,使得浏览器专用的库也可以在App和H5里使用,比如echart、threejs等参考
  • 兼容微信小程序 JS SDK,丰富的小程序生态内容可直接引入uni-app,并且在App侧通用,参考
  • 兼容微信小程序自定义组件,并且App、H5侧通用,参考

这些丰富的生态兼容,是rn和flutter无法享受的。

JIT和AOT

JIT (Just-In-Time - 实时编译) 代表 javaScript、python

AOT (Ahead-Of-Time - 预先编译) 代表 C/C++ java (注意java的特殊性)

  • JIT:吞吐量高,有运行时性能加成,可以跑得更快,并可以做到动态生成代码等,但是相对启动速度较慢,并需要一定时间和调用频率才能触发 JIT 的分层机制
  • AOT:内存占用低,启动速度快,可以无需 runtime 运行,直接将 runtime 静态链接至最终的程序中,但是无运行时性能加成,不能根据程序运行情况做进一步的优化

JSBridge

从 微信 JS-SDK 认识 JSBridge

H5和app之间如何通信

深入浅出JSBridge:从原理到使用

Hybrid开发中JSBridge的实现

JSBridge 就像其名称中的『Bridge』的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道。

所谓 双向通信的通道:

  • JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。
  • Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

Native -> Web

Native 调用 JavaScript 的方式本质就是 执行拼接 JavaScript 字符串,这就好比我们通过 eval() 函数来执行 JavaScript 字符串形式的代码一样,不同的系统也有相应的方法执行 JavaScript 脚本。


Android 4.4之后提供了evaluateJavascript来执行JS代码,并且可以获取返回值执行回调:

String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {

  }
});

iOS的UIWebView使用stringByEvaluatingJavaScriptFromString

NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];

Web -> Native

  • 拦截URL Scheme :Native的WebView拦截Web页面发出的特定格式的网络请求
  • 拦截prompt API :Native的WebView拦截Web页面中的window.prompt等api的调用
  • Native API注入:Native在javaScript环境上下文直接注入javascript方法以供调用

注入 API 和 拦截 URL SCHEME

https://juejin.cn/post/6844903585268891662#heading-5

注入 API

注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

// 安卓环境配置
WebSettings webSettings = mWebView.getSettings();
// Android容器允许js脚本,必须要
webSettings.setJavaScriptEnabled(true);
// Android 容器设置侨连对象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");

// Android中JSBridge的业务代码
private Object getJSBridge() {
    Object insterObj = new Object() {
        @JavascriptInterface
        public String foo() {
            // 此处执行 foo  bridge的业务代码
            return "foo" // 返回值
        }
        @JavascriptInterface
        public String foo2(final String param) {
            // 此处执行 foo2 方法  bridge的业务代码
            return "foo2" + param;
        }
    }
    return inserObj;
}

// js调用原生的代码
// JSBridge 通过addJavascriptInterface已被注入到 window 对象上了
window.JSBridge.foo(); // 返回 'foo'
window.JSBridge.foo2(); // 返回 'foo2:test'
  /*
      总结:
      1. ios7 才出现这种方式,在这之前js无法直接调用Native,只能通过JSBridge方式调用
      2. JS 能调用到已经暴露的api,并且能得到相应返回值
      3. ios原生本身是无法被js调用的,但是通过引入官方提供的第三方“JavaScriptCore”,即可开发api给JS调用
  */
  // WKWebview  ios8之后才出现,js调用native方法
  // ios 代码配置 https://zhuanlan.zhihu.com/p/32899522
  // js调用
  window.webkit.messageHandlers.{name}.postMessage(msgObj);

  /*
      * 优缺点
      ios开发自带两种webview控件 UIWebview(ios8 以前的版本,建议弃用)版本较老,
      可使用JavaScriptCore来注入全局自定义对象
      占用内存大,加载速度慢
      WKWebview 版本较新 加载速度快,占用内存小
  */

但是,注意:

前端js就可以通过window上全局对象方法 来调用一些native的方法,前端需要去了解这个全局对象,是在webview初始化时候注入的,还是在页面加载完之后注入的,也就是同步注入还是异步注入的问题,如果是异步注入的,则需要前端的代码中,添加对象的ready监听机制

URL SCHEME

这种方式不推荐,只适用于demo或低频场景

因为多次连续调用时,客户端只会收到最后一次消息

缺陷

  • 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
  • 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。

为什么选择 iframe.src 不选择 locaiton.href ?

因为如果通过 location.href 连续调用 Native,很容易丢失一些调用。

location.href可能会引起页面的跳转丢失调

弹窗拦截

native会劫持webview onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt并进行重写

JSBridge 如何引用

由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。

它的优点在于:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,它的缺点是:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。

由 JavaScript 端引用

直接与 JavaScript 一起执行。

与由 Native 端注入正好相反,它的优点在于:JavaScript 端可以确定 JSBridge 的存在,直接调用即可;缺点是:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge