Skip to content

Latest commit

 

History

History
494 lines (461 loc) · 20 KB

20180502-母亲节H5活动页实现总结-张泽.md

File metadata and controls

494 lines (461 loc) · 20 KB

PPT链接:母亲节活动H5复盘

总结文章如下(源代码见SVN库,Statics目录下):

距离活动页正式上线还有2天,心情比较复杂,需要写点东西沉淀下来。

首先交待一下背景

大概十几天前,产品妹子和运营妹子把我拉到了一个小黑屋,一脸坏笑地对我说,“母亲节快要到了,我们有一个活动页需要你支持一下”。我心想,“随便找个{易企秀}啊、{云凤蝶}啥的搭一下不就完了嘛,还要我来写一个?”。本着{能推就推}的原则,我也就暂且坐下来听听她们的需求细节。运营妹子说,“来,我给你看一个H5页面示例”,反手就拿出来了一个网易的活动链接,深夜,男同事问我睡了吗……,“我们就想做成这样子的一个效果,网上的那些模板我们都看过了,都做不了,所以就只要请大神帮帮忙”。哟呵,这首先拿了个大厂的案例给我来了个下马威,马上又大神大神的叫着,你说这我要是告诉人家“我不会”是多尴尬呀。“这很简单,不就一个H5吗,最多两个小时就给你搞定,不过你们得先把效果、图片啥的都准备好”

过了两天

需求大概确定了,运营妹子扔过来一个DEMO链接,“差不多就是这个样子了”。

布局

首先很明确这是一个独立的分页H5,每一页上会有一些动画或者视频,然后加上一个加载页,大概也就这些东西。

<!DOCTYPE html>
<html>
    <head>
    </head>
    <body id="wrapper"> 
        <section id="container">
            <section id="main-page-0" class="main-page loading current-page" data-index='0'></section>
            <section id="main-page-1" class="main-page" data-index='1'></section>
            <section id="main-page-2" class="main-page" data-index='2'></section>
            <section id="main-page-3" class="main-page" data-index='3'></section>
            <section id="main-page-4" class="main-page" data-index='4'></section>
            <section id="main-page-5" class="main-page" data-index='5'></section>
        </section>
    </body>
</html>
  • 总共就6个页面,分别是P0-加载页,P1-5
  • 每一个页面都平铺整个屏幕
  • 默认显示加载页display:block,其他所有页面都display:none
  • .current-page表示当前显示页面

如何保证每一页都平铺整个屏幕?

方案一:最开始想到的方案

.main-page{
    width: 100vw;
    height: 100vh;
}

感觉这样挺好,但是因为vwvh对于部分机型尚不兼容,所以不轻易采用这种写法。

方案二:后来看了一下{易企秀}的实现方案

body{
    width: 100%;
    min-height: 100%;
    position: fixed;
}
.main-page{
    width: 100%;
    height: 100%;
}

实现翻页

用户在滑动时,需要隐藏当前页面,显示上/下一页。借鉴项目以前的代码:需要在页面初始化后调用initTouch()给body绑定touch相关事件

    var $body = document.getElementsByTagName('body')[0];   // body
    // 页面触摸事件
    var touchStartListener = function (event) {
        var touches = event.targetTouches;
        if (touches.length == 1) {
            this.x = touches[0].clientX;
            this.y = touches[0].clientY;
        }
    }
    var touchMoveListener = function (event) {
        if(!isAllowTouch) return false;
        var touches = event.targetTouches;
        if (touches.length == 1) { //一个手指在屏幕上
            var x1 = touches[0].clientX, //移动到的坐标
                y1 = touches[0].clientY;
            var deltaY = y1 -this.y;
            var deltaX = x1 -this.x;
            if ((Math.abs(deltaY) < Math.abs(deltaX)) && (deltaX < 0) &&((x1 + 20) < this.x)){
               // 左滑
            }
            if ((Math.abs(deltaY) < Math.abs(deltaX)) && (deltaX > 0) &&((x1 - 20) > this.x)){
                // 右滑
            }
            if ((Math.abs(deltaY) > Math.abs(deltaX)) && (deltaY < 0) &&((y1 + 20) < this.y)){
                // 上滑
            }
            if ((Math.abs(deltaY) > Math.abs(deltaX)) && (deltaY > 0) &&((y1 - 20) > this.y)){
                // 下滑
            }
        }
    }
    var touchEndListener = function (event) {}
    var touchStartListenerThrottle = _throttled(touchStartListener, 50);
    var touchMoveListenerThrottle  = _throttled(touchMoveListener,  50);
    var touchEndListenerThrottle   = _throttled(touchEndListener,   50);
    var initTouch = function () {
        $body.addEventListener("touchstart", touchStartListenerThrottle);
        $body.addEventListener("touchmove", touchMoveListenerThrottle);
        $body.addEventListener("touchend", touchEndListenerThrottle);
    }
    var removeTouch = function () {
        $body.removeEventListener("touchstart", touchStartListenerThrottle);
        $body.removeEventListener("touchmove", touchMoveListenerThrottle);
        $body.removeEventListener("touchend", this.touchEndListenerThrottle);
    }

我们是用touchstarttouchmove/touchend来模拟滑动事件,理论上touchmove会持续(节流函数下会隔一段时间)触发,因此在实际情况下一次滑动仅且只能翻一页。节流函数如下:

// 节流函数
function _throttled(func, wait, options){
    // throttled节流
    var timeout, context, args, result
    var previous = 0
    if (!options) options = {}

    var later = function() {
        previous = options.leading === false ? 0 : Date.now()
        timeout = null
        result = func.apply(context, args)
        if (!timeout) context = args = null
    }

    var throttled = function() {
        var now = Date.now()
        if (!previous && options.leading === false) previous = now
        var remaining = wait - (now - previous)
        context = this
        args = arguments
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout)
                timeout = null
            }
            previous = now
            result = func.apply(context, args)
            if (!timeout) context = args = null
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining)
        }
        return result
    }

    throttled.cancel = function() {
        clearTimeout(timeout)
        previous = 0
        timeout = context = args = null
    }

    return throttled;
}

全屏自适应

每一页全屏实现了,但是如何实现每一屏内容自适应。 首先将屏幕分为三层:背景底层(纯色)、背景层、内容层(图片、文字、动画、视频等)。

  • 对于背景底层,只需设置背景色即可background-color
  • 对于背景层,分两种情况,有些需要平铺整个屏幕background-size:cover,有些则主要贴底background-position:bottom center,代码如下:
#main-page-3{
    background-image: url('../imgs/m/p1_2_3_shadow.png'), url('../imgs/m/p3_vision.png'), url('../imgs/m/p3_bg.png');
    background-size: cover, 100% auto, cover;
    background-repeat: no-repeat, no-repeat, no-repeat;
    background-position: center, bottom center, center;
}
  • 对于内容层,需要参照.main-page绝对定位显示即可

模拟加载进度

理论情况下我们是根据整个H5页面资源的加载情况来输出加载进度,但是实际情况下是很难准确的获取到这些信息的,因此大多数情况下的H5加载都是模拟的,这次的H5因为涉及到视频的播放,所以可以认为视频加载完成,则页面就加载完成,而在此之前都随机增长:

// 随机生成加载进度,模拟视频加载
function _randomProgress(initValue, step, duration, maxValue){
    var num = initValue;
    var tid = setTimeout(function () {
        num += parseInt(Math.random()*step);
        if(num >= maxValue){
            num = maxValue;
            clearTimeout(tid);
            tid = null;
            $('#loading-progress').text(num);
        }else{
            _randomProgress(num, step, duration,maxValue);
        }
        $('#loading-progress').text(num);
    }, duration);
}

页面切换和动画

首先我用了一个全局变量currentPageIndex来定位当前页面,当触发页面滑动时,首先隐藏当前页面,改变currentPageIndex的值,显示当前页面,显示当前页面的动画。

其次可以明确的是页面切换动画是逻辑上独立的两个过程,但是在实际情况中,两者是存在回调的关系的,例如页面切换后,就需要执行当前页面的动画,动画结束定格几秒后,就需要进行页面切换。其中前面还可以通过手动触发翻页来完成。

// 页面切换动效
var handlePageSwitch = function (step) {
    if((currentPageIndex <= 1 && step === -1) || (currentPageIndex === 5 && step === 1)) return false; // 限制页面滑动边界条件
    isAllowTouch = false;
    var $currentPage = $('#main-page-'+currentPageIndex);
    $currentPage.hide();
    currentPageIndex = currentPageIndex + step;
    pageAnimate(step);
}
// 页面动画
var pageAnimate = function (step) {
    var fadeInTime = 500;
    var $currentPage = $('#main-page-'+currentPageIndex);
    console.log('P' + (currentPageIndex-step) +'-P' + currentPageIndex);
    switch('' + currentPageIndex + step){
        // 0->1
        case '11':
            $currentPage.fadeIn(fadeInTime, function () {
                video.play();
                isAllowTouch = true;    // 测试
            });
            break;
        // 1->2
        case '21':
            video.currentTime = 0;
            video.pause();
            $currentPage.addClass('scale-page');
            $currentPage.fadeIn(fadeInTime, function () {
                console.log(1);
                setTimeout(function () {
                    $('#main-page-2 .words').fadeIn();
                }, 256)
                isAllowTouch = true;
            });
            break;
        // 2->3
        case '31':
            $('#main-page-2 .words').hide();
            $currentPage.removeClass('scale-page');
            $currentPage.fadeIn(fadeInTime, function () {
                $('#main-page-3 .words').fadeIn();
                isAllowTouch = true;
            });
            break;
        // 3->4
        case '41':
            $('#main-page-3 .words').hide();
            $currentPage.fadeIn(fadeInTime, function () {
                isAllowTouch = true;
            });
            break;
        // 4->5
        case '51':
            $currentPage.fadeIn(fadeInTime, function () {
                isAllowTouch = true;
            });
            break;
        // 5->4
        case '4-1':
            $currentPage.show();
            setTimeout(function () {
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 4->3
        case '3-1':
            $currentPage.show();
            setTimeout(function () {
                $('#main-page-3 .words').fadeIn();
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 3->2
        case '2-1':
            $('#main-page-3 .words').hide();
            $currentPage.removeClass('scale-page');
            $currentPage.show();
            setTimeout(function () {
                setTimeout(function () {
                    $('#main-page-2 .words').fadeIn();
                }, 256)
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 2->1
        case '1-1':
            $('#main-page-2 .words').hide();
            $currentPage.show();
            video.play();
            setTimeout(function () {
                isAllowTouch = true;
            }, fadeInTime);
            break;
        default:
    }
}

这里在页面动画中,我将各种具体情况都分条进行了处理,即“P0-P1,P1-P2,P2-P3,……,P5-P4,……”。所以当进入某一页时,需要把前一页的动画隐藏。即页面之前的动画处理逻辑耦合了。所以当需求发生变化的时候,例如页面动画时间变一变、渐入时间变一变,就很难维护,逻辑很混乱。

梳理后,代码如下:

// 页面切换
// @params step 1-前进;0-后退
var _pageChange = function(step){
    if((currentPageIndex <= 1 && step === 0) || (currentPageIndex === 5 && step === 1)) return false; // 限制页面滑动边界条件
    isAllowTouch = false;                                           // Step0:页面滑动状态设为false
    // var pageFlag = '' + currentPageIndex + step;
    $currentPage().hide();                                          // Step1:上一个页面直接消失
    _initPageAnimation();                                           // Step2:初始化当前页面内容及动画
    currentPageIndex += step ? 1 : -1;                              // Step3:页面索引改变
    $body.style.backgroundColor = bgColorArr[currentPageIndex];     // Step4:修改页面背景色

    $currentPage().fadeIn(pageSwitchFadeInTime, function () {       // Step5:页面渐入或+缩放动画
        if(currentPageIndex === 1 && step) audio.play();
        _pageAnimation();                                           // Step6:页面动画
        isAllowTouch = true;                                        // Step7:页面滑动状态设为true
    });
}

只需管理当前页面的动画效果,增加一个页面动画初始化的逻辑:

// 初始化页面动画/页面回复
var _initPageAnimation = function () {
    $('.init').hide();
    video.currentTime(0);
    video.pause();
}
// 页面动画
var _pageAnimation = function () {
    if(currentPageIndex != 0 && currentPageIndex != 5) $('#arrow').show();  // 左滑箭头显示
    switch(currentPageIndex){
        case 0:
            break;
        case 1:
            $('#arrow').show();
            video.play();
            break;
        case 2:
            $('#main-page-2 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 2) return false;
                setTimeout(function () {
                    if(currentPageIndex != 2) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 3:
            $('#main-page-3 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 3) return false;
                setTimeout(function () {
                    if(currentPageIndex != 3) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 4:
            $('#main-page-4 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 4) return false;
                setTimeout(function () {
                    if(currentPageIndex != 4) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 5:
            $('#arrow-up').show();
            break;
        default:
    }
}

编码前没能做好设计!后期有时间考虑借助面向对象思想实现

视频处理

因为之前从未接触过H5下视频播放的问题,但是一经手才发现处理好视频真不是一件容易的事情。

方案一:使用原始video标签 在分析上面提到的那个网易的H5页面时,发现他用的也是原生的video标签(现在想来,如果利用了什么库,最后呈现在浏览器的也可能是video标签),只不过我发现其中有这么一段代码(混淆后):

var c = new XMLHttpRequest;
    c.open("GET", r, !0),
    c.responseType = "blob",
    c.onload = function() {
        if (200 === this.status && "video/mp4" === this.response.type) {
            var i = this.response
                , a = (window.URL || window.webkitURL || window || {}).createObjectURL(i);
            n(s),
            e(l),
            o.src = a
        } else
            t()
    }
    ,
    c.onerror = function(e) {
        console.log(e),
        t()
    }
    ,
    c.send()

发现用到了一个blob对象,虽然不明觉厉,自己也手把手的用上了:

// 视频初始化
var _initVideo = function (cb) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', videoPath, true);
    xhr.responseType = 'blob';
    xhr.onload = function() {
        if (200 === this.status && "video/mp4" === this.response.type){
            var res = this.response;
            var url = (window.URL || window.webkitURL || window || {}).createObjectURL(res);
            video.src = url;
        }else{
            video.src = videoPath;
            videoLoaded = true;
            cb && cb();
        }
    }
    xhr.onerror = function(e) {
        console.log(e);
        video.src = videoPath;
        videoLoaded = true;
        cb && cb();
    }
    xhr.send();
    video.addEventListener('loadstart', function () {
        videoLoaded = true;
        handlePageSwitch(1);
        cb && cb();
        console.log('video start load.');
    });
    video.addEventListener('ended', function () {
        // 视频播放结束
        console.log('video is over!');
        handlePageSwitch(1);
    });
}

本来视频播放也没啥问题,但老是提心吊胆,感觉早晚出bug。

方案二:借助video.js库 后来就换了种实现方式——借助video.js,重新写了下视频的逻辑:

var _initVideo = function () {
    video.ready(function () {
        videojs.log('Your player is ready!');

        this.on('loadedmetadata', function () { // 因为iOS下video无法自动加载,因此没办法触发canplaythrough-当浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的音频/视频时,会发生 canplaythrough 事件。
            if(isVideoLoaded) return false;
            setTimeout(function () {
                console.log('loading@ '+100+'%');
                $loadingProgress.text(100);  // 进度置为100
                isVideoLoaded = true;
                setTimeout(function () {
                    _pageChange(1);
                }, 1000);   // 加载完成1s后进入下一页
            }, 2000);
        });

        this.on('ended', function() {
            videojs.log('Awww...over so soon?!');
            setTimeout(function () {
                _pageChange(1);
            }, 2000);   // 视频播放完成2s后进入下一页
        });
    });
}

发现两个问题:

  • video在iOS下无法自动加载(暂未解决,将触发条件移至loadedmetadata) 理论上,当音频/视频处于加载过程中时,会依次发生以下事件:loadstart、durationchange、loadedmetadata、loadeddata、progress、canplay、canplaythrough

  • 部分机型(如红米、部分iPhone6)播放黑屏 发现是分辨率过高的问题,调整后可以播放。

视频定位

还有一个棘手的问题是,我们的视频是需要在一个容器内播放的,如何将video定位到容器内需要考虑。

首先和设计师沟通,我们制作的视频尺寸是1:2(mix2录制),那给我的容器框也应该是1:2的。

其次因为要满足各手机尺寸自适应的问题,我需要将容器作为背景贴底居中显示,那此时我只需要将video外包裹一个div相对屏幕绝对居中。

如何保证div的比例,借助padding相对于父容器width实现。

.video{
    width: 67%; // 父容器width的67%
    height: 0;
    padding-bottom: 134%; // 父容器width的134%
}

小结

总体下来,本以为2个小时就能搞定的,最终因为各方面因素做了持续10天左右(当然主要原因还是需求不断变更的问题,哈哈),但是这个“小”的H5,最终还是没能做到尽善尽美,优化之路还很长。