聊聊双11互动主玩法中前端技术亮点

发布于 大漠

在上一篇《手淘Web页面Bar和纵向适配的设计》中聊了互动项目中Bar的工业化标准设计以及对刘海设备带来的变化。我把这一点称为标准化Bar设计给适配带来的优势。今天这篇文章中主要想再花点时间聊聊今年手淘“PK赢能量”互动项目中前端有哪些技术亮点和尝鲜。当然文章涉及到的技术点估计有很多同学都有接触或者使用过。毕竟CSS仅仅是一门表现层语言。废话不多说,直接进入主题吧!

面对场景

估计有很多同学已经参与“PK赢能量”互动游戏中,但还是花一点点时间聊一下技术面对的场景(指的是CSSer),这样更好开始讲述我们今天的故事。

大家不难发现,为了营造双十一过节的氛围,设计往往都是非常热情,奔放的。颜色多,颜色艳等等。这对于Web开发人员是件头痛的事情,为什么头痛呢?比如一个开发面对这些场景时:

undefined

边框是渐变的都算了,还是七彩的,还是七彩的,想哭!

undefined

上图的场景相对而言比第一张要简单多了,但对于带阴影的Tooltips,还是令人生畏的,特别还是带渐变的三角。

一个破提示框,除了带阴影都算了,小三角还是不规则的!昵玛,不规则都算了,还有渐变!还让不让前端活!估计此时前端对视觉设计师应该是这样的:

undefined

其实前端对视觉还是很有爱的,因为前端耐操。

undefined

既然都这么皮实了,那就再撸一发,顺便用代码把下面的也解了:

undefined

图片能省就省嘛,这些都是钱!说不定老板一高兴,给你加薪晋级!

undefined

其实我们是一群有追求的人,越难的事情,对我们来说越具挑战性,越有那么一种冲动。

undefined

谁说不是呢?这叫作!人家说不作死不会死!

其实类似上面的场景,对于互动团队的同学而言是家常便饭,见怪不怪了!而往往面对这样的场景,大家第一意识形态就是我用图片解决一切!现在谁还差那么一点带宽呢?包个月,几十G来了,解决一切

谁说不是呢?但很多时候用图片也有用图片难处:

  • 难适应产品多变的需求
  • 难扩展,总不可能备好成千上万种尺寸的图吧
  • 难维护,这么多图,哪是用哪
  • 浪费资源
  • 影响性能
  • 等等... 想到了再继续加

我还是想打破一下规矩,借着双十一大促的活动来验证一些技术点。因为:

** 扛得住双十一的,还有什么不能扛呢?以后可以说,咱绝对耐操!**

渐变边框

我把七彩的边框称为渐变边框,这样显得更为专业一点。通过设计图,不难发现,边框采用了渐变颜色,如下图所示:

undefined

对CSS了解的同学,要实现类似这样的渐变边框效果,首先会想的是CSS的 border-image 属性。的确如此,我首先想的也是该属性,而且该属性可以很容易实现类似的效果,比如:

.gradient-border{
    border: 5px solid transparent;
    border-image: linear-gradient(to bottom, #0099CC, #F27280);
    border-image-slice: 1;
}

效果如下:

undefined

虽然border-imagelinear-gradient()配合在一起,能实现渐变的边框效果,但它也有一定的缺陷性,比如我们项目中的按钮是带圆角的。那么对于这种情形,就算是你使用了border-radius也是无用:

undefined

这是因为border-image中引用的是一张不带圆角的图片(linear-gradient()就相当于一张背景图)。也就是说,如果你需要一个带圆角的渐变边框,那么使用border-image是有局限性的,除非人肉为其准备带圆的背景图,或者有更好的办法通过代码绘制一个带圆角的背景图~

此路似乎在这个项目中行不通,只能考虑换用别的方法。仔细一想,我可以把带有渐变边框的元素分成两层:

undefined

这样一来似乎要容易的多了,一层一个元素:

<div class="gradient-border">
  	<div class="content"></div>
</div>

甚至我们还可以通过伪元素::before::after来模拟一个层。比如下面这个示例:

.gradient-border {
    --borderWidth: 5px;
    border-radius: var(--borderWidth);
    background: #fff;

    &::before {
        content: '';
        position: absolute;

        top: calc(-1 * var(--borderWidth));
        left: calc(-1 * var(--borderWidth));
        height: calc(100% + var(--borderWidth) * 2);
        width: calc(100% + var(--borderWidth) * 2);
        background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
        border-radius: calc(2 * var(--borderWidth));
        z-index: -1;
    }
}

效果如下:

undefined

是不是感觉越来越接近设计稿了:

undefined

加个元素或加个伪元素总是那么的不舒服,那怎么办呢?有没有更好的方案。其实CSS的世界是很有魅力的,只要你敢去想,有很多东西你意想不到。

既然可以分成元素层让两个渐变或两张图叠加在一起做一个差值,从而实现效果。那么为什么不可以直接在背景中采用两个层(两张背景图)叠加在一起

这是一个很好的方案,也是一个大胆的思路。到目前为止,CSS的多背景方案已经是一个很成熟的技术方案。这样一来,咱们就可以在background来做我们想要的效果了:

.gradient-border{
    background-image: 
        radial-gradient(circle at 50% 0%, #fff000 50%,#ffcd00 100%),
        linear-gradient(101deg, #ffc46d, #fa0055);
    background-origin: border-box;
    background-clip: padding-box, border-box;
}

每一个关键点的颜色我们都可以在设计稿中获取:

undefined

很多时候都不用这么复杂,如果你的稿子是Sketch的话,就更简单了,你可以直接从设计稿中把相应的样式复制过来。这也就是我为什么喜欢Sketch的原因。

就是这么的简单,效果出来了,只需要设计师点个头了:

看上去很简单吧!不知道你是否有想到过这样的方案?方案虽然简单,但这里有几个点需要特别的强调:

  • 运用多背景时,第一个背景的层级最高,显示在最前面
  • background-origin设置为border-box
  • background-clip设置为padding-box(也可以是content-box),但模拟边框的部分需要是border-box

这里最为关键的就是 background-originbackground-clip 灵活的配合在一起使用。至于这两个属性如何使用,就不在这里科普了,感兴趣的同学可以自己去查阅相关文档。

咱们进一步思考一下,如果有一天,设计师或者需求方想要的效果不是规则图形,或者说想要的渐变边框能带动效的。会不会继续让我们蒙蔽呢?在CSS中,虽然animationtransition能让你的元素动起来,而且效果还能不错,但在CSS中的动画也是有一定的局限性的,到目前为止,很难在背景图像(这里说的是gradient相关属性绘制的背景图)做动画。

**简单地说,使用animationtransition很难改变渐变的状态**。(但借助CSS Houdini还是可以做到的)。这已经超出我们今天这篇文章探讨的范围。我们还是回到今天的主题上来。

为了尽量的满足设计师的需求,就算是不规则的渐变边框或者让你的渐变边框动起来,我们也要想办法。这里给大家推荐一个未来的CSS特性:clip-path 。这个属性到目前为止已经得到近80%主流浏览的支持。在不久的未来,而对这样的需求,我们就可以很轻易的实现。比如:

background: linear-gradient(120deg, #00F260, #0575E6, #00F260);
  background-size: 300% 300%;
  clip-path: polygon(
    0% 100%, 
    3px 100%, 
    3px 3px, 
    calc(100% - 3px) 3px, 
    calc(100% - 3px) calc(100% - 3px), 
    3px calc(100% - 3px), 
    3px 100%, 
    100% 100%, 
    100% 0%, 
    0% 0%);

甚至你配上animation可以让你的渐变边框动起来:

你也可以借助Clippy工具,绘制你想要的不规则图形,再把渐变运用上去,可以得到很多不规则的渐变边框效果:

当然,clip-path也有他的局限性,因为其目前支持的绘制图形的函数有限,只有polygon()circle()ellipse()rect()等。如果要绘制类似我们设计稿中的按钮,还是无法达到目标的。

提示框

对于提示框的绘制,其实没有啥技术含量在里面,这里较为蛋疼的是,如查提示框是带有阴影的,那么处理起来还是有一些细节的。

把提示框的阴影拆分出来,借助::before::after的伪元素来模拟box-shadow。另个三角通过一个矩形旋转来处理:

通过一个小动画来回放提示框阴影效果的处理:

上面看到的效果仅仅是纯色的,很多时候背景色是渐变的。我们来看一个简单的小示例,看看渐变的是如何做出来的:

再一次动画回放一下实现原理:

此处有一个调试小细节,就是三角的颜色和主体渐变色的连接,如果从设计稿上不好获取的话,可以通过浏览器调试工具中的颜色拾得器获取,像下面这样操作,拾起主体连接处颜色。

对于阴影的处理,除了box-shadow属性之外,还可以使用filter:drop-shadow或者配合filter: blur相关属性也能得到较好的阴影效果。

有关于CSS中的阴影处理的细节,网上有一篇文章介绍得非常详细,值得花时间阅读一下

或许很多同学会问,为什么要用伪元素来做阴影呢?直接在元素上使用box-shadow不就可以?如果你仔细看了上面动画,不难发现,如果阴影直接在元素上使用box-shadow的话,对于小三角形的阴影是较难处理的。除此之外还有一个最为关键的原因,如果在阴影上想做点小动效的话,那么会有性能问题存在。因为**box-shadow的动画变化会损害性能** 。如果要实现最小的重新绘制,应该创建一个伪元素并对其opacity元素进行动画处理,使其以每秒60帧的动画模仿运动物体相同的效果。比如像下面这样使用:

/* 设置更大的阴影并将之隐藏 */ 
.make-it-fast::after { 
    box-shadow: 0 5px 15px rgba(0,0,0,0.3); 
    opacity: 0; 
    transition: opacity 0.3s ease-in-out: 
} 
/* 鼠标悬停时实现更大阴影的过渡显示 */ 
.make-it-fast:hover::after { 
    opacity: 1;
}

来看个效果对比:

认真观察这个实例,比较我们在其中使用的不同技巧。你是不是会说两者效果看起来一样。唯一不同的是我们如何应用阴影并对其进行动画处理。在左边实例中,我们鼠标hover(悬浮)时,对box-shadow应用了动画效果。而在右边的实例中,我们用::after添加了一个伪元素并对其设置了阴影,并对该元素的opacity元素进行了动画处理。

如果你使用开发工具尝试了其中之一,您应该会看到类似这样的东西 (绿色条表示已经绘制,其越少越好):

当你悬停在左边的卡片(在box-shadow上应用动画)与悬浮在右边的卡片(对其伪元素的opacity应用动画)进行相比时,你会很明显的发现有更多的重新绘制。

上面看到的提示框都是有规则的,但有的时候,我们的提示框下面不是一个小三角形,是其他的形状,比如像下图这样的:

而对这样的效果,使用CSS绘制是需要有一定耐力的,只是耗时间,但实现原理并不复杂,最简单的就是借::before::after绘制两个不同的矩形叠加在一起,然后使用border-radius让形状看起来像我们想要的效果。这里就不贴代码了,感兴趣的同学,自己可以动手尝试一下,或者阅读@Nicolas Gallagher大神早期写的一篇博客《Pure CSS speech bubbles》。

绘制图形

CSS现在的具备的能力越来越强大,时至今日,我们项目中的很多东西都可以直接通过代码来完成,比如下图中这些东东:

设计稿中还有一些,上图没有全部罗列出来,这里抽几个典型的案例来说。

对于电商行业而言,优惠卷是必不可少的一个东东。我们每次做互动项目,涉及到奖品的时候,都会离不开这个东东。

上图是不是感觉很眼熟。

以往实现这个效果,很多时候都是直接采用背景图片来做的。这次不同,同样采用代码来完成背景图相关的事项。而且这个背景图是带有渐变的。代码和原理都非常的简单:

div {
    min-width: 702px;
    min-height: 160px;
    border-radius: 12px;
    background-image:linear-gradient(to bottom, #FF2655 0%, #FF4F26 100%), linear-gradient(to right, #fff, #fff);
    background-size: 210px 100%, cover;
    background-repeat: no-repeat;
    background-position:right center;
    position: relative;

    &::before,
    &::after {
        content: '';
        position: absolute;
        width: 20px;
        height: 20px;
        background: radial-gradient(circle, #6c00af 50%, transparent 50%),
        radial-gradient(circle, #6c00af 50%, transparent 50%);
      background-size: 20px 20px;
        right: 200px;
    }
    &::before {
        top: -10px;
    }
    &::after {
      	bottom: -10px;
    }
}

执行上面的代码,你看到的效果将会是像下图:

基于这个原理,那么其他形式的背景图都不难处理了。上面这种方法还不是实现内凹角的最佳方案。随着CSS的maskingSVG技术越来越成熟之后,我们就可以采用mask和SVG的结合,实现任意形状的内凹角效果,比如下图这样的:

有关于这方面的介绍可以阅读@ANA TUDOR的《Scooped Corners in 2018》一文。中文推荐阅读《CSS如何实现内凹角效果》一文。

再来看PK进度条的效果:

最早采用的方案是使用渐变来完成:

div  {
    width: 608px;
    height: 90px;
    border-radius: 45px;
    position: relative;

    &::before,
    &::after {
      	content: '';
      	position: absolute;
      	left: 0;
      	right: 0;
      	top: 0;
      	bottom: 0;
    }

    &::before {
      	background: 
            linear-gradient(0deg, #FF1515 0, #FF1515 0) top left, 
            linear-gradient(300deg, transparent 90px, #FF1515 0) bottom right;
        background-size: 51% 100%;
        background-repeat: no-repeat;
        border-radius: 45px 0 0 45px;
        z-index: 2;
    }

    &::after {
      	background:
            linear-gradient(0deg, #2F5FFC 0, #2F5FFC 0) top right, 
            linear-gradient(60deg, transparent 90px, #2F5FFC 0) bottom left;
        background-size: 51% 100%;
        background-repeat: no-repeat;
      	border-radius: 0 45px 45px 0;
    }
}

虽然外形看上去和我们的效果很类似,但实际还是有很大的差距的,仔细对比不难发现,设计稿的渐变是从上往下进行渐变。如果我们按照设计稿的渐变方式来写的话,就很难使用linear-gradient绘制带有透明区域的斜切角。针对这种现象,斜切通过transform来完成:

效果是不是更有质感了。

CSS绘制Icons已经不是新东东了。记得在15年的CSS Conf大会上看到Adobe的设计师@文婷分享的一个话题,就是使用CSS绘制Icons:

在互联网上有关于这方面的案例还有很多。至于如何绘制实现,这里就不做过多的阐述了。回到我们的主题中来。项目中也有对应的一些Icon,而且这些Icon我们也可以使用CSS来绘制。比如:

类似于这些Icons我们都是可以使用CSS直接绘制出来的。需要注意的一点是,比如绘制箭头需要注意圆角处理。比如:

状态控制

这次项目还有另一个特色,就是活动页底部Bar状态多样化:

除了逻辑复杂之外,展示风格较为复杂。

比如提示框在不同状态下位置的控制,按钮位置控制等。以往控制这些展示网格,一般情况下都是结合逻辑一起来处理,在不同的状态下给容器添加不一样的类名。但这次和逻辑解耦,直接通过CSS来判断。比如提示框的展示:

.action {
    margin: 0 30px;
    position: relative;

    &:first-child .tooltip {
        left: 0;
        transform: none;

        &::before {
          	left: 60px;
        }
    }

    &:last-child .tooltip {
        left: auto;
        right: 0;
        transform: none;

        &::before {
            left: auto;
            right: 60px;
        }
    }

    &:first-child:last-child .tooltip {
        left: 50%;
        right: auto;
        transform: translate(-50%, 0);

        &::before {
            left: 50%;
            transform: translate(-50%);
        }
    }
}

是的,其实就是这么简单,仅仅通过CSS的选择器来控制了.tooltip展示位置。对于按钮的展示相对而言没有那么复杂,因为我们的布局采用的是Flexbox布局,客户端可以自动帮我们做相应的计算。只不过这里有一个额外的需求,在猫客不需要展示“进入群聊”按钮,有这个群聊按钮的要进行绝对定位,距离屏幕左侧有一个固定的值。

.action-position {
    position: absolute;
    left: 6px;
    top: 30px;
}

.action-position + .is-group-left {
    margin-left: 160px;
    margin-right: 10px;
}

一次失败性的尝试

在项目开始的时候,就打算使用CSS的自定义属性来进行开发,因为CSS的自定义属性可以帮助我节省很多的代码量,特别是在按钮、提示框和模态弹框上的运用。我只需要声明几个自定义属性,在调用的时候局部修改已定义好的自定义好的属性即可。这样一来既好维护,又能省事不少。

直到项目提测之后,发现在iOS8.0的系统下对CSS的自定义属性会失效。这样不得不重新将自定义属性重新覆盖掉。

在未来不久之后,不需要兼容iOS8.0系统的话,我们就可以大胆的在项目中使用CSS自定义属性了。

除此之外,在Vue项目中适配iPhoneX刘海机型时,直接使用calc()env()时,在编译过程中会报错。如果你碰到这样的现象,可以采取迂回战术。先声明一个自定义属性,然后在calc()中和var()结合一起使用即可:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
    --safe-area-inset-bottom: env(safe-area-inset-bottom);
    --safe-area-inset-top: env(safe-area-inset-top);
}
.nav-bar ~ .content {
    padding-top: calc(var(--safe-area-inset-top) + var(--navBarHeight));
}
.footer-bar ~ .content {
    padding-bottom: calc(var(--safe-area-inset-bottom) + var(--footerBarHeight))
}

为了避免对自定义属性不支持的设备,建议把上面的代码放置到@supports()函数中执行。

标准化设计Bar

根据工业标准化的标准对Bar进行设计,这样做更有利于对刘海机进行通用适配。有关于这方面的介绍可以阅读《手淘Web页面Bar和纵向适配的设计》一文。

纵向适配的尝试

用户终端屏幕纵向适配一直困惑着我自己,并且一直在探讨这方面的最佳技术解决方案。可惜的是,直到现在还没有找到一个最佳或者通用的技术方案。不过在这次项目中,对于一屏示的页面,为了更好的适配高屏和短屏的设备,我们在部分元素之间的间距采用了vh做为单位,同时配合不同的媒体查询对重要元素(位置有明显差异化)进行特殊处理。比如:

@media 
    only screen and (min-width : 375px) 
    and (min-height : 812px) 
    and (-webkit-device-pixel-ratio : 3){
    @supports (padding-bottom:env(safe-area-inset-bottom)){
        .page-invitation-help {
            --safe-area-inset-top: env(safe-area-inset-top);
            padding-top: var(--safe-area-inset-top);
          }

        .page-invitation {
          	margin-top: 140px;
        }

        .page-title {
          	top: -80px;
        }

        .page-invitation .page-footer {
          	bottom: -90px;
        }
    }
}

来不及尝试的srcset

移动终端众多已不是什么怪事了,不同的终端有不同的分辨率,不同的DPR,但我们现在不管针对什么样的终端都在采用@2x(DPR为2)资源。比如背景图片,产品图片,装饰元素等。这样一来,对于还在使用@1x屏的用户是不友好的,人家本来不需要那么多带宽来加载资源,咱就这样强奸了人家;同时对于高于@2x的用户(比如@3x@3.5x@4x)也不友好,给人提供的图片不是最清晰图片。

那么给你的用户提供最佳的图片资源其实是我们应该探讨和思考的问题。原本想在这个项目中使用img元素的新属性srcsetsizes,达到真正意义上给用户终端提供最正确的图片资源。

<img 	srcset="/source-375@1x.jpeg 1x, /source-375@2x.jpeg 2x, /source-375@3x.jpeg 3x" src="/source-375@1x.jpeg" alt="Load the required images" />

由于前期调研不够充分,错过了在项目中尝试srcset技术方案!

除了img中的srcsetsizes方案以外,HTML5的<picture>元素也可以达到类似的效果。

不管使用哪种方案,如果想做到给终端提供最正确的图片资源,那么运用在background-image的图片就要改变使用方式,这也面临着图片的适配处理

虽然这次未能尝试,但机会还是很多的,希望能在下次的项目中尝试使用,然后再跟大家分享使用心得。

有关于如何给设备终端提供正确的图片资源更详细的介绍,可以阅读前段时间整理的《给Web页面提供正确图像的姿势》一文。

总结

写到这里,总算是结束了,零零总总写了不少。总结了项目中一些自己使用心得,特别是如何利用一些新技术来替代图片,从而尽可能的减少项目资源的加载,另外有哪些技术的运用能让我的开发越来越轻松。或者将来的技术能给我们或者我们的用户带来的一些变化,实实在在的变化。

文章涉及到的点仅是个人观点,仅供参考。如果有何不对之处,烦请路过的大婶拍正,如果你有其他想分享的建议或经验,欢迎在下面的评论中与我一起共享!(^_^)Wmns Air Jordan 1 Retro High OG 'Twist'