优秀的编程知识分享平台

网站首页 > 技术文章 正文

移动端重构实战系列:0-4 章(移动端基础技术)

nanyue 2024-10-24 11:49:50 技术文章 3 ℃

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

”本系列教程为实战教程,是自己移动端重构经验及思想的一次总结,也是对sheral UI的一次全方位剖析,首发在imweb和w3cplus两大站点及“前端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. 本系列教程的css3前缀统一由PostCSS 插件autoprefixer处理。

  2. 如无特别申明,所有的@mixin均定义在sandal的_mixin.scss中。

  3. 本系列教程主要是分析sheral UI的实现。

1.基础知识

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

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

viewport

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

<meta name="viewport" content="width=device-width, initial-scale=1.0">

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定义:

// retina border// 0.5px实现 ios9@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的名字是我自己起的(大概的意思是单行列表),要实现的东西为sheral的line 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%bar-line { line-height: $barHeight - 10px;

padding: 5px 10px;

position: relative;

display: block;

overflow: hidden;

@if $activeStateSwitch{ //是否开启:active样式

&: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; // 添加最上和最下的1px,形成封闭

.line-item::before { left: 10px; // 缩进10px

}

}.line-list--after-v { // 右箭头通过after生成

.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 top & bottom%border-tb { position: relative;

&::before { content: "";

@include retina-one-px-border(top);

z-index: 1; // 第一个元素点击的时候防止active背景色遮盖了1px

}

&::after { content: "";

@include retina-one-px-border(bottom);

}

}// item arrow, 右侧箭头跳转指向%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中,代码如下:

// flex等分@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} { // 自动加.成class

flex: 1;

width: 1%;

}

}

}

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

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

// table 等分@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中,代码如下:

// line equal@mixin line-equal-gap($gap: 10px, $lr: true, $children: li) {

display: flex;

@if $lr { // 左右边缘是否有gap

padding-left: $gap;

padding-right: $gap;

} @if $children == li { // 默认使用li元素

#{$children} { flex: 1;

width: 1%;

&:not(:first-of-type){ margin-left: $gap;

}

}

} @else { // 否则使用class

.#{$children} { flex: 1;

width: 1%;

&:not(:first-of-type){ margin-left: $gap;

}

}

}

}

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

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

$cardFlexSwitch: false !default; // 默认使用float$cardGap: 10px !default; // 默认间距为10px$carLineNum: 2 !default; // 目前只支持2 或 3 等分.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'; // 加上in表示进入class

this.outClass = className + '-out'; // 加上out表示离开class

this.inCallback = inCallback; // 进入动画结束后回调函数

this.outCallback = outCallback; // 离开动画结束后回调函数

this.animationend = this.whichEndEvent(); // 使用animationend事件

this.endBind = this.end.bind(this); // 绑定this

} // 进入

enter() { this.ele.classList.add(this.inClass); // animation动画结束之后,移除该class

this.ele.addEventListener(this.animationend, this.endBind);

} // 离开

leave() { this.ele.classList.add(this.outClass); // animation动画结束之后,移除该class

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();

}

} // 判断end事件,可独立为一个基础功能

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:本系列教程未完待续,正在码字中...

内置福利

在后台回复福利,可以获取下载《全球移动技术大会2016》PPT集合。

言归正传我们在微信群中推出了《早读课》,每日分享一篇我们认真精选的文章(不仅限于前端开发类的),其目的是帮助开发者来学习有价值的东西。想加微信群的朋友,直接添加我的微信号:icepy_1988,过后我进行审核,审核之后会邀请你入群。想加QQ群的朋友,可以直接添加:418898836,答对问题即可入群。

关注我们

扫二维码 或搜索 fed-talk ,关注我们的微信公众号,也欢迎你将它分享给自己的朋友。

Tags:

最近发表
标签列表