年终了,听说你也在开发年终盘点?也许你可以看看这篇腾讯 ABCmouse 圣诞年终盘点活动页的踩坑实战记录。
圣诞节的时候 ABCmouse 为用户精心准备了一份圣诞礼物,你也想看下吗?快来扫下这个神奇的二维码...
好吧,知道你可能不想扫码 '__' ,直接看下图吧(截取了其中一段)
当然了,这篇文章不是介绍整个开发过程(实际上本身开发周期很短,开发才三天,另外两天bugfix和视觉还原,时间非常赶)。这篇文章主要记录我在开发的过程的过程的一些经验总结和遇到的坑。
坑一:视频坑
这次的年终盘点在前面半部分是一个视频,点击播放视频完成(或者跳过)之后正式进入主页。
划重点: 在视觉设计初期我跟视觉反抗过,建议尽量不要在活动页做内联视频播放,有的浏览器会挟持video标签的播放,使用自己的方式实现,特别Android,会有很多兼容性问题,会比较影响用户体验。 事实证明也确实如此,且听我一一道来。 不过视频里的小老鼠真的好卡哇伊...
播放视频时内联播放,这里视频播放只限制在微信和QQ内才能内联播放。其他手机自带浏览器直接会跳过这个视频播放,后面我简单说明下原因。
为了实现视频的内联播放,我们可以借助video标签的playsinline属性:
代码语言:javascript复制<video
webkit-playsinline="true"
playsinline="true"
></video>
另外为了能在视频播放的时候在视频上方显示跳过按钮,这里我们需要用到X5内核视频播放的一个属性 x5-video-player-type
设置为h5-page之后,这样就可以控制视频在网页内部同层播放,同时也可以在视频上方显示html元素。(具体可以看这里: H5同层播放器接入规范[1])
<video x5-video-player-type="h5-page"></video>
因为浏览器的自身自动播放策略,视频的自动播放需要用户在当前页面上有用户行为产生,或者设置视频静音属性 muted
,才能自动播放。而我们的视频在前 7.23s
的时候会有视频音乐的,因此播放时不能设置为静音,所以无法做成自动播放,于是做成了如上图所示, 用户点击时才能开始播放
。(另外在Android上是无法自动获取视频的第一帧的,所以这里让视觉取第一帧图片给你吧)。
点击视频,终于要开始视觉设计的超级卡哇伊的视频了。但是这些都是什么鬼。。。
1、在Android设备下出现小窗播放
时间很紧张,这里没处理。(o(╥﹏╥)o)
2、在Android设备下小窗播放完成后出现广告页?
这个可不行。
解决方案:在视频播放完成后马上调用播放并暂停。
代码语言:javascript复制if (lib.browser.os.android) {
video.play();
setTimeout(function(){
video.pause();
});
}
其实在X5内核中还可以考虑使用 mtt-playsinline
属性来强制使用系统播放器,从而拒绝视频被拦截植入推荐视频。时间不够当时没考虑到上面去。
<video
mtt-playsinline="true"
></video>
3、百花齐放、百舸争流的 Android 浏览器的视频播放问题
这里图片太多,就不一一放了。亲测出现过:
- 自动横屏播放
- 悬浮置顶播放
- 自动全屏播放
- ....
有些事,它想做,我也解决不了。。。跟视觉讨论,客户群体主要还是在微信和QQ,所以在手机自带浏览器里摸下小老鼠的屁股后直接跳过视频播放,直奔主题。
坑二:音频坑
视频问题不完美解决后,你以为完了?我之前说过: 视频播放到7.23s的时候需要自动播放背景音乐
,此时的小老鼠往上抛,出现 叮叮当叮叮当...
的背景音乐,是不是很有节奏感?但是...
1、Android切换背景音乐的时候视频暂停播放
没错就是卡在这里...
需要注意: 在Android设备上视频播放后同时使用audio标签播放音频时会导致视频卡住。
幸亏组里缺什么也不会缺大佬,大佬说:这个问题我遇到过,你用 WebAudio
播放音频就 OK 了。关于 WebAudio 你可以点这里[2],崇拜ing...(IMWeb 前端团队火热招聘中~快来投递简历吧!)
代码语言:javascript复制解决方案:在Android设备中使用WebAudio播放音频,而在其它设备中使用audio标签进行播放。(疑问解答:为什么不统一用WebAudio?,因为在另外一个需要中出现过播放视频时播放音频在IOS设备中出现过破音,没错就是
破音
)
if (lib.browser.os.android) {
this.player = new WebAudioPlayer(this.src);
} else {
this.player = new AudioPlayer(this.src);
}
ps: 其中 WebAudioPlayer
和 AudioPlayer
是自己封装的一个简单库
2、iOS下音频自动播放失效?
音频的自动播放策略和视频的一样,设置静音或者有用户行为。但是点击播放视频的时候不是已经有了用户行为,为什么还是播放不了? iOS出于安全机制,不允许audio和video自动播放
,所以当切换播放音频播放时还是无法自动播放。
解决方案:在点击触发视频播放的时候同时触发音频播放,只是马上暂停。(在这里你可以做下音频预加载)
代码语言:javascript复制this.bgmusic.play();
setTimeout(function() {
self.bgmusic.pause();
});
写到这里,其实我很困了...
3、切换后台后背景音乐未停止播放
这个其实应该大家都遇到过,这里简单记录下解决方案:监听下 visibilitychange
事件,网页被挂起时暂停背景音乐即可。呼起时继续播放。
document.addEventListener("visibilitychange", function(event) {
if (document.hidden) { // 网页被挂起
beforePlayPaused = !self.bgmusic.isPlaying();
self.bgmusic.pause();
} else { // 网页被呼起
if (beforePlayPaused) {
return;
}
self.bgmusic.play();
}
});
除了坑,那还有一部分就是细节了。
细节一:下雪的效果
毕竟圣诞,毕竟小孩,有一个下雪的效果是不是小孩子更喜欢?
这里的效果很简单,我们可以使用 Canvas 绘制就可以,因为是全屏雪,所以这里可以把 Canvas 的层级放后一点,防止覆盖上面层级的用户操作。
实现思路
可以将雪花和下雪的效果拆分为两个实体类 Snowflake
和 Snow
。其中雪花可以给它一些 透明度
、 大小
、 水平和垂直方向速度
等属性,当然还有它的水平和垂直坐标,然后每帧更新下雪花的位置即可。甚至你可以给它来点风,让它看起来更真实。
下雪的时候以屏幕宽度为维度,设置雪花的数量用来控制雪的密度。如:
代码语言:javascript复制_initSnowflakes: function() {
var level = this.options.level,
baseNum = LevelEnum.hasOwnProperty(level) ? LevelEnum[level] : LevelEnum.middle,
snowflakesNum = parseInt(this.windowWidth / baseNum);
for (var i = 0; i < snowflakesNum; i ) {
this.snowflakes.push(new Snowflake(this.options));
}
}
然后绘制到canvas上,最后使用帧动画来改变雪花的位置来实现下雪的效果。
我已经将这部分代码进行了抽离并发布到github上,有兴趣的可以了解下: Canvas下雪效果[3]
性能优化点
- 使用
requestAnimationFrame
实现帧动画 - 雪花数量的控制
- 监听
visibilitychange
事件,在切换后台的时候暂停 Canvas 动画,因为在 Android 设备上切换后台后定时器还是在运行的。
细节二:骚气的进度条
你可能没注意到,在页面的左上角挂了一串彩灯,每进入一个场景,灯就会亮一盏。
好吧,这里我还是用canvas绘制的,但是时间不够,我本来可以做的足够好,而不是目前这么粗糙的效果...
需要注意的是彩灯亮了之后是一个渐变,这里使用了 createRadialGradient
径向渐变来绘制灯光效果。但是 iOS12.3
(忘记版本了反正是最新的)这个方法可以执行但是是无效的。还没有时间找找原因,这里简单做了个判断如果是IOS直接使用纯色填充。
if (lib.browser.os.android) {
fillStyle = context.createRadialGradient(locationX, locationY, radius, locationX radius, locationY radius, radius);
fillStyle.addColorStop(0, '#FFF');
fillStyle.addColorStop(1, '#FFF800');
} else {
fillStyle = '#FFF800';
}
细节三:滑雪橇的小老鼠和视差的背景 (远景、树木和雪地)
小老鼠使用了简单的 CSS 骨骼动画( 但是我觉得没啥区别,因为没做很精细 ),完整的骨骼动画至少需要将手拆成很细(如上肢、下肢、滑雪棍)、同时利用 CSS 做骨骼动画有个很坑的点,就是层级,自己可以去实现下。最好使用 Canvas 实现。至于视差其实给三个元素不同的速度即可,很简单。篇幅很长了,这里不展开讲。
细节四:渐变的轮播的文字
为了让文字轮播不至于很生硬,如果不加渐变,滚动的时候会出现文字裁剪的效果。 加上渐变后会让整个过程很流畅,但是实际上要实现这个效果并不简单。
你可以试着想一下,雪因为要不遮挡建筑和文字等,所以层级会放的比较低,所以这里的雪对应的canvas层级会比文字的层级要低,如果直接对文字容器两端新增遮罩并设置渐变或者高斯模糊(blur)的话虽然能起到遮罩效果。但是透明度不仅针对文字,对它下面层级的元素也同样有效果(因为这里文字容器需要设置为透明背景)。这样雪经过渐变的时候会出现穿透的效果,影响用户体验。
简单看个效果图(这里我把颜色放的比较深,更容易看见效果):
可以看到雪花被遮罩挡住了,也变得有渐变了。
解决方案:使用 -webkit-mask-image
,有兴趣的可以自己去了解下:
-webkit-mask-image: linear-gradient(to bottom,
rgba(255,255,255,0) 0,
rgba(255,255,255,.6) 6%,
rgba(255,255,255,1) 25%,
rgba(255,255,255,1) 75%,
rgba(255,255,255,.6) 85%,
rgba(255,255,255,0) 100%);
细节五:steps 逐帧动画
在这次的年终盘点中充斥着很多逐帧动画,使用 animation
的 steps
函数完成毕竟还是比较有限,我简单写了一个工具方法来完成我这次的效果:
var FrameAnimation = function() {
return this.initialize.apply(this, arguments);
};
FrameAnimation.prototype = {
initialize: function(frames) {
this.frames = Array.isArray(frames) ? frames : [];
this.timer = null;
this.delayTimer = null;
},
/**
* 执行完成之后的回调
* @param {Function} finish
*/
start: function(finish) {
var self = this,
frames = this.frames;
finish = typeof finish === 'function' ? finish : function() {};
if (!frames.length) {
finish();
return;
}
var runAnimation = function(frame) {
var animationFinish = function() {
if (frames.length === 0) {
finish();
return;
}
// 可能存在delay
self.delayTimer = setTimeout(function() {
self.delayTimer = null;
runAnimation(frames.shift());
}, parseInt(frame.delay, 10) || 0);
};
// 可能无法控制持续时间
if (!frame.hasOwnProperty('duration')) {
frame.animation(function() {
animationFinish();
});
} else {
var duration = parseInt(frame.duration, 10) || 0
frame.animation();
self.timer = setTimeout(function() {
self.timer = null;
animationFinish();
}, duration);
}
};
runAnimation(frames.shift());
},
/**
* 停止动画
*/
clear: function() {
this.frames = [];
if (this.delayTimer) {
clearTimeout(this.delayTimer);
this.delayTimer = null;
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
};
return FrameAnimation;
使用如下: 虽然简单,但是能满足我所有的需求了。
代码语言:javascript复制this.enterFrame = new FrameAnimation([
// 诞我有你
{
duration: 500,
animation: function() {
self.titleUpElem.addClass("show");
}
},
// 作伴前行
{
duration: 600,
delay: 100,
animation: function() {
self.titleDownElem.addClass("show");
}
},
// 老鼠飞入
{
duration: 200,
animation: function() {
self.mouseElem.addClass("show");
}
}
]);
其实其中还有很多细节,特别是对于 CSS 的使用,这里不一一列举,毕竟篇幅很长了,也很晚了。
开发效率
这种场景切换用 Layers
面板最适合了,嘿嘿...
涉及到动画比较多的场景,也可以通过一些现有的动画可视化工具进行参数调优,如:http://jeremyckahn.github.io/stylie/ 等。
当然,如果你只想调整贝塞尔曲线的参数: cubic-bezier.com 也许是一个不错的选择。
性能
因为动画性能的文章太多,这里规整下,不进行深入探讨。
- 雪碧图(尤其是动画效果特别多的活动页时特别重要)
- 图片的压缩(你可以通过 https://tinypng.com/ 在线压缩)
- 视频和音频资源文件的压缩(视频初始为:15M -> 1.5M,音频7.8M -> 851 KB)清晰度压完觉得还是大那就抽帧吧,再不行就缩短时间。
- 性能优化时间不够?开启实时帧率吧,关注掉帧
- 使用transform和opacity,减少重排(reflow)和重绘(repaint)
- 硬件加速尽量不要直接用在初始时,在开始动画的时候再使用,使用完成后及时关掉,类似这样,开始动画的时候追加class,动画完成后移除
@keyframes abcmouseMove {
0% {
transform: translate3d(0vw, 0vw, 0vw) skew(0deg);
}
25% {
/* skew一下,让老鼠有用力的感觉 */
transform: translate3d(25vw, 0vw, 0vw) skew(-5deg);
}
50% {
transform: translate3d(50vw, 0vw, 0vw) skew(0deg);
}
100% {
transform: translate3d(100vw, 0vw, 0vw) skew(0deg);
}
}
- 不要轻易使用will-change,除非实在没办法,使用完成后马上移除掉。
- 使用js动画requestAnimationFrame也不要忘记了哦~
- 不行,不想写了,要吐了...
更多CSS性能优化深入了解,可以看下我们结一(结衣)老师的这篇文章 搞定这些疑难杂症,向 CSS 3 动画说 Yes [4]
参考链接
- H5同层播放器接入规范 https://user-gold-cdn.xitu.io/2020/1/5/16f75f8933b03580
- Web Audio API https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API
- Canvas下雪效果 https://github.com/abcmouse-frontend/snow
- 搞定这些疑难杂症,向 CSS 3 动画说 Yes https://imweb.io/topic/5643850eed18cc424277050e
IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。
我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂、企鹅辅导 及 ABCMouse 三大产品。
扫码关注 腾讯IMWeb前端团队