2024年1月

项目包括一个loading组件以及Swiper控制的内容组件Swiper的每个Slide也是一个组件,通过Swiper滑动控制切换不同的显示组件,DOM结构如下所示:

<template>
  <div class="main">
    <Loading v-show="showLoading" class="loading" :loadingText="loadingText"></Loading>

    <swiper v-show="!showLoading">
      <swiper-slide v-for="(item, index) in componentQueue">
        <component
          ref="components"
          :is="item"
          direction="vertical"
          />
      </swiper-slide>
    </swiper>
</template>

Swiper不能滑动

对于IOS12及以下系统,Swiper11版本无法通过触摸手势触发滑动操作,调试发现直接调用slideNextslidePre方法是可以切换下一项和上一项的。由于手动监听DOM上的touchstart、和touchmove事件是可以正常触发的,因此解决方法是通过监听touchstart、和touchmove事件根据手指移动情况判断上滑以及下滑操作,并分别调用slideNextslidePre方法实现滑动切换。

Swiper不能正常显示

在项目开发时刚开始使用的是v-if控制loading页内容页容器的显隐,但这样内容页DOM结构并不会生成,loading过程中内容页的图片资源也不会提前加载,loading页无法完成“预加载”资源的功能。更换为v-show之后虽然没有这个问题,但由于DOM没正常显示引起Swiper没有正常初始化,从而导致轮博图出现不能正常显示的问题。Swiper提供了update方法,官方文档称“应该在手动添加/删除幻灯片、隐藏/显示幻灯片或使用Swiper进行任何自定义DOM修改后调用该方法”。因此通过在初始化后添加了一个定时器调用update方法解决。
然而在IOS12及以下版本的手机上这个问题又复现了,推测是定时器的延迟时间不合适导致的。最终解决方案是设置observer属性为true,这样Swiper将会自己启动检测,每当显示/隐藏、添加/删除子元素时都会自动进行更新。

IOS12打开页面报错Unexpected token '.'

问题发现在IOS12及以下版本。这个错误是在loading页加载时抛出的,根据log信息并不能直接定位到具体报错代码行列,只能定位至问题的组件。因此只能逐行代码进行查错,先把<template><script>注释掉,再逐步放开里面的代码,最终发现是使用可选链运算符?的两行代码导致的报错。因此确定是项目使用的vue3+vite的技术栈不兼容导致的,未对可选链?运算符进行转换,低版本浏览器不能识别而报错。
解决方法:

  • 使用其他写法替换可选链?运算符
  • 安装模块@babel/plugin-proposal-optional-chaining,并配置babel.config.ts

    module.exports = {
        presets: ['@vue/app'],
        plugins: ["@babel/plugin-proposal-optional-chaining"]
    }

html2canvas转图片触发DOM动画

项目中有一个页面进入时有约3s的入场动画,同时此页面还有保存图片的需求。保存图片使用的是html2canvas插件,这样就产生了一个问题,html2canvas在绘制DOM时会重新触发页面的入场动画,最终截取到的图片是动画过程中的一帧,并不是最终的样式,因此需要在保存图片时禁止动画播放,并且在关闭保存动画组件后还不能触发及影响页面动画。
前期采用的方案是临时加了一个css类:

animation-duration: 0.001s !important;
animation-delay: 0.001s !important;

设置动画的过渡时间和延迟时间为0.001s,目的是能够快速执行动画。
然而在IOS12及以下手机上这两个属性并不能取得预期效果。
最终方法是使用以下属性,直接取消动画:

opacity: 1 !important;  // DOM元素默认opacity是0,使delay动画执行前隐藏元素
animation: none;

图片旋转动画不显示

需求是需要在点击图片时,图片会开始旋转,再次点击,图片停止旋转。
原本的方法是通过设置animation-play-state属性来控制动画的暂停和播放,并且能够保留暂停时动画的状态。

.music-play {
    animation-play-state: running;
    -webkit-animation-play-state: running;
}
.music-pause {
     animation-play-state: paused;
     -webkit-animation-play-state: paused;
}

但是在IOS11系统中,这个属性不能正常工作。
最终解决方案是:除IOS11及以下外的其他系统仍使用animation-play-state属性,IOS11及一下系统使用如下样式控制图片旋转:

.music-play-compatible {
    animation: spin 2s linear infinite;
}

.music-pause-compatible {
    animation: none !important;
}

通过直接设置animationnone达到暂停动画的目的,不足之处是再次播放动画时,不会保留暂停时的状态。

资源缓存策略

项目中有大量的图片资源和一个音乐资源,为了不让用户在资源加载完成之前就进入系统中,因此在loading页需要对资源加载情况进行判断,在满足条件之后才允许用户进入系统。本项目对于Image对象使用complete属性判断图片资源是否加载完成,对于音乐资源则通过audio对象的readyState属性判断音乐资源的加载情况,readyState有以下几种状态:

  • 0 = HAVE_NOTHING - 音频/视频尚未初始化
  • 1 = HAVE_METADATA - 音频/视频是就绪的,但尚未加载
  • 2 = HAVE_CURRENT_DATA - 音频/视频是当前的,但未完全加载
  • 3 = HAVE_FUTURE_DATA - 音频/视频的未来帧已加载,但尚未加载完
  • 4 = HAVE_ENOUGH_DATA - 音频/视频已加载完毕

大多数情况下,当readyState大于等于3时既可以不影响音乐播放也不会造成太久的等待时间,有良好的用户体验。这种方式在Chrome浏览器以及测试范围内的安卓手机都可以正常工作,但是在IOS系统(包括最新版)中出现了readyState一直为1的情况。
这是因为Chrome浏览器以及安卓手机在loading页时,虽然音乐没有播放,但是可以正常加载资源,但IOS上的策略是必须要触发音乐播放的事件后才开始加载资源。
最终处理方法是:

  • 进入loading页后,js触发音乐静音播放,然后再暂停播放并复位播放进度,从而触发音乐资源加载且不影响正常的播放
  • 检测资源加载是否完成,做一个兜底,记录检测到图片资源加载完成但音乐资源未加载完成后的事件,若超过所设置的超时时间,便认为第一条策略未正常执行,音乐资源未触发加载,跳过音乐资源的加载状态判断

IOS端Height属性异常

此问题主要出现在IOS上,本意是设置页面高度为100%使页面高度撑满屏幕,但IOS上的表现是会溢出可视区域的,因为IOS100%高度会把工具栏的高度也算上了。另外使用vh也存在同样的问题。在查资料过程中发现了一个叫做DVH(Dynamic Viewport Unit)动态适口单位的属性,可以完美解决百分数以及vh的缺点,但是dvh是新单位,兼容性不好。最终采用了js获取可视区域高度设置为页面高度的方法:

let element = document.querySelector('#app');
// 设置元素的高度为视口的高度
if (element) {
    element.style.height = window.innerHeight + 'px'
}

过去几个月中有两个需求都用到了html2canvas插件,目的是使Web中的DOM结构转为图片,但由于html2canvas的原理限制,遇到了一系列问题:

跨域问题

html2canvas转图片的原理是将传入的dom元素渲染到canvas画布上,然后利用canvas的toDataURL方式转位图片的。然而html2canvas在将dom元素绘制在canvas上时,如下html2canvas加载图片的核心代码所示:

return await new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve(img);
            img.onerror = reject;
            //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
            if (isInlineBase64Image(src) || useCORS) {
                img.crossOrigin = 'anonymous';
            }
            img.src = src;
            if (img.complete === true) {
                // Inline XML images may fail to parse, throwing an Error later on
                setTimeout(() => resolve(img), 500);
            }
            if (this._options.imageTimeout > 0) {
                setTimeout(
                    () => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
                    this._options.imageTimeout
                );
            }
        });

对于img会重新发起网络请求获取图片,因为是js发起的请求,所以对于有跨域限制的图片会提示跨域错误,而且可以注意到当传入的options参数设置useCORS时,是会设置img.crossOrigin = 'anonymous'的。
要想解决这个问题,最简单的方法就是在图片服务器上进行设置允许跨域,但是这种方式在服务器自己不可控的情况下是行不通的。所以在解决问题的过程中就想着是否有前端侧的方法来避免这个问题呢?网上有很多文章介绍说可以给图片设置crossorigin="anonymous"html2canvas的option设置userCORS: true、提前把跨域的网络图片转为base64等方法来解决,但是经过一系列折腾后发现,这些方法都是需要服务端的支持才行得通的。可行的方法主要包括:

  • 服务端配置跨域访问
  • 设置代理服务器
  • 图片数量固定且不多的情况下可选择放置在项目中

图片显示不完整问题

当dom元素有滚动条时在生成图片后可能会出现图片不完整,部分区域是空白的情况,可以调用图下代码 dom.scrollTo(0, dom.scrollHeight)控制滚动条至页面底部解决。另外还可能出现图片如果有偏移的话,可添加配置scrollY:0, scrollX:0,解决。

所生成图片边缘有缝隙/空白问题

由于不同设备的屏幕像素密度不一样,html2canvas在计算宽高的时候可能出现了误差导致。解决方法:

  • 设置宽度或高度减去几个像素,刚好把空白的地方裁掉(当然前提是在不影响图片内容的情况下)
  • 如果是纯色背景,可以设置html2cavas配置里的背景色为相同颜色即可

透明图片异常显示的问题

很奇怪的异常,最开始这个图片是以背景图方式显示的,最后换成img标签形式展示就没这个问题了。

由dom元素的动画引起的显示不完整问题

问题的根因是html2canvas在解析传入的dom节点之前会先去克隆一个副本,这个操作的过程可能就重新触发了动画的播放条件,于是html2cavas在将dom绘制至canvas时可能只是截取到了动画过程中的一个状态。解决这个问题的本质是要在html2cavans绘制dom时,dom的动画是不执行的或者动画已经执行完毕,具体解决方法包括:

  • 另外创建一个不可视且无动画版本的dom作为html2canvas的入参
  • 利用定时器保证在动画播放完毕时再执行html2canvas
  • 声明一个css类,设置规则animation-duration: 0s !important; animation-delay: 0s !important;使动画执行时间可延迟时间都为0s,这样dom会直接处于动画执行完毕的状态,当执行html2canvas之前动态添加这个类,当执行完毕后再删除掉这个类,不影响后续动画的执行。