标签: mobile

  • 移动网页高度自适应最佳实践

    移动网页高度自适应最佳实践

    移动 Web 开发就要在“螺蛳壳里做道场”。移动设备限于屏幕尺寸,不得已左支右绌,既要多呈现内容,又要保证功能不要缺失。普通内容类网页还好,自然往下滚就行了;开发 Web App 的时候,当我们因为某种原因,需要限制滚动区域的时候,就很难处理。

    这篇文章会分享我的一些经验,希望能节省大家摸索的时间。

    无用单位:vh, svh, dvh, lvh

    很早以前,我们就有了 vwvh 单位,分别指代视窗宽高的 1%。需要注意的是,移动浏览器的视口包含被各种组件占据的部分,所以 vw 就比较好用,因为没有干扰;但是高度上,被通知栏、地址栏等占用的“临时”空间就会成为我们的麻烦。

    为了妥善利用屏幕空间,在我们上下滚屏的时候,大多数手机浏览器都会把地址栏、工具栏、或者通知栏隐藏起来,这就导致浏览器的可视面积其实会不断变化。原本就没用的 vh 便更没用了……于是后面新增了 svhdvhlvh 三种 长度单位,但其实帮助不大,因为当我们需要限制容器高度的时候,通常来说就不能让页面自由滚动。

    因为这几个长度单位过于没用,所以我就不详细介绍了。感兴趣的同学可以看下 TailwindCSS 里的演示:https://tailwindcss.com/docs/height#viewport-height

    虚拟键盘则让这个问题雪上加霜,因为虚拟键盘的显示和隐藏都不会影响这几个长度单位,所以当我们需要手动控制容器高度、位置的时候,就会很难做。

    最佳实践:常规页面,交给浏览器

    首先我们要信任浏览器,能够留给浏览器处理的,尽量交给浏览器原生处理。

    比如,常规页面,长一点,留给浏览器自然滚动。文本框输入的时候,浏览器会自动聚焦和滚动,通常情况下没什么问题,基本体验有保证。

    最佳实践:输入框文字不小于 16px

    如果文本框 font-size 小于 16px,iOS Safari 下,当文本框获得焦点,Safari 会自动放大整个页面;而失去焦点的时候,页面并不会自动缩小到 100%,所以就很蛋痛。

    解决方案有几个:

    1. 取消缩放。会使得可用性评价恶化,不推荐。
    2. blur 时自动恢复 100%。增加特性就是增加 bug 的可能,我觉得能不用就不用。
    3. 保持字体大小。应该大部分时候都更简单有效。

    最佳实践:使用 dvh 并解决兼容问题

    虽然但是,当我们需要固定高度的时候,表示视窗净高度的 dvh 仍然是我们最佳选择。

    不过,在我写文章的现在,dvh 的兼容性不是很好,所以必须做好兼容性配置。我建议用 JS 结合 CSS 变量来做。在 <head> 里插入这段 JS:

    // 首先,判断是否支持 dvh 单位
    if (!CSS.supports('height', '100dvh')) {
      // 如果不支持,就定义 --app-height 为视口高度,即 window.innerHeight
      document.body.style.setProperty('--app-height', window.innerHeight + 'px');
      // 当屏幕缩放时,改变内容高度。因为 resize 事件触发很频繁,所以使用节流减少性能损耗
      let timeout;
      function onResize() {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          clearTimeout(timeout);
          document.body.style.setProperty('--app-height', window.innerHeight + 'px');
        }, 500);
      }
      window.addEventListener('resize', onResize);
    }
    

    然后定义 CSS 样式:

    :root {
      --app-height: 100dvh;
    }
    
    .h-dvh-app {
      height: var(--app-height);
    }
    

    如果使用 TailwindCSS,那么在配置文件里增加配置即可:

    export default {
      theme: {
        extend: {
          spacing: {
            'dvh-app': 'var(--app-height)',
          },
        },
      },
    }        
    

    最佳实践:使用 CSS 变量解决虚拟键盘

    只是限制高度为 100dvh,当虚拟键盘弹出之后,因为视口缩小,很可能会出现问题。此时 window.resize 事件也不会触发,所以我们应该侦听文本框的 focus 事件,动态改变容器高度;并在文本框 blur 之后,恢复高度。

    此时,我们可以借助 CSS 变量的“默认值”功能,即 var(--custom-value, --default-value) 来处理。当我们需要暂时的高度以应对虚拟键盘时,设置 --custom-value;之后,移除 —custom-value,恢复到预定义的 --app-height

    首先,修改 css,定义 --input-height: initial,这个值会被认为是空值而忽略。

    :root {
      --input-height: initial; 
      --app-height: 100dvh;
    }
    
    .h-dvh-app {
      height: var(--input-height, var(--app-height));
    }
    

    然后侦听输入框的 focusblur 事件:

    async function onTextareaFocus(): Promise<void> {
      // 桌面端忽略这个需求
      if (window.innerWidth > 640) return;
    
      // 给虚拟键盘弹出一些时间
      await sleep(300);
      const { innerHeight } = window;
      document.body.style.setProperty('--input-height', `${innerHeight}px`);
      // 需要的话,可以在这里插入一个滚动
    }
    async function onTextareaBlur(event: FocusEvent): Promise<void> {
      // 同样,也给虚拟键盘收起留一些时间
      await sleep(250);
      document.body.style.removeProperty('--input-height');
    }
    

    总结

    至此,遵守以上最佳实践之后,基本上我们可以妥善处理移动网页里的浏览器高度。当然,并不完美,比如,iOS Safari 在输入 position: sticky 里的文本框时,会凭空多出一大块空白,很烦,但是没办法解决。可以绕开,但是我觉得绕开的方案更难用。

    希望这篇文章对大家有用。如果你对移动网页开发有什么问题,欢迎留言讨论。

  • 文章预告:移动 Web 开发四大坑

    虽然明知道没什么人看,不过也先放个预告吧,其实是怕自己到时候忘记了。

    《移动开发四大坑》,分享这次移动泡泡开发过程中踩到的坑和解决方案:

    1. 缺少标准 flexbox,没法自动换行,导致列表排版出问题
    2. tapclick 之间的 300ms 引来的各种问题
    3. position:relative 也可能是罪魁祸首
    4. 很多 JS API 都不是默认开启的,得用原生实现
  • 悲催的Android Webview——记新版广告墙开发

    悲催的Android Webview——记新版广告墙开发

    前一阵很辛劳,所以荒废了博客。前几天终于完成了这项艰苦卓绝的工程:HTML5版广告墙,决定写篇文章,记录一下踩过的坑。

    项目介绍

    广告墙属于典型的列表式应用:打开后是无尽列表,通过滑动手指驱使列表滚动,上拉加载更多内容,下拉刷新列表(这次没做)。单击列表中的某项,打开详细页;单击上面的后退按钮,退回上一级页面。

    考虑到目标平台是Android系统,内嵌WebKit内核,我采用了时下流行的HTML5+CSS3技术,并且做好了向下兼容的准备。为了减少代码量,我放弃了jQuery,也没有考虑半调子Zepto,全部使用原生开发。操作上,开始想用Hammer.js封装手势,后来处理滚动效果处理得不好,最终选择了iScroll 5。模板方面,前端使用Handlebars.js,后端使用Mustache,所以编译时还写了段代码做转换。

    项目代码托管在github上,因为前端不涉及到业务逻辑,所以仓库是公开的,有兴趣的同学以clone下来看看。

    技术选型大概聊这些,接下来开始进入正题。

    性能vs兼容性

    实际上,随着硬件的发展,对于列表式这种非性能敏感性应用来说,性能早已不是制约其发展的首要因素了。另一方面,有一定专业精神的前端如我,平时也会注重积累各种性能优化的点,并不断在项目中实验、实施——所以我就很搞不明白,技术实力强大如jQuery,为啥会弄出个jQuery Mobile这种完全不能用的东西出来……

    相反,在这个项目中,由于平台分裂带来的兼容性问题成了最大的绊脚石。尤其是这些兼容性问题,极少找到现成的解决方案……下面举两个例子:

    Vivo X909 和z-index

    正常情况下,position:absolute;的dom节点在不显式设置z-index属性区分层级的时候,默认后面的节点会显示在前面的节点上方。大多数浏览器里都是这样,可是Vivo X909就不行。

    列表大家都知道,点击某项,详情会从右边滚动进来。由于没有设置-webkit-tap-highlight-color(现在想想应该做的,下周回去加上吧),所以这个bug当时的的表现是“单击没有反应”。这种情况很难调试,我开始以为JS有错,试了很久,后来配置好Weinre(见上篇博客),才找到症结所在。最后把几个图层分别加上z-index,问题解决。

    早期Android的animation-fill-mode

    我们仍有20%的用户在使用2.3.x版本的Android,所以这块市场必然不能放弃。在小米2上调是好的动画,在老HTC G10上,滑出的图层在动画结束后会消失。这次好在有了上次的经验,直接上Weinre;加上caniuse.com也有明确的描述,所以很快解决了。


    各种难以定位、缺少文档、找不到解决方案的兼容性问题层出不穷,让我不禁感慨:本以为买上了WebKit的康庄大道,谁知尼玛这豆腐渣工程下面全是坑啊……

    滚屏

    前面提到,开始时间比较富裕,我也比较自信,想借助Hammer.js,慢慢打磨出色的滚动体验。后来无尽的bug环伺左右,实在无法专心研究,便退而求其次,使用iScroll了。

    iScroll如今已经发展到5,其作者自信的表示:“我尽全力提升了其在Android平台上的表现,我相信现在它已经达到了极致。”实验效果确实很理想,无论是高配置的小米,还是老旧的HTC G10,都表现出色。将来有空的时候得好好研究下他是怎么做的。

    工程实践

    随着时代发展,Web开发这种脚本语言也开始分裂成开发、部署两个阶段,这是个好事情。核心代码需要为维护、测试留下足够的空间,势必在性能表现上有所不足,部署时根据应用场景的不同输出不同的结果正好能解决这个问题。这个项目中我使用Grunt,花费了不少精力在编写Gruntfile.js上。

    开发时,我将测试用的数据,按照设计好的数据结构写在define.js中,生产环境中用服务器生成真实数据来替代。不同的应用场景,也用其配置。如今,目标支持的6个场景都能稳定工作,效果不错。

    Windows下不好配ruby,使用SASS是个问题,不过后来找到了Prepros,问题迎刃而解。

    技术细节分享

    简单构造选择器

    jQuery功能确实强大,不过实在太重型了;备胎Zepto轻便一些,不过功能也弱一些,语法做不到全兼容,用起来不舒服。仔细考虑这个项目的需求,无非是:找到某个元素,给它添加事件侦听;或者判断它是否包含某个class,那么自己写一个直接到Dom节点的选择器就足够了。后来看到篇文章,里面详细测试了querySelectorgetElementById众的性能,发现还是后者大大领先——尤其在移动平台上,于是基本放弃了用前者的念头。

    这个选择器实现起来很容易:

        var $ = window.$ = function (selector, root) {
          root = root || document;
          var dom;
          switch (selector.charAt(0)) {
            case '#':
              selector = selector.substr(1);
              dom = document.getElementById(selector);
              break;
    
            case '.':
              selector = selector.substr(1);
              dom = root.getElementsByClassName(selector)[0];
              break;
    
            default :
              dom = root.getElementsByTagName(selector)[0];
              break;
          }
          return dom;
        };
    

    另外,$还可以用来当命名空间,保存一些需要全局使用的变量和方法。

    使用Handlebars.js生成预编译模板

    预编译模版可以降低运行时的运算量,对于移动开发这种能省则省的应用场景,实在是必备之物。另一方面,为了方便开发,把模版放在HTML里更好调整。好在有“编译输出”这个步骤,可以两全其美。

    Handlebars.js的预编译是把模版转换成根据数据生成HTML的JS代码,可以使用uglify压缩。编译之前最好过滤掉多余的空格和换行符,这样生成的JS文件也会小很多。

    当然,再预编译也不如直接渲染HTML来的快,所以在服务器端生成页面的时候,一定要包含实际内容,让用户尽快得到信息。而PHP没有现成的Handlebars实现(我也不打算写),好在有mustache.php,并且handlebars兼容mustache(其实不完全,也被坑了),所以我写了一小段代码将Handlebars模版转换成Mustache模版

    使用attr()实现CSS中的多语言

    除了传统的中文版,这次还要制作英文版。英文版与中文版的功能几乎完全一致,只是文字全部换成英文。最简单的做法就是搞个en.html——因为使用了模版,JS里不包含任何文字内容;但是“上拉加载更多”这个功能目前使用CSS的:after实现——初衷是不修改dom,不引起relayout——当然,反正我用着Sass,增加个语言包不难,不过我还是希望把修改集中在一个地方。

    后来查了下CSS表达式的兼容性,发现Android 2.1起就支持,那就好办了,先把各阶段文案放到HTML的属性里:

        // 中文
        <div id="list" data-normal="继续上拉,加载更多广告" data-more="松开加载" data-loading="加载中..." data-no-more="没有其它广告了,再看看吧" data-error="加载失败,请稍候重试" data-over=""></div>
        // 英文
        <div id="list" data-normal="Pull up to load" data-more="Release to load" data-loading="Loading..." data-no-more="No more ads" data-error="Load failed" data-over=""></div>
    

    然后在CSS里使用attr()访问即可

        #list.over
          &:after
            content: attr(data-over)
    
        #list.more
          &:after
            content: attr(data-more)
    
        #list.loading
          &:after
            content: attr(data-loading)
    
        #list.no-more
          &:after
            content: attr(data-no-more)
    
        #list.error
          &:after
            content: attr(data-error)
    

    WebKit中z-indextranslateZ(0)混用,导致click事件半失灵的解决

    1. 要在浏览器里触发下载,只要链接到一个浏览器打不开的文件就行。链接跳转是在click触发的浏览器内建行为,换言之,链接跳转依赖于click的正常触发。
    2. clicktouch event晚200~300ms,所以为了保证用户体验,我们通常用taptouchstart + touchend)来替代click响应用户操作
    3. 所以一个“点击下载按钮”的操作,就会先响应tap事件,弹出详情浮层,告知用户获得积分的最终条件,然后等待浏览器开始下载
    4. CSS动画最好使用translateZ(0)促使浏览器启用GPU加速
    5. 这个bug在以上条件下产生,影响这个过程

    之前我在PC版Chrome里也曾见识过这个bug(最新版没验证,大概20+的时候吧)。具体表现为,一个层,它的translateZ有赋值,它可能被移到某处(通过translateXtranslateY,或者动画过程中),不过还未到达那里,但是它仍然会拦截那里mousedown/mouseup事件,导致click事件无法正常触发。

    这个bug非常难排查,因为它并不影响tap;视觉上看不到那个层,会想当然的认为不应该跟那个层有关系;并且,WTF,如果按得时间稍长,比如半秒,就可以触发click……因为触发条件比较复杂,我甚至很长时间没找到规律。最后只好请出“小黄鸭调试法”,试图向索隆解释发生了什么,然后发现无法正常下载的按钮都是被详情浮层遮盖着,继而回忆起以前解决过类似问题。

    最后解决这个问题的方案我其实不很满意,不过确实能顺利工作:

    1. 层的当前位置不会覆盖按钮,先不给层增加动画
    2. tap后,给click留出足够的时间(我这里用400ms),再给层添加动画
    3. 确保click可以响应,通过setTimeout给层添加动画——本来300ms就测试通过,不过为了保险,我还是决定写成400

    仍然悬而未决的问题

    虽说项目已经快上线了,但仍有几个不解之谜……

    1. 某些手机仍然无法下载(启动<a>
    2. 快速拖动的时候可能把内容拖走(应该是触发了浏览器默认行为)

    总结

    做完项目回头看,系统分裂造成的兼容性问题比想象中多很多,原先以为最多是有些2.x版本不支持,做向下兼容就好,谁知还有各种实现细节问题。不过,在PC Chrome + 手机Chrome + Weinrealert之后,只要有耐心,大多数问题时可以解决的。

    说完开发效率,另一方面再来看看产品表现。我们对性能不需要太乐观,也不用太悲观,敏感型应用,老老实实用原生开发,或编译成原生应用;非敏感型,跨平台高兼容性(咦,这眼泪哪里来的)的优势很明显。通过我实际看到移动端的表现,我们这些页面仔应该更有信心,将来移动大市场里,Web App的份额绝对不比Native App要少。

    这项事业任重而道远,需要我们一起努力。

  • 本地部署weinre帮助移动开发

    本地部署weinre帮助移动开发

    weinre是个开源项目,用来在Web开发中做远程调试的工作,相当给力。后来也捐给了Apache基金会,并且从ruby移植到了JavaScript,现在可以直接通过npm安装。在最近的广告墙大重构中,这个工具帮了我很大的忙。

    安装weinre首先要装node.js,后者在Windows和Mac上直接下包就能装,并且自动配置环境,很方便;在Linux上需要自己编译一次,也不复杂,我之前有篇日志写了,现在还好使,可以看看。

    node.js环境搞定后,直接用npm就可以安装weinre了,直接装在全局中最好:

    sudo npm install weinre -g

    接下来启动weinre,我在这里卡了一阵,大概因为不太了解端口侦听的缘故,我以为直接启动就好,结果从外面连不上(手机连不上连点反应都没有,很难排查),因为侦听的是localhost的ip,也就是127.0.0.1。后来尝试绑定内网ip,才算解决问题:

    # 8081是想找一个不常用的端口,后面的ip是我在内网的ip
    weinre --httpPort 8081 --boundHost 192.168.10.54

    这次无论是本机还是手机都可以正常访问了。然后Mac这里可能还会遇到点小问题。虽然我关闭了防火墙,但是Unix自带的ipfw还在工作,会阻止从外面过来的访问(不知道为啥80没问题),所以要给8081端口专门的许可:

    sudo ipfw add 8081 allow from any to any

    之后就万事大吉了,在HTML里添加 <script src="http://192.168.10.54:8081/target/target-script-min.js#meathill"></script> (域名根据具体情况修改),然后打开 http://192.168.10.54:8081/client/#meathill 就可以查看了,如果有多台终端在调试的话,还可以点选目标机器。

    好了,享受下weinre带来的方便快捷的移动端调试吧。(不自己截图了,借用了weinre官网上的图片)

    weinre使用截图
    weinre使用截图

  • Mobile Web开发笔记

    Mobile Web开发笔记

    这里记录使用Phonegap开发移动应用期间发现。

    性能

    使用jQuery托管事件对阻止冒泡的影响

    表现

    解决 iOS webkit 使用CSS动画时闪烁的问题

    CSS 兼容性

    CSS 属性 iOS Android IE 其它
    border-radius 3.x -webkit- Firefox 3.6- -moz-
    Safari 4- -webkit-
    box-shadow 2.3 -webkit- Safari 5- -webkit-

    网摘

    Developing Better PhoneGap Apps: Float Mobile Learning

    重点包括

    1. 使用tap取代click(这点我还需要进一步测试,我发现zepto+phonegap没法捕获tap事件,不知道是我不是敲键盘的姿势不对)
    2. 使用css动画取代js动画

    随记

    1. “click”和“tap“不是一回事儿,“click”会延迟300~400ms才处理,时间上会接近taphold之类的事件,“tap”更快
    2. 给元素加上:active伪类,可以给用户更快的反馈
    3. 调试不太容易,debug.phonegap.com似乎已经关了,得想办法把自己的weinre服务搭起来。暂时可以在js里写console.log(),接受一个参数,会在log cat输出。