将新的CSS技术引入到生产中

发布于 大漠

时间如梭,又一年双11圆满收官了。作为技术宅男除了买买买之外还能和大家聊的就是前端技术了。

"淘宝盖楼"上热搜了,我想你应该也参加了双十一淘宝盖楼吧,是不是觉得特别的酸爽。今年有幸参与盖楼的互动活动的开发中备感荣幸,因为我们的努力让全民都爽了。在开始之前要先感谢一下曾经努力的自己

楼盖完了,我们回过头来聊聊这次多人互动PK在开发的时候用了哪些有意思的前端技术。感兴趣的同学,请继续往下阅读。

去年双十一能量PK互动项目中我们用了些有意思的前端技术,要是你感兴趣的话,可以阅读《聊聊双11互动主动法中前端技术亮点》一文。

SVG运用和优势

先来说图标(Icon)的使用。整套视觉下来,页面上的图标(或类似图标)的场景还是蛮多的,比如:

仔细分析一下,有些图标是具有共性的,或者说只有一点点差异:

别的图标暂且不表,有两个地方是有共性的:

  • 箭头图标是相同的,如果说差异,就是箭头有没有带容器箭头容器的颜色有差异
  • 微标(票房状态的图标,比如“平局”、“获胜”等),在不同的地方仅仅是颜色大小的差异

自有Web技术到现在,Web上使用图标的技术也随着时代的变迁在改变:

  • <img>引用独立的Icon图片文件
  • `background-image`替代`<img>`标签,引用独立的Icon图片文件
    
  • 将众多Icon图片合并在一起(俗称**Sprites**),使用`background-image`和`background-position`来控制Icon图标的显示和位置
    
  • Icon Font的使用,比如[阿里IconFont](https://www.iconfont.cn/)和[Font Awesome](https://fontawesome.com/v4.7.0/icons/)
    
  • 内联SVG和SVG Sprites
    

有关于上述技术方案的差异性和利弊,这里不做过多的阐述,如果你对这方面的讨论感兴趣的话,我建议你移步阅读以前整理的《Web中的图标》一文。

在这次的项目中,我采用了SVG Sprites相关的技术。在聊为什么采用SVG Sprites技术之前,先来对上面提到的两个典型的图标做一个简单的分析。

直接用下图来描述箭头图标的构造(一图胜过千言万语):

从上图中我们可以看出来,最共用的是箭头部分对应的SVG代码:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">	
<svg t="1568275614301" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2246" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
    <path d="M448 672c-6.4 0-19.2 0-25.6-6.4-12.8-12.8-12.8-32 0-44.8L531.2 512 422.4 409.6c-12.8-12.8-12.8-32 0-44.8s32-12.8 44.8 0l128 128c12.8 12.8 12.8 32 0 44.8l-128 128C467.2 672 454.4 672 448 672z" p-id="2247"></path>
</svg>

你可能发现了,在上图中箭头有不同的颜色,比如#FF1545#fff,可以借助SVG的fillstrock属性,不管SVG标签行内添加样式,还是在CSS样式表中添加样式,可以将fillstrock设置为currentColor

currentColor是CSS中第一个变量属性值,最大的特性就是可以根据当前color来决定值。

如果你对currentColor感兴趣的话,还可以阅读:

箭头容器就很好控制了,通过CSS很好的设置。

该方案最大的优势,我们只需要一个箭头的矢量图(SVG的path就可以很好解决)。第二种思路要比上面这种略差一点,因为需要两个SVG的代码(两个不同的.svg)文件。一个是像上面所示的代码,不带容器;另外一个就是像下面这样的SVG代码,带圆形容器:

代码如下,也是用SVG的path绘制出来,同样的fillstrock设置为currentColor

<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 1024 1024">
    <path d="M512 64C264.96 64 64 264.96 64 512s200.96 448 448 448 448-200.96 448-448S759.04 64 512 64zm149.76 471.968L501.504 694.464c-6.24 6.144-14.368 9.248-22.496 9.248-8.256 0-16.512-3.168-22.752-9.504-12.416-12.576-12.32-32.8.256-45.248L593.92 513.056 457.632 376.384c-12.48-12.512-12.448-32.768.064-45.248 12.512-12.512 32.768-12.448 45.248.064l158.912 159.36c.032.032.032.064.064.096s.064.032.096.064c2.944 2.976 5.056 6.432 6.592 10.048.064.128.224.256.256.384 4.736 11.616 2.368 25.44-7.104 34.816z"/>
</svg>

不管采用哪种方式,都可以达到我们内联使用SVG。只不过上面这种方式是属于人肉方式。这种方式除了人肉处理之外,还有另一个弊端:在不同的地方引用同一个箭头(SVG代码)会生具有同样的代码,让你的代码变得冗余

随着工程化越来越强大,我们很多重复性的事情可以交给工程来处理。比如说,在工程中配置相关的事项,工程会帮我们处理SVG的代码。比如,将项目中的特定文件夹(比如/icons/)下的所有.svg文件合并在一起,并以SVG Sprites的方式将SVG代码内联到index.html

这样做的最大优势是“开发者无需关注内联的SVG Sprites代码是如何生成的,只需要将被使用的.svg文件存入到指定的文件目录中”。然后在调用的时候通过SVG的<use href="#id">调用SVG Sprites中<symbol id>即可。在React或Vue这样的JavaScript框架中,我们还可以创建一个独立的组件,比如Icon组件:

interface IconProps {
    type: string;
    width?: string;
    height?: string;
    className?: string;
    styleName?: string;
}

const defaultProps: IconProps = {
    type: '',
    width: '1em',
    height: '1em',
};

function Icon(props: IconProps) {
    const { type, width, height, ...rest } = {
        ...defaultProps,
        ...props,
    };

    return (
        <svg
            width={width}
            height={height}
            fill="currentColor"
            {...rest}
            xmlns="http://www.w3.org/2000/svg"
            dangerouslySetInnerHTML={{
                __html: `<use xlink:href="#icon-${type}" href="#icon-${type}"></use>`,
            }}>
                {/* <use href={`#icon-${type}`}></use> */}
        </svg>
    );
}

export default Icon;

如此一来,使用的时候也会变得简单:

<Icon type="rightarrow" />

就是这么的简单。对于在工程中如何配置自动生成SVG Sprites,可以查阅读《如何在Vue项目中使用SVG Icon》一文。这里不做更多的阐述。

这次在使用SVG Sprites踩了一个坑,那就是使用<use href="#idname">调用SVG的<symbol id="name">时在部分移动终端不能正常的渲染SVG图标,后来查阅相关文档发现是JSX引起的。所以在构建Icon组件的时候使用:

dangerouslySetInnerHTML={{__html: `<use xlink:href="#icon-${type}" href="#icon-${type}"></use>` }}

来替代

<use href={`#icon-${type}`}></use>
    

接下来再来说“徽标”的使用:

正如上图所示,他们之间有很多共性,不同是“颜色”和“文本内容”。用内联的SVG非常的有益,或者说,我们可以构建一个独立的组件,比如IconBadge

interface IconBadgeProps {
    text?: string;
}

const IconBadge = (props: IconBadgeProps) => {
    const { text } = props;

    return (
        <svg xmlns="https://www.w3.org/2000/svg" width="70" height="70" viewBox="0 0 70 70">
            <g fill="none" fillRule="evenodd">
                <circle cx="35" cy="35" r="34.3236486" stroke="currentColor" strokeWidth="1.3527027" />
                <circle cx="35" cy="35" r="31.739062" stroke="currentColor" strokeWidth="0.52187604" />
                <g fill="currentColor">
                  	<path d="M35.18046574 13.08946812l-1.9177418 1.00821657.3662561-2.13543695-1.5514858-1.51232485 2.1441006-.31155606.9588709-1.94288472.958871 1.94288472 2.1441005.31155606-1.5514857 1.51232485.3662561 2.13543695zM47.1029719 16.08946832l-1.9177418 1.0082165.3662561-2.1354369-1.5514858-1.5123249 2.1441006-.311556.9588709-1.94288476.958871 1.94288476 2.1441006.311556-1.5514858 1.5123249.3662561 2.1354369zM23.10297162 16.08946814l-1.91774187 1.00821657.3662561-2.13543695-1.55148576-1.51232485 2.14410059-.31155606.95887094-1.94288472.95887093 1.94288472 2.14410059.31155606-1.55148576 1.51232485.36625611 2.13543695z" />
                </g>
                <g fill="currentColor">
                  	<path d="M35.56785426 58.10022588l1.9177418-1.00821657-.3662561 2.13543695 1.5514858 1.51232485-2.1441006.31155606-.9588709 1.94288472-.958871-1.94288472-2.1441005-.31155606 1.5514857-1.51232485-.3662561-2.13543695zM23.6453481 55.10022568l1.9177418-1.0082165-.3662561 2.1354369 1.5514858 1.5123249-2.1441006.311556-.9588709 1.94288476-.958871-1.94288476-2.1441006-.311556 1.5514858-1.5123249-.3662561-2.1354369zM47.64534838 55.10022586l1.91774187-1.00821657-.3662561 2.13543695 1.55148576 1.51232485-2.14410059.31155606-.95887094 1.94288472-.95887093-1.94288472-2.14410059-.31155606 1.55148576-1.51232485-.36625611-2.13543695z" />
                </g>
                <text
                    fill="currentColor"
                    fontSize="48"
                    fontWeight="500"
                    letterSpacing=".50666669"
                    textAnchor="middle">
                        <tspan x="10" y="44">
                            {text}
                        </tspan>
                </text>
            </g>
        </svg>
    );
};

export default IconBadge;

调用的时候,只要给text传递不同的值,比如:

<IconBadge text="获胜" />
    

颜色的控件和前面所说的一样,通过fill设置currentColor来控制即可。

像上面这样使用,你可能会有一种顾虑,这说多的SVG代码,<path>中那么复杂,怎么撸出来。事实上没有那么复杂,时至今日,我们借助一些矢量图的编辑软件可以很容易的获取这些SVG代码。

只不过导出来的代码有些冗余,但我们在将导出来的SVG代码用到项目中之前,可以先使用在线工具**svgomg** 对SVG进行优化:

如果你的开发体系是React,还可以借助在线工具**svg2jsx** 直接将SVG代码转成JSX代码,然后放到你的项目中,避免不少的麻烦:

看上去很好,可事实却又是残酷的。最终因为“文字数量”的不一致以及不同平台对文本渲染的差异(特别是在安卓终端设备),渲染出来的效果无法让人接受。正因为如此,对于项目中徽标的使用,还是采用了<img>的方式。

回过头来,为什么不使用IconFont呢?有关于这方面的争论,社区一直没有停止过,比如:

要是对这方面的话题感兴趣,可以花点时间阅读上面的文章。这里简单罗列一下他们之间的差异:

  字体图标 SVG图标
图标是矢量 浏览器会以字体解析它,所以浏览器会以文字的方式来对图标做抗锯齿处理,这可以导致字体图标没有期待中的那么锐利 SVG是XML文件,浏览器直接解析XML文件,直接就是矢量图形,图标锐利,体积也小
可控制性 可以通过font-sizecolortext-shadow等CSS来控制图标 除了字体图标一样的CSS控制方法之外,还可以单独控制一个复合SVG图标中的某一部分,也可以给图标描边
控制图标位置 图标位置会受到line-heightvertical-alignletter-spacing等属性影响 SVG图标的大小就是很精确的SVG图形的大小
图标加载 跨域时没有合理的CORS头部、字体文件未加载、@font-face在Chrome中的bug和不支持@font-face的浏览器等,这些原因都会造成字体图标渲染失败 SVG图标就是文档本身,只要支持SVG的浏览器,都能正常的渲染
语义化,易访问性 为了更好的显示图标,通常使用伪元素或伪类来做,这样做语义化较差 SVG图标就是一个小图片。SVG的语义就是”我是一张图片“,感觉可能更好
易用性 使用一个已造好的字体图标集从来都不有效,因为有太多的图标未使用。而创建一个你自己的字体图标集也不是轻松的事情,需要懂得相关的编辑工具或应用软件 SVG图标会简单一些,因为你可以自己手动地操作,如果需要的话,你可以使用相关的编辑工具
浏览器支持度 得到非常好的支持性,可以一直支持到IE6,在Opera mini,Android 2.1,Windows Phone 7.5-7.8得到支持 浏览器支持性一般,IE8和Android 2.1以及其以下浏览器不支持。不支持可以采用降级处理,但不并完美

2019年的React Conf大会上,Facebook工程师分享的话题中就有一个有关于IconFont图标和SVG图标的。

Facebook 团队通过优化,将图标大小从 4046.05KB 降低到了 132.95kb,体积减少了惊人的 96.7%,减少体积占总包体积的 19.6%

这个优化是非常可人的,如果用这个来算钱的话,你懂的。

其实现方式并不我们想象的那么复杂,很简单。下面是原始图标使用的代码:

<FontAwesomeIcon icon="coffee" />
<Icon icon={["fab", "twitter"]} />
<Button leftIcon="user" />
<FeatureGroup.Item icon="info" />
<FeatureGroup.Item icon={["fail", "info"]} />

在编译期间通过 AST 分析,将所有字符串引用换成了图标实例的引用,利用 Webpack 的 Tree-Shaking 功能实现按需加载,从而删除了没有使用到的图标。

import {faCoffee,faInfo,faUser} from "@fontawesome/free-solid-svg-icons"
import {faTwitter} from '@fontawesome/free-brands-svg-icons'
import {faInfo as faInfoFal} from '@fontawesome/pro-light-svg-icons'

<FontAwesomeIcon icon={faCoffee} />
<Icon icon={faTwitter} />
<Button leftIcon={faUser} />
<FeatureGroup.Item icon={faInfo} />
<FeatureGroup.Item icon={faInfoFal} />

有关于这方面更详细的介绍,可以阅读《font-awesome-codemod》或者 《Creating a custom transform for jscodeshift》。

从某种意义上说明了 IconFont 注定被淘汰,因为字体文件目前无法按需加载,只有全部使用 SVG 图标的项目才能使用这种优化

除此之外,SVG Icon还有很多自身的优势,比如说你的Icon是一个多颜色的,那么使用SVG自身元素以及属性可以很好的控制图标的局部颜色:

另外,给图标添加动效,SVG也非常的灵活。特别是你希望给自己的产品在交互的时候带有一些微动效元素,那么SVG将是一个不错的选择,比如:

有关于SVG Animation这方面的案例在Codepen上比比皆是

CSS伪元素是把利器

CSS伪元素中的::before::after对于CSSer来说是把利器,这两个伪元素配合W3C的另外一个规范 CSS Generated Content Module Level 3 中的content可以创建出两个伪元素。这样一来,一个HTML元素就具备多个盒模型,即有多个背景和边框等,正如下图所示:

借助该特性,在实际开发中能帮我们解决很多问题。

就拿这次项目来说吧,使用CSS伪元素生成内容的地方也很多,比如分隔线以及相同图形镜像等:

绘制图形的边角料,比如Tooltips、丝带边角(一般三角形为多):

伪元素除了在UI上还原可以帮助我们做很多意想不到的事情(效果),在UX上也是有帮助的。就拿元素或控件的可点击区域为例吧。不管是在PC端还是在移动端,我们都会涉及到可点击区域的体验。作为开发者,除了精确的还原视觉UI效果之外,还应该考虑用户体验的问题:

如果不考虑可点击区域,就会造成用户难于点击(不管是PC端上使用鼠标,还是在移动端用手指触控)。在这次互动的项目中同样有这样的需求。比如点击某个图标(实际UI尺寸大小小于控件设计规范),提示框(Tooltips)显示出来:

为了改变这一现象,不少同学会考虑在Icon图标容器上添加click事件,从而扩大可点区域;也有同学会考虑在Icon外额外添加一个容器,来扩大可点区域。事实上,我们还可以通过伪元素来扩大可点区域。比如下图这样的一个效果,不仅在一个button上可点击,而是借助伪元素让整个卡片都是可点击区域:

上面列出的仅是这次互动项目中有关于“伪元素”使用的场景,其实伪元素可用的场景还有很多,如果你对这方面感兴趣的话,可以阅读《伪元素能帮助我们做些什么》一文。

CSS选择器还能这样用

随着CSS不断的发展,这些年来她的变化也非常的大。其中选择器模块(Selectors Level 4)就给我们带来很大的便利。特别是在处理一些随着状态会变会UI的场景。比如说:

随着状态不同,按钮有不同的个数

可能很多同学选择的方案是会给不同的状态定义不同的类名来控制布局的差异性。时至今日,我们可以不再依赖于脚本代码来帮我们做判断,完全可以把这种简单性的判断交给客户端去处理。

.modal__footer > div:last-child {
    margin-right: 0;
}

.modal__footer > div:first-child:last-child {
    margin: 0;
    min-width: 400px;
}

还可以让组合选择器的条件更多一些,比如使用:not()

如上图所示,可以很好的控制:last-childmarginpaddingborder之类的控制。甚至还可以将多个:not()和其他的伪类选择器结合,比如隐藏元素的样式处理:

.sr-only:not(:focus):not(:active) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    white-space: nowrap;
    clip: rect(0 0 0 0);
    clip-path: inset(100%);
}

有关于CSS选择器Level更多的介绍,可以查阅《初探CSS 选择器Level 4》一文。

换个姿势使用盒阴影

说到盒阴影,大家首先想到的是box-shadow属性。虽然box-shadow能帮助我们实现大部分的效果,但是在面对一些场景,他是心有余,而力不足。比如:

对于不规则的图形添加阴影,以往是找设计师的同学在图片上做好阴影,但现在开始,我们可以使用代码来更灵活的应对视觉稿风格的调整。比如上图中“盾牌”的阴影,就是使用CSS filterdrop-shadow()来完成的:

.game__badge img {
    filter: drop-shadow(0 4px 4px rgba(0, 0, 0, .3));
}

事情就是这么的简单,而且filter中的drop-shadow()还可以做很多有意思的东西。如果你对这方面知识感兴趣的话,这里推荐你花点时间阅读 @ChokCoco 老司机整理的《你所不知道的 CSS 阴影技巧与细节》一篇博文。

说到filter的话加点额外的料,CSS的filter还可以做很多效果,除了让我们能通过样式代码处理图片效果之外,还可以给一些元素做特殊效果,比如下面这样的两个场景切换:

我最开始使用的就是filter: grayscale(1);

CSS的filter还有一个孪生兄弟,CSS混合模式(background-blend-modemix-blend-mode)。

继续回到盒阴影上面来吧。box-shadow经常用来实现 3D按钮 或者用来实现较有质感的效果。但在性能上也受诸多的挑战,特别多级阴影的使用以及和动态改变阴影效果场景中。虽然先天有一定的缺陷,但我们可以后天来弥补。比如说,借助前面说到的CSS伪元素::before::after来实现这种3D按钮更有优势。

box-shadow转换为伪元素实现阴影效果,对于性能是较大帮助的。特别是有动效的地方,可以让阴影效果更为平滑:

上面的录屏中的Demo来自于@tobiasahlin在《How to animate box-shadow with silky smooth performance》一文中提供的Demo

如果你使用浏览器开发者工具查看的话,两个效果之间的渲染的差异有多大:

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

CSS Masking和Clipping

说到绘制图形,很多同学首先想到的是SVGCanvas,其实CSS的一些特性也可以帮助我们绘制图形的,她也是非常强在的,强大到什么程度呢?给大家上一个堪称CSS绘制的艺术作品的截图:

上图是@Diana Smith最新作品,另外在社区中有一个网站名叫CSS ART,收集了很多使用CSS绘制的作品。

看到上面的这些作品,只能感叹“没有做不到的,只有想不到的”!话又说回来,这东东并没有实际价值,但很多时候使用CSS绘制图形,还是能帮助我们减少应用的资源加载。而且也有很多CSS绘制的图形是有实用场景的。比如社区中非常有名的网站**A Single Div**:

是不是觉得不可思议,要是你也想尝试着使用一个div绘制出一个图形(你喜欢的图形),其思路和具体操作步骤可以参阅读@Lynn Fisher和@Robert Nyman一起写的一篇教程《Single Div Drawings with CSS》。

咱们回到项目中的来,在这次项目中使用CSS绘制图形(取代图片)场景并不很多。这里拿下图来举例:

这里拿三角形的绘制来说吧。

在CSS中绘制三角形,首先想到的会是border属性,因为该方案大家最早接触,也是最熟悉的方案之一:

正如前面时间在《提示框组件的实现给我带来的思考和探索》一文中和大家聊的一样,绘制三角形的方案有很多种,特别是随着CSS Masking和Clipping越来越成熟之后,我们可以使用clip-path来绘制三角形(或别的你想要的图形 ):

类似上图所示,clip-path轻易的帮助我们绘制了直角三角形,代码也非常的简单:

.report__results::before {
    clip-path: polygon(100% 0, 100% 100%, 0 100%);
}

.report__results::after {
    clip-path: polygon(0 0, 100% 100%, 0 100%);
}

对于Tooltips上面的三角形,同样可以类似下图所示的一样,使用clip-path绘制出等边三角形或等腰三角形:

这次项目中的Tooltips是个半透明的效果,如果将三角形和容器分离开,在衔接处总是会有一定的瑕疵,如果采用《提示框组件的实现给我带来的思考和探索》中提到的绘制Tooltips的方案来绘制的话,就可以完美的解决!感兴趣的同学,可以在你的下一个项目中尝试使用clip-path来帮助你完成。

再一次和CSS自定义属性失之交臂

CSS自定义属性(CSS Custom Properties for Cascading Variables Module Level 1)和 CSS Grid特性可以说是2019年中有关于CSS方面最新的特性。特别是CSS自定义属性在很多组件库和大型应用中可以看到其身影。

记得去年在总结双十一中使用到的技术亮点中就提到过,在项目中尝试着使用CSS自定义属性。但最终是以失败告终。 为了能将CSS自定义属性成功(无缝)带到互动前端开发体系中来。在双十一之前的技术储备和调研的时候,就针对该特性做过降级处理。即通过工程来解决,简单地说是借助WebpackPostCSS Preset Env能力,自动对不支持CSS自定义属性的设备做降级处理:

可惜的是在干净的工程体系跑通了,放到双十一项目中的编译环境未能成功。至今还没有定位到相同的配置为什么没有生效的原因,因此这次又和CSS自定义属性的使用失之交臂。但我和小伙伴们并没有放弃,我想我们能找到原因,并解决它。期待在下一个互动项目中能成功的使用CSS自定义属性。

就我个人而言,我愿意尝试着使用所有的新东西,并且我也钟情于CSS自定义属性,正如《CSS 自定义属性在Web组件中的应用》一文中所提到一些特性,她真的很灵活,很强大!

踩中了100vh的坑

vwvhCSS的视窗单位,现在我们项目中针对于各种终端的适配方案,采用的都是vwvw-layout适配)。其中100vh对于处理全屏高度的布局非常的有效。针对这样的布局,我们在项目中都会使用:

html, body {
    min-height: 100vh;
}

但这次我们在分享回流的全屏页面中踩中了一个100vh的坑:

在iOS系统下的带有刘海和安全区域的设备,比如iPhone X、XS、XR等都会有白边的现象。

当看到这个问题的时候,我自己也一脸朦逼,一直以来都这么用的,从未碰到类似的现象。后来在排查该问题的时候,发现其核心问题是移动端浏览器(Chrome和Safari)有一个功能,地址栏有时可见,有时会隐藏,从而改变了视窗的可见区域大小(在手淘的WebView容器也有类似的功能,顶部状态栏的隐藏)。这些浏览器没有将100vh的高度调整视窗高度为变化时屏幕的可视区域,而是将100vh设置为隐藏地址栏的浏览器高度。

为了解决这个白边现象,只好通过别的Hack形式来绕开。Hack思路很简单,100vh应该和我们屏幕的高度(screen.height)是相等的。然后再借助CSS自定义属性,在HTML的根元素<html>上显式声明一个自定义属性--vh。代码大致如下:

try {
    var vh = screen.height * 0.01;

    document.documentElement.style.setProperty('--vh', vh + 'px');

    window.addEventListener('resize', function () {
        var vh = screen.height * 0.01;
        document.documentElement.style.setProperty('--vh', vh + 'px');
    });
} catch (error) {}
    

有了这个--vh的值之后,CSS就可以很好的控制了:

.page__container {
    min-height: 100vh;
}

@supports (padding-top: constant(safe-area-inset-top)) {
    html,
    body,
    .page__container {
        min-height: calc(var(--vh, 1vh) * 100) !important;
    }
    .page__container {
        padding-top: var(--safe-area-inset-top);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

@supports (padding-top: env(safe-area-inset-top)) {
    html,
    body,
    .page__container {
        min-height: calc(var(--vh, 1vh) * 100) !important;
    }
    .page__container {
        padding-top: var(--safe-area-inset-top);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

最终的效果完美的。

overflow触发Flexbox的边缘情况

业务中很多场景会有类似下图这样的需求:

虚线框中内容是带有滚动的。因为容器的高度是固定的,内容很有可能会超过容器的高度。

在目前Flexbox布局为主流布局的场景下,很有可能会在滚动容器上显式设置flex:1

了解Flexbox的同学都或多或少的知道,Flexbox中的flex计算是件复杂的事情,正因为计算的较为复杂,在滚动容器中很有可能显式设置overflow-yscroll时,滚动并没有生效。

这不是Flexbox的Bug,是特性!

在部分浏览器直接会把布局整乱:

事实上,这真不是Bug,在结果上和样式的正确使用,就不会出现这样的现象。比如:

对应的HTML结构也很简单:

<div class="main-container"> // flex-direction: column;
    <div class="fixed-container">Fixed Container</div> // height: 100px;
    <div class="content-wrapper"> // min-height: 0; 
        <div class="overflow-container">
            <div class="overflow-content">
            Overflow Content
            </div>
        </div>
    </div>
</div>

关键样式代码:

.main-container {
    display: flex;
    flex-direction: column;
}

.fixed-container {
    height: 100px;
}

.content-wrapper {
    display: flex;
    flex: 1;
    min-height: 0; /* 这个很重要*/
}

.overflow-container {
    flex: 1;
    overflow-y: auto;
}

关键部分是设置了flex:1的Flex项目需要显式的设置min-height:0,即滚动容器的父元素。这其实是Flexbox布局的一个边缘情况。

在Flexbox布局中,除了这个边缘情况之外,在水平方向带有marginpadding的也有一个边缘,具体的可以参阅:

最后再说一下,Flexbox布局中的flex是一个很有意思的东东,不花点时间是没法彻底搞清楚他的。如果你对这方面的感兴趣,可以阅读:

注意,这里既然说到了overflow,那么在CSS的世界中,overflow有很多边缘情况。如果你对overflow方面的感兴趣的话,可以阅读《你所不知道的CSS Overflow Module》一文。

CSS条件特性

随着移动终端的品牌越来越多,我们前端在适配上面临的挑战也越来越大,和当年适配PC端各种浏览器已无较大的差异,甚至要比PC时代更难以应付。比如说,屏幕垂直方向的适配。由于不同的终端,其屏幕的高度也有差异化,对于全屏布局,要在不同高度的终端实现完美的适配,可以说是件痛苦的事情:

前端很多时候都是在适配中纠结着活着,这一点不假

在这次互动开发中,同样面临着这样的问题。比如说浮层,设计师要求的是这样(设计师想得非常的周到,还给我们提供了详细的适配示意图):

对于这样的差异化布局,要实现起来还好,并不复杂。借助CSS的条件特性就可以,比如我们熟悉的媒体查询

.bottomsheet__dialog {
    position: absolute;
    top: 400px;
    right: 0;
    bottom: 0;
    left: 0;
    transform-origin: center bottom;
    border-radius: 40px 40px 0 0;
    background: #fff;
    padding-top: 122px;
}

@media only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3){
    .bottomsheet__dialog {
        top: 500px;
    }
}

@media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3){
    .bottomsheet__dialog {
        top: 500px;
    }
}

这是比较粗暴一点的做法,只考虑了两种情形。如果你要再细化一点也可以按上面的思路来处理,只不过工作量就上去了。这需要我们去平衡。

上面这样的场景似乎好处理,但对于下面这样的场景,那就不是件易事了:

设计师和业务方都希望所有的设备都在不出现滚动条,不被截取主要元素的条件下完全的全屏显示

作为有追求的偶,我还是很想满足设计师和业务的需求,可是,我心中早就万马奔腾:

后来他说告诉我,这样的效果也是能接受的:

既然如此,那还不如让类似iPhoneXS、iPhone8的设备全屏幕显示,类似iPhone4的设备出现滚动条(毕竟这个量越来越少)。

行了行了,就这样撸吧:

.share__body {
    padding-top: 0;
}

@supports (padding-top: constant(safe-area-inset-top)) {
    .share__back {
        top: calc(var(--safe-area-inset-top) + 12px);
    }
}

@supports (padding-top: env(safe-area-inset-top)) {
    .share__back {
        top: calc(var(--safe-area-inset-top) + 12px);
    }
}

@media only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3){
    .share__body {
        padding-top: 106px;
    }
}

@media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) {
    .share__body {
        padding-top: 106px;
    }
}

活好不好,就看这下了。

话说回来,有关于这方面的探讨,社区也有是的,比如凹凸实验室提到的方案和@Martijn Cuppens的RFS方案,都值得我们去探索一下。

事后再试一试

CSS的条件特性还有另一个作用,可以用来检测浏览器是否支持CSS的属性。比如我们要检测浏览器是否支持display: grid,就可以使用@supports()来做检测。在这次项目中,我们也有使用这方面的特性。比如下面这样的一个效果:

注意input框中的光标,它是红色的,是红色的,是红色的。重要的事情说三遍。

在CSS中,我们现在可以使用caret-color属性来控制光标颜色了,只不过该属性有些浏览器是不支持了。面对这样的场景,我们就可以使用@supports函数来做判断。在支持的浏览器中使用caret-color属性,在不支持的浏览器上,我们则使用text-shadow来模拟:

.modal__control {
    appearance: none;
    text-shadow: 0 0 0 #666; /* 文本颜色 */
    color: #ff0036; /* 光标颜色 */
    -webkit-text-fill-color: transparent;
}

@supports (caret-color: red) {
    .modal__control {
        color: #666; /* 文本颜色 */
        caret-color: #ff0036; /* 光标颜色 */
    }
}

如果你对caret-color属性感兴趣的话,可以阅读《如何改变表单控件光标颜色》一文。

如何让图片更好的适配于不同的容器

先来上一个需求的截图:

对于上图示意的需求,用户头像效果都是类似的,不同的是在不同宽度容器中,头像容器大小有所不同。前端拿到的数据是相同的(用户头像和头像框是固定的一个尺寸)。

不希望在不同的容器下获取不一样的尺寸

在这个前置条件下,我们前端怎么通过CSS让图像能在不同容器中等比缩放呢?

有些同学可能会说,background-size就可以,只不过background-size是用来控制背景图片的,对于img的话不太适用。这里要说的是,我们不应该把object-fit遗忘了,因为object-fit就可以很好的帮助我们处理图片根据容器进行缩放:

比如:

.media__object img {
    object-fit: contain;
    object-position: center;
}

如何将艺术字带到Web中

像下图这样,用到艺术字体的场景,以往都是依赖于图片来处理:

使用图片除了可扩展性不好之外,还额外增加资源的加载,应用运营变更也不方便。虽然说CSS的@font-face可以加载字体实现,但有一个致命的问题是,中文字体包太大:

就比如上图,为了那么几个艺术字,要加载近3M的字体文件,得不尝失。如果说有方案可以根据我们需要加载的文本而降低字体文件包的大小,那么是不是一个不错的选择。在社区中有很多类似的工具可以帮助我们按需加载需要的文案。比如字蛛就是一个很不错的中文字体压缩器

使用字蛛非常的容易,执行下面的命令全局安装字蛛:

npm install font-spider -g

你可以在你的本地创建一个项目(任何一个项目,只要你喜欢)。并且把你需要字体文件放到该目录下同时创建一个index.html文件。并且将下面的样式添加到该文件中:

<style>
    @font-face {
        font-family: 'fzzyk';
        src: url('./FZZYK.eot');
        src:
            url('./FZZYK.eot?#font-spider') format('embedded-opentype'),
            url('./FZZYK.woff') format('woff'),
            url('./FZZYK.TTF') format('truetype'),
            url('./FZZYK.svg') format('svg');
        font-weight: normal;
        font-style: normal;
    }
    h1{
        font-family: fzzyk;
    }
</style>
    

同时在<h1>标签中输入你将会用到的文字。

保存好之后,在你的命令终端执行类似下面这样的命令:

font-spider *.html

这样可以根据你在<h1>输入的文字生成新的字体包,从而减少原始字体包的大小,而且也不会影响具体的使用:

从上图中你可以看到压缩后的字体文件大小。这个时候你只需要在使用到艺术字体的地方,将font-family设置成@font-face中显式声明的字体,比如此例中是 fzzyk。 设置该字体的方案就是设计师所需要的艺术字体:

你可以运用于font相关属性,达到你自己想要的效果,比如改变颜色使用color、改变大小使用font-size,粗线可以使用font-weight等。

如果你在项目中采用该方案,那么记得使用font-display属性,你可以选择swap。它不仅提供了自定义字体和内容的可访问性之间的最佳平衡,它还提供了和使用JavaScript脚本相同的字体加载行为。如果你在页面上有想要加载的字体,但是最终也可以不加载,这时你就可以考虑使用fallback或者optional作为font-display的值。

不过也有很多同学担心字体加载对性能有较大影响,甚至影响字体渲染,给用户造成不好的体验。事实上,社区针对这方面也有很多的探讨,如果你字体加载性能方面优化感兴趣的,可以阅读下面这些教程:

虽然前期对该方案的调研和自测都没有问题,但是未能在该项目中使用这个方案。主要原因是运营同学没办法预判在活动期间会不会改变方案,如果会改变文案,那么就必须重新生成字体,重新发布。这样做是有一定风险的。从这次活动来看,整个期间并未发生这样的事情。

除了上面这种方案之外,我们还可以将需要使用到的文字由设计师设计成矢量图。简单地说,将每个文字设计成一个独立的矢量图,以SVG的方式将每个文字导出成独立的.svg文件,然后使用前面提到的SVG Sprites技术,通过<use>来调用。虽然这是一个可行性方案,但成本相对来说是较高的。不太建议使用。

可访问性没有你想得那么难

有心栽花花不开,无心插柳柳成荫!

可访问性,很多人又喜欢将其称为无障碍设计。原本这次是没有这样的需求,项目上线之后,希望在该活动能让有障碍人士也能参与。即要做可访问性。幸运的是,我个要自从今年参与的项目都会将这方面的东西做进来,所以说顺理成章的就快速完成了这个额外的需求。

只不过没想到的是,自己一个小小的举动能为这么多有障碍的人士顺利参与到盖楼游戏中来。说实话,还是有小小的成就感和自豪感。

看上去似乎不错,但事实上,对于Web可访问性方面这次做得并不多,因为很多细节并没有做好。这么说是有一定依据的,仅仅对整个活动的主链路做了相应的处理,很多控件的联动,细节是没有做很好的处理。虽然没做好,但这是一个好的开始,希望后续的项目中能将这次不足之处得到较好的完善。

回到可访问性方面上来,有很多同学会认为做这方面的事情得不尝失,更主要的是没有意识去做这方面的事情。这不是技术问题,是心态问题,意识问题。在此我建议大家,先改变这种意识,我们应该在参与的每个项目中尽量的去做,哪怕做得不是非常的好,但也是一个开始,而且是一个很好的开始,随着时间的推移以及自己这方面知识的积累,慢慢地就会好起来。

另外一个原因是,不是大家不愿意去做,而是觉得给Web网页或Web应用程序做可访问性不是一件易事。其实我想说的是,如今给一个Web网站或应用做可访问性方面的事情,已不是件难事。为什么这么说呢?答案有两点。

其一,针对可访问性方面,有很多规范和工具可以帮助我们快速构建,比如:

如果要说得再简单一点呢?我们平时在开发项目的时候,只需尽可能:

  • 使用有语义化的HTML标签
  • 构建较好的DOM结构
  • 合理适当的用好WAI-ARIA中的rolestate
  • 管理好元素焦点
  • ...

就能构建好一个基本上达标的可访问性应用。

话又说回来,如果你阅读过《WAI-ARIA Authoring Practices 1.1》,你会发现,我们现在构建的组件库(或者社区中已有的组件库)和该规范中 Design Patterns and Widgets 非常接近。换句话说,我们在构建基础组件的时候就可以参考下面这份清单来处理可访问性相关的细节:

另外,我们还可以在工程链路上加强可访问性方面的检测。在后续中将考虑把相关的检测功能加到脚手架中,这样有利于更好的处理可访问性方面的东西。

Web可访问性所涉及到的知识点还是蛮多的,这是一个细活,要做好还是需要付出应的成本的。在这里也无法阐述得更清楚,如果你对这方面感兴趣的话可以阅读《创建可访问性网站并不是想得那么难》一文。

后续会继续为Web可访问性方面做一些探究,希望在之后的路上能和大家一起成长。

小结

最后非常感谢大家花时间阅读这篇文章,希望大家能有所收获。