移动端重构实战系列:0-4 章

2,169 阅读13分钟
原文链接: mp.weixin.qq.com

本文系来自腾讯imweb团队 结一大大 关于移动端重构经验以及思想的实战系列,推荐点击左下角的阅读原文。

”本系列教程为实战教程,是自己移动端重构经验及思想的一次总结,也是对sheral UI的一次全方位剖析,首发在imwebw3cplus两大站点及“前端Talk”微信公众号,其余所有标注或没有标注来源的均为转载。“

——imweb 结一

0.sandal & sheral


sandal是什么

简单来说,sandal是基于sass的一个移动端css的基础库,提供了一些基础的重置,常用的mixin,如flex布局,等分,水平垂直居中,常用图标等,基于它你可以非常方便快速地扩展出你需要的UI组件,其整体结构设计如下图:

_function.scss集成了所有的基础功能,并且不带任何样式,而_core.scss则在function的基础上加入了重置样式,ext文件夹则是三个扩展文件,可根据个人需要自由导入,具体介绍及使用请参考sandal 文档

sheral是什么

sheral是基于sandal扩展的UI组件库,目前包括了btn,dialog,header,card,form,toast,line,media,progress等常用的25+组件。你可以直接调用,也可以根据自己的需求定制你的组件。

所有组件文件均可以在sheral components中查阅,demo效果见sheral UI

sandal与sheral的关系,就如jquery与其插件的关系。所以退一万步说如果sheral的UI真的不合你意,你也可以基于sandal提供的基础功能,快速构建一套你自己的UI库。这也是我把这两个分开开发的原因。

PS:sheral目前只专注重构这块,所以js写得比较简略,只是为了简单演示使用,同时欢迎感兴趣的小伙伴加入重构或转成其他js组件库。

放肆还是克制

理清了这两者关系之后,这里扯出另一个话题,UI库的度在哪里?

如果要适应各种场景,就必然会增加代码量,而各种情况又不一定能全部用上,那冗余的代码必然是个累赘,要是换个人开发那更是不敢动了;而如果太简单,必然又无法发挥一个UI库的作用,所以这必然是一个纠结的问题。

正如《后会无期》中说道:“喜欢就会放肆,但爱是克制”。

为了遵循克制这原则,在组件的头部,我们经常会看见一些带有switch标识的开关组件,有默认会true的,也有为false的,你可以根据你的需要选择开或者关来决定是否生成该样式。

于是在sheral的UI开发中,不仅实现适用多种场合,更是合理有节制的控制了代码的冗余,同时也留有进一步扩展的余地,这才是sheral的态度。

其他说明

1.基础知识

距离上个移动端重构系列已是两年了(不得不感叹时间是把杀猪刀)。这次将会带来实战系列,将欠下两年的债现在还上,给七年的重构赋予一次新生。

既然是新的开始,先简单说下这个系列要用到的一些技术吧。同时也是对移动端重构一些技术的一个简单回顾。

viewport

关于viewport详细请参考移动前端开发之viewport的深入理解

css3选择器

结构伪类选择器已经成为列表类的标配了,不掌握都不好意思切页面了。

CSS3 选择器——属性选择器

CSS3 选择器——伪类选择器

css选择器支持一览表

CSS选择器查阅

伪元素(::before, ::after)

我会告诉你,下面的retina 1px大多数都是采用伪元素来生成的,除此之外,还有更多实用的,我会在接下来的重构教程中演示

A Whole Bunch of Amazing Stuff Pseudo Elements Can Do

学习使用:before和:after伪元素

伪元素的content使用

百分比

据说百分之八十的人入门移动端重构的第一个问题就会问:是不是所有的当要用百分比单位啊。这可以从侧面可以反应出百分比有多重要,下面是关于

关于移动端百分比宽度的几种实现

新单位——rem,vw,vh...

接上第一个问题,第二个问题是:那是不是要用rem?

CSS3的REM设置字体大小

rem不是神农草,治不了移动端百病

vw, vh等新单位介绍(安卓4.4+支持)

PS:然而,我们这个系列的教程并没有用到以上这些高大上单位,不过你还是需要了解,尤其是下面的vw, vh系列的单位,因为以后将会是个得力的助手

flex

不用多介绍,大名如雷贯耳。传说中的布局利器,听说学好这个分分钟搞定页面,一边撩汉/妹子,一边切页面不是笑谈。

一个完整的Flexbox指南

Flex 布局教程:语法篇

A Complete Guide to Flexbox

retina 1px

用一首来说就是”眼前的黑不是黑,你说的1px是什么1px“,下面就是各种奇淫技巧实现:

在retina屏中实现1px border效果

Retina屏的移动设备如何实现真正1px的线?

PS: 安卓4.3- 不支持background-size的百分比,所以选用这个办法的要三思,另ios9已经实现@support,所以配合0.5px,实现起来就更简单了,下面附上sandal中的mixin定义:

@mixin retina-one-px() {
    @supports (border-width: 0.5px) {
        @media only screen and (-webkit-min-device-pixel-ratio: 2), screen and (-webkit-min-device-pixel-ratio: 3) {
            border-width: 0.5px;
        }
    }
}

fixed

除了曾经的1px不再是1px,曾经的fixed也不再是我们熟悉的fixed了,再搞下去都要得fixed恐惧症了。

首先css3的transform等给我们带来了fixed的相对定位问题,其次虚拟键盘的弹出也给fixed制造出各种bug(有的虚拟键盘会改变窗口大小,而有些非默认的虚拟键盘则是以弹层的形式覆盖在上面的,所以并没有改变窗口大小,也就没有办法通过window的onresize事件来监听了)

Web移动端Fixed布局的解决方案

深入理解CSS中的层叠上下文和层叠顺序

我们现在一般android采用fixed布局;ios采用absolute,然后中间滚动使用-webkit-overflow-scrolling: touch;。如果还不行就具体问题具体分析。

图片高度占位

跟pc的不一样,移动端的图片很多都不是固定的宽高的(icon图标与头像等一些小图还是固定大小的),所以就面临一个问题:不能设置一个具体的高度,于是就会出现加载过程其他内容随着图片的加载慢慢向下移动。

那如何解决这个问题呢?

给图片提供一个容器,设置高度为0,根据宽度按照图片的比例使用paddin-top得到一个高度值,然后图片绝对定位设置宽高为100%即可,如图片尺寸为200*100(则高度为宽度的二分之一):

.img-wrap{    position: relative;    height: 0;    padding-top: 50%;// 图片宽度的一半
}
.img{
    position: absolute;    top: 0;    left: 0;    width: 100%;    height: 100%;
}

css中如何做到容器按比例缩放

居中

居中,居中,还是居中,重要的话说三次!!!

这里除了之前css2时代的常规方法,我们更多的使用css3的transform及flex方法,而img或video的最新object-position还得等待兼容的时代

Centering in CSS: A Complete Guide

object-fit

等分

这个跟前面的居中一样一样一样重要的,几乎打开一个页面就可以看到。上次在imweb上也发起了关于这个的一个问题讨论—— item宽度固定,剩余间距等分实现方案探讨

目前等分大概分为三种:

  1. 不考虑间距,item等分

  2. 间距为固定值如10px,剩余宽度item等分

  3. item宽度为固定指,剩余间距平分

这次我将会在这个实战系列中把这三种情况一一剖析。

css3动画

这年头不会一两招css3动画,都不好意思说自己会css了。

css3 transform 101

Advanced CSS3 2D and 3D Transform Techniques

css3 transtion 101

css3 animation 101

CSS3: Animations vs. Transitions

css3动画疑难杂症一览

2.line list

这个line list的名字是我自己起的(大概的意思是单行列表),要实现的东西为sheralline list,对应的scss组件为_line-list.scss,下图为line-list的一个缩影:

这个UI应该是每个移动端网页都必备的,而且使用场景也是非常的丰富,所以这里我们采用一步步循序渐进的方式去重构。

先说下整个过程中要解决的问题:

  • retina 1px

  • 分割线缩进

  • 整行点击

  • 单页应用或跳转页面

  • 如何方便扩展

最简模式

html结构

.line-list>.line-item

结构方面,标签可以是ul.line-list>.line-item或者div.line-list>a.line-item,前者用于单页应用,后者用于链接跳转。

关键scss代码

.line-item {    @extend %bar-line;}.line-list {    background: #fff;
    + .line-list {        margin-top: 10px;
    }
}

由于这种line item的样式使用场景较多,所以我们封装了一个%bar-line,定义在sandal_mixin.scss文件中(下面如无特殊说明,mixin和%均在该文件定义),如下:

%bar-line {    line-height: $barHeight - 10px;
    padding: 5px 10px;
    position: relative;
    display: block;
    overflow: hidden;
    @if $activeStateSwitch{ 
        &:active,
        &:hover {            background-color: darken($colorF, 3%);
        }
    }
    &:not(:irst-of-type)::before { // 使用伪元素生成retina 1px
        content: "";
        @include retina-one-px-border;
    }
}

下面解读下上面的scss代码:

  • retina 1px我们在sandal里面封装了个mixinretina-one-px-border($direction: top, $color: $colorBorder),直接传入相应参数调用即可。

  • 把1px挂在除第一个元素之外的伪元素before上,而第一个最上面和最后一个最下面的1px将会在父元素上实现,那样中间line-item之间的1px就很容易扩展实现缩进。

  • 每个line item的高度为44px(ios 的标准高度为44px),实现方法为line-height + padding,为什么不是直接line-height:44px,这就涉及到我们下面更多的扩展形态了。

右箭头跳转模式

保持html结构不变,追加class实现所需的功能:

  • item之间的1px缩进,最开始和最末位的不缩进

  • 右侧箭头

.line-list--indent {    @extend %border-tb; 
    .line-item::before {        left: 10px; 
    }
}.line-list--after-v { 
    .line-item {        padding-right: 30px;
        @extend %item-v-right;        
    }
}

PS:这里缩进用的伪元素before的1px left定位来实现的,看到过有些方法是设置item的border-bottom,然后设置item的margin-left: 10px,这种实现方法是错误的,因为点击的不是整行了(缺了margin left的10px),当然也可以内嵌一个inner元素设置inner元素的margin left,或空元素定位等

同样考虑到比较常用,在sandal中封装了两个%,分别为%border-tb%item-v-right,具体代码为:

%border-tb {    position: relative;
    &::before {        content: "";
        @include retina-one-px-border(top);
        z-index: 1; 
    }
    &::after {        content: "";
        @include retina-one-px-border(bottom);
    }
}%item-v-right {
    &::after {        content: "";
        @include v-arrow;
        color: $colorC;
        position: absolute;
        right: 15px;
        top: 50%;
        margin-top: -1px;
        transform: rotate(45deg) translate(0, -50%);
        box-sizing: border-box;
    }
}

选择模式

选择模式分为单选和多选,单选同样可以保持结构不变,通过after元素生成选中的对钩;而多选则可以添加i.icon-checbox元素。对钩和icon checkbox都是css绘制,使用currentColor,item选中时直接改变color即可,具体代码如下:

.line-list--select {    .line-item {        padding-right: 30px;
        &.active {            color: $primary; 
            &::after { 
                content: "";
                display: block;
                width: 14px;
                height: 8px;
                border-bottom: 2px solid currentColor;
                border-left: 2px solid currentColor;
                transform: rotate(-52deg) translate(0, -50%);
                box-sizing: border-box;
                position: absolute;
                top: 50%;
                right: 8px;
                margin-top: -4px;
            }
        }
    }
}.line-list--multi-select {    .active{        color: $primary;
        .icon-checkbox{            color: $primary;
        }
    }
}

复杂模式

这里我们将采用flex,一行大概分为三栏:图标icon(固定宽度),中间内容(剩余宽度),右边操作或提示(switch,提示文字或数字,右箭头)。如果你要兼容的手机不支持flex,那也没关系,这个结构也足够你使用绝对定位或float布局了,完全不需要再更改结构。

.line-list--flex {    .line-item {        display: flex;
        align-items: center;
        padding-right: 0;

        .item-icon,        .item-img,        .icon-switch,        .remind-num,        .item-append{            margin-right: 10px;
        }        .item-bd { 
            flex: 1;
            margin-right: 10px;
            width: 1%;
        }        .item-append{            color: $color9;
        }        .icon-v-right {            width: 30px;
            height: 30px;
            color: $colorC;
            margin-left: -10px;
        }        .remind-num {            position: static;
            line-height: 1.5;
        }
    }
}

3.各种等分

单行,不考虑间距等分

以sheral的nav list为例:

.nav-list{    @include equal-flex(nav-item);}

equal-flex的mixin定义在sandal中,代码如下:

@mixin equal-flex($children: li) {
    display: flex;
    $childrenEle: li div p a span strong;
    @if index($childrenEle, $children) { 
        #{$children} {            flex: 1;
            width: 1%;
        }
    }    @else {
        .#{$children} { 
            flex: 1;
            width: 1%;
        }
    }
}

参数部分可以是常用的li div p a span strong几个元素,也可以是class,会自动加.

除了使用flex等分之外,我们还可以使用table办法来等分,同样sandal里面也定义了一个equal-table的mixin,代码如下:

@mixin equal-table($children: li) {
    display: table;
    table-layout: fixed;
    width: 100%;
    $childrenEle: li div p a span strong;
    @if index($childrenEle, $children) {
        #{$children} {            display: table-cell;
        }
    }    @else {
        .#{$children} {            display: table-cell;
        }
    }
}

间距相等,剩余item平分

分为单行及多行情况,单行直接flex就好,而多行的flex老版本兼容不是很好,所以不建议使用,直接用原始的float。

先说单行的,以sheral的line equal的第一个为例:

.equal--gap{    @include line-equal-gap($children: line-equal-item);}

line-equal-gap的mixin同样定义在sandal中,代码如下:

@mixin line-equal-gap($gap: 10px, $lr: true, $children: li) {
    display: flex;
    @if $lr { 
        padding-left: $gap;
        padding-right: $gap;
    }    @if $children == li { 
        #{$children} {            flex: 1;
            width: 1%;
            &:not(:first-of-type){                margin-left: $gap;
            }
        }
    }    @else { 
        .#{$children} {            flex: 1;
            width: 1%;
            &:not(:first-of-type){                margin-left: $gap;
            }
        }
    }
}

通过flex来实现,如果左右边缘也有间隙,则设置左右padding,然后设置子元素的非第一个元素的margin-left

关于多行的可以参考sheral的card实现,这里以卡片2为例,关键代码如下:

$cardFlexSwitch:       false !default; $cardGap:              10px !default; $carLineNum:           2 !default; .card-list {    @if $cardFlexSwitch {
        display: flex;
        flex-wrap: wrap;
    } @else {
        overflow: hidden;
    }    .card-item {        position: relative;
        width: 100% / $carLineNum;

        @if not $cardFlexSwitch {
            float: left;
        }        .item-img {            width: 100%;
        }        .item-tt {            line-height: 30px;
        }
    }
}.card-list--gap{  padding-left: $cardGap / 2;
  padding-right: $cardGap / 2;

 .card-item{      margin-bottom: $cardGap;
      padding-left: $cardGap / 2;
      padding-right: $cardGap / 2;
  }
}

float的主要思路为设置宽度n等分,然后间距由padding或嵌套的inner元素margin来实现。

PS:这里考虑到flex与float的无缝切换,所以flex思路同样设置宽度的n等分,而不是单行的那种margin方法。

item相等,剩余间距平分

单行的demo为line equal的第二个。这里使用的另一个mixin: line-equal-item,其实现思路是通过flexjustify-content: space-between;进行变化使用。

@mixin line-equal-item($lr: true, $children: li) {
    display: flex;
    justify-content: space-between;
    @if $lr {
        &::before,
        &::after {            content: "";
        }
    }
}

多行的话,跟上面的card实现差不多,具体的间隙计算公式可以参考item宽度固定,剩余间距等分实现方案探讨

本篇文章主要是对sandal中几个等分mixin的具体实践,简直是分分钟实现等分的节奏,当然这背后的mixin的定义是几经磨难,花费了大量心血的,感兴趣的可以开始试试了(如果你要兼容的安卓机很古老,连最老版本的flex box都不支持,那就只好干巴巴的看着了,转头去写float吧)。

4.进入离开动画

进入离开动画

在sandal的_animation.scss中我们定义了fade-in/out, shrink-in/out, up-in/out, down-in/out, left-in/out, right-in/out六组基础动画,下面我们以fade-in/out为例说明如何使用:

直接调用mixin:

@include animation-fade-in;@include animation-fade-out;

编译出的css为:

.fade-in, .fade-out {  -webkit-animation-duration: 0.3s;  animation-duration: 0.3s;  -webkit-animation-fill-mode: both;  animation-fill-mode: both;
}.fade-in {  -webkit-animation-name: fadeIn;  animation-name: fadeIn;
}@-webkit-keyframes fadeIn {
  0% {    opacity: 0;
  }
  100% {    opacity: 1;
  }}@keyframes fadeIn {
  0% {    opacity: 0;
  }
  100% {    opacity: 1;
  }}.fade-out {  -webkit-animation-name: fadeOut;  animation-name: fadeOut;
}@-webkit-keyframes fadeOut {
  0% {    opacity: 1;
  }
  100% {    opacity: 0;
  }}@keyframes fadeOut {
  0% {    opacity: 1;
  }
  100% {    opacity: 0;
  }}

当然为了扩展,mixin还定义了两个参数:animation-fade-in($className: fade, $from: 0)animation-fade-out($className: fade, $to: 0),第一个表示要用的class名字(会自动补上in/out),第二个表示opacity值(from为起始,to为结束)

现在css的动画class已经有了,接下来就是用js把这两个class分别添加到进入和离开的时候。

es6 封装动画进入离开类

export class AnimateInOut {
    constructor({ele, className, inCallback, outCallback}) {        this.ele = ele.nodeType === 1 ? ele : document.querySelector(ele);        this.inClass = className + '-in'; 
        this.outClass = className + '-out'; 
        this.inCallback = inCallback; 
        this.outCallback = outCallback; 

        this.animationend = this.whichEndEvent(); 
        this.endBind = this.end.bind(this); 
    }    
    enter() {        this.ele.classList.add(this.inClass);        
        this.ele.addEventListener(this.animationend, this.endBind);
    }    
    leave() {        this.ele.classList.add(this.outClass);        
        this.ele.addEventListener(this.animationend, this.endBind);
    }    
    end() {        var ele = this.ele,
            eleClassList = ele.classList,
            isIn = eleClassList.contains(this.inClass), 
            isOut = eleClassList.contains(this.outClass); 

        ele.removeEventListener(this.animationend, this.endBind);        if(isIn) {
            eleClassList.remove(this.inClass);            this.inCallback && this.inCallback();
        }        if(isOut) {
            eleClassList.remove(this.outClass);            this.outCallback && this.outCallback();
        }
    }    
    whichEndEvent() {        var k
            el = document.createElement('div');        var animations = {            "animation" : "animationend",            "WebkitAnimation": "webkitAnimationEnd"
        }        for(k in animations) {            if(el.style[k] !== undefined) {                return animations[k];
            }
        }
    }
}

PS:注意这里我们采用的animation动画,而不是transition动画,因为transition动画从none到block的时候,直接添加动画的class是不会有动画效果的(除非使用回调函数或promise),而animation动画从none到block的时候添加动画class是可以的。这里不想设计得太复杂,所以直接使用animation动画

调用

function leaveEnd() {    console.log('hello the world');
}var animateInOut = new AnimateInOut({ele: $el, className: 'fade', outCallback: leaveEnd});animateInOut.enter();animateInOut.leave();

PS:本系列教程未完待续,正在码字中...