移动端H5页面开发踩坑总结

1,007 阅读10分钟

移动端H5页面和PC端页面开发虽然用到的技术栈一样(angular、vue、react以及跨端框架taro、uniapp等都适用),移动端H5页面要注意的问题比PC端要多得多,尤其是兼容性问题,安卓和ios两大机型,还有安卓和iOS里的不同版本的机型,由于用户使用的版本不同,为了兼顾绝大部份用户,往往需要适配较低版本的机型,比如安卓5,安卓6等,还有目前出现的折叠屏都需要适配,下面就我在开发项目过程中出现的问题进行总结,代码是基于 taro 框架下的代码。

兼容性问题

兼容性问题是移动端H5页面开发遇到的最典型和突出的问题,下面分ios特有、安卓特有、两者均有三个方面来总结。

ios特有

滑动吸顶问题

一个很常见的场景:某个元素在滑动时当滑动消失后将此元素吸顶。这个问题用 position: stickly 很容易解决,但是这个属性的兼容性并不好,尤其是在低端机型上将失效,通用的解决思路就是计算滑动的距离,当元素消失后设置元素的 position: fixed,这样做可以解决问题,且兼容性好,但在 ios 机型上需要注意几个问题。

  1. fixed 定位的元素尽量放在 DOM 结构的最外层,否则滚动起来时 fixed 定位的元素会很不稳定。例如:
<View>
    <View style={{position: fixed}}></View>
    <View>...</View>
<View>
  1. 由于 fixed 定位的元素脱离文档流,为了不影响原来的布局,用 { display: isNeedTop ? block : none } 来控制 fixed 定位元素的显示与隐藏,同时需要滚动吸顶的元素在固定位置的显示用 { visibility: isNeedTop ? hidden : visible } 来显示隐藏,隐藏时可以保留占据原来的位置。
  2. 在计算目标元素是否滚动到被隐藏时可以用 IntersectionObserver,比计算 scrollTop 值来判断显示还是隐藏好用得多。需要注意的是当页面跳转到另一个页面时 IntersectionObserver会执行,并认为交叉比例为 0 ,当返回原页面时会有闪烁。可以在 componentDidHide() 里结束监听, 在 componentDidShow 再打开监听解决问题。如果框架没有相应的生命周期钩子可以用 visibilitychange 来监听页面的显示与隐藏。

IntersectionObserver除了用在监听元素的显示与隐藏外还可以:

  1. 图片懒加载,当图片占位符出现在可视区域时才加载图片
  2. 埋点曝光,当目标元素完全出现在可视区域时触发上报
  3. 表格无限加载,当滚动到列表最下方时请求数据,当列表最下方出现在视口时请求数据

IOS滚动不流畅问题

在滚动的元素上添加CSS

    overflow-y: auto; 
    -webkit-overflow-scrolling : touch;

在iOS 13之后,不需要再设置-webkit-overflow-scrolling:touch了,因为所有可滚动的框架,或者设置 overflow 滚动的元素默认都是弹性效果了。

多个webview顶部导航栏共用问题

ios 中多个 webview 页面的原生头部导航栏是共用的,只要任意一个 webview 页面修改了头部导航栏,其他 webview 页面的头部导航栏也会跟着改变,这一点和安卓有区别,安卓的每个 webview 页面的头部导航栏是独立的,所以在 iOS webview 页面中,如果要保持每个 webview 页面的导航栏独立,要在进入某个 webview 页面时在页面将要出现的钩子函数中重新设置下导航栏。

安卓特有

flex容器不能被正常撑开

有些低版本安卓手机上 flex 容器内的 flex-item 元素的宽度或高度(flex布局方向水平下就是宽度,垂直下就是高度)不能被正常的撑开, 即使 flex-item 元素的子元素设置了 height 或者 width 也不行。猜测是依然会按照弹性计算各个 flex-item 所占的大小来布局,导致元素宽度或高度塌陷。 可以在不需要参与弹性的 flex-item 元素上设置 flex:0 0 auto(简写为 flex: none) 可以解决这个问题。

文本无法居中

在某些低版本安卓机上文本无法居中,通常偏上,不要用 line-height 让文本居中,用 flex 布局,通常用两层 flex 布局。

webview 的第一个页面无法监听返回

Android webview的第一个页面无法监听返回,也就是说安卓机上某个 webview 里如果只有一个页面记录,点击顶部导航栏的返回按钮不会生效。解决思路为:可以通过 pushState 让他变成第二个页面,如果在第二个页面返回,那么就应该关闭 webview。

export function rewriteBack(fn?: Function): void {
      if (fn) {
         if (isInApp() && isAndroid() && window.history.length === 1) {
            window.history.replaceState({ page: 1, key: '0' }, 'page1');
            window.history.pushState({ page: 2, key: '0' }, 'page2');
            window.addEventListener('popstate', (e) => {
            // alert(JSON.stringify(e.state));
            if (e && e.state && e.state.page < 2) {
               setTimeout(() => {
                  // 关闭 webview
               }, 100);
            }
            });
         }
         originBack = window.back;
         window.back = (): any => {
            return fn.apply(this);
         };
      }
}
// 还原返回按键功能
export function resumeBack(): void {
   window.back = originBack;
}

两者均有

多行文本超出部分加...问题

多行文本超出部分加...的问题,一般可以用如下样式解决:

{
    display: -webkit-box; 
    // 文本行数
    -webkit-line-clamp: n; 
    -webkit-box-orient: vertical; 
    overflow: hidden; 
    text-overflow:  ellipsis;
}

但是这个布局的兼容性并不好,在一些 iOS 或安卓机型上都有问题,“...“有可能显示不全或显示不出来,解决思路如下:

  1. 文本样式用 { max-height: n行文本高度; overflow: hidden }
  2. 在 componentDidMount 里判断 element.scrollHeight > element.clientHeight, 如果为 true 说明超过了 max-height 的高度,需要在最后一行行尾显示"...",执行下一步,否则正常显示。
  3. 需要自己加"...",“...”的文本样式为{ float: right; clear: both }
  4. 给文本的容器加 before 伪元素,::bnefore{content: ''; float: right; width: 0; height: n-1行文本高度 },这样可以将第三步的“...”元素抵到最后一行。

如果“...”后面还有其他元素,比如“展开”按钮,实现方式类似,可以参考我的另一篇文章移动端带图标(元素)的多行文本超过n行隐藏显示

类数组对象问题

类数组对象是指具有类似数组结构的对象,但不具备数组的所有方法和功能。它们通常具有数值索引和length属性,但没有原生的数组方法(如forEachmapreduce等)。常见的类数组对象包括函数的arguments对象、NodeList对象和字符串。

当我们用 document.querySelectorAll 获取到一个  NodeList 对象,并且需要遍历时,如果直接调用类数组对象的 forEach 方法在很多机型上不会报错,能正常运行,但是在 android 6上会报错,需要将类数组对象转换为数组,可以通过 Array.from() 来转换成一个数组 Array.from()

折叠屏问题

F2 图表适配问题

项目上的图表采用的是蚂蚁金服的 F2 图表,在展开或收起折叠屏时图表的大小并没有改变,无法做到自适应,解决思路是监听 window 的 resize 事件,调用图表的 changeSize 方法改变图表大小重新绘制图表。

  1. 随机生成chartId
// 避免不同的图表组件具有相同的canvasId属性,导致rerender的时候找错dom节点
const randomChartId = (name: string): string => {
    return `${name}-${String(Math.random()).substr(-4)}`;
};
  1. 将图表注册到全局,便于后续 changeSize
const registChart = (chartId: string, chart: Chart, fn?: Function): ChartPairs => {
     globalCharts[chartId] = {
        chart,
        fn,
     };
     return globalCharts;
 };

3、将所有注册在全局的chart调整大小

const changeGlobalChartSize = (): void => {
  for (const key in globalCharts) {
    if (Object.prototype.hasOwnProperty.call(globalCharts, key)) {
      const c = globalCharts[key];
      chartResize(key, c.chart, c.fn);
    }
  }
};
const chartResize = (canvas: HTMLElement | string, chart: Chart, fn?: Function): void => {
  let els;
  if (typeof canvas === 'string') {
    els = document.getElementsByClassName(canvas);
  } else {
    els = [canvas];
  }
  if (chart && els.length) {
    Array.from(els).forEach((el: HTMLElement) => {
      setTimeout(() => {
        el.style.width = '100%';
        el.style.height = '100%';
        el.removeAttribute('width');
        el.removeAttribute('height');
        const w = el.offsetWidth;
        const h = el.offsetHeight;
        chart.changeSize(w, h);
        if (fn) {
          fn({ width: w, height: h });
        }
      }, 200);
    });
  }
};

4、在入口处监听 resize 事件

window.addEventListener(
     'resize',
     debounce(() => {
        // 调整F2图表大小
        changeGlobalChartSize();
     }, 300),
);

这样在有图表的地方只需要调用 registChart 方法,将图表注册到全局,折叠屏改变屏幕大小后会将注册到全局的图表重新绘制。

折叠屏其他适配问题

可以用媒体查询来解决,折叠屏展开和收起时媒体查询可以检测到屏幕大小变化,从而使相应的属性生效。

   // 当屏幕宽度大于 400px 时 content 选择器会生效
   @media screen and (min-width: 400px) {
      .content {
         justify-content: space-between;
      }
   }

   // 当屏幕宽度小于 375px 时 right 选择器会生效
   @media screen and (max-width: 375px) {
      .right {
         flex: 1;
         min-width: 0;
      }
   }

框架问题

由于项目用的是 taro 框架,这里主要说明下 taro 框架需要注意的问题。

componentDidMount

由于 componentDidMount 代表 React 组件组装完毕交给 Taro 处理, Taro 刷新到 UI 是有延迟的,如果要在 componentDidMount里获取元素属性值,通常需要加延时才能获取到准确的元素属性。

setState

Taro(包括 Taro 3)连续通过回调函数调用setState拿到的上一个 state 会是同一个。框架会合并setState的调用,不管是在 onClick 里连续调用还是在 setTimeout 里连续调用, 框架都会合并 setState 的调用(批量更新)。这一点和 react 是有区别的。

对于 Taro 而言,setState 之后,你提供的对象会被加入一个数组,然后在执行下一个 eventloop 的时候合并它们。Taro 的状态更新一定是异步的。

componentDidShow / componentDidHide

taro 的 componentDidShow / componentDidHide 只对父组件有效,子组件无效,如果要在子组件里使用 componentDidShow / componentDidHide,可以在子组件里用 Taro.eventCenter 来监听路由变化,可以将代码抽离出来做一个公共监听代码。

   export class TaroPageListener {
      private enterPage = window.location.hash;
      constructor(onShow?: Fn, onHide?: Fn) {
         Taro.eventCenter.on('__taroRouterChange', () => {
            const nextPath = loGet(window, 'location.hash', '');
            if (this.enterPage && nextPath) {
            const isShow = this.enterPage === nextPath;
            if (isShow && onShow) {
               onShow();
            } else if (onHide) {
               onHide();
            }
            }
         });
      }
   }

ScrollView

Taro 的 ScrollView 组件默认绑定了 onTouchMove 事件,且阻止了冒泡,如果 ScrollView 作为子组件,在父组件上绑定的 onTouchMove 事件无法被触发,

需要在 ScrollView 组件上手动绑定 onTouchMove = {() => {}},这样 ScrollView 在触发 onTouchMove 事件时会执行手动绑定的函数,事件可以冒泡出去了。

Taro2、react、Taro3 组件复合时的更新差异

在 react 中 children 作为 props 或 组件作为 props 时,children 或组件只是 props,因此它们不会受到状态更改的影响,但是 Taro2 里这种情况依然会更新,例如:

Compnent1:
<View>this.props.children</View>

Component2:
<Component1>  
    <View></View>  
</Component1>

在 react 中  Compnent1 组件更新时 Component2 不会更新,但是 Taro2 里 Component2 依然会更新,Taro3 里不会更新,因为 Taro3 运行的是真正的 react 运行时。

其他问题

分享图片问题

例如分享图片到微信/朋友圈,需要用到截图技术,采用的方案为用第三方库 html2canvans, 将需要截取的dom结构转为 canvans, 再调用 canvans.toDataUrl() 转为 base64格式的数据,转换过程中需要注意的问题:

  1. 如果截取的 dom 结构中有 Image 元素,且图片需要跨域获取,则用 html2canvans 转为 canvans 后是一副被污染的 canvans,调用 toDataUrl() 会抛跨域安全错误,需要解决跨域问题(通常用代理服务器解决)。
  2. 截取的 dom 结构中,背景图用 Image 来实现,如果用 css 方式(backgroundImage)来实现,截取的图片会很模糊。

混合内容引起的问题

例如在uat或生产环境下是 https 域名,如果某个页面引入了一个 http 协议的图片、脚本、iframe等,虽然 image、script、iframe 标签的 src 属性可以跨域,但是内容有可能加载不出来,因为安全的 https 页面引入了不安全的内容,也就是页面成了混合内容,为了保证安全,有的浏览器会阻止获取 http 的内容,但是有些低版本的浏览器可能不会阻止。

关于混合内容的问题参考:web.dev/what-is-mix…