网站首页 > 技术文章 正文
(本文系来自腾讯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的态度。
其他说明
本系列教程的css3前缀统一由PostCSS 插件autoprefixer处理。
如无特别申明,所有的@mixin均定义在sandal的_mixin.scss中。
本系列教程主要是分析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宽度固定,剩余间距等分实现方案探讨
目前等分大概分为三种:
不考虑间距,item等分
间距为固定值如10px,剩余宽度item等分
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里面封装了个mixin
retina-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 ,关注我们的微信公众号,也欢迎你将它分享给自己的朋友。
猜你喜欢
- 2024-10-24 初探animation中steps()属性(animation steps属性)
- 2024-10-24 HTML5(九)——超强的 SVG 动画(htmlsvg动画代码)
- 2024-10-24 自定义日历(二)(自定义日历控件)
- 2024-10-24 Flutter简单动画Animation运用(flutter 视频教程)
- 2024-10-24 css3中动画animation中的steps()函数
- 2024-10-24 移动端渲染原理浅析(移动端渲染原理浅析设计)
- 2024-10-24 iOS 事件处理机制与图像渲染过程(简述ios中的事件响应机制)
- 2024-10-24 Android 开机问题分析(android无法开机)
- 2024-10-24 GoogleCTF + zer0ptsCTF + ImaginaryCTF 2023 笔记
- 2024-10-24 决战“金三银四”,中高级Web前端大厂面试秘籍:CSS篇
- 11-26Win7\8\10下一条cmd命令可查得笔记本电脑连接过的Wifi密码
- 11-26一文搞懂MySQL行锁、表锁、间隙锁详解
- 11-26电脑的wifi密码忘记了?一招教你如何找回密码,简单明了,快收藏
- 11-26代码解决忘记密码问题 教你用CMD命令查看所有连接过的WIFI密码
- 11-26CMD命令提示符能干嘛?这些功能你都知道吗?
- 11-26性能测试之慢sql分析
- 11-26论渗透信息收集的重要性
- 11-26如何查看电脑连接过的所有WiFi密码
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)