Quantcast
Channel: w3cplus
Viewing all 1557 articles
Browse latest View live

手淘Web页面Bar和纵向适配的设计

$
0
0

记得在去年双11的互动页面中,我们折腾了一波iPhone8、iPhone8 plus和iPhone X的适配,特别是iPhone X刘海区域的适配。针对这方面的页面适配,沉淀出相应的适配方案。而今年的双11期间,苹果又推出了iPhone XS、iPhone XR和iPhone XR Max以及众多的安卓刘海设备。言外之意,前端在这方面的适配变得越来越复杂。面对众多场景,我们应该怎么去面对呢?接下来聊聊我在今年双11的主互动玩法中是怎么处理的。

面对的场景

在双11主互动玩法(最近手淘热门活动)双11合伙人 组队PK人气 集能量瓜分10亿红包。这是一个纯Web页面,在整个活动页面中有两个场景的适配是较为蛋疼的。首先就是页头和页脚的位置适配。

由于刘海机的增加,顶部和底部的安全区域成为不定因素,所以这一部分面对的难度和不定因素也变多。稍后我们会深度的剖析这部分从设计到开发应该怎么来处理。

另外一个场景是纵向(垂直)方向的适配。

其实纵向的适配一直是全屏模式的一种痛点。在不同高度的终端上如果要达到一样的效果,实现成本是非常高的。至少目前的各种方案都存在这样的问题。

线上最终效果

先来看看养眼的高端机iPhone X 和 iPhone XS Max:

再来看看普通的iPhone设备(iPhone6, iPhone6+, iPhone7+,iPhone8),我身边的设备都跑了一圈:

最后来看看众生相的安卓设备(我只是截取了部分):

是不是顿时感觉前端苦逼。

理解设计

前面的都是一些前奏,似乎有点长,但不要紧,接下来我们进入正题。

在开始编码之前,理解设计是非常的重要,特别是在这样的复杂场景(主要说的是复杂的适配场景)。整个项目的设计无法一一来进行剖析,这里就拿两个特别之处。因为我们要解的是Web页面的Bar,顶部Bar和底部的Bar。先上一张图:

优秀的设计师会给我提供多种状态,或者告诉我不同状态的尺寸。比如分享的页面,其底部就具备两种尺寸:

短屏下是尺寸是750px * 315px,理解成iPhoneX设备之下,都采用的是这个尺寸,当然背景图片也是如此;另外一个尺寸是750px * 448px,对应iPhone X及新出的iPhone XS/XR/XS Max。事实上这样的理解并不完全正确,因为有一些安卓的设备也具备类似于iPhone X的尺寸或更大的尺寸。暂时先忽略这个现象吧。

另外就是页面的顶部Bar和底部Bar的尺寸了。在iPhone X的设计中,顶部Bar的高是128px(设计师告诉我顶部Bar有两种尺寸,iPhone X以下的是128px,iPhone X 及其他带刘海的设备尺寸是176px),底部Bar尺寸是200px

做过iPhone X 适配,都知道有安全区域一说,或者说顶部刘海和底部Home键的位置(大小),但对于新出来的设备(不管是iOS系列还是Android系列)有好多的不确定。就算是抛开这些新设备,也面临着不少的问题。比如说Bar的尺寸,应该怎么确定,是不是跟着设计稿走就行;纵向布局应该怎么处理短屏和高屏。这一切的一切对于前端来说,在不同的项目中都会有未知的问题出现。那么问题来了,我们可以通过设计的标准或者说技术的手段来规避吗?

在我的理解中应该是可以的。为什么这么说呢?我们先来理解设计的标准(不同的设备出现,他都有一套完成的设计标准)。在接下来的内容我们以iOS系列为依据来做判断,因为安卓我真心的惧怕,也没有抽出那么多的时间一款款的测试(这对于做技术的而言是不好的一点)。

理解标准

理解标准需要具备一些基础,比如说一些术语的描述,如果你感兴趣的话,建议你移步阅读一下《移动端上的设计和适配》一文。如果没有这些基础也不会太防碍你继续往下阅读。

在苹果发布新款设备的时候,不管是设计师还是前端开发,都急切的想知道这些设备的各种参数,比如说分辨率、PPI之类的。

这里给大家特别推荐一个网站Vizdevices,可以让大家轻易的获取有关于主流移动设备的各项参数。比如下面的截图:

估计大家更为关心的是如何在项目中适配:切几倍的图用几倍的尺寸刘海怎么适配(新机是不是和iPhoneX一样)等等。到目前为止,最起码在这个项目中,前端和视觉设计师约定的尺寸还是以750px宽度:

而且我们构建工具中有关于单位之间的转换依旧约定的还是以750px为基础。也就是说目前主流还是以@2x为基准做设计稿,然后提供@2x图给前端,当然也有的设计师会额外提供@3x的切图给到开发人员。

手机适配采用几倍图与PPI有关系,也就是像素密度,所以我们可以理解为什么iPhone4、5、6之间分辨率和屏幕尺寸不一样,但是同样采用@2x图的原因,是因为它们有同样的PPI(326ppi。但新款的设备就不一样了:

从上图中我们可以获知,iPhone XS与iPhone X的数据是一致的。因此我们可以得出iPhone XS可以像iPhone X一样,提供@3x的尺寸:

而iPhone XS Max的PPI和iPhone X/XS是一样的,都是458PPI,只是其分辨率和Viewport要更大。由此可以推论出iPhoneXs Max使用的同样是三倍图@3x

从屏幕(Viewport)宽高比例来看:

  • iPhoneXS Max宽度1242/3=414pt,iPhone8 Plus宽度1242/3=414pt,两者的宽度一致
  • iPhoneXS Max高度2688/3=896pt,iPhone8 Plus高度2208/3=736pt

iPhoneXS Max比iPhone8 Plus长一截,多了160pt

因此,我们这次的设计,iPhoneXS Max也跟着iPhone X走的:

接着我们最后来看iPhone XR这款设备。其PPI是326,分辨率为828px * 1792px,屏幕宽度比为:414px * 896px。而iPhone其他款设备,比如iPhone 7 Plus、iPhone 7,甚至iPhone 5,它们的PPI都是326。也就是说iPhoneXR与苹果二倍图的PPI是一致。如此就可以推论出iPhoneXR使用的是二倍图@2x

同样从页面宽高比例来看:

  • iPhoneXR宽度828/2=414pt,iPhoneXS Max宽度1242/3=414pt
  • iPhoneXR高度1792/2=896pt,iPhoneXS Max高度2688/3=896pt

如果你够仔细的话,不难发现,从设备的宽度(Viewport)比来看,iPhoneXR 和 iPhoneXS Max是一样的,如下图所示:

对于设计和开发而言,他们不同之处就是iPhoneXR使用的是@2x图或尺寸,而iPhoneXS Max是@3x图或尺寸:

上面就是有关于苹果新机和老设备之间的差异。但这仅仅是iOS系列,我们还需要面对的是复杂的Android系列,但这一块在这篇文章不做过多阐述,因为我目前还不具备方面能力,能把这里面的故事说清楚。

有了这些基础和概念,我们就可以继续下一步的探讨了。

这些新尺寸对我们Web页面的设计和布局有何影响?如何去适配?

其实这才是我们一线开发人员最为关心的东西。

苹果自从iOS11开始,抛出一个安全区域的概念(其实现在安卓刘海机也有安全区域存在)。对于Web页面设计和开发都有必要对这一概念有所了解。那先花一点时间来了解一下安全区域,如果你对这方面很熟悉,你可以跳过这一节,继续往下阅读。

安全区域

安全区域(Safe Area),一个熟悉又陌生的词语。

熟悉是因为在平面设计中,由于印刷裁切过程中的误差,设计师需要给设计稿预留出「出血」 位置,确保设计内容在安全区域中;陌生又是因为在互联网设计中已极少被提及。

这里指的安全区域不仅仅针对于iOS的设备,只不过以iOS设备为例。所以这里所指的设备安全区域指的是屏幕内适合放置控件的安全区域。

在没有状态栏和其他东西的 iPhone 8 里,Safe Area 是指整个屏幕。

当加入状态栏后,Safe Area 便向下减少了 20pt。当我们加入 Navigation 的时候,Safe Area 又减少了 44pt。同理,我们再加入 Tabbar 的时候,Safe Area 又减少了 44pt(PS:此处更正, Tabbar 高度应该是 49pt)。

在 iPhone X 里,当我们没有使用状态栏时,Safe Area 依然和上下边有一定的距离。按照我的测量,此时距离底部应该是 43pt,距离顶部应该是 44pt

同理,加入不同 Bar 之后,iPhone X 的 Safe Area 都会有相应的变化。

后续设计中,iPhone XS/XR/XS Max可以按这个距离做为设计依据。

遵守 Safe Area (安全区域) 的界定

拿苹果官网针对于iPhone X的安全区域来举例。

状态栏。遵守安全区域的界定,在状态栏下面留出适当的空间。避免为状态栏高度预设值,这可能会导致您的内容被状态栏遮挡或形成错位。

圆弧展示角和传感器槽。您的 App 的内容元素和控制按键应避开屏幕角落和传感器槽,让其在填满屏幕的同时不被角落切割。

主屏幕指示器。为使 App 的内容和控件始终保持清晰可见且便于点按,请确保您的 App 不会干扰主屏幕指示器。

屏幕边缘手势。iPhone X 显示屏利用屏幕边缘手势来访问主屏幕,App 切换器,通知和控制中心。请避免对这些手势造成干扰。您可将控件移到安全区域并调整用户界面。极少数情况下,您可以考虑使用边缘保护:用户的首个滑动手势将被视为 App 内的特定手势,第二次滑动才会被视为系统手势。

确保您的代码能适应不同的屏幕宽高比。许多 App 会根据特定的宽度,高度或宽高比来定位其内容。请检查您的内容是否已正确缩放并定位。

调整视频的缩放度。iPhone X 上的视频内容应填满屏幕。但是,如果这导致顶部或底部被切割,或侧面裁剪太多,则应将视频拉伸或缩小以配合屏幕。当 AVPlayerViewController自动管理时,基于AVPlayerLayer的自定义视频播放器需要选择适当的初始视频重力设置,并允许用户根据自己的喜好在 aspect (固定宽高比) 和 aspectFill (固定宽高比且全屏) 观看模式之间进行切换。

安全区域布局

新发布的iPhoneXS、XS Max、XR都采用了全面屏设计,因此我们必须保证布局填满屏幕,并且考虑到交互操作,要留出安全区域,才不会被圆角、刘海影响使用,布局的左右边距可根据产品自定义,这些点与iPhoneX是相同的。

上面提到过,iPhoneXS与iPhoneX尺寸大小完全一致,所以页面布局也是一样的。我们只需要懂得怎样适配到iPhoneXS Max以及iPhoneXR的布局就可以了(两者的的逻辑像素是一致的,均为414pt * 896pt,区别在于一个是@3x,一个是@2x)。

这样设计也更能符合苹果官方所提的设计理念。设计布局要填充整个屏幕,这里有两块区域需要额外考虑:

屏幕顶部,即StatusBar部分

这条状态栏本来并没有可发挥的空间,但是iPhone的StatusBar与NavigationBar(以下简称NavBar)背景是可以通栏的,以达到一种完全沉浸式体验的设计。

大部分的APP应该也是没有影响的(主流NavBar都采用纯色背景,StatusBar背景沿用NavBar的背景),但是对于那些做了NavBar视觉效果的设计师就要考虑了,你的渐变色背景、或者带底纹的背景、还包括电商平台商品图是通栏展示的商品图,多少会对实际效果产生一些影响。

比如,NavigationBar是渐变色背景的,由于iPhoneX/XS/XR/XS Max的Status+Nav高度增加,我们1242 x 192(@3X)的背景图会被等比例拉伸至这两块区域并且剪辑多余部分。

屏幕底部

针对于iPhoneX/XS/XR/XS Max设备,屏幕底部的虚拟区,替代了Home键,高度为34pt

指示灯区域是一个带着系统功能的内容显示区域,这就意味着它可以展示内容;同时如果你的底部是TabBar,那么指示灯区域背景会来自于TabBar背景的延伸;如果我们是一个feed流的页面,底部则会展示次屏feed流的局部。

鉴于圆角、传感器、指示灯区域的影响,iPhoneX/XS/XR/XS Max给出了设计布局的安全区意见:

再考虑必要的NavBar、TabBar,主题内容显示的安全区需要根据设计需求进行考虑。根据实际需要,我们添加的所有控件都应当在安全区内,如各类型的Button、Edit Menu、Pickers、Sliders等等。

从设计的角度做适配

这个时候设计也是痛苦的,其实仔细思考一下,我们还是有一定的应对方案。比如下面的两种方案,都值得我们在平时的设计中做考量。

如果我们在设计的时候以iPhone8(375pt * 667pt)为基准做设计稿,先得到iPhoneXR:由于都是@2x,首先需要将画板宽度拉宽为414pt,高度拉高为896pt(与我们做iPhone5到iPhone6的宽高变化处理是一样的道理),状态栏由20pt变高为44pt,在底部加上主页指示器(Home Indicator)高度为34pt,导航栏以及标签栏高度不变。我们发现iPhoneXR内容呈现的比iPhone8要多一些。

有了iPhoneXR后,直接等比例放大1.5倍就可以得到iPhoneXS Max。

另外一个方案是,我们在设计的时候以iPhoneX(375pt * 812pt)为基准做设计稿,先得到iPhoneXS Max:由于都是@3x,首先需要将画板宽度拉宽为414pt,高度拉高为896pt(与方法一同理),状态栏、导航栏、标签栏、主页指示器的高度均不用更改。有了iPhoneXS Max后,直接等比例缩小2/3就可以得到iPhoneXR,很简单~。

当然,这仅仅是其中两种设计的适配方案,事实上应该还有其他的方案。如果你在这方面有更多的经验或成功的案例,欢迎与在下面的评论中与我们一起共享。

从开发的角度做适配

自从去年过年项目开始,互动前端开发都开始基于vw来进行布局和适配的开发。在构建工具中,我们采用了postcss-px-to-viewport这款PostCSS插件。

// .postcssrc.js

module.exports = {
    "postcss-px-to-viewport": {
        viewportWidth: 750,      // (Number) The width of the viewport.
        viewportHeight: 1334,    // (Number) The height of the viewport.
        unitPrecision: 3,        // (Number) The decimal numbers to allow the REM units to grow to.
        viewportUnit: 'vw',      // (String) Expected units.
        selectorBlackList: ['.ignore', '.hairlines'],  // (Array) The selectors to ignore and leave as px.
        minPixelValue: 1,        // (Number) Set the minimum pixel value to replace.
        mediaQuery: false        // (Boolean) Allow px to be converted in media queries.
    },
}

其中有两个非常关键字段:viewportWidthviewportHeight, 其对应的就是设计稿的长和宽,按照我们以前是以iPhone8的分辨率:750px * 1334px。如果设计稿是按iPhone X进行设计的话,对应的viewportWidthviewportHeight就需要做出相应的调整,将调整为:1125px * 2436px。对应的配置文件更换成:

// .postcssrc.js

module.exports = {
    "postcss-px-to-viewport": {
        viewportWidth: 1125,      // (Number) The width of the viewport.
        viewportHeight: 2436,     // (Number) The height of the viewport.
        unitPrecision: 3,         // (Number) The decimal numbers to allow the REM units to grow to.
        viewportUnit: 'vw',       // (String) Expected units.
        selectorBlackList: ['.ignore', '.hairlines'],  // (Array) The selectors to ignore and leave as px.
        minPixelValue: 1,        // (Number) Set the minimum pixel value to replace.
        mediaQuery: false        // (Boolean) Allow px to be converted in media queries.
    },
}

Web页面的Bar适配设计

前面花了很大的篇幅在探讨有关于设备的标准以及设计适配的处理方案和需要注意的细节。从这里开始,我们就来看看Web页面Bar适配设计和实现细节。

在我们的Web页面,对于Bar而言,常见的情形有两种,Bar是固定的,另外一种是不固定的(随着页面一起滚动的)。而我们这次的项目就是固定的(其实一开始是不固定的)。其实是不是固定并不重要,重要的是我们把设计和实现方案容入到标准中去。只有这样,我们的技术方案才会更适合于各种场景。

回过头来看我们的标准和设计。

简单的理解,不带刘海的设备(iPhone 8Plus 及以下),其StatusBar高度是20pt + 44pt(Status为20pt,NavBar是44pt)。而带刘海的设备(iPhoneX/XS/XR/XS Max),其StatusBar高度变成了44pt + 44pt。这只是针对iOS系列的设备,对于Android系列,这个尺寸目前未知。所以后续聊的尺寸都是以iOS为基准!!!

根据上面的尺寸,就可以理解为,Web页面顶部Bar的尺寸是44pt88px),而前面提到过,设计师告诉我们,iPhone X之下的设备,顶部Bar是64pt128px),iPhoneX之上的设备,顶部Bar是88pt176px)。看到这两组数据,是不是有点晕呼。事实上没有那么复杂,因为设计师提供的尺寸是StatusBar,记住,这个尺寸是StatusBar(状态栏 + Bar)。但是对于我们前端开发而言,所说的Bar就是Bar,并没有包括状态栏。那么问题来了,状态栏有没有高度有没有必要包含进来。如果包含进来,Bar的高度就是128px176px。这就变成两难的竞地。不管选择哪一个值,都不是理想的值,因为这样一来会造成在部分设备中顶部的Bar不是标准的88px,这个项目在部分安卓机就有这方面的现象,比如下图所示:

因此,我把顶部Bar的高度(height)设置为88px。这样做的主要原因是,我不需要纠结是128px还是176px。因为Bar就是Bar,Status就是Status。可能大家会问,全屏的项目,如果高度只设置88px,企不是会被Status遮住。这样的想法是对的,但我们这个时候应该通过别的方式把Status的高度填进来。比如说padding-topmargin-top之类的(填充容器高度的方法有很多种,这仅仅是最简易的方式而且)。当然,也有同学会问,就算是使用padding-top来撑高,以至Bar不会被Status遮住,难道就不会面临padding-top的值是40px还是88px。此时想哭!

其实,这个时候我们应该想到CSS的另一个特性:即env()。该属性以前是iOS的私有属性,现在也被纳入到W3C规范草案中了。我们可以给其传递safe-area-inset-*参数。

有关于CSS的env()函数和safe-area-inset-*相关的介绍这里就不再花篇幅来阐述了,感兴趣的同学,可以阅读《iPhone X的缺口和CSS》一文。

也就是说,我们在设置padding-top的值,不需要显式的设置,可以通过env()函数帮我们自动获取。这个时候,自然能做到不同的设备取得值不同。

.nav-bar {
    height: 88px;
    padding-top: env(safe-area-inset-top);
}

这个方式对于Android目前还不凑效。所以有可能会造成部分Android显示有缺陷,需要设备来进行验证。主要看你的Web页面是不是能穿透到状态栏(Status)。

同样的,对于Web页面底部的Bar也可以使用类似的原理(相对顶部Bar要简单一些,只需要考虑iPhoneX/XS/XR/XS Max)。从上面的图我们可以获得底部Bar高度也是44pt88px),如果底部是Tabbar的话(一般是Tabbar),其高度是49pt96px)。假设你的Web页面是一个Tabbar:

.tab-bar {
    height: 96px;
    padding-bottom: env(safe-area-inset-bottom)
}

现在回到我们的项目中来,先来看设计稿:

通用的底部Bar,其高度是200px(带Home键的设备)。从上面可以获知,底部Home键的高度是34pt68px)。这样一来,对于不带Home键的设备,底部Bar的高度应该是:200px - 68px = 132px。但设计师又告诉我,不带Home键的Bar高度是150px。那么问题来了,这个时候我们就要自己做出选择了。这里说一下我的做法,类似于顶部bar设计,我把height设置为普通设备的高度,也就是150px。对于带Home键的设备,同样借用env()来出处理,所以代码就像下面这样:

.footer-bar {
    height: 150px;
    padding-bottom: env(safe-area-inset-bottom)
}

对于其他页面,其高度也按类似的原理进行处理。说了这么多,我们通过示例来演示一波。

先上二维码,请使用手机淘宝扫下面的二维码:

结构很简单:

<!-- App.vue -->
<template>
    <div id="app">
        <NavBar />
        <FooterBar />
        <div class="content">
            <main>
                <div>Main content</div>
                <ul>
                    <li v-for="(item, index) in num" :key="index">{{ index++ }}</li>
                </ul>
            </main>
        </div>
    </div>
</template>

大家可以根据自己的需要做出相应的调整,其中有一个细节需要注意的是,固定元素nav-barfooter-bar在主内容.content前面,这主要是为了配合CSS选择器做一些元素控制。

CSS很简单:

// Navbar.vue style
.nav-bar {
    background: orange; 
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
}
.nav {
    height: 88px;
    background: #f36;
    color: #fff;
    font-size: 30px;
    display: flex;
    justify-content: center;
    align-items: center;
}

.nav h1 {
    font-size: 30px;
    margin: 0;
}
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: var(--safe-area-inset-top);
    }
}

// FooterBar.vue Style
.footer-bar {
    background: green;
    position: fixed;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 2;
}
.bar {
    height: 150px;
    background: linear-gradient(to bottom, #ff8136 0%, #ff0f4a 100%);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
}
@supports (padding-top:env(safe-area-inset-top)){
    .footer-bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

// App.vue style
body {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

#app {
    width: 100vw;
    height: 100%;
}

.content {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1;
    overflow-y: auto;
    scroll-behavior: smooth;
    background-color: #fff;
    -webkit-overflow-scrolling: touch;
}

.content > * {
    transform: tranlateZ(0);
    will-change: transform;
}

main {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 0 16px;
}
.nav-bar ~ .content {
    padding-top: 88px;
}
.footer-bar ~ .content {
    padding-bottom: 150px;
}

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

其中关键点的代码:

// NavBar.vue
.nav {
    height: 88px;
    //...
}
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: var(--safe-area-inset-top);
    }
}

// FooterBar.vue
.bar {
    height: 150px;
    //...
}
@supports (padding-top:env(safe-area-inset-top)){
    .footer-bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

// App.vue
.nav-bar ~ .content {
    padding-top: 88px;
}
.footer-bar ~ .content {
    padding-bottom: 150px;
}

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

上面的代码应该不需要做相应的阐述,大家应该都懂的。只不过此处借用CSS条件特性@supports对安全区域做了一些判断,只有支持env(safe-area-inset-*)的设备才能识别这段CSS。如果不这么使用,在Android设备下会出一个Bug,那就是padding-top: calc(var(--safe-area-inset-top) + 88px)会覆盖padding-top: 88px。从而影响.content在Android上的布局显示。

最终的效果如下,先来看iOS系列的效果:

再来看一下Android系列的效果:

不知道这样的效果,您是否能接受。

事实上,上面的代码还可以进行优化,但我把怎么优化的方案和后面的案例结合起来讨论。

纵向适配布局

什么是纵向适配布局?

平时的项目中,或许你碰到过Web页面全屏(关键是单屏)显示,为了让高屏(比如iPhoneX、iPhone XS Max等终端)和短屏(比如iPhone 4、iPhone 5之类的终端)都能较为美观,比如像下面这样的效果(设计师专门为不同的设备做了一些微调):

对于前端开发的同学而言,成本相对而言是要更高的。那么接下来,我们来看看,面对这样的布局,我们又应该怎么去做适配。

拿左侧的页面来举例。同样先上个二维码,使用手机淘宝扫描下面的二维码:

先来看效果:

从左右分别是iPhone 8,iPhone X和iPhoneXS Max上的效果,其他终端的效果就不上图了。感兴趣的可以扫上面的二维码。

简单的说一下,这个页面同样有一个顶部Bar和底部Bar,和前面的页面不同的是,顶部Bar只有一个返回按钮,而底部Bar变得更大了。对于布局的结构是一样的,这里不上代码了。不同的是,底部Bar的代码做了相应的调整:

/*Footerbar style*/
.footer-bar {
    position: fixed;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 2;
}
.bar {
    height: 315px;
    box-sizing: content-box;
    ...
}

@supports (padding-top:env(safe-area-inset-top)){
    .bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

/* App.vue style*/
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar ~ .content {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: calc(var(--safe-area-inset-top) + 88px);
    }
    .footer-bar ~ .content {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: calc(var(--safe-area-inset-bottom) + 315px);
    }
}

其实顶部Bar和底部bar并没有太多的差异,只是高度的值发生了变化。这个Demo其实并不太复杂,而且差异性也并不大。在这样的环境底部,元素和元素之间的间距我们可以根据屏幕的高度来做相对计算,比如vh。因为不同屏幕的高度是不一样的。比如上例中,.title.share之间有一个间距,从设计稿中可以获知是30px。在以前,我们都是使用的是横向比来做的(根据屏幕的宽度转换成vw),在750px的设计稿,其值将是30 / 7.5 = 4vw。但对于纵向屏幕的适配,这个时候我们应该根据屏幕的高度来进行相对定位,或许更为理想一些,拿iPhone8设计稿来说,他的高度是1334px(对应的是100vh)。同样的,30 / 13.34 = 2.24887556vh

有个细节特别重要,并不是所有元素之间的间距都建议采用vh做单位,大家应该根据自己的设计稿,如果高底屏有较大差异化时,建议采用vh,否则还是更建议使用vw

当然,有的时候采用vh并不一定能完全达到所有设计的需求,那么碰到这样的场景怎么办呢?暴力一点的方案,有的同学采用window.innerWidth / window.innerHeight和一个最佳比例做比较,如果大于这个阈值,给body添加一个类名,然后再对个别元素位置进行调整。对于我而言,我个人更趋向于采用CSS的媒体查询。比如:

@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;
        }
    }
}

这种方案令人头痛的或许就是断点的确定。比如iPhoneXS Max可以通过:

@media only screen and (min-width: 414px) and (max-width: 767px),
only screen and (min-height: 896px),
only screen and (min-device-pixel-ratio: 3) {
    /* 对应的样式 */
}

大多数媒体属性可以带有min-max-前缀,用于表达“最低...”或者“最高...”。例如,max-width:12450px表示应用其所包含样式的条件最大是宽度为12450px,大于12450px则不满足条件,不会应用此样式。

因此,对于高屏的设备,不管是iOS系列还是Android系列,我们应该找到一个最佳的临界值。这个估计需要花费一定的时间。不过在vizdevices网站提供了众多主流终端设备的媒体查询语句。感兴趣的同学,或者有心的同学,可以尝试一下。

针对不同屏幕采用不同的位置控制。我目前能想到较好处理高,短屏适配一种方案。

终端安全区域适配总方案

通过前面的两个示例,我们看到了,对于不同的页面或者需求,面对的场景是不同的。为了简化或者说让方案更趋于适用性,我们可以借助CSS自定义属性,把顶部Bar和底部Bar的height:root中定义成两个变量:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
}

除此之外,还会使用到顶部安全区域和底部安全区域的高度,同样可以把他们在:root中声明自定义属性:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
    --safe-area-inset-bottom: env(safe-area-inset-bottom);
    --safe-area-inset-top: env(safe-area-inset-top);
}

那么对应带有Bar的页面,处理安全区域,咱位就可以这样写:

.nav-bar{
    height: var(--navBarHeight, 88px);
    box-sizing: content-box;
}
.footer-bar {
    hegiht: var(--footerBarHeight, 150px);
    box-sizing: content-box;
}

.nav-bar ~ .content {
    padding-top: var(--navBarHeight, 88px)
}
.footer-bar ~ .content {
    padding-bottom: var(--footerBarHeight, 150px)
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        padding-top: var(--safe-area-inset-top);
    }
    .footer-bar {
        padding-bottom: var(--safe-area-inset-bottom)
    }

    .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))
    }
}

但我们的页面,有时候是没有固定的顶部Bar和底部Bar。换句话说,页面只有.content。即:

<!-- 带Bar的页面模板 -->
<div id="app">
    <NavBar />
    <FooterBar />
    <div class="content"></div>
</div>

变成了:

<!-- 不带Bar的页面模板 -->
<div id="app">
    <div class="content"></div>
</div>

所以我们的通用解决方案也要做相应的调整:

: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{
    height: var(--navBarHeight, 88px);
    box-sizing: content-box;
}
.footer-bar {
    hegiht: var(--footerBarHeight, 150px);
    box-sizing: content-box;
}

.nav-bar ~ .content {
    padding-top: var(--navBarHeight, 88px)
}
.footer-bar ~ .content {
    padding-bottom: var(--footerBarHeight, 150px)
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        padding-top: var(--safe-area-inset-top);
    }
    .footer-bar {
        padding-bottom: var(--safe-area-inset-bottom)
    }

    .content {
        padding-top: var(--safe-area-inset-top);
        padding-bottom: var(--safe-area-inset-bottom);
    }

    .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))
    }
}

总结

文章篇幅有点长,简单的总结一下。文章先从终端设计的标准(iOS终端设备)出发,帮助我们更清楚了了解终端的顶部Bar和底部Bar如何设计更趋于标准化。这样一来,前端开发借助CSS的一些特性,比如通用兄弟选择器(E 〜 F),env()函数,分别对带有Bar和不带有Bar的安全区域控制。对于纵向屏幕的适配,建议在合适的元素之间采用vh做为度量单位,如果高、短屏有较大差异化设计,更建议采用CSS媒体查询来做相应的处理。

如果大家对这方面有更好的建议,欢迎在下面的评论中分享出您的宝贵经验!如果文章中有何不对之处,欢迎各位大婶斧正!(^_^)


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

$
0
0

在上一篇《手淘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页面提供正确图像的姿势》一文。

总结

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

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

聊聊Flexbox布局中的flex的演算法

$
0
0

到目前为止,Flexbox布局应该是目前最流行的布局方式之一了。而Flexbox布局的最大特性就是让Flex项目可伸缩,也就是让Flex项目的宽度和高度可以自动填充Flex容器剩余的空间或者缩小Flex项目适配Flex容器不足的宽度。而这一切都是依赖于Flexbox属性中的flex属性来完成。一个Flex容器会等比的按照各Flex项目的扩展比率分配Flex容器剩余空间,也会按照收缩比率来缩小各Flex项目,以免Flex项目溢出Flex容器。但其中Flex项目又是如何计算呢?他和扩展比率或收缩比率之间又存在什么关系呢?在这篇文章中我们将一起来探来。

在Flexbox布局中,容器中显示式使用display设置为flexinline-flex,那么该容器就是Flex容器,而该容器的所有子元素就是Flex项目。

简介

在这篇文章中,我们将要聊的是有关于flex属性的事情,特别是如何使用该属性来计算Flex项目?在开始之前,先来简单的了解一下flex属性。

在Flexbox中,flex属性是flex-grow(扩展比率)、flex-shrink(收缩比率)和flex-basis(伸缩基准)三个属性的简称。这三个属性可以控制一个Flex项目(也有人称为Flex元素),主要表现在以下几个方面:

  • flex-grow:Flex项目的扩展比率,让Flex项目得到(伸张)多少Flex容器多余的空间(Positive free space)
  • flex-shrink:Flex项目收缩比率,让Flex项目减去Flex容器不足的空间(Negative free space)
  • flex-basis:Flex项目未扩展或收缩之前,它的大小是多少

在Flexbox布局中,只有充分理解了这三个属性才能彻底的掌握Flex项目是如何扩展和收缩的,也才能更彻底的掌握Flexbox布局。因此掌握这三个属性,以及他们之间的计算关系才是掌握Flexbox布局的关键所在。

相关概念

在具体介绍flex相关的技术之前,先对几个概念进行描述,因为理解了这几个概念更有易于大家对后面知识的理解。

主轴长度和主轴长度属性

Flex项目在主轴方向的宽度或高度就是Flex项目的主轴长度,Flex项目的主轴长度属性是widthheight属性,具体是哪一个属性,将会由主轴方向决定。

剩余空间和不足空间

在Flexbox布局中,Flex容器中包含一个或多个Flex项目(该容器的子元素或子节点)。Flex容器和Flex项目都有其自身的尺寸大小,那么就会有:Flex项目尺寸大小之和大于或小于Flex容器情景:

  • 当所有Flex项目尺寸大小之和小于Flex容器时,Flex容器就会有多余的空间没有被填充,那么这个空间就被称为Flex容器的剩余空间(Positive Free Space)
  • 当所有Flex项目尺寸大小之和大于Flex容器时,Flex容器就没有足够的空间容纳所有Flex项目,那么多出来的这个空间就被称为负空间(Negative Free Space)

举个例子向大家阐述这两个情形:“假设我们有一个容器(Flex容器),显式的给其设置了width800pxpadding10px,并且box-sizing设置为border-box”。根据CSS的盒模型原理,我们可以知道Flex容器的内宽度(Content盒子的宽度)为800px - 10px * 2 = 780px

假设Flex容器中包含了四个Flex项目,而且每个Flex项目的width都为100px,那么所有Flex项目的宽度总和则是100px * 4 = 400px(Flex项目没有设置其他任何有关于盒模型的尺寸),那么Flex容器将会有剩余的空间出来,即780px - 400px = 380px。这个380px就是我们所说的Flex容器的剩余空间:

假设把Flex项目的width100px调到300px,那么所有Flex项目的宽度总和就变成了300px * 4 = 1200px。这个时候Flex项目就溢出了Flex容器,这个溢出的宽度,即1200px - 780px = 420px。这个420px就是我们所说的Flex容器的不足空间:

上面演示的是主轴在x轴方向,如果主轴变成y轴的方向,同样存在上述两种情形,只不过把width变成了height接下来的内容中,如果没有特殊说明,那么所看到的示例都仅演示主轴在x轴的方向,即flex-directionrow

min-content 和 max-content

min-contentmax-content是CSS中的一个新概念,隶属于CSS Intrinsic and Extrinsic Sizing Specification模块。简单的可以这么理解。

CSS可以给任何一个元素显式的通过width属性指定元素内容区域的宽度,内容区域在元素paddingbordermargin里面。该属性也是CSS盒模型众多属性之一。

记住,CSS的box-sizing可以决定width的计算方式。

如果我们显式设置width为关键词auto时,元素的width将会根据元素自身的内容来决定宽度。而其中的min-contentmax-content也会根据元素的内容来决定宽度,只不过和auto有较大的差异

  • min-content: 元素固有的最小宽度(内容)
  • max-content: 元素固有的最大宽度(内容)

比如下面这个示例:

如果内容是英文的话,min-content的宽度将取决于内容中最长的单词宽度,中文就有点怪异(其中之因目前并未深究),而max-content则会计算内容排整行的宽度,有点类似于加上了white-space:nowrap一样。

上例仅展示了min-contentmax-content最基本的渲染效果(Chrome浏览器渲染行为)。这里不做深入的探讨论,毕竟不是本文的重点,如果感兴趣,欢迎关注后续的相关更新,或者先阅读@张鑫旭 老师写的一篇文章《理解CSS3 max/min-contentfit-contentwidth

回到我们自己的主题上来。

前面在介绍Flex剩余空间和不足空间的时候,我们可以得知,出现这两种现象取决于Flex容器和Flex项目的尺寸大小。而flex属性可以根据Flex容器的剩余空间(或不足空间)对Flex项目进行扩展(或收缩)。那么为了计算出有多少Flex容器的剩余空间能用于Flex项目上,客户端(浏览器)就必须知道Flex项目的尺寸大小。要是没有显式的设置元素的width属性,那么问题就来了,浏览器它是如何解决没有应用于绝对单位的宽度(或高度)的Flex项目,即如何计算?

这里所说的min-contentmax-content两个属性值对于我们深入的探讨flex属性中的flex-growflex-grow属性有一定的影响。所以提前向大家简单的阐述一正是这两个属性值在浏览器中的渲染行为。

简单的总结一下:min-content的大小,从本质上讲,是由字符串中最长的单词决定了大小;max-content则和min-content想反. 它会变得尽可能大, 没有自动换行的机会。如果Flex容器太窄, 它就会溢出其自身的盒子!

Flex项目的计算

在Flexbox布局当中,其中 flex-growflex-shrinkflex-basis都将会影响Flex项目的计算。接下来我们通过一些简单的示例来阐述这方面的知识。

flex-basis

flex-basis属性在任何空间分配发生之前初始化Flex项目的尺寸。其默认值为auto。如果flex-basis的值设置为auto,浏览器将先检查Flex项目的主尺寸是否设置了绝对值再计算出Flex项目的初始值。比如说,你给Flex项目设置的width200px,那么200px就是Flex项目的flex-basis值。

如果你的Flex项目可以自动调整大小,则auto会解析为其内容的大小,这个时候,min-contentmax-content变会起作用。此时将会把Flex项目的max-content作为 flex-basise的值。比如,下面这样的一个简单示例:

flex-growflex-shrink的值都为0,第一个Flex项目的width150px,相当于flex-basis的值为150px,而另外两个Flex项目在没有设置宽度的情况之下,其宽度由内容的宽度来设置。

如果flex-basis的值设置为关键词content,会导致Flex项目根据其内容大小来设置Flex项目,叧怕是Flex项目显式的设置了width的值。到目前为止,content还未得到浏览器很好的支持。

flex-basis除了可以设置autocontentfillmax-contentmin-contentfit-content关键词之外,还可以设置<length>值。如果<length>值是一个百分比值,那么Flex项目的大小将会根据Flex容器的width进行计算。比如下面这个示例:

Flex容器显式设置了width(和box-sizing取值有关系,上图为border-box的示例结果),那么flex-basis会根据Flex容器的width计算出来,如果Flex容器未显示设置width值,则计算出来的结果将是未定义的(会自动根据Flex容器的宽度进行计算)。

在Flexbox布局中,如果你想完全忽略Flex项目的尺寸,则可以将flex-basis设置为0。这样的设置,基本上是告诉了浏览器,Flex容器所有空间都可以按照相关的比例进行分配。

来看一个简单的示例,Flex项目未显式设置width情况之下,flex-basis不同取值的渲染效果:

到写这篇文章为止,使用Firefox浏览器查看效果更佳。

当Flex项目显式的设置了min-widthmax-width的值时,就算Flex项目显式的设置了flex-basis的值,也会按min-widthmax-width设置Flex项目宽度。当计算的值大于max-width时,则按max-width设置Flex项目宽度;当计算的值小于min-width时,则按min-width设置Flex项目宽度:

有关于flex-basis属性相关的运用简单的小结一下:

  • flex-basis默认值为auto
  • 如果Flex项目显式的设置了width值,同时flex-basisauto时,则Flex项目的宽度为按width来计算,如果未显式设置width,则按Flex项目的内容宽度来计算
  • 如果Flex项目显式的设置了width值,同时显式设置了flex-basis的具体值,则Flex项目会忽略width值,会按flex-basis来计算Flex项目
  • 当Flex容器剩余空间不足时,Flex项目的实际宽度并不会按flex-basis来计算,会根据flex-growflex-shrink设置的值给Flex项目分配相应的空间
  • 对于Flexbox布局中,不建议显式的设置Flex项目的width值,而是通过flex-basis来控制Flex项目的宽度,这样更具弹性
  • 如果Flex项目显式的设置了min-widthmax-width值时,当flex-basis计算出来的值小于min-width则按min-width值设置Flex项目宽度,反之,计算出来的值大于max-width值时,则按max-width的值设置Flex项目宽度

flex-grow

前面提到过,flex-grow是一个扩展因子(扩展比例)。其意思是,当Flex容器有一定的剩余空间时,flex-grow可以让Flex项目分配Flex容器剩余的空间,每个Flex项目将根据flex-grow因子扩展,从而让Flex项目布满整个Flex容器(有效利用Flex容器的剩余空间)。

flex-grow的默认值是0,其接受的值是一个数值,也可以是一个小数值,但不支持负值。一旦flex-grow的值是一个大于0的值时,Flex项目就会占用Flex容器的剩余空间。在使用flex-grow时可以按下面的方式使用:

  • 所有Flex项目设置相同的flex-grow
  • 每个Flex项目设置不同的flex-grow

不同的设置得到的效果将会不一样,但flex-grow的值始终总量为1,即Flex项目占有的量之和(分子)和分母相同。我们来具体看看flex-grow对Flex项目的影响。

当所有的Flex项目具有一个相同的flex-grow值时,那么Flex项目将会平均分配Flex容器剩余的空间。在这种情况之下将flex-grow的值设置为1。比如下面这个示例,Flex容器(width: 800pxpadding: 10px)中有四个子元素(Flex项目),显式的设置了flex-basis150px,根据前面介绍的内容,我们可以知道每个Flex项目的宽度是150px,这样一来,所有Flex项目宽度总和为150px * 4 = 600px。容器的剩余空间为780px - 600px = 180px。当显式的给所有Flex项目设置了flex-grow1(具有相同的值)。这样一来,其告诉浏览器,把Flex容器剩余的宽度(180px)平均分成了四份,即:180px / 4 = 45px。而flex-grow的特性就是按比例把Flex容器剩余空间分配给Flex项目(当然要设置了该值的Flex项目),就该例而言,就是给每个Flex项目添加了45px,也就是说,此时Flex项目的宽度从150px扩展到了195px150px + 45px = 195px)。如下图所示:

特别声明,如果Flex项目均分Flex容器剩余的空间,只要给Flex项目设置相同的flex-grow值,大于1即可。比如把flex-grow设置为10,就上例而言,把剩余空间分成了40份,每个Flex项目占10份。其最终的效果和设置为1是等效的。

上面我们看到的均分Flex容器剩余空间,事实上我们也可以给不同的Flex项目设置不同的flex-grow值,这样一来就会让每个Flex项目根据自己所占的比例来占用Flex容器剩余的空间。比如上面的示例,把Flex项目的flex-grow分别设置为1:2:3:4。也就是说把Flex容器的剩余空间分成了10份(1 + 2 + 3 + 4 = 10),而每个Flex项目分别占用Flex容器剩余空间的1/102/103/104/10。就上例而言,Flex容器剩余空间是180px,按这样的计算可以得知,每一份的长度是180px / 10 = 18px,如此一来,每个Flex项目的宽度则变成:

  • Flex1: 150px + 18px * 1 = 168px
  • Flex2: 150px + 18px * 2 = 186px
  • Flex3: 150px + 18px * 3 = 204px
  • Flex4: 150px + 18px * 4 = 222px

最终效果如下图所示:

前面两个示例向大家演示了,Flex项目均分和非均分Flex容器剩余的空间。从示例中可以看出来,flex-grow的值都是大于或等于1的数值。事实上,flex-grow还可以设置小数。比如,给所有Flex项目设置flex-grow的值为0.2。由于Flex项目的flex-grow的值都相等,所以扩展的值也是一样的,唯一不同的是,所有的Flex项目并没有把Flex容器剩余空间全部分完。就我们这个示例而言,四个Flex项目的flex-grow加起来的值是0.8,小于1。换句话说,四个Flex项目只分配了Flex容器剩余空度的80%,按上例的数据来计算,即是180px * .8 = 144px(只分去了144px),而且每个Flex项目分得都是36px144px / 4 = 36px或者 144px * 0.2 / 0.8 = 36px)。最终效果如下图所示:

上面的示例中,flex-basis都显式的设置了值。事实上,flex-growflex-basis会相互影响的。这也令我们的Flex项目计算变得复杂化了。比如说,flex-basis的值为auto,而且没有给Flex项目显式的设置width。根据前面的内容我们可以得知,此时Flex项目的大小都取决于其内容的max-content大小。此时Flex容器的剩余的空间将由浏览器根据Flex项目的内容宽度来计算。比如接下来的这个示例,四个Flex项目都是由其内容max-content大小决定。同时将flex-grow都设置为1(均匀分配Flex容器剩余空间)。具体的数据由下图所示(Chrome浏览器计算得出的值):

特别注意,不同浏览器对小数位的计算略有差异,上图是在Chrome浏览器下得出的值。所以最终加起来的值略大于Flex容器的宽度708px

针对这样的使用场景,如果你想让所有Flex项目具有相同的尺寸,那么可以显式的设置Flex项目的flex-basis值为0flex: 1 1 0)。从flex-basis一节中可以得知,当flex-basis值为0时,表示所有空间都可以用来分配,而且flex-grow具有相同的值,因此Flex项目可以获取均匀的空间。如此一来Flex项目宽度将会相同。

flex-basis还可以由其他值为设置Flex项目的宽度,这里不再一一演示。感兴趣的同学可以自己根据flex-basis的取值写测试用例。换句话说,如果你理解了前面介绍的flex-basis内容,就能更好的理解flex-growflex-basis相结合对Flex项目分配Flex容器剩余空间的计算。也将不会再感到困惑。

flex-shrink

flex-shrinkflex-grow类似,只不过flex-shrink是用来控制Flex项目缩放因子。当所有Flex项目宽度之和大于Flex容器时,将会溢出容器(flex-wrapnowrap时),flex-shrink就可以根据Flex项目设置的数值比例来分配Flex容器的不足空间,也就是按比例因子缩小自身的宽度,以免溢出Flex容器。

flex-shrink接收一个<number>值,其默认值为1。也就是说,只要容器宽度不足够容纳所有Flex项目时,所有Flex项目默认都会收缩。如果你不想让Flex项目进行收缩时,可以设置其值为0,此时Flex项目始终会保持原始的fit-content宽度。同样的,flex-shrink也不接受一个负值做为属性值。

基于上面的示例,简单的调整一下参数,所有Flex项目都设置了flex: 0 0 300px,可以看到Flex项目溢出了Flex容器:

在这个示例中,由于flex-shrink显式的设置了值为0,Flex项目不会进行收缩。如果你想让Flex项目进行收缩,那么可以把flex-shrink设置为1

从上图的结果我们可以看出,当所有Flex项目的flex-shrink都设置为相同的值,比如1,将会均分Flex容器不足空间。比如此例,所有Flex项目的宽度总和是1200pxflex-basis: 300px),而Flex容器宽度是780pxwidth: 800pxpadding: 10px,盒模型是border-box),可以算出Flex容器不足空间为420px1200 - 780 = 420px),因为所有Flex项目的flex-shrink1,其告诉浏览器,将Flex容器不足空间均分成四份,那么每份则是105px420 / 4 = 105px),这个时候Flex项目就会自动缩放105px,其宽度就由当初的300px变成了195px300 - 105 = 195px)。

这个示例演示的是Flex项目设置的值都是相同的值,其最终结果是将会均分Flex容器不足空间。其实flex-shrink也可以像flex-grow一样,为不同的Flex项目设置不同的比例因子。比如1:2:3:4,这个时候Flex项目就不会均分了,而是按自己的比例进行收缩,比例因子越大,收缩的将越多。如下图所示:

就上图而言,所有Flex项目的flex-shrink之和为101 + 2 + 3 + 4 = 10),此时把Flex容器不足空间420px分成了十份,每一份42px420 / 10 = 42px),每个Flex项目按照自己的收缩因子相应的去收缩对应的宽度,此时每个Flex项目的宽度就变成:

  • Flex1: 300 - 42 * 1 = 258px
  • Flex2: 300 - 42 * 2 = 216px
  • Flex3: 300 - 42 * 3 = 174px
  • Flex4: 300 - 42 * 4 = 132px

按照该原理来计算的话,当某个Flex项目的收缩因子设置较大时,就有可能会出现小于0的现象。基于上例,如果把第四个Flex项目的flex-shrink设置为15。这样一来,四个Flex项目的收缩因子就变成:1:2:3:15。也就是说把Flex容器不足空间分成了21份,每份占据的宽度是20px420 / 21 = 20px)。那么Flex项目的宽度就会出现0的现象(300 - 15 * 20 = 0)。这个时候会不会出现无空间容纳Flex项目的内容呢?事实上并不会这样:

在Flexbox布局当中,会阻止Flex项目元素宽度缩小至0。此时Flex项目会以min-content的大小进行计算,这个大小是它们利用任何可以利用的自动断行机会后所变成的

如果某个Flex项目按照收缩因子计算得出宽度趋近于0时,Flex项目将会按照该元素的min-content的大小来设置宽度,同时这个宽度将会转嫁到其他的Flex项目,再按相应的收缩因子进行收缩。比如上例,Flex项目四,其flex-shrink15,但其宽度最终是以min-content来计算(在该例中,Chrome浏览器渲染的宽度大约是22.09px)。而这个22.09px最终按照1:2:3的比例分配给了Flex项目一至三(Flex1,Flex2和Flex3)。对应的Flex项目宽度就变成:

  • Flex1: 300 - 20 * 1 - 22.09 / 6 * 1 = 276.334px
  • Flex2: 300 - 20 * 2 - 22.09 / 6 * 2 = 252.636px
  • Flex3: 300 - 20 * 3 - 22.09 / 6 * 3 = 228.955px
  • Flex4: min-content,在该例中大约是22.09px

对于该情形,计算相对而言就更为复杂一些了。但浏览器会很聪明的帮你处理这些场景,会倾向于给你合理的结果。只不过大家需要知道这样的一个细节,碰到类似的场景才不会一脸蒙逼(^_^)

flex-grow可以设置一个小于1的值,同样的,flex-shrink也可以设置一个小于1的值,比如我们给所有的Flex项目设置flex-shrink的值为0.2,你将看到的结果如下:

从结果的示例图中我们可以看出来,当所有Flex项目的收缩因子(flex-shrink)总和小于1时,Flex容器不足空间不会完全分配完,依旧会溢出Flex容器。好比该例,flex-shrink的总和是.8,分配了Flex容器剩余空间420px80%,即336px(还有84px剩余空间未完全分配完),由于每个Flex项目的收缩因子是相同的,好比前面的示例,都设置了1类似,把分配的空间336px均分为四份,也就是84px,因此每个Flex项目的宽度由当初的300px变成了216px300 - 84 = 216px)。这个其实和flex-grow类似,只不过flex-shrink只是收缩而以。

Flex项目计算公式

Flex项目伸缩计算是一个较为复杂的过程,但它们之间还是有据可查。@Chris@Otree对该方面就有深入的研究。他们给Flex项目的计算总结出了一套计算公式,具体公式如下:

@Chris还依据这套公式写了一个JavaScript的案例,来模拟Flex项目计算:

flex常见的值

大部分情形之下,我们都是使用flex属性来设置Flex项目的伸缩的值。其常见值的效果有:

  • flex: 0 autoflex:initial,这两个值与flex: 0 1 auto相同,也是初始值。会根据width属性决定Flex项目的尺寸。当Flex容器有剩余空间时,Flex项目无法扩展;当Flex容器有不足空间时,Flex项目收缩到其最小值min-content
  • flex: autoflex: 1 1 auto相同。Flex项目会根据width来决定大小,但是完全可以扩展Flex容器剩余的空间。如果所有Flex项目均为flex: autoflex:initialflex: none,则Flex项目尺寸决定后,Flex容器剩余空间会被平均分给是flex:a uto的Flex项目。
  • flex: noneflex: 0 0 auto相同。Flex项目根据width决定大小,但是完全不可伸缩,其效果和initial类似,这种情况下,即使在Flex容器空间不够而溢出的情况之下,Flex项目也不会收缩。
  • flex: <positive-number>(正数)与flex: 1 0px相同。该值使Flex项目可伸缩,并将flex-basis值设置为0,导致Flex项目会根据设置的比例因子来计算Flex容器的剩余空间。如果所有Flex项目都使用该模式,则它们的尺寸会正比于指定的伸缩比。

默认状态下,伸缩项目不会收缩至比其最小内容尺寸(最长的英文词或是固定尺寸元素的长度)更小。可以靠设置min-width属性来改变这个默认状态。

如何掌握Flex项目的大小

通过前面的内容介绍,应该可以了解到Flex项目的大小计算是非常的复杂。如果要真正的理解Flex项目是如何工作的话,最为关键的是理解有多少东西参与影响Flex项目。我们可以按下面这样的方式来进行思考。

怎么设置Flex项目的基本大小

在CSS中设置一个元素的基本大小可以通过width来设置,或者通过min-widthmax-width来设置元素的最小或最大宽度,在未来我们还可以通过contentmin-contentmax-contentfit-content等关键词来设置元素的大小。对于Flex项目,我们还可以通过flex-basis设置Flex项目大小。对于如何设置Flex项目的基本大小,我们可以围绕以下几点来进行思考:

  • flex-basis的值是auto?Flex项目显式的设置了宽度吗?如果设置了,Flex项目的大小将会基于设置的宽度
  • flex-basis的值是auto还是content?如果是auto,Flex项目的大小为原始大小
  • flex-basis的值是0的长度单位吗?如果是这样那这就是Flex项目的大小
  • flex-basis的值是0呢? 如果是这样,则Flex项目的大小不在Flex容器空间分配计算的考虑之内

更为具体的可以参阅flex-basis相关的介绍。

我们有可用空间吗?

如果Flex容器没有剩余空间,Flex项目就不会扩展;如果Flex容器没有不足空间,Flex项目就不会收缩:

  • 所有的Flex项目的宽度总和是否小于Flex容器的总宽度? 如果是这样,那么Flex容器有剩余空间,flex-grow会发挥作用, 具体如何发挥作用,可以参阅flex-grow相关的介绍
  • 所有的Flex项目的宽度总和是否大于Flex容器的总宽度? 如果是这样,那么Flex容器有不足空间,flex-shrink会发挥作用,具体如何发挥作用,可以参阅flex-shrink相关的介绍

分配空间的其他方式

如果我们不想把Flex容器的剩余空间扩展到Flex项目中,我们可以使用Flexbox中其他属性,比如justify-content属性来分配剩余空间。当然也可以给Flex项目设置margin值为处理Flex容器剩余空间。不过这一部分没有在这里阐述,如果感兴趣的话,不仿阅读一下Flexbox相关的介绍。

总结

很久以为,一直以为Flexbox布局中,Flex项目都会根据Flex容器自动计算。而事实上呢?正如文章中介绍的一样,Flex项目的计算是相当的复杂。设置Flex项目大小的值以及flex-basisflex-growflex-shrink的设置都会对其有较大的影响,而且它们的组合场景也是非常的多,并且不同的场景会造成不一样的结果。

当然,文章中所介绍的内容或许没有覆盖到所有的场景,但这些基本的演示或许能帮助大家更好的理解Flex项目是如何计算的。最后希望该文对大家有所帮助,如果你有更深的了解欢迎在下面的评论中与我一起分享。如果文章中有不对之处,还望各路大婶拍正。

有关于CSS的一些新东西

$
0
0

上个月2018年TPAC会议刚结束没多久,@Rachel Andrew在Smashing Magazine上面就发表了一篇文章《The CSS Working Group At TPAC: What’s New In CSS?》介绍了CSS中将会有的一些新东西,同时我们国内@安佳 大大也发了一篇有关于参加该会议的总结。就在这个月,@Rachel Andrew在瑞典.马尔默举办的2018年Øredev 开发者大会上分享了一个话题就是有关于CSS的一些新东西,同时她还分享了另一个话题《2019年布局有哪些工具包》。我阅读了@Rachel Andrew在该会议上分享的两个话题,但今天主要想根据@Rachel Andrew分享的第一个话题做一些总结:CSS有哪些新东西。希望对大家有所帮助。

先上@Rachel Andrew分享的PPT:

如何获取CSS的新特性

很多同学估计都有类似的问题,怎么才能第一时间获取到有关于CSS相关的新特性呢?有关于这个问题,并不是一个很复杂的问题,不过@Rachel Andrew还是围绕这个方面做了一些阐述。

CSS工作组只是W3C工作组中的一个小组。CSS工作组一直坚持透明原则,它内部所有的交流都是公开的,并邀请公众来关注和参与讨论:

  • 绝大多数的讨论都发生在工作组的邮件列表中:www-style。这个邮件列表是公开布档的,欢迎任何人的参与
  • 每周都会召开一次电话会议,时长一小时。该会议并不向非工作组成员开放,但会议会被记录在W3C的IRC服务器上的#css频道。这些会议也会整理出来发布到邮件列表中
  • 还有每个季度会有一次面对面会议,也会记录下来。在获得工作组主席的许可之后,这类会议也通常会对观察员开放(就是旁听)

所有这些都是W3C进程的一部分,任何决定都是通过这样的方式来产生的。此外,那些真正负责把这些决定写成规范的人员叫作规范编辑。规范编辑可能是W3C的工作人员、浏览器开发者、相关专业的特邀专家,也可能是会员公司的职员,他们全职从事此项工作,为了共同利益去推进标准。

另外,在Github上专门有一个仓库csswg-drafts对CSS的一些提案提供了一些讨论的场地,大家对感兴趣的话题可以参与讨论,也可以从中获取到第一手相关资料。

正如@小倩今年在CSS Conf大会上分享的时候也提到过,W3C还是需要大家一起参与的,如果你感兴趣的话,可以按照下面这样的方式来参与:

如何理解规范的形成

任何一个规范的形成都是一个漫长的过程,到目前为止,W3C对Web标准制定的Web标准和草案接近1161个,包括WDCRPRPERRECretNote7种:

  • WD(Working Draft 工作草案):不稳定也不完整。目的是创建当前规范的一个快照,也能征求 W3C 和公众的意见
  • CR(Candidate Recommendation 候选推荐标准):所有的已知 issues 都被解决了,向 implementor 征集实现;AC 正式审查,可能有三种结果:成为标准、返回工作组继续完善、废弃(此阶段的很有可能成为标准,且如有改动,则需给出改动原因)
  • PR(Proposed Recommendation 提案推荐标准):从CRPR需要全面的 test suite 和实现报告,以证明每个特性都在至少2款浏览器里实现了,意味着其质量足以成为REC。此时,W3C 成员再最后一次 review 下规范(一般不会有实质性的改动;若有,则只能再发布一个新的WD或CR)
  • PER(Proposed Edited Recommendation 已修订的提案推荐标准)
  • REC(Recommendation 推荐标准,通常称之为 standard,即事实标准):此时,就不会有太多变动了,当然依然可以收勘误。它可能成为:Edited Recommendation 编辑推荐标准。微小改动,然后生成推荐的 Revised Edition;Amended Recommendation 修订推荐标准。不增加新功能的实质性更改;SPSD Superseded Recommendation 被取代的推荐标准(缺少足够的市场相关性)
  • ret(Retired 退役的)
  • Note(Group Note 工作组说明):不打算成为标准的文档。已经停止使用了。通常记录规范以外的信息,eg.规范的用例及其最佳实践、解释规范被弃用的原因

对于CSS的每项规范大致都会经历以下几个过程:

  • 编辑草案(ED):这是一项规范的初始阶段,可能非常粗糙。对这个阶段没有什么要求,也不保证它会被工作组批准。但它也是各个修订版本的必经阶段,每次变更都是先从一个 ED 中产 生的,然后才会发布出来
  • 首个公开工作草案(FPWD):一项规范的首个公开发布版本,它应该准备就绪,以接受工作组的公开反馈
  • 工作草案(WD):在第一个 WD 之后,还会有更多的 WD 出来。 这些 WD 会吸收来自工作组和更广阔的社区的反馈,一版接着一版小幅改进。浏览器的早期实现通常是从这个阶段开始的,厂商基本不太可能对更早阶段的草案提供实验性的支持
  • 候选推荐规范(CR):这可以认为是一个相对稳定的版本。此时比较适合实现和测试。一项规范只有具备一套完整的测试套件和两个独立的实现之后,才有可能继续推进到下一阶段
  • 提名推荐规范(PR):这是 W3C 会员公司对这项规范表达反对意见的最后机会。实际上他们很少在这个阶段提出异议,因此每个 PR 推进到下一阶段(也是最后一个阶段)只是时间问题
  • 正式推荐规范(REC):一项 W3C 技术规范的最终阶段

用W3C上的一张图来简要的向大家展示一下一个CSS属性诞生的历程:

版本之争

随着前端社区开始有介绍CSS Selectors Level 4相关的文章开始,很多人把这个称之为CSS4选择器,也在说CSS3还未成为规范,CSS4就要来了,真心学不动了。为此@Rachel Andrew特别花了一点时间阐述了:

CSS发展至今,将不会有CSS版本之称,只会有模块的Level一说

有关于这个话题,早在2016年@Rachel Andrew特意写了一篇文章《Why there is no CSS4 - explaining CSS Levels》做出相关的解释。

想想,这就是我与大神之间的差距!

针对这个问题,我在前几天写的一篇博文《揭开CSS的面纱》中也提到过:

由于CSS 的各个模块在近些年里以不同的速度在推进,我们已经越来越难以把这些规范以CSS3、CSS4这样的方式来划分了,而且这样也难以被大众理解和接受。

所以,大家以后不要再把CSS按CSS3或者CSS4来称谓了,我们应该改变以前的习惯,按功能模块发布的版本号来称呼他们。这样才不会给别人造成误解或困惑!

CSS 的一些新东西

开篇有点过长,咱们还是开始进入到真正的主题吧。@Rachel Andrew给我们分享了CSS的一些新东西:

  • CSS Grid Layout & Subgrid
  • CSS Box Alignment
  • Gap
  • Intrinsic Sizing Keywords
  • Scroll Snap
  • Scrollbars
  • Shapes
  • Conic Gradients
  • Aspect Ratio Units
  • Exclusions
  • CSS Houdini
  • Meet Feature Queries

接下来简单的聊一下,如果要深入的聊,估计都足够写本书来聊了。

CSS Grid Layout & Subgrid

CSS Grid Layout到目前为止已经有Level 1Level 2两个版本。而Subgrid是属于CSS Grid Layout Level 2中的一部分。CSS Grid Layout中的很多特性都得到了很多主流浏览器的支持,而且@Rachel Andrew预计在2019年将会成为主流布局方式之一。话又说回来,CSS Grid Layout能这么成熟和得到浏览器的支持,离不开@Rachel Andrew的功劳,因为她一直在推进该特性的向前发展。

CSS Grid Layout对于开发者而言是一件好事,他将改变Web布局的模式,因为在CSS Grid Layout之前的布局模式都是一维布局,只有Grid是二维布局。Grid很强大,但其涉及到相关概念也特别的多,如果要彻底的了解或掌握她,还是需要花不少的时间去学习。当然最好是能多写一些案例。有关于这方面的介绍,这里就不做过多的详细介绍,感兴趣的话可以阅读站上有关于CSS Grid Layout相关的文章

最近我自己也在拿Flexbox和Grid做一个对比,希望通过这种对比的方式能更好的向大家介绍清楚Flexbox和Grid布局的差异性,能让大家更好的掌握Web布局的技巧。

CSS Box Alignment

CSS Box Alignment目前是Level 3,主要用于控制各种布局方法中项目是如何对齐的。由于不同布局方法在对齐方面有不同的约束,因此Box Alignment的一些行为依赖于布局方法。该规范定义了三种对齐方式:

  • 位置对齐startendcenterself-startself-endflex-startflex-endleftrgiht
  • 基线对齐baselinefirst baselinelast baseline
  • 分布式对齐stretchspace-betweenspace-aroundspace-evenly

而我们接触最多的应该是Flexbox布局中控制Flex项目对齐方式用到的属性,比如:

.flex {
    display: flex;
    align-items: center;
    justify-content: center;
}

而在Grid布局中也可以用这样的方式来实现对齐:

.grid {
    display: grid;
    align-items: start;
    justify-content: space-between;
}

也就是说,以后不管是在Flexbox布局还是Grid布局中,控制元素对齐的方式都将会通过该规范中的一些特性来完成。而该规范中提到的特性不仅仅是上面提到的那部分。更详细的可以阅读相关规范

要彻底理解CSS Box Alignment规范中提到的特性,还需要对CSS的一些基础特性要有彻底的了解,不然只能理解其表面上的特性。

有关于这方面相关的特性介绍,给大家推荐@Chen Hui Jing2018年的Btconf Berlin上分享的视频

对应的PPT可以点击这里阅读

如果还想更深入的了解有关于CSS Box Alignment的话,下面这几篇文章或许对你有所帮助:

Gap

在Web布局中总是避免不了控制区域间的间距,比如早前的Grid Framework就是通过marginpadding来控制。而在CSS Multi-column Layout Module Level 1中使用属性column-gap来控制列与列之间的间距:

但在Flexbox布局中,如果想要控制Flex项目之间的间距时,大部分还是通过margin之类来完成,当然在容器有可用空间时,还会使用其对齐系统来控制间距。而CSS Grid 布局有点类似于多列布局一样,有专门的属性(grid-gapgrid-column-gapgrid-row-gap)来控制网格轨道的大小:

但不久之后,不管是我们熟悉的Flexbox布局还是不太熟悉的网格布局,甚至是多列布局中,控制Flex项目(网格轨道、列)间距,可以统一使用gaprow-gapcolumn-gap,其中gapcolumn-gaprow-gap两属性的简写属性。

.grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    column-gap: 20px;
    row-gap: 20px;
}

.flex {
    display: flex;
    flex-wrap: wrap;
    column-gap: 20px;
    row-gap: 20px;
}

Intrinsic Sizing Keywords

Intrinsic Sizing Keywords指的是CSS Intrinsic & Extrinsic Sizing Module Level 3中指定盒子大小的属性。在CSS中指定一个盒子(即元素)大小都知道可以用width/heightmin-width/heightmax-width/height等属性。而这些属性可接受的值常常是autononeinheritinitialunset或者带<length>单位的数值。事实上,除了这些属性值之外,我们还可以使用其他的一些关键词来设置盒子的大小。

  • min-content
  • max-content
  • fit-content

Scroll Snap

CSS Scroll Snap Module Level 1是什么?我习惯性把其称为CSS滚动捕捉。那什么又是滚动捕捉呢?比如说这样的一个效果:“将一个元素锁定在滚动视窗之中”。以前实现这样的一个效果,就算是使用原生的JavaScript来实现,也不是一件轻易的事情。该模块的出现就能较轻易的实现:

通过在x以及y轴上定义“捕捉点”(Snap Points)来控制滚动容器的滚动行为。当用户在水平或垂直方向滚动时,利用捕捉点,滚动容器会捕捉到内容区域的某一点。

Scroll Snap Points主要提供了以下几个属性:

  • scroll-snap-type:定义在滚动容器中的一个snap点如何被严格的执行
  • scroll-snap-type-x:定义在滚动容器中水平轴上snap点如何被严格的执行
  • scroll-snap-type-y:定义在滚动容器中垂直轴上snap点如何被严格的执行
  • scroll-snap-coordinate:结合元素的最近的祖先元素滚动容器的scroll-snap-destination定义的轴,定义了元素中xy坐标偏移的位置。如果元素已经变型,snap坐标也以相同的方式进行变型,为了使元素的snap点向元素一样被显示。
  • scroll-snap-destination:定义滚动容器的可视化viewport 中元素snap点的xy坐标位置
  • scroll-snap-points-x:定义滚动容器中内容的snap点的水平位置
  • scroll-snap-points-y:定义滚动容器中内容的snap点的垂直位置
  • scroll-snap-align:元素相对于其父元素滚动容器的对齐方式。它具有两个值,xy。如果你只使用其中的一个,另外一个值默认相同
  • scroll-snap-padding:与视觉窗口的滚动容器有关。工作原理也相近与正常的内边距,值设置一致。此属性具有动画效果,所以如果你想要对齐snap点进行滚动,这将是一个很好的而选择

有关于这方在更详细的介绍建议阅读下面这些文章:

Scrollbars

CSS Scrollbars Module Level 1给开发者提供了自定义容器滚动条的个性化样式。在Webkit内核提供了-webkit-scrollbar(由七个伪元素)属性,可以轻易的帮助我们实现自定义(个性化)滚动条UI风格。

  • ::-webkit-scrollbar:整个滚动条
  • ::-webkit-scrollbar-button:滚动条上的按钮(下下箭头)
  • ::-webkit-scrollbar-thumb:滚动条上的滚动滑块
  • ::-webkit-scrollbar-track:滚动条轨道
  • ::-webkit-scrollbar-track-piece:滚动条没有滑块的轨道部分
  • ::-webkit-scrollbar-corner:当同时有垂直和水平滚动条时交汇的部分
  • ::-webkit-resizer:某些元素的交汇部分的部分样式(类似textarea的可拖动按钮)

基于七个伪元素,在Webkit内核下可以实现类似下面这样个性化的滚动条UI风格:

CSS Scrollbars Module Level 1模块提供了新的CSS属性scrollbar-colorscrollbar-width可以来设置滚动条颜色和宽度:

Shapes

Shapes最早的身影是在CSS Exclusions and Shapes Module Level 3出现的,后来才独立出来成为一个模块CSS Shapes Module Level 1。该模块提供的特性可以让开发者打破以前那种规规矩矩的页面布局。可以轻易的实现类似下图这样的Web布局效果:

有关于Shapes具体的使用可以参阅下面相关文章:

Conic Gradients

Conic Gradients最早是由@Lea Verou提出的,而且还为其写了一个Polyfill。是一个绘制圆锥渐变效果的一个属性。

上面只是其最简单的一些效果,他能做的事情更多,比如Codepen上收集到的有关于conic-gradient的效果就很强大:

值得庆幸的是,conic-gradient属性也被纳入到了CSS Image Values and Replaced Content Module Level 4体系,不久的将来就可以像lineaar-gradientradial-gradientrepeating-linear-gradientrepeating-radial-gradient一样的使用。另外除了conic-gradient之外还有repeating-conic-gradient属性,这样一来,渐变的特性就更强大了。

有关于conic-gradient更多的介绍可以阅读下面的这些文章:

Aspect Ratio Units

在Web布局中,有的时候会对某个区域特别是图片要根据宽高比进行处理。宽高比在影视制作中又被称之为长宽比,指的是一个视频的宽度除以它的高度所得到的比例,通常表示为x:yx × y,其中的冒号和叉号表示中文的“比”之意。目前,在电影工业中最常被使用的是anamorphic比例(即2.39:1)。传统的4:3仍然被使用于现今的许多电视画面上,而它成功的后继规格16:9则被用于高清晰度电视或数字电视上。常见的比例:

以往实现这样的效果都是依赖于其他的手段来实现,比如说把容器height设置为0,然后将padding-toppadding-bottom设置为宽高比例的百分值。也可以通过paddingcalc()padding和CSS自定义属性等来实现。有关于这方面的介绍可以阅读下面这几篇文章:

为了让广大开发者能更好的处理这样的效果,CSS在CSS Intrinsic & Extrinsic Sizing Module Level 4模块中提供了一个长宽比的单位:aspect-ratio。只不过这个属性还没有成为规范。不过大家对这个属性有何看法的话,可以通过www-style@w3.org与CSSWG联系。

Exclusions

Exclusions不好怎么翻译,以免造成错误的翻译,还是直接称之为Exclusions吧。那么Exclusions指的是什么呢?我还是用一张图来给大家做解释吧。

CSS Exclusions就是致力于解决文本围绕图片(当然也可以是其他的元素)方式。它看上去类似于CSS Shapes,但又和CSS Shapes有很大的区别,它不需要依赖浮动,也不管是否设置了position的值为absoluterelative或者fixed。允许内容围绕一个内联元素。如上图所示。

CSS Exclusions模块引入了两个新属性和值:

  • wrap-flow:设置Exclusion区域以及内容围绕的方式
  • wrap-margin:设置Exclusion区与周边围绕区域的间距

是不是很有意思,有关于其更深入的介绍建议花点时间阅读下面的文章:

CSS Houdini

CSS Houdini 是由一群来自 Mozilla, Apple, Opera, Microsoft, HP, Intel, IBM, Adobe 与 Google 的工程师所组成的工作小组,志在建立一系列的 API,让开发者能够介入浏览器的 CSS engine 操作,带给开发者更多的解決方案,用来解决 CSS 长久以来的问题:

  • Cross-Browser issue
  • CSS Polyfill 的制作困难

简单的来说,CSS Houdini是通过JavaScript来扩展CSS。另外,有兴趣的读者可以直接从这里 CSS Houdini Drafts看详细內容。

从@安佳分享的文章中可以获知,今年的TPAC会议上,CSS Houdini有两处改动:

  • CSS Layout 的 API 做了调整,比较重大的改动有:API 是基于 async函数,而不是 generators了(详见 Run a Work Queue);之前返回 dictionary,现在是返回带有 dictionaryFragmentResult构造函数;传给 layout的 Edges 对象现在也会包含滚动条的 padding
  • CSS Animation Worklet 升级为 FPWD

写这篇文章的时候,CSS Houdini具体的进展如下:

有关于更多的介绍或资讯可以参考下面的链接:

向大家特别推荐一个视频,@Sam Richard 分享的 《Magic tricks with Houdini》:

Meet Feature Queries

这里提到的是CSS的查询功能,满足条件的查询功能。在CSS条件查询规范CSS3 Conditional Rules Specification)提供了@supports@media@viewport相关属性。而其中@supports作用就是用来查询浏览器是否支持CSS的特性。比如:

@supports使用起来很简单,这里就不做过多阐述,有关于这方面更多的介绍可以阅读下面的文章:

其他

@Rachel Andrew在Smashing Magazine上面就发表了一篇文章《The CSS Working Group At TPAC: What’s New In CSS?》中还提到了其他的一些特性在上文中没有提到的,比如说伪类:where()和逻辑属性之类的。有关于逻辑属性和值的了解,我也是初次接触不多,前段时间整理了一篇相关的文章,感兴趣的同学可以阅读《理解CSS的逻辑属性和值》一文。

总结

上面是我自己对PPT的一些理解以及做出的相关整理。大部分涉及到的只是CSS的部分。对于前端开发者要获取的不仅仅是CSS的一些新特性,如果你还可更轻易的获取一些相关信息,可以通过Web API Links来进行了解。

如果你有其他的一些想法或经验,欢迎在下面的评论中与我们一起分享。

如何使用JavaScript构建模态框插件

$
0
0

作为一位Web开发人员而言,模态框(Modal)并不会陌生。就我个人而言,我更为熟悉的是怎么通过CSS来编写一个模态框以及怎么通过CSS给模态框添加一些动效。正好最近工作中也和Modal框杠上了。另外想更好的设计一个模态框用来满足业务需求的普遍性和实用性,甚至是达到可配置性。所以一直在探究模态框相关的知识,同时正好看到了@Ken Wheeler的教程,对于我这样的菜鸟,能很好的了角如何使用原生的JavaScript来构建一个可用的模态框插件,另外为以后如何使用Vue构建更为灵活的模态框组件打下坚实的基础。如果你对该文章感兴趣,或者你也正在加强JavaScript的学习和实战,欢迎继续往下阅读,或许对你有所帮助。

模态框是什么

模态框在前端组件中是一个非常常见的组件。其位于Web应用程序主窗口之上的一个元素。他创建了一个新的模式,该模式禁止用户操作应用程序的主窗口,但它以弹窗的模式在应用程序主窗口之上显示。用户可以在返回应用程序主窗口之前与弹框进行交互操作。

模态框的设计,如果设计或执行不好将会影响主链路的操作,防碍任务的完成。为了确保不影响主链路的操作,一个模态框至少应该包括下面内容:

一个优秀的Modal框主要包含的部分有:

  • 模态框的蒙层:modal-overlay
  • 模态框头部:modal-header
  • 模态框主体:modal-body
  • 模态框脚部:modal-footer
  • 关闭按钮:modal-close

刚才也提到过,模态框毕竟是在应用程序主窗口上显示,所以需要给用户提供关闭模态框的途径。常见的方式有:

  • 取消按钮
  • 关闭按钮
  • ECS
  • 点击模态框窗体外的区域关闭模态框

因此,我们要设计一个模态框,也需要考虑这些因素。

构建模态框插件

接下来,我们来看看怎么使用原生的JavaScript来构建一个模态框插件。通过这个学习你将掌握或需要掌握以下几个知识点:

  • CSS的transitionanimation相关知识点
  • JavaScript DOM操作相关知识点
  • JavaScript 构造器和构造函数
  • JavaScript 事件监听
  • JavaScript 函数

如果你是一个初学者,还是值得花一点时间阅读该文,如果你是位JS大神,欢迎您拍正文章中的不足。

选择设计模式

首先要确定设计模态弹出框的结构并选择一个设计模式。我们的目的是要创建一个模态弹出框,并且可以真正的运用于我们的项目中。这里将会用到闭包相关的知识,因为闭包可以用来创建一个私有域,可以在其中控制提供哪些数据:

// 创建一个立即调用的函数表达式来包装我们的代码
(function() {
    var privateVar = "在控制台中console.log找不到我"
}());

我们想为插件添加一个构造函数方法,并将其公开。IIFE是全局的,因此this的关键词指向的是window。让我们使用this将构造函数附加到全局作用域:

// 创建一个立即调用的函数表达式来包装我们的代码
(function(){
    // 定义构造器
    this.Modal = function () {

    }
}())

我们将Modal变量指向一个函数,从而创建一个函数对象,现在我们可以用new关键词实例化它,如下所示:

var myModal = new Modal()

console.log(myModal) // => Object {}

上面的代码创建了一个对象的新实例。不幸的是,我们的对象在这一点上并没有做什么,所以接下来给这个对象加点其他的东西。

有关于闭包更多的知识点可以阅读下面相关文章:

选项(Options)

回顾一下我们的需求,我们首要的任务是允许用户自定义选项(options)。实现这一点的方法就是创建一组默认的选项,然后将其与用户提供的对象合并。

(function(){
    // 定义构造函数
    this.Modal = function () {
        // 创建引用的全局元素
        this.closeButton = null; // 关闭按钮
        this.modal = null; // 模态弹出框
        this.overlay = null; //模态弹出框蒙层

        // 自定义默认选项
        var defaults = {
            className: 'fade-and-drop',
            closeButton: true,
            content: '',
            maxWidth: 600,
            minWidth: 280,
            overlay: true
        }

        // 通过扩展arguments中传递的缺省值来创建选项
        if (arguments[0] && typeof arguments[0] === 'object') {
            this.options = extendDefaults(defaults, arguments[0])
        }
    }

    // 使用用户选扩展默认值的方法
    function extendDefaults(source, properties) {
        var property;
        for (property in properties) {
            if (properties.hasOwnProperty(property)) {
                source[property] = properties[property]
            }
        }
        return source
    }
}())

首先,创建了被引用的全局元素。这些都很重要,这样一来就可以在插件的任何地方引用Modal。接下来,我们添加了一个默认(defaults)选项对象。如果用户不提供选项(options),就会使用默认选项;如果用户提供了就会覆盖默认选项。那么我们怎么知道用户有没有提供选项呢?这里的关键是arguments对象。这是每个函数内部的一个神奇对象,它包含通过参数传递给它的所有东西的数组。因为我们只期望一个参数,一个包含插件设置的对象,所以我们检查以确保arguments[0],并且它确实是一个对象。

如果条件达得到,就会使用extendDefaults私有域的方法合并这两个对象。extendDefaults接受一个对象,将会遍历它的属性(properties),如果不是其内部属性(hasOwnProperty),就将它分配给源对象(source)。我们现在可以配置我们插件和选项对象。

var myModal = new Modal({
    content: 'Howdy',
    maxWidth: 600
})

这个时候在控制台中打印出myModal,其结果如下图所示:

为了提供一个公共主法,可以将它附加到Modal对象的原型上(prototype)。当你向对象的原型中添加方法时,每个新实例共享相同的方法,而不是为每个实例创建新方法。这在性能上也具有较大的优势,除非有多级子类化,不然在这种子类化中,遍历原型链会抵消性能提升。我们还添加了注释,并对组件进行了结构化。这样我们就有三个部分:构造函数公共方法私有方法

// 创建一个立即调用的函数表达式来包装我们的代码
(function(){
    // 定义构造函数
    this.Modal = function () {
        // 创建引用的全局元素
        this.closeButton = null; // 关闭按钮
        this.modal = null; // 模态弹出框
        this.overlay = null; //模态弹出框蒙层

        // 自定义默认选项
        var defaults = {
            className: 'fade-and-drop',
            closeButton: true,
            content: '',
            maxWidth: 600,
            minWidth: 280,
            overlay: true
        }

        // 通过扩展arguments中传递的缺省值来创建选项
        if (arguments[0] && typeof arguments[0] === 'object') {
            this.options = extendDefaults(defaults, arguments[0])
        }

        // 公用方法
        Modal.prototype.open = function() {
            // open方法的对应的代码
        }

        // 私有方法

    }

    // 使用用户选扩展默认值的方法
    function extendDefaults(source, properties) {
        var property;
        for (property in properties) {
            if (properties.hasOwnProperty(property)) {
                source[property] = properties[property]
            }
        }
        return source
    }
}());

var myModal = new Modal({
    content: 'Howdy',
    maxWidth: 600
})

console.log(myModal)

它不做任何功能性的工作,但是它保持了所有内容的组织性和可读性。

有关于函数中的arguments更多的介绍可以阅读下面相关文章:

有关于构造函数相关的知识可以阅读下面相关文章:

核心功能

现在我们对模态框的插件架构有了一定的了解,它包括了:构造函数选项公共方法。但它还不能做什么?接下来我们就要给他们添加相应的核心功能。所以我们再来看看,一个模态框应该做什么:

  • 构建一个模态元素并将其添加到页面中
  • 将选项(options)中的className指定一个类名,并将其添加到模态元素中
  • 如果选项中的closeButtontrue,则添加关闭按钮
  • 如果选项中的content是 HTML 字符串,则将其设置为模态元素中的内容
  • 如果选项中的contentdomNode,则将其内部内容设置为模态元素的内容
  • 分别设置模态的maxWidthminWidth
  • 如果选项中的overlaytrue,则给模态框添加一个蒙层
  • 当模态框显示时,添加一个scotch-open类名,可以在 CSS 中使用它来定义一个open状态
  • 当模态框关闭时,删除scotch-open类名
  • 如果模态框的高度超过视窗的高度,还可以添加一个scotch-anchored类,这样就可以处理这个场景的样式展示

构建自己的模态框

接下来,我们创建一个私人的方法,使用我们自己定义的选项来构建一个模态框:

function buildOut() {
    var content, contentHolder, docFrag;

    // 如果内容是HTML是字符串,则追加HTML字符串;如果内容是domNode,则追加其内容

    if (typeof this.options.content === 'string') {
        content = this.options.content
    } else {
        content = this.options.content.innerHTML
    }

    // 创建一个DocumentFragment
    docFrag = document.createDocumentFragment()

    // 创建modal元素
    this.modal = document.createElement('div')
    this.modal.className = 'modal' + this.options.className
    this.modal.style.minWidth = this.options.minWidth + 'px'
    this.modal.style.maxWidth = this.options.maxWidth + 'px'

    // 如果closeButton的值为true,添加close按钮
    if (this.options.closeButton === true) {
        this.closeButton = document.createElement('button')
        this.closeButton.className = 'modal-close close-button'
        this.closeButton.innerHTML = 'x'
        this.modal.appendChild(this.closeButton)
    }

    // 如果overlay的值为true,添加蒙层
    if (this.options.overlay === true) {
        this.overlay = document.createElement('div')
        this.overlay.className = 'modal-overlay' + this.options.className
        docFrag.appendChild(this.overlay)
    }

    // 创建内容区域,并添加到modal中
    contentHolder = document.createElement('div')
    contentHolder.className = 'modal-content'
    contentHolder.innerHTML = content
    this.modal.appendChild(contentHolder)

    // 把modal插到DocumentFragment中
    docFrag.appendChild(this.modal)

    // 把DocumentFragment插到body中
    document.body.appendChild(docFrag)
}

首先获取目标内容并创建一个DcoumentFragment(文档片段)。文档片段用于构造DOM外部的DOM元素集合,并用于累计地向DOM添加我们构建的内容。如果content是字符串,则将内容变量设置为options值;如果我们的contentdomNode,我们通过innerHTML将内容变量设置为它的内部HTML。

接下来,我们创建modal元素,并向其添加classNameminWidthmaxWidth样式。同时使用默认的modal类来创建模态框的初始样式。然后,基于options的值,有条件地以相同的方式创建关闭按钮和模态框的蒙层。

最后,我们将content添加到一个变量为contentHolderdiv元素中,并将其插入到modal元素中。再把modal元素添加到DocumentFragment中,然后把DocumentFragment插入到body中(插入到</body>标签前)。这样一来,我们就在页面上创建了一个模态框。

有关于DocumentFragment和DOM操作相关的知识,可以阅读下面文章进行扩展:

事件

这个模态框不会自动关闭,这也是我们希望的效果。为了能让用户关闭模态框,我们需要一个关闭按钮,并在关闭按钮上添加事件,让用户能点击按钮来关闭模态框。或者在模态框的蒙层上绑定相应的事件,让用户点击蒙层也能关闭模态框。接下来,我们创建一个函数,来实现这个功能:

function initializeEvents() {
    if (this.closeButton) {
        this.closeButton.addEventListener('click', this.close.bind(this))
    }

    if (this.overlay) {
        this.overlay.addEventListener('click', this.close.bind(this))
    }
}

上面的代码中使用addEventListener方法绑定click事件,将回调传递给尚未创建的close方法。注意,我们不只是调用close,而是使用bind方法并把this传递给它,它引用了我们的Modal对象。这确保我们的方法在使用this关键词时具有正确的上下文。

有关于JavaScript事件相关知识的扩展可以阅读下面相关文章:

打开模态框

还记得前面说到过,我们创建过open的公共方法,接下来给它添加相应的功能:

Modal.prototype.open = function() {
    // 创建Modal
    buildOut.call(this)

    // 初始化事件侦听器
    initializeEvents.call(this)

    // 向DOM中添加元素之后,使用getComputedStyle强制浏览器重新计算并识别刚刚添加的元素,这样CSS动画就有了一个起点
    window.getComputedStyle(this.modal).height

    // 检查Modal的高度是否比窗口高,如果是则添加modal-open 和 modal-anchored类名,否则添加modal-open类
    this.modal.className = this.modal.className + (this.modal.offsetHeight > window.innerHeight ? ' modal-open modal-anchored' : ' modal-open')

    this.overlay.className = this.overlay.className + ' modal-open'
}

在打开我们的模态框之前首先要创建它。这里使用call()方法来调用buildOut()方法,类似使用bind()进行事件绑定时的方法。只需要把this值传递给call()方法。然后调用initializeEvents(),以确保适用的事件都得到了绑定。

模态框的显示和隐藏是基于类名来实现的。当你向DOM中添加一个元素,然后添加一个类名时,浏览器可能不会干预初始样式,因此你永远不会看到模态框从初始状态过渡的效果。这也就是window.getComputedStyle的作用之处。调用该函数将迫使浏览器重新计算,从而识别模态框的初始状态,让我们模态框的过渡效果看起来较为逼真。最后给模态框添加modal-open类名。

但这并不是全部。我们希望模态框在浏览器窗口中能水平垂直居中,但如果模态框的高度超过了视口(浏览器窗口),那么模态框就会看起来很奇怪。这里使用三元运算符来检查高度,如果模态框高度大于视口高度时,我们会给模态框同时添加modal-anchormodal-open类名,反之则只添加modal-open类名。其中modal-anchor类名就是来处理模态框高度大于视口高度的样式。

关闭模态框

前面也提到过了,我们需要有一个功能,让用户可以关闭模态框。因此我们需要创建另一个公用方法close()

Modal.prototype.close = function(){
    // 存储this
    var $this = this

    // 移除打开模态框时添加的类名
    this.modal.className = this.modal.className.replace(' modal-open', '')
    this.overlay.className = this.overlay.className.replace(' modal-open', '')

    // 监听CSS的transitionEnd事件,然后从DOM中删除节点
    this.modal.addEventListener(this.transitionEnd, function(){
        $this.modal.parentNode.removeChild($this.modal)
    })

    this.overlay.addEventListener(this.transitionEnd, function(){
        if ($this.overlay.parentNode) {
            $this.overlay.parentNode.removeChild($this.overlay)
        }
    })
}

为了让模态框移出有一个过渡效果,我们可以删除modal-open类名。这个同样适用于模态框的蒙层。但我们还没有结束。我们必须将模态框从DOM中删除,但如果不等动画完成就把它删除,效果上看上去会非常的奇怪。可以通过监听this.transitionEnd事件来监听过渡什么时候完成。浏览器对于过渡结束有不同的事件名称,因此编写了一个方法来检测使用哪一个,并在构造函数中调用它。如下所示:

function transitionSelect() {
    var el = document.createElement('div')

    if (el.style.WebkitTransition) {
        return 'webkitTransitionEnd'
    }

    return 'transitionEnd'
}

在JavaScript中call()apply()bind()三个方法对于前端开发者而言也是很重要的,如查你想扩展这方面的知识,可以阅读下面相关文章:

简单的小结

写到这里,我们已经构建了一个属于自己的模态弹出框。加上注释之类的,我们也仅仅用了差不多100行JavaScript代码就实现了。下面是整个模态框的所有代码:

// 创建一个立即调用的函数表达式来包装我们的代码
(function(){
    // 定义构造器
    this.Modal = function() {

        // 创建引用的全局元素
        this.closeButton = null // 创建关闭按钮
        this.modal = null       // 创建模态框元素
        this.overlay = null     // 创建模态框蒙层

        // 确定正确的前缀(浏览器私有前缀)
        this.transitionEnd = transitionSelect()

        // 定义默认的options
        var defaults = {
            className: 'fade-and-drop',
            closeButton: true,
            content: '',
            maxWidth: 600,
            minWidth: 280,
            overlay: true
        }

        // 通过扩展arugments中传递的缺省值来创建选项
        if (arguments[0] && typeof arguments[0] === 'object') {
            this.options = extendDefaults(defaults, arguments[0])
        }
    }

    // 公有方法

    // 关闭模态弹出框
    Modal.prototype.close = function() {
        // 存储this
        var $this = this

        // 移除打开模态框时添加的类名
        this.modal.className = this.modal.className.replace(' modal-open', '')
        this.overlay.className = this.overlay.className.replace(' modal-open', '')

        // 监听CSS的transitionEnd事件,然后从DOM中删除节点
        this.modal.addEventListener(this.transitionEnd, function(){
            $this.modal.parentNode.removeChild($this.modal)
        })

        this.overlay.addEventListener(this.transitionEnd, function(){
            if ($this.overlay.parentNode) {
                $this.overlay.parentNode.removeChild($this.overlay)
            }
        })
    }

    // 打开模态框
    Modal.prototype.open = function() {
        // 创建模态框
        buildOut.call(this)

        // 初始化事件侦听器
        initializeEvents.call(this)

        // 向DOM中添加元素之后,使用getComputedStyle强制浏览器重新计算并识别刚刚添加的元素,这样CSS动画就有了一个起点
        window.getComputedStyle(this.modal).height

        // 检查Modal的高度是否比窗口高,如果是则添加modal-open 和 modal-anchored类名,否则添加modal-open类
        this.modal.className = this.modal.className + (this.modal.offsetHeight > window.innerHeight ? ' modal-open modal-anchored' : ' modal-open')

        this.overlay.className = this.overlay.className + ' modal-open'
    }

    // 私有方法
    function buildOut() {
        var content, contentHolder, docFrag;

        // 如果content是HTML字符串,则追回HTML字符串
        // 如果content是domNode,则追回其内容
        if (typeof this.options.content === 'string') {
            content = this.options.content
        } else {
            content = this.options.content.innerHTML
        }

        // 创建一个DocumentFragment
        docFrag = document.createDocumentFragment()

        // 创建modal元素
        this.modal = document.createElement('div')
        // 设置模态框元素的类名
        this.modal.className = 'modal ' + this.options.className
        // 设置模态框样式(尺寸)
        this.modal.style.minWidth = this.options.minWidth + 'px'
        this.modal.style.maxWidth = this.options.maxWidth + 'px'

        // 如果options中的closeButton值为true,则创建关闭按钮
        if (this.options.closeButton === true) {
            this.closeButton = document.createElement('button')
            this.closeButton.className = 'modal-close close-button'
            this.closeButton.innerHTML = '×'
            this.modal.appendChild(this.closeButton)
        }

        // 如果options中的overlay值为true,则给模态框添加一个蒙层
        if (this.options.overlay === true) {
            this.overlay = document.createElement('div')
            this.overlay.className = 'modal-overlay ' + this.options.className
            docFrag.appendChild(this.overlay)
        }

        // 创建模态框内容区域,并插入到模态框中
        contentHolder = document.createElement('div')
        contentHolder.className = 'modal-content'
        contentHolder.innerHTML = content
        this.modal.appendChild(contentHolder)

        // 把模态框插入到 DocumentFragment中
        docFrag.appendChild(this.modal)

        // 把DocumentFragment插入到body中
        document.body.appendChild(docFrag)
    }

    // 使用用户选扩展默认值的方法
    function extendDefaults(source, properties) {
        var property
        for (property in properties) {
            if (properties.hasOwnProperty(property)) {
                source[property] = properties[property]
            }
        }
        return source
    }

    // 初始化事件监听器
    function initializeEvents() {
        // 给关闭按钮添加click事件,点击关闭模态框
        if (this.closeButton) {
            this.closeButton.addEventListener('click', this.close.bind(this))
        }

        // 给蒙层添加click事件,点击关闭模态框
        if (this.overlay) {
            this.overlay.addEventListener('click', this.close.bind(this))
        }
    }

    // 选择正确的浏览器私有前缀
    function transitionSelect() {
        var el = document.createElement('div')
        if (el.style.WebkitTransition) {
            return 'webkitTransitionEnd'
        }

        return 'transitionend'
    }
}())

模态框基本样式

接着给模态框添加一些基本样式:

.modal-overlay {
    position: fixed;
    will-change: transform;
    z-index: 9999;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;

    opacity: 0
    background-color: rgba(0,0,0,.65);

    transition: 1ms opacity ease;

    &.modal-open {
        opacity: 1;
    }
}

.modal {
    position: absolute;
    z-index: 10000;
    top: 50%;
    left: 50%
    transform: translate(-50%,-50%);

    max-width: 80vw;
    padding: 30px 20px;

    transition: 1ms opacity ease;
    opacity: 0;

    border-radius: 4px;
    background-color: #fff;

    &.modal-anchored {
        top: 2vh;
        transform: translate(-50%, 0);
    }

    &.modal-open {
        opacity: 1;
    }
}

.modal-close {
    font-family: Helvetica,Arial,sans-serif;
    font-size: 24px;
    font-weight: 700;
    line-height: 12px;

    position: absolute;
    top: 5px;
    right: 5px;

    padding: 5px 7px 7px;

    cursor: pointer;

    color: #fff;
    border: 0;
    outline: none;
    background: #e74c3c;

    &:hover {
        background: #c0392b;
    }
}

上面的样式代码非常的简单。只是确保模态框和模态框的蒙层默认情况下显示。离开默认状态时有一个1mstransition效果(即opacity01的一个过渡效果),而这个效果会通过transitionEnd事件来触发。另外通过transformtranslate()函数,让模态框在视口中水平垂直居中。如果模态框应用了modal-anchored类名,那么模态框只水平居中,且距离视口顶部2vh

除此这外,还可以通过options中的className给模态框添加自定义动画,因此我们可以使用options选项中className的默认值fade-and-drop来定义这个动画:

// 默认动画效果
.modal-overlay.fade-and-drop {
    display: block;
    opacity: 0;
    transition: 500ms opacity 500ms ease;

    &.modal-open {
        top: 0;
        transition: 500ms opacity ease;
        opacity: 1;
    }
}

.modal.fade-and-drop {
    top: -300vh;
    opacity: 1;
    display: block;
    transition: 500ms top ease;

    &.modal-open {
        top: 50%;
        transition: 500ms top 500ms ease;

        &.modal-anchored {
            transition: 500ms top 500ms ease;
        }
    }
}

上面的代码实现了一个淡入淡出的过渡效果。

创建模态框

到目前为止,我们已经创建了一个模态框插件,也为该插件添加了一些默认的CSS。但在Web页面上并看不到模态框的身影。那是因为我们还没有通地模态框插件来创建自己的模态框。接下来,我们来看看怎么创建一个模态框。

创建一个模态框很简单,只需要使用new关键词就可以创建一个模态框,然后把这个新创建的模态框对象赋值给一个变量,比如myModal

var myModal = new Modal()

如果我们把myModal打印出来,可以看到像下图这样的一个信息:

我们在Modal插件中创建了两个公共方法,open()close()。如果我们要调用这两个方法,可以使用myModal.open()。但这样会报错:

Modal插件中提供了options(一些有关于模态框插件的默认选项),另外我们也可以给模态框传一些参数,比如:

var myModal = new Modal({
    content: `<p>我是一个模态框</p>`,
    maxWidth: 600
})

myModal.open()

这个时候,模态框就出来了,而且打印出来的myModal相关的信息也变了:

另外,新创建的模态框有一个默认的动画效果,这个动画效果就是fade-and-drop类名中设置的效果:

如果我们想要一个属于自己的模态框动效。方法也很简单,只需要在Modal插件的optionsclassName选项中添加一个类名,我们可以像下面这样使用:

var myModal = new Modal({
    className: 'custom-animation',
    content: `通过className自定义一个类名,创建模态框动效`,
    maxWidth: 600
})

myModal.open()

这个时候,默认的fade-and-drop的类名变成了custom-animation,如下图所示:

接下来,要给custom-animation添加一些样式,用来设置实现自己动效的一些样式代码:

.modal.custom-animation {
    transition: 500ms transform ease;
    transform:translate(-50%, -50%) scale(0);

    &.modal-open{
        transform: translate(-50%, -50%)  scale(1);
    }
}

效果如下:

到此为止,我们主要学习的是如何通过JavaScript来创建一个模态框插件,并没有聊怎么编写模态框。如果我们想插件在实例化时就能自动打开,那么就需要在options中添加新的一个选项autoOpen。该值在默认状态中是false

var defaults = {
    //... 以前的选项
    autoOpen: false
}

接下来,只要在插件中检测autoOpen是否为true,如果为true,则启动open()方法来打开模态框:

if(this.options.autoOpen === true) this.open()

实例

估计大家都清楚模态框的使用场景,一般情况,模态框是不会显示的,只有用户触发了,比如点击了某个按钮,才显示模态框。接下来,我们来看看自己写的模态框插件的使用:

总结

文章中的主要内容是介绍如何使用原生的JavaScript来构建一个模态框。其实这里主要介绍的是介绍模态框插件的构建,而不是怎么写一个模态框。另外,这篇文章对于一些初学JavaScript或者更想深入的学习JavaScript的同学而言应该是有所帮助的。好比我自己,在这里就获取了不少的知识点,所以我也希望你也能从该文中得到你自己想要学习的知识点。

虽然该文介绍的是如何构建一个模态框插件,但最终也可以让创建一个模态框,并且还可以根据模态框提供的选项,让你实现自己想要的模态框,另外还可以配上一些CSS动效,让你的模态框效果可以变得更佳。当然,模态框插件的构建只是其中的一个示例,而该文中最终的目的是希望你能从这篇文章中学到如何使用JavaScript来构建插件,构建你自己任何想要的插件。如果你感兴趣的话,不仿 根据该文提供的思路和技巧,尝试着去构建你的插件。如果你构建出来了,欢迎在下面的评论中与我们一起共享。

最后要说的是,整篇文章的思路以及代码来自@Ken Wheeler的《Building Your Own JavaScript Modal Plugin》一文。这里特感谢@Ken Wheeler为我们初学JavaScript的同学提供这么优秀的教程和源码。

图解CSS:条件 CSS

$
0
0

在CSS的世界中,总是有很多实验性的属性先行,正因为这些先行者在不断的探索新的特性,才让CSS越来越强大。而这些实验性的特性并没有立马得到众多浏览器的支持,为了能让这些实验性特性能在部分支持的浏览器上运行,同时又能让不支持的浏览器做相应的降级处理。那么我们就会需要根据相关的条件进行判断。这也就是条件CSS的由来。

条件CSS的简介

条件CSS(Conditional CSS)现在被纳入到 CSS Conditional Rules Module Level 3模块中。事实上,条件CSS的开发源于在多数浏览器上修正CSS渲染bug的需求,以确保尽量多的用户看到正确的网站设计。核心思想是建立在IE条件注释方法,并扩展到包含其他的浏览器,而且将条件声明内联到CSS定义里面。

但随着技术不断的革新,条件CSS现在很少使用条件注释这样的方式来做条件判断,而是提供一些CSS特性(比如,CSS的@规则)及其相关的JavaScript API允许我们在满足特定条件时应用样式或行为。

需要注意的是,如果所有浏览器都能正确地执行W3C发布的CSS标准,那么条件CSS就没有需求了。但是,CSS在不同浏览器渲染总是会有或多或少的bug存在,而且往往都及其让人沮丧。条件CSS给我们提供了一个简单的方法来解决这些问题。加上文章开头也提到过了,在CSS的社区中总是有很多先驱者在不断的探索和创造一些实验性CSS特性。我们也可以通过条件CSS在一些已得到的浏览器上先用起来。

条件CSS分类

到目前为止,条件CSS主要有三个@规则:

  • @media
  • @supports
  • @viewport

其中@media@supports两个规则是我们常见的规则,也是真正的条件CSS,而@viewport并不常见,但也不是真正的条件。

CSS中的@规则

既然条件CSS运用到的也是CSS的@规则,那么我们很有必要先简单的了解一下CSS的@规则。

CSS的@规则(at-rule)是一条语句,它为CSS提供了执行或如何执行的指令。

@规则的每个语句都是以@开头,后面直接跟着相应的关键词,这些关键词充当CSS应该做什么的标识符。尽管每个@规则都有它的变体,但这也是最常见的语法规则。

CSS的@规则主要分为常规规则嵌套规则两大类。

常规规则

常规规则的语法较为简单,类似下面这样:

@[关键词](规则)

常规规则常见的主要:

@charset

大家不知道有没有印象,早期在创建.scss.less文件时都会要求在第一行中使用@charset来声明字符集,比如:

@charset 'utf-8'

在某些CSS属性(比如content)中使用非ASCII字符或样式表包含UTF-8等非ASCII字符时,@charset规则非常有用。另外,@charset规则必须是样式表中的第一个元素,并且前面不能有任何字符。用户代理必须忽略样式表开头之外的任何@charset规则。如果定义了几个@charset规则,则只使用第一个。

@import

@import允许用户从其他样式表导入样式规则。比如:

@import url("https://fonts.googleapis.com/css?family=Libre+Baskerville");
@import url("print.css") print;
@import url("tv.css") projection, tv;
@import 'custom.css';
@import "common.css" screen, projection;
@import url('landscape.css') screen and (orientation:landscape);

导入样式规则时,就好像文件的内容就在规则所在的位置一样。这些规则必须先于所有其他类型的规则,@charset规则除外,否则@import规则会不生效。

在实际项目中不建议使用@import来引用其他CSS样式文件。这样做不但请求多,还会造成阻塞。

@namespace

@namespace规则对于很多同学而言会感到陌生。从词面上来了解,它是用来声明一个命名空间前缀,并将其与给定的命名空间关联起来。然后可以在命名空间限定的名称中使用此命名空间前缀。该规则对于将CSS应用到XHTML中特别有用,这样一来,XHTML元素就可以用作CSS中的选择器。可以使用定义的命名空间来限制泛型、类型和属性选择器,只选择该命名空间中的元素。@namespace规则通常只在处理包含多个命名空间的文档时有用,比如包含内联SVG或MathML的HTML,或者包含多个词汇表的XML。

@namespace url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

/* 和所有XHTML中的a元素匹配,因为XHTML是默认的命名空间 */
a {
    color: red;
}

/* 和所有SVG中的a元素匹配 */
svg|a {
    color: blue;
}

/* 同时匹配XHTML和SVG中的a元素 */
*|a {
    color: orange;
}

使用@namespace规则有几点要注意:

  • 任何@namespace规则都必须遵循所有@charset@import规则,并位于样式表中所有其他@规则和样式声明之前
  • @namespace规则可以用于定义样式表的默认命名空间。定义默认命名空间时,所有通用选择器和类型选择器(不包含属性选择器)仅应用于该命名空间中的元素
  • @namespace规则还可以用于定义命名空间的前缀。如果泛型、类型和属性选择器的前缀是命名空间的前缀,那么该选择器只在元素或属性的命名空间和名称匹配时才匹配

嵌套规则

嵌套规则和常规规则不同的是,在规则后面会带一个花括号{},括号中会嵌套一些样式规则:

@[关键词] {
    /* 样式规则 */
}

CSS的嵌套规则主要有:

@font-face

CSS的@font-face规则允许我们引用自定义的字体,该规则消除了依赖于计算机上安装的有限字体数量的需求。在使用自定义定体时,需要先使用该规则来声明:

@font-face { 
    [ font-family: <family-name>; ] || 
    [ src: [ <url> [ format(<string>#) ]? | <font-face-name> ]#; ] || 
    [ unicode-range: <urange>#; ] || 
    [ font-variant: <font-variant>; ] || 
    [ font-feature-settings: normal | <feature-tag-value>#; ] || 
    [ font-stretch: <font-stretch>; ] || 
    [ font-weight: <weight>; ] || 
    [ font-style: <style>; ] 
} 

每个@font-face规则为每个字体描述符(隐式或显式)指定一个值。规则中没有给出显式值的部分使用每个描述符列出的初始值。这些描述符仅适用于定义它们的@font-face规则的上下文中,而不适用于文档语言元素。没有关于描述符应用于哪些元素或这些值是否由子元素继承的概念。当给定的描述符在给定的@font-face规则中多次出现时,只使用最后一个描述符声明,并忽略该描述符所有先前声明。

另外,该规则允许选择与设计目标密切匹配的字体,而不是将字体选择限制为给定平台上可用的字体。一组字体描述符定义字体资源的位置,包括本地或外部的位置,以及单个外观的样式特征。多个@font-face规则可用于构造具有多种字体的字体族。使用CSS字体匹配规则,用户代理可以选择性地只下载所需的字体。

在使用@font-face时也有些细节需要注意:

  • 作用域的限制。Web字体受到作用域的限制,因此@font-face规则中引用的字体资源必须与使用它们的页面位于相同的作用域,除非使用HTTP访问控制来放宽这一限制
  • 不考虑指定文件的MIME类型,因为没有为TrueTypeOpenTypeWOFF字体定义MIME类型
  • @font-face不能在CSS选择器中声明

在项目中使用@font-face引用自定义定体涉及很多细节,有关于这方面的细节,后续我们将会花费一个章节来专门介绍。

@keyframes

@keyframes规则主要用来声明一个动画,在嵌套的规则中指定了动画各个节点(帧)的样式规则。

@keyframes <keyframes-name> { 
    <keyframe-block-list> 
}

@keyframes只是声明了一个动画,如果没有被animation-name属性调用的话,那么该规则中的样式并不会起任何的作用。另外要使用关键帧列表有效,它必须包含动画开始和结束状态的规则,即0%from)和100%to)。如果没有指定这两个时间偏移量,那么@keyframes声明就会无效,解析器将会忽略它,并且不能用于animation中。

如果我们通过JavaScript来操作@keyframes中的规则,可以使用CSSOM中的CSSKeyframesRule

特别注意:在@keyframes规则中声明的样式规则会覆盖元素中的样式规则,哪怕是带有!important加强权重的样式规则。

有关于@keyframes更深入的介绍,我们将会放到CSS动画相关的章节来阐述。

@media

@media规则是条件CSS中的一种,其条件是一个媒体查询。它由一个媒体查询列表(可以是空的)和一组规则组成。规则的条件是媒体查询的结果。

@media <media-query-list> { 
    <group-rule-body> 
}

@supports

@supports规则是条件CSS中的另一种,也是一条件组规则,其条件测试用户代理是否支持CSS属性/值对。它可以用于编写样式表,这些样式表在可用时使用新特性,但在不支持这些特性时将可以优雅地降级。

@supports <supports-condition> { 
    <group-rule-body> 
}

@viewport

@viewport规则事实上不是条件CSS中的一种。该规则在CSS中定义了一组嵌套的描述符,这些描述符主要用来控制移动设备上的viewport设置。

@viewport { 
    <group-rule-body> 
}

比如:

@viewport {
    min-width: 640px;
    max-width: 800px;
}

@page

@page规则主要用于打印文档时候修改一些CSS属性。使用@page我们只能改变部分CSS属性,例如间距属性margin, 打印相关的orphans, widows, 以及page-break-*, 其他CSS属性会被忽略。

@page <page-selector-list> { 
    <page-body> 
}

@document

@document规则指定应用于特定页面的样式的条件。该规则可以指定一个或多个匹配函数,如果其中任何一个函数应用于URL,则该规则将对具有该URL的文档生效。比如说,这个CSS文件被子站A调用,和被子站C调用,我们可以通过域名匹配来执行不同的CSS样式。这样,我们可以有效避免冲突,或者防止外链之类。

@document 
    /* 页面URL需要是 */
    url(https://www.w3cplus.com/),

    /* 页面URL的开头必须是... */
    url-prefix(www.w3cplus.com/blog/),

    /* 该域上的所有页面 */
    domain(w3cplus.com),

    /* 所有https协议页面 */
    regexp("https:.*")
    {

        /* 开始样式 */
        body { 
            color: #444;
        }

}

@font-feature-values

@font-feature-values规则主要用于给定字体家族的替代符号的索引定义命名值。它允许在font-variant-alternates中使用一个公共名称来替换OpenType中不可激活的特性,从而在使用多种字体时简化CSS。

@font-feature-values <family-name># { 
    <feature-value-block-list> 
} 

来看一个小示例:

/* 在Font One中激活 cool-style 风格的字体 */
@font-feature-values Font One {
    @styleset {
        cool-style: 12;
    }
}

/* 在Font Two中激活 cool-style 风格的字体 */
@font-feature-values Font Two {
    @styleset {
        cool-style: 4;
    }
}

/* 与字体无关 */
.cool-look {
    font-variant-alternates: styleset(cool-style);
}

@counter-style

@counter-style规则可以允许我们定义自定义的计数器的样式。计数器样式由@counter-style规则中的描述符来指定,主要由systemsymbolsadditive-symbolsnegativeprefiexsuffixrange等组成。该规则的一般形式是:

@counter-style <counter-style-name> { 
    [ system: <counter-system>; ] || 
    [ symbols: <counter-symbols>; ] || 
    [ additive-symbols: <additive-symbols>; ] || 
    [ negative: <negative-symbol>; ] || 
    [ prefix: <prefix>; ] || 
    [ suffix: <suffix>; ] || 
    [ range: <range>; ] || 
    [ pad: <padding>; ] || 
    [ speak-as: <speak-as>; ] || 
    [ fallback: <counter-style-name>; ] 
} 

具体使用的时候可以像下面这样:

@counter-style circled-alpha {
    system: fixed;
    symbols: Ⓐ Ⓑ Ⓒ;
    suffix: "";
}

li {
    list-style: circled-alpha;
}

上面列出了CSS的@规则,其中@charset@import@font-face@keyframes@media@supports是我们常见或已在项目中有见过的@规则;而@namespace@viewport@page@document@font-feature-values@counter-style等规则是我们不怎么常见。

在众多CSS的@规则中,@media@supports@viewport又被称为是条件CSS。这也是我们这一章节中重点。那么接下来,我们详细的来聊聊这三个规则。

条件CSS之@media

就前端开发者而言都避免不了面对众多设备终端的适配。而设备终端可谓是绫罗满目,用下图来形容一点不为过:

面对这样的场景,早在2010年社区就提出了响应式设计(Responsive Design)的概念。

响应式设计指的是,你的Web应用程序或Web页面应该从宽屏显示器到手机终端屏幕的所有东西上都显示得一样的好。

这是一种Web设计和开发的方法,他的最初目的就是在可限的空间最好方式展示最全、最优内容(布局)。它削除了网站在移动端和桌面端之间的差别。换句话说,用户在拖拉浏览器改变视口时能以最佳的方式实现Web应用程序或Web页面的布局。

而在响应式设计中,最为关键的就是条件CSS中的媒体查询,即@media媒体查询可以有条件的应用CSS规则,它告诉浏览器应该忽略或应用哪些CSS规则,而这些都取决于用户的设备终端。

媒体查询让我们将相同的HTML内容在不同的设备终端运用不同的CSS规则,最终向用户呈现不同的布局效果。因此,与其为智能手机维护一个网站,为笔记本电脑或台式机维护另一个网站,还不如借助媒体查询的特性来为不同的终端设备维护相同的HTML结构,然后再利用不同的规则展示不同的布局风格。这从维护成本上来说,还是有利的。

既然媒体查询这么强大,那么我们应该怎么来使用媒体查询呢?或者说媒体查询包含了哪些知识呢?这也是我们接下来要了解和学习的东西。

媒体查询是什么?

媒体查询是一种条件CSS,它提供了一种规则,让我们在符合条件的情况之下调用正确的CSS规则。而这个条件可以根据使用的设备类型视口大小屏幕像素密度甚至设备方向

简单地说,媒体查询使用@media规则,后面跟着一个媒体类型、零个或多个媒体特性,或者一个或多个媒体类型和一个或多个媒体特性。然后再符合条件的特定范围输出正确的CSS规则。

媒体查询语法

媒体查询包含一个可选的媒体类型媒体特性表达式(零个或多个)最终会被解析为true(符合条件规则)或false(不符合条件规则)。如果媒体查询中指定的媒体类型匹配展示文档所使用的设备类型,并且所有的表达式的值都是true,那么该媒体查询的结果为true

先忽略此图的具体意思,随着后续内容的完善,我们都能看懂此图的含义。

在实际使用@media规则时,不管是使用哪种方式,都是可以使用的,比如:

<!-- link元素中的CSS媒体查询 -->
<link rel="stylesheet" media="screen and (max-width: 800px)" href="example.css" />

<!-- import导入CSS中的媒体查询 -->

@import url(example.css) screen and (color), projection and (color);

<!-- 样式表中的CSS媒体查询 -->
<style>
    @media screen and (max-width: 600px) {
        :root {
            color: green;
        }
    }
</style>

当媒体查询中的条件规则为true时,其对应的样式表或样式规则就会遵循正常的级联规则进行应用。即使媒体查询的规则返回的是false<link>标签和@import指向的样式规则也将会被下载,但是它们不会被应用到页面上。

针对媒体查询的语法规则,我们可以用一张细化的图来描述:

从上图可以看出,整个媒体查询规则中主要包含了三个部分:媒体类型媒体特性逻辑操作符。接下来,我们主要围绕着这三个方面进行展开。

媒体查询类型

媒体查询类型简称为媒体类型,它是媒体查询条件中的重要部分之一。媒体类型允许你在不同的媒体类型上指定相应的样式文件。原始媒体类型集是在HTML4中定义的,主要用于<link>元素上的媒体属性,比如screenprint等。比如下面的代码,大家应该不会感到陌生:

<link href="style.css" media="screen" />
<link href="print.css" media="print" />

而事实上,除了我们常见的allscreenprint媒体类型之外,还有其他的一些媒体类型:

媒体类型描述
all所有设备
screen电脑屏幕
print文档打印或打印预览模式
braille盲文
embossed盲文打印
handheld手持设备
speech‘听觉’类似的媒体类型
tty用于使用固定密度字母栅格的媒体,比如电传打字机和终端
tv用于电视机类型的设备
projection用于方案展示,比如幻灯片

不幸的是,媒体类型作为区分具有不同样式需求的设备的一种方式已经是不够的。一些原本非常不同的类别,比如屏幕(screen)和手持设备(handheld)已经显著地融合在一起。其他类型,比如ttytv,暴露了与全功能计算机显示器的标准的有用差异,因此对使用不同样式的目标有用,但是媒体类型的互斥定义使它们难以以合理的方式使用;相反,它们独有的方面可以用媒体特性来处理。

对于媒体类型,我们常见的使用方式主要有:

<link href="style.css" media="screen print" />

@import url("style.css") screen;

<style media="screen">
    @import url("style.css");
</style>

@media screen{
    selector{rules}
}

媒体特性

刚才也提到过了,目前就媒体类型(设备终端)有一定的缺陷,因此为了更好的使用媒体查询规则,还会借助媒体特性来加强条件规则方面的判断。比如下表所列:

媒体特性是否接受minmax的前缀描述
width输出设备渲染区域(可视区域的宽度或打印机纸盒的宽度)的宽度
height输出设备渲染区域(可视区域的高度或打印机纸盒的高度)的高度
device-width输出设备的宽度(整个屏幕或页的宽度,而不仅仅像文档窗口一样的渲染区域)
device-height输出设备的高度(整个屏幕或页的高度,而不仅仅像文档窗口一样的渲染区域)
aspect-ratio输出设备目标显示区域的宽高比
device-aspect-ratio输出设备的宽高比
orientation指定了设备处于横屏(宽度大于高度)模式还是竖屏(高度大于宽度)模式
resolution指定输出设备的分辨率(像素密度)

上面这些是我们常见的一些媒体特性,但还有一些我们不常见的媒体特性,比如:

媒体特性是否接受minmax的值描述
color指定输出设备每个像素单元的比例值,如果设备不支持输出颜色,则该值为0
color-index指定输出设备中颜色查询表中的条目数量
grid判断输出设备是网格设备还是位图设备,如果设备是基于网格的,该值为1,否则为0
monochrome指定了一个黑白(灰度)设备每个像素的比例数。如果不是黑白设备,该值为0
scan描述了电视输出设备的扫描过程
update指定输出设备是否具有内容渲染后修改外观的能力,接受none(无)、slow(慢)和fast(快)三个值
overflow-block指设备处理块内容溢出的处理行为,主要包含nonescrolloptional-pagedpaged四个值
overflow-inline指设备处理内联内容溢出的处理行为,主要包含nonescroll两个值
color-gamut指设备可以显示的颜色的大致范围
pointer指设备(如鼠标)的存在性和准确性。如果存在多个指针设备,则指针媒体特性必须反映由用户代理决定的“主”指向设备的特征
hover指设备是否具有指针悬浮在元素上的能力
any-pointer确定是否有可用的设备指针与请求的条件匹配
any-hover确定是否有可用的设备指针可以悬停

上面两个表格中是 Media Queries Level 4所列出的媒体特性,有常用的,也有少见的,但具体何时使用什么样的媒体查询特性来增强@media规则条件,需要根据具体的需求来判断。稍后我们会举一些简单的示例,来增强大家对媒体特性的理解。

除此之外,Media Queries Level 5草案在媒体特性方面还进行了增强,纳入到Level 5的媒体查询特性主要有:

媒体特性是否支持minmax前缀描述
light-level用于查询设备所使用的环境光级,允许作者相应地调整文档的样式
environment-blending用于查询用户的显示特征,从而调整文档的样式。作者可以根据显示技术调整页面的视觉效果或布局,以增加吸引力或提高可读性
scripting用于查询当前文档是否支持脚本语言,比如JavaScript
nverted-colors指示内容是否正常显示,或颜色是否被反色
prefers-reduced-motion用于检测用户是否请求系统将其使用的动画或运动的数量减少到最小
prefers-reduced-transparency用于检测用户是否请求系统使用的最小化透明或半透明层效果
prefers-contrast用于检测用户是否请求系统增加或减少相邻颜色之间的对比度
prefers-color-scheme用于检测用户是否请求系统使用浅颜色或深颜色主题

特别声明,Level 5中提到的媒体特性都和系统设置有所关系,另外这些特性还处于草案阶段,随时都有可能会更改或删除。这里所列仅供参考。

就上面所列的几个表格,大家都会觉得媒体特性类型众多,为了能更好的帮助大家理解相关的使用,接下来以常用的媒体特性为例,给大家列几个示例。

不知道大家有没有发现,在上面的几个表格中,有一个选项 是否接受minmax的前缀。因为在媒体特性中,大多数的媒体特性都可以带有minmax的前缀,比如说min-widthmax-width,用于表达 “最小的...”或者 最大的...。用两张图来帮助大家来理解minmax的实际含义。

使用minmax前缀是主要为了避免与HTML或XML中的<>字符相冲突。如果你觉得使用<>符更易于理解的话,那么可以使用postcss-media-minmax插件来帮助你。

有了这些基础,我们来看看示例。

如果你想向最小宽度的20em的手持设备或屏幕应用样式表,你可以使用下面这样的媒体查询规则:

@media handheld and (min-width: 20em), screen and (min-width: 20em) {
    /* 样式规则 */
}

如果你想大宽度在20em36em之间的屏幕运用不同的样式规则,则可以使用下面这样的媒体查询规则:

@media screen and (min-width: 20em) and (max-width: 36em) {
    /* 样式规则 */
}

如果你想大最小宽度为375px和最小高度为812px的屏幕上运用相应的样式规则,则可以像下面这样写:

@media only screen and (min-width: 375px) and (min-height: 812px) {
    /* 样式规则 */
}

另外你要是想通过横屏或竖屏来区分,则可以像下面这样:

@media only screen and (min-width: 812px) and (orientation: landscape) {
    /* 横屏样式规则 */
}

@media only screen and (min-width: 375px) and (orientation: portrait) { 
    /* 竖屏样式规则 */
}

甚至还可以根据屏幕分辨率来写:

@media
    only screen and (min-device-pixel-ratio: 3),
    only screen and (min-resolution: 384dpi),
    only screen and (min-resolution: 3dppx) { 
        /* Retina屏幕下的样式规则 */
}

上面看到的仅仅是其中的一部分,也是我们常见的一些。对于不常见的媒体特性的规则,这里只向大家展示Level 5中的prefers-reduced-motion特性。为什么要特意介绍这个特性呢?主要是因为这个特性特别有意思。因为该特性可能通过特性检测区分并对一些配置较差或主动开启系统减弱动态效果的用户进行体验优化。

减弱动态效果设置是系统的一个设置,无论是在MacOS还是iOS时都隐藏的比较深。

对于MacOS系统,可以根据下面这个操作路径进行操作:

进入 系统偏好设置 =>辅助功能 =>显示器

开启 减弱动态效果,如下图所示:

对于iOS系统,可以根据下面这个操作路径进行操作:

进入 设置 =>通用 =>辅助功能

开启 减弱动态效果, 如下图所示:

开启「减弱动态效果」可以有效地降低 MacOS/iOS 系统糟糕的晕眩效果性能开销,从而达到系统更流畅的功效。具体使用的时候,就可以借助媒体特性的规则来处理:

@keyframes aniName {
    / *声明动画,在@规则中有介绍过 * /
}

.background {
    animation: aniName 10s infinite alternate;
}

/* 开启 减弱动态效果 的设备会禁用 aniName动画 */
@media screen and (prefers-reduced-motion) {
    / * 禁用不必要的动画 * /
    .element {
        animation: none;
    }
}

如果借助CSS的自定义属性(后面会花一个章节专门介绍),可以让上面的代码变得更为简单:

:root {
    --anim-duration: 10s
}

@media screen and (prefers-reduced-motion) { 
    :root {
        --anim-duration: 0s;
    }
}

.element {
    animation: aniName var(--anim-duration) infinite alternate;
}

如果你看不懂这段代码并不要紧,随着你接触或学习了CSS自定义属性之后,你会觉得它们原来是这么的简单(^_^) !

这个功能非常适合为低配置设备的用户以及追求性能的用户做体验优化,因为很多用户会开启 减弱动态效果来在旧设备上提升系统流畅度。

逻辑操作符

从上面示例代码我们不难发现,在@media规则中有出现过and这样的关键词。而这个关键词在媒体查询中被称为逻辑操作符。在@media可用的逻辑操作符,除了and之外,还有notonly等,这些逻辑操作符构建复杂的媒体查询规则。这几个逻辑操作符有点类似于JavaScript中的。其中:

  • and操作符用来把多个媒体规则组合成一条媒体查询规则,对成链式的特征进行请求,只有当每个规则都为真时,结果才为真(有点类似于的概念)。
  • not操作符用来对一条媒体查询规则的结果进行取反(有点类似于的概念)
  • only操作符仅在媒体查询规则匹配成功的情况下被应用于一个样式,这对于防止让选中的样式在老式浏览器中被应用到

若使用了notonly操作符,必须明确指定一个媒体类型!

另外,可以将多个媒体查询规则以逗号(,分隔放在一起,只要其中任何一个为,整个媒体查询规则语句返回就是。相当于or操作符。

接下来,我们分别来看看每个操作符具体使用的细节。

and

and逻辑符主要是让你将多个媒体查询规则(多个媒体属性或媒体属性与媒体类型)合并在一起。一个基本的媒体查询规则,即一个媒体属性与默认指定的all媒体类型,就像下面这样子:

@media (min-width: 20em) {
    :root {
        --font-size: 100%;
    }
}

如果你只想在横屏时应用这个规则,你可以使用and逻辑符,加上orientation特性,比如:

@media (min-width: 20em) and (orientation: landscape) {
    :root {
        --font-size: 100%;
    }
}

上面的规则是查询仅在可视区域宽度不小于20em并在横屏的设备下有效。如果,你仅想在电视媒体上应用,那么可以继续使用and逻辑符来合并媒体类型:

@media tv and (min-width: 20em) and (orientation: landscape) {
    :root {
        --font-size: 100%;
    }
}

not

not逻辑符应用于整个媒体查询规则,在媒体查询规则为时返回。比如monochrome就用于彩色显示设备上或一个600px的屏幕应用于min-width: 700px属性查询上。在逗号媒体查询列表中not仅会否定它应用到它应用到的媒体查询上而不影响其它的媒体查询。not逻辑符仅能应用于整个媒体查询规则,而不能单独地应用于一个独立的媒体查询规则。例如,not在下面的媒体查询中最后被计算:

@media not all and (monochrome) {
    /* 样式规则 */
}

上面的规则等价于:

@media not (all and (monochrome)) {
    /* 样式规则 */
}

而不是:

@media (not all) and (monochrome) {
    /* 样式规则 */
}

再来看一个示例:

@media not screen and (color), print and (color) {
    /* 样式规则 */
}

等价于:

@media (not (screen and (color))), print and (color) {
    /* 样式规则 */
}

or和 逗号分隔符

or逻辑操作符相当于JavaScript逻辑运符中的,即,当你的媒体查询规则中有多个的时候,只要有一个规则符合条件,其结果就是true。另外,在媒查询中,还有一个特殊规则,那就是逗号(,分隔符,其效果等同于or逻辑操作符。当使用逗号分隔的媒体查询规则时,如果任何一个媒体查询规则返回的是真,那对应的样式规则就会生效。逗号分隔的列表中每个媒体查询规则都是独立的,一个媒体查询规则中的操作符并不会影响其它的媒体查询规则。也就是说,逗号分隔的媒体查询规则列表能够作用于不同的媒体属性、类型和状态

例如,如果你想在最小宽度为760px或横屏的手持设备上应用一组样式,可以这样写:

@media (min-width: 760px), handheld and (orientation: landscape) {
    /* 样式规则 */
}

正如上面代码所示,如果一个800px宽度的屏幕设备,将返回真,逗号前一部分规则相当于@media all and (min-width: 760px)将会应用于该设备并且返回真,尽管我的屏幕媒体类型并不与第二部分(逗号后面一部分规则)的手持媒体类型相符合。同样的,如果我是一个500px宽的横屏手持设备,尽管第一部分因为宽度并不匹配,但第二部分仍匹配,因此整个媒体查询也会返回值。

only

only逻辑操作符有点特殊,它隐藏了旧浏览器的整个查询(防止老旧的浏览器不支持带媒体属性的查询而应用到给定的样式)。换句话说,旧的浏览器不理解only逻辑操作符,因此会忽略整个媒体查询规则。否则只会无效:

@media only all and (min-width: 320px) and (max-width: 480px) {
    /* 忽略老的浏览器样式规则 */
}

not逻辑操作符类似,only逻辑操作符对于使用媒体类型是不可选的。

不支持媒体查询Level 3规则的浏览器已经非常少见了,所以在大多数情况之下,使用only逻辑操作符是不必要的。

标准设备的媒体查询规则

前面也提到过,媒体查询规则@media是为响应式设计而生,它是响应式设计的一个主要组成部分,主要作用就是为正确的设备提供最佳的样式规则,确保最好的用户体验。但市场上有着大量不同的设备终端,要为所有设备终端提供最匹配的媒体查询规则,匹配最佳样式规则,决不是一件简单的事情。这里罗列了一些媒体查询规则,这些媒体查询规则是针对于众多标准和流行的设备终端而匹配的。非常值得大家收藏,在关键时刻的时候能派上用场。

简单地说,根据您的设计而不是特定的设备选择断点是一种明智的方法。但有时你只需要用一点点代码就可以为一个特定的场景提供相应的样式规则。

手机和手持设备

/* iPhone 4 和 4s */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 480px)
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 480px)
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: portrait) {
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 480px)
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: landscape) {
    /* 样式规则 */
}

/* iPhone 5, 5S, 5C 和 5SE  */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 568px)
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 568px)
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: portrait) {
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 568px)
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: landscape) {
    /* 样式规则 */
}

/* iPhone 6, 6S, 7 和 8  */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 667px) 
    and (-webkit-min-device-pixel-ratio: 2) { 
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 667px) 
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: portrait) { 
    /* 样式规则 */
}
/* 模屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 667px) 
    and (-webkit-min-device-pixel-ratio: 2)
    and (orientation: landscape) { 
    /* 样式规则 */
}

/* iPhone 6+, 7+ 和 8+  */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 414px) 
    and (max-device-width: 736px) 
    and (-webkit-min-device-pixel-ratio: 3) { 
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 414px) 
    and (max-device-width: 736px) 
    and (-webkit-min-device-pixel-ratio: 3)
    and (orientation: portrait) { 
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 414px) 
    and (max-device-width: 736px) 
    and (-webkit-min-device-pixel-ratio: 3)
    and (orientation: landscape) { 
    /* 样式规则 */
}

/*  iPhone X  */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 812px) 
    and (-webkit-min-device-pixel-ratio: 3) { 
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 812px) 
    and (-webkit-min-device-pixel-ratio: 3)
    and (orientation: portrait) { 
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 375px) 
    and (max-device-width: 812px) 
    and (-webkit-min-device-pixel-ratio: 3)
    and (orientation: landscape) { 
    /* 样式规则 */
}

/* iPhone XS Max和 XR  */
/*横屏和竖屏*/
@media only screen 
    and (min-device-width: 414px)
    and (max-device-width: 896px)
    and (-webkit-device-pixel-ratio: 3) {
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 414px) 
    and (max-device-height: 896px) 
    and (orientation : portrait) 
    and (-webkit-device-pixel-ratio: 3){
    /* 样式规则 */
}
/*横屏*/
@media only screen 
    and (min-device-width: 414px) 
    and (max-device-height: 896px) 
    and (orientation : landscape) 
    and (-webkit-device-pixel-ratio: 3){
    /* 样式规则 */
}

/*  Google Pixel  */
/* 横屏和竖屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 3) {
    /* 样式规则 */
}
/* 竖屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 3) 
    and (orientation: portrait) {
    /* 样式规则 */
}
/* 横屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 3) 
    and (orientation: landscape) {
    /* 样式规则 */
}

/*  Google Pixel XL  */
/* 竖屏和横屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 4) {
    /* 样式规则 */
}
/* 竖屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 4) 
    and (orientation: portrait) {
    /* 样式规则 */
}
/* 横屏 */
@media screen 
    and (device-width: 360px) 
    and (device-height: 640px) 
    and (-webkit-device-pixel-ratio: 4) 
    and (orientation: landscape) {
    /* 样式规则 */
}

平板电脑

/*  iPad 1, 2, Mini 和 Air  */
/* 横屏和竖屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (-webkit-min-device-pixel-ratio: 1) {
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (orientation: portrait) 
    and (-webkit-min-device-pixel-ratio: 1) {
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (orientation: landscape) 
    and (-webkit-min-device-pixel-ratio: 1) {
    /* 样式规则 */
}

/*  iPad 3, 4 和 Pro 9.7"  */
/* 竖屏 和 横屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 竖屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (orientation: portrait) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 横屏 */
@media only screen 
    and (min-device-width: 768px) 
    and (max-device-width: 1024px) 
    and (orientation: landscape) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}

/*  iPad Pro 10.5"  */
/* 竖屏 和 横屏 */
@media only screen 
    and (min-device-width: 834px) 
    and (max-device-width: 1112px)
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 竖屏 */
/* 最小设备宽度和最大设备宽度设置相同的值,以免与PC端相 */
@media only screen 
    and (min-device-width: 834px) 
    and (max-device-width: 834px) 
    and (orientation: portrait) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 横屏 */
/* 最小设备宽度和最大设备宽度设置相同的值,以免与PC端相 */
@media only screen 
    and (min-device-width: 1112px) 
    and (max-device-width: 1112px) 
    and (orientation: landscape) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}

/*  iPad Pro 12.9"  */
/* 竖屏 和 横屏 */
@media only screen 
    and (min-device-width: 1024px) 
    and (max-device-width: 1366px)
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 竖屏 */
/* 最小设备宽度和最大设备宽度设置相同的值,以免与PC端相 */
@media only screen 
    and (min-device-width: 1024px) 
    and (max-device-width: 1024px) 
    and (orientation: portrait) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}
/* 横屏 */
/* 最小设备宽度和最大设备宽度设置相同的值,以免与PC端相 */
@media only screen 
    and (min-device-width: 1366px) 
    and (max-device-width: 1366px) 
    and (orientation: landscape) 
    and (-webkit-min-device-pixel-ratio: 2) {
    /* 样式规则 */
}

笔记本电脑

对于笔记本电脑的媒体查询规则,相对来说更为复杂一些。我们应该不要针对特定的设备来设计媒体查询规则,而应该针对屏幕的大小范围来设计,然后再区分是否为视网膜屏幕(Retina):

/*  非Retina屏  */
@media screen 
    and (min-device-width: 1200px) 
    and (max-device-width: 1600px) 
    and (-webkit-min-device-pixel-ratio: 1) { 
    /* 样式规则 */  
}

/* Retina屏 */
@media screen 
    and (min-device-width: 1200px) 
    and (max-device-width: 1600px) 
    and (-webkit-min-device-pixel-ratio: 2)
    and (min-resolution: 192dpi) { 
    /* 样式规则 */
}

上面罗列的是一些常见设备的媒体查询规则,如果你想获取更多的设备媒体查询规则,可以在VIZ DEVICES网站上查询。

媒体查询API

在DOM中有一个特性,可以通过JavaScript来获取媒体查询的结果。可以使用MediaQueryList接口和它的方法来实现。一旦创建了MediaQueryList对象,咱们就可以通过它来检查查询结果,或者也可以设置一些属性,来实现当查询结果变化时,自动接收到通知。

创建媒体查询列表

在获取媒体查询结果之前,首先要创建MediaQueryList对象,用来存储媒体查询。为了实现这个目的,需要使用window.matchMedia()方法。

举个例子,比如你想设置一个查询列表用来判定设备屏幕处于横屏还是竖屏,那你可以像下面这样编码:

window.matchMedia("(orientation: portrait)");

当设备屏幕是横屏时,MediaQueryList对象中的matches属性返回的值为false,如下图所示:

反之,当你的设备是竖屏时,MediaQueryList对象中的matches属性返回的值为true,如下图所示:

从上面的示例也可以看出来,matchMedia()方法的使用很简单,只需要给这个方法传一个mediaQueryString参数,该参数是一个字符串,表示即将返回一个新MediaQueryList对象的媒体查询。返回来的MediaQueryList对象包含两个属性:

  • media,是一个DOMString类型,返回一个序列化的媒体查询列表
  • matches,返回的是一个布尔值,匹配则为true,否则为false

比如下面这个示例:

window.matchMedia("(min-width: 400px)")
// => MediaQueryList {media: "(min-width: 400px)", matches: true, onchange: null}
window.matchMedia("(min-width: 400px)").media
// => "(min-width: 400px)"
window.matchMedia("(min-width: 400px)").matches
// => true

那么在一些情况之下,就可以借助新返回的MediaQueryList对象的matches属性做一些事情,比如:

if (window.matchMedia("(min-width: 400px)").matches) {
    // 如果视窗宽度大于或等于400px,返回的值为true
    // 可以针对符合条件的情况下做一些想做的事情
} else {
    // 视窗小于400px的情况下,做一些自己想做的事情
}

接收和终止媒体查询的通知

如果想要接收媒体查询的提醒,我们就需要注册一个监听器来帮助我们,这样做要比手动查询更为有效。可以在MediaQueryList对象上使用addListener()方法,这样就通过实现MediaQueryListListener接口来指定一个监听器。比如下面这个示例:

let receiveMediaQueryMessage = window.matchMedia('(orientation: portrait)')
receiveMediaQueryMessage.addListener(handleOrientationChange)

function handleOrientationChange (receiveMediaQueryMessage) {
    if (receiveMediaQueryMessage.matches) {
        console.log('现在处在竖屏')
    } else {
        console.log('现在处在横屏')
    }
}
handleOrientationChange(receiveMediaQueryMessage)

当你的手持设备不断的在横屏与竖屏之间切换时,console.log打印出来的值也会随之变化,如下图所示:

上面的示例,咱们通过window.matchMedia()方法创建了一个屏幕方向检测的查询列表receiveMediaQueryMessage,并且添加了一个事件监听。需要注意的是,当我们添加监听之后,我们其实直接调用了一次监听。这会让我们的监听器以目前设备的屏幕方向来初始化判定代码。也就是说,如果我们代码中设定的设备处理竖屏模式,而实际上它在启动时处理横屏模式,那么我们在后面的判定就会出现矛盾。然后我们可以在handleOrientationChange()函数中来查看媒体查询结果,并且可以设置屏幕方向变化后的逻辑处理代码。

上面示例演示的是,使用addListener()来接收媒体查询的通知,如果你不想再要接收媒体查询值变化的相关通知时,可以在MediaQueryList上调用removeListener()方法来移除监听,比如:

receiveMediaQueryMessage.removeListener(handleOrientationChange)

使用CSS媒体查询常的缺点

CSS的@media规则(媒体查询)是为某些东西设计的,虽然很多人都说媒体查询是响应式Web设计的基石,但事实上那不是响应式Web设计。听起来是不是有些矛盾,那么根据我的经验,我来说说CSS媒体查询时常碰到的问题。

不直观

直到现在为止还没有哪位Web开发者说CSS媒体查询是直观的。虽然定义媒体查询规则非常简单,但是并不总是非常清楚,媒体查询规则在真实的浏览器,真实的设备和无数的场景中情况又是如何?

比如:

@media only screen and (min-width: 320px) and (max-width: 480px) {
    /** 样式规则*/
}

上面的媒体查询规则的意思是:当浏览器视窗的宽度是320px480px之间时符合媒体查询规则,返回的值是true,就会调用对应的样式规则。事实上,当你想做一些更具体的事情时,比如设备是平板电脑的横屏操作中,应用一个样式规则,这并不完全是确定的或直观的。设置一个媒体查询来做到这一点并不是不可能,但它绝对不是直观的。

限制条件

CSS媒体查询是动态的,允许你在CSS中定义条件语句。例如,如果视口位于这个和那个之间,那么执行另一段样式规则。然而,你这只是考虑了视口方面的限制,但事实上许多Web设计中还会考虑使用的场景。比如说,移动端的TabBar,对于不同的系统场景,他们的布局是有所不同的。例如,iOS设备,系统TabBar常常通栏位于屏幕底部,而Android设备却刚好相反,位于屏幕顶部。

那么问题来了,CSS媒体查询规则又怎么才能根据系统对某些UI元素设置不同的样式呢?

就算是你绕着走,可以使用CSS媒体查询来做到这一点,但也不能这么做,因为CSS媒体查询不是用于任何特性构建的。除此之外,你可能还需要通过CSS进行许多其他定制,但是当你需要不同程度的简单到高级条件时,媒体查询就不是一个很好的解决方案了。

不是本地扩展

CSS媒体查询是嵌入在浏览器中的一个功能特性。这也意味着它不是本机可扩展的。也就是说,不能通过CSS接口为本机添加额外的CSS媒体查询规或增加其功能。即使新的CSS媒体查询特性得到了Web标准流程的认可,要达到普实性,也还需要较长的一段时间。此外,并不是所有添加的特性都对你有用,因此,如果你没有得到你想要的东西,那么你需要找到其他方法来解决你的问题。

当然,有一种方法可以扩展CSS,但是你必须对JavaScript相关的知识要有深入的了解,而对于大多数Web开发人员来说,这不是一个实用的过程。

开发效率低

使用媒体查询为不同的场景实现不同的Web效果(响应式Web设计),有可能造成你的代码量成倍增加,因为你要为不同的断点添加单独的样式规则。比如:

:root {
    --font-size: 100%;
}

body {
    font-size: var(--font-size);
}

/* 竖屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 480px) 
    and (-webkit-min-device-pixel-ratio: 2) 
    and (orientation: portrait) {
    :root {
        font-size: 80%;
    }
}
/* 横屏 */
@media only screen 
    and (min-device-width: 320px) 
    and (max-device-width: 480px) 
    and (-webkit-min-device-pixel-ratio: 2) 
    and (orientation: landscape) {
    :root {
        --font-size: 120%;
    }
}

除了代码量的增加之外,还会增加工作流程的复杂性。为什么这么说呢?

了解响应Web设计的同学都应该知道,CSS媒体查询大部分在处理布局上的调整。因此,要做更多的事情,甚至还有的时候需要借助JavaScript来弥补其不足之处。同时为你后其的测试也增加了倍数量的工作。

响应Web性能

由于CSS媒体查询的工作方式,你最终需要更多的CSS代码,才能满足你的Web设计需求。根据HTTPArchive.org过去对响应式Web设计的一个数据统计可以得知,CSS文件的大小在过去五年中增加了114%,HTML文件大小的增长在同一时期达到了53%的峰值。

这种特殊的情况对您的网站的性能是会有一定的影响,因为在实现CSS媒体查询之后,它的速度肯定比以前慢,特别是对于使用不太理想的移动宽带网络的移动设备。而且,除了文件大小增加的问题之外,CSS媒体查询中没有任何内部机制可以真正提高Web页面的性能。

既然CSS媒体查询存在这么多的不足,那么为什么还有很多Web开发人员使用该技术来实现响应式的Web设计呢?其实这也是有一定的历史原因的。

最初的CSS媒体查询是用来辅助你在Web浏览器中实现一些特殊的效果,比如说,在高屏和短屏的设备中做一些元素上距离调整等。但后来,CSS媒体查询被社区用来承担整个响应式Web设计的重任。这好比,你只能吃一碗饭,然后店家非得让让吃十确碗饭,你说冤屈不。

因此,在一些Web的特殊场景,CSS媒体查询规则,还是具有一定的特殊能力,能帮助众多Web开发人员解决一些痛点。这也是CSS媒体查询规则最初设计的初衷。因此大家不要迷信CSS媒体查询规则是万能的,它只是在最适合的场景使用才是最佳的。

条件CSS之@supports

Web开发者都知道,不同的浏览器(不管是现代浏览器还是老版本的IE浏览器)对Web页面的解析都会有所不一样,为了让Web页面在这些浏览器下渲染达到基本一致的效果,给用户更好的体验,开发者有时候需要为他们提供不同的样式代码。

而早期的时候,大多都是依赖于JavaScript来检测,然后提供不同的样式规则,很多时候为了节省时间和成本,会直接使用Modernizr这样的第三方JavaScript库来完成。但这样做真的有用吗?除了要懂怎么检测之外,还需要了解更多的有关于浏览器渲染机制,这样一来对于很多开发人员而言是较为痛苦的。

不过,幸运的是,条件CSS除了@meida之外,还提供了另一个条件CSS规则,即 @supports规则,它允许我们可以根据浏览器对CSS特性的支持情况来定义不同的样式。这对于我们来说是非常重要的。

@supports规则作用是什么

用一句话来说,@supports规则的作用是:

用来查询浏览器是否支持CSS的特性!

前面也提到过,前端社区有很多大神一直在致力于CSS技术的革新和新特性的推进。所以每一年都会有不少的新特性或CSS的实验性特性的出现,但由于各大浏览器厂商对于这些新特性的支持度是有所差异,甚至支持的友好度也是不一致的。换句话说,或许你知道新出现的CSS特性,但又担心浏览器不支持,从而又不敢使用。那么这个时候就可以借助于@supports特性来完成对浏览器的检测。从而可以放心的在支持的浏览器中使用这些新特性CSS,对于不支持的浏览器可以轻易的提供一些降级处理。让你的作品在用户面前展示最佳的效果。

@supports规则

@supports规则和@media规则有点类似。在@规则(@supports)后面会紧跟一个或多个条件语句<supports-condition>,再紧跟着{},在{}中嵌套着你想要的CSS规则。如果条件符合,即返回的值是true,浏览器就会运用{}中的CSS样式规则;反之,条件不符合,返回的值是false,浏览器即会忽略{}中的样式规则。用一张图来描述,大家会更易于理解:

@supports中的条件规则是由一个或多个由不同的逻辑操作符组成的表达式声明组合而成的。使用小括号()可以调整这些表达式之间的运算优先级。而其中的条件表达式就是CSS声明,也就是一个CSS属性后跟一个属性值,比如property:value。比如说,display: flex这样的一个表达式,对于支持的浏览器将会返回true,反之不支持的浏览器则会返回false

@supports的使用

就使用方面,@supports要比@media简单和直观的多。比如下面这个小示例:

<article class="artwork">
    <img src="myimg.jpg" alt="cityscape">
</article>

HTML的结构非常的简单。一个article标签套了一个img元素,没啥特殊之处,主要来看CSS方面的运用:

/* 支持mix-blend-mode 浏览器将会运用的CSS规则 */
@supports (mix-blend-mode: overlay) { 
    .artwork img { 
        mix-blend-mode: overlay; 
    } 
} 

/* 不支持mix-blend-mode 浏览器将会运用的CSS规则 *
@supports not(mix-blend-mode: overlay) { 
    .artwork img { 
        opacity: 0.5; 
    } 
}

上面的示例代码将会告诉浏览器,如果支持mix-blend-mode:overlay样式规则,将会执行@supports (...) {...}中的CSS:

.artwork img { 
    mix-blend-mode: overlay; 
} 

对于不支持的浏览器则会执行@supports not(...) {...}中的CSS:

.artwork img { 
    opacity: 0.5; 
} 

对应的渲染效果如下图所示:

逻辑操作符

对于逻辑操作符方面@supports@media非常类似,它具有的逻辑操作符有andornot

and逻辑操作符

and操作符相当于JavaScript逻辑操作符中的。在CSS中的@supports逻辑符and,其意思是将两个或多个表达式连在一起,如果每个表达式返回的值都是true,则@supports的表达式就为;如果其中有一个为假,表达式都将返回为。比如下面这个示例,当且仅当两个原始表达式同时为真时,整个表达式才为真:

@supports (display:table-cell) and (display: list-item) {
    /* 样式规则 */
}

or逻辑操作符

or操作符相当于JavaScript逻辑操作符中的。在CSS中的@supports逻辑符or,其主要用来将两个原始的表达式做逻辑或后生成一个新的表达式,如果两个原始表达式的值有一个为真或者都为真,则生成的表大家式也为真。简单地说,只要表达式中有一个为真,最终的值就是为值。比如:

@supports (display: -webkit-flex) or (display: -moz-flex) or (display: flex) {
    /* 样式规则 */
}

not逻辑操作符

not操作符相当于JavaScript逻辑符中的。在CSS中的@supports逻辑符not可以放在任何表达式的前面来产生一个新的表达式,新的表达式为原表达式的值的否定(即非)。也就是说,如果表达式的值为真,加上not其新的表达式的值就是假;如果表达式的值为假,加上not其新的表达多的值就是真。

@supports not(mix-blend-mode: overlay) { 
    /* 样式规则 */
}

在CSS中的@supports的逻辑符除了单个使用之外,还可以混合在一起使用,比如:

@supports ((margin-left: 0px) or (float:left)) and (background-color: yellow) {
    /* 样式规则 */
}

注意: 在使用andor操作符时,如果是为了定义多个表达式的执行顺序,则必须使用小括号。如果不这样做的话,该条件就是无效的,会导致整个规则失效。

@supports对应的JavaScript API

一般情况之下,我们可以使用JavaScript来检测CSS属性是否被浏览器支持,经典的做法是使用in操作符来检测:

if("backgroundColor" in document.body.style){
    document.body.style.backgroundColor = "red";
}

@supports出现之后,我们可以使用另一个API来检测浏览器是否支持CSS。而这个API就是CSS.supports,每个支持@supports规则的浏览器也将支持这个函数:

if(CSS.supports("(background-color: red) and (color:white")){
    document.body.style.color = "white";
    document.body.style.backgroundColor = "red";
}

CSS.supports()静态方法返回一个布尔值,用来检测浏览器是否支持一个给定的CSS特性。在使用当中,可以通过两种方式来调用CSS.supports()

// 第一种方式 
CSS.supports('mix-blend-mode', 'overlay'); // => true 

// 第二种方式 
CSS.supports('(mix-blend-mode: overlay)'); // => true

如果浏览器支持@supports中的条件则返回true,否则返回false

这样一来,就可以很容易根据.supports()来做一个判断,做一些事情。比如说,如果浏览器支持mix-blend-mode: luminosity我就给目标元素添加luminosity-blend类名,否则就给目标元素添加noluminosity类名。

var init = function() { 
    var test = CSS.supports('mix-blend-mode', 'luminosity'), 
        targetElement = document.querySelector('img'); 
    if (test) { 
        targetElement.classList.add('luminosity-blend'); 
    } else { 
        targetElement.classList.add('noluminosity'); 
    } 
}; 
window.addEventListener('DOMContentLoaded', init, false);

条件CSS之@viewport

先要声明一点,其实@viewport并不是真正的条件CSS规则,时至今日,@viewport规则被纳入到CSS Device Adaptation Module Level 1模块。估计大部分同学都并未接触过该规则,但对于HTML中<meta>元素的viewport应该并不会感到太陌生,即 用来设置视口

<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no">

上面的代码会指示浏览器如何对网页尺寸和缩放比例进行控制:

  • 使用元视口代码控制浏览器视口的宽度和缩放比例
  • 添加width=device-width以便与屏幕宽度(以设备无关像素为单位)进行匹配
  • 添加initial-scale=1以便将CSS像素与设备无关像素的比例设为1:1
  • 确保在不停用用户缩放功能的情况下,你的网页也可以访问

除了设置initial-scale之外,你还可以在视口上设置以下属性:

  • minimum-scale
  • maximum-scale
  • user-scalable

但是,设置后,这些属性可以停用用户缩放视口的功能,可能会造成网页访问方面的问题。

而在CSS中,提供了一个@viewport规则,可以让我们在CSS中设置类似于<meta>标签中的viewport。简单地说,@viewport规则让我们可以对文档的大小进行设置viewport。这个特性主要被用于移动设备,但是也可以用在支持类似“固定到边缘”等特性的桌面浏览器,比如微软的Edge浏览器。

比如:

<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no" />

我们就可以使用CSS来描述,而且起到同等的效果:

@viewport {
    width: device-width;
    initial-scale: 1;
    zoom: 1;
    min-zoom: 1;
    max-zoom: 3;
    user-zoom: fixed;
}

CSS中的@viewport规则中描述符是针对每个文档的,不涉及继承。因此,使用inherit关键词的声明将被删除。而且它的工作方式类似于@page规则,并遵循CSS的级联顺序。因此,@viewport规则中描述符将覆盖前面规则中的描述符。

正如上面的示例代码,@viewport规则中包含的规则符常见的主要有:

规则属性描述
min-width设置viewport的最小宽度
max-width设置viewport的最大宽度
width同时设置min-widthmax-width
min-height设置viewport的最小高度
max-height设置viewport的最大高度
height同时设置min-heightmax-height
zoom设置初始缩放系数
min-zoom设置最小缩放系数
max-zoom设置最大缩放系数

小结

通过上面内容的学习,我想大家对于CSS中的@规则应该有了一个基础的了解。但该文花费更多的篇幅是来阐述条件CSS。简单的说,条件CSS主要有@media@supports规则,他们都是根据一定的条件表达式,提供相应的样式规则。当条件表达式返回的值是true,对应嵌套的CSS规则就会被浏览器渲染;反之即会忽略。而@media大多数在为响应式Web设计服务,而@supports能更好的为你给浏览器提供不同的样式规则,特别是面对于一些CSS新特性的时候,你可以借这个规则为先进的设备提供最佳、最优的样式规则;而对于不支持新特性的浏览器又能轻易的提供相应的降级样式,从而能更好的为用户提供最佳的样式规则。对于@viewport规则,也是CSS@规则中的一种,有些人也将其称为条件CSS规则的一部分,但事实它并不是真正的CSS规则。不过,它能让你在CSS样式中更好的设置视口相应的参数。

最后再提一句,不管是条件CSS还是CSS的@规则,大特定的场合下都能起到意想不到的功效。当然,大家在使用的时候,应该根据自己的场景选择最适合的方案。比如在响应式设计的时候,选择@media就较好,但对于处理刘海设备适配的时候,@supports就比较好;而对于纵向适配的时候,@supports@media结合可能会更好。所以说,没有最好的,只有最合适的。

如何在Vue项目中使用SVG Icon

$
0
0

Web中对于Icon的使用已经是非常频繁的一件事情了,而且很有图标的使用会让你的Web应用程序或Web网页面变得更具可交互性和可使用性。早前在《Web中的图标》一文中和大家一起探讨了如何在Web中使用图标。其中不同的使用方式都具有各自的优势,但随着技术的革新,其中SVG的图标在Web中的使用也越来越频繁,并且其具备的优势也越来越明显。正因如此,在自己的项目中使用SVG的图标的场景也越来越多,因此,今天想和大家一起聊聊如何在Vue的项目中更好的使用SVG图标。

单个Icon组件的使用

在Vue官方网站专门有一节内容,向大家介绍了如何在项目中更好的使用SVG图标系统,而且这个系统是可具编辑的。先来看最基础的一个示例,这个示例是基于Vue CLI3的基础上构建的。通过官网提供的命令,使用CLI命令创建一个Vue项目,比如:

vue create svg-icon-app

采用默认的模板来构建了一个svg-icon-app项目。我们先跟着官网提供的思路来一步一步往下走,在components目录下创建一个新的目录icons。并将相应的SVG图标以一种标准化的方式命名,比如:

components/icons/IconBox.vue
components/icons/Facebook.vue
components/icons/Twitter.vue
components/icons/GooglePlus.vue

其中IconBox.vue是SVG图标的一个基础图标组件,在这个组件中使用了slot,其模板如下:

<!-- IconBox.vue -->
<template>
    <svg xmlns="http://www.w3.org/2000/svg"
        :width="width"
        :height="height"
        viewBox="0 0 18 18"
        :aria-labelledby="iconName"
        role="presentation">
        <title
            :id="iconName"
            lang="en">{{ iconName }} icon</title>
        <g :fill="iconColor">
            <slot />
        </g>
    </svg>
</template>

你可以像上面这样使用这个基础图标,唯一可能要做的就是根据你图标的viewBox来更新其viewBox。在基础图标组件里会有widthheighticonColor以及iconNameprops,这样我们就可以通过props对其动态更新。iconName将会同时用在<title>的内容及其用于提供可访问性的id上。

<!-- IconBox.vue -->
<script>
    export default {
        name: 'IconBox',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        data () {
            return {

            }
        }
    }
</script>

currentColor会成为fill的默认值,于是图标就会继承color的值(currentColor是CSS的一个很优秀的属性)。我们也可以根据需求传递一个不一样的颜色值。

特别声明,viewBox是SVG中的一个重要的概念,如果你从未接触过的话,建议你花点时间对该概念进行了解。你可以点击这里、或这里、还有这里进行了解。

IconBox组件完成了之后,接下来把对应的图标组件完善。

示例中的几个图标,都来自于Font Awesome提供的SVG图标。

除此之外,还可以通过iconmonstrIcoMoon平台,甚至还可以使用NucleoApp来获取SVG图标。特别是NucleoApp来管理你的SVG图标:

我们通过编辑器打开其中一个.svg文件,比如facebook.svg,其代码会是这样的:

<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="facebook-square" class="svg-inline--fa fa-facebook-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
    <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z" id="Path"></path>   
</svg>

我们只需要将其中<path>部分代码放到图标组件的<template>中,比如facebook.svg的放到对应的Fackbook.vue组件:

<!-- Facebook.vue -->
<template>
    <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z" id="Path"></path>      
</template>

<script>
    export default {
        name:'Facebook'
    }
</script>

对于GooglePlus.vueTwitter.vue组件类似。

为了更好的演示官网提供的示例,将在components目录中新创建一个.vue文件,将引用我们创建的FacebookTwitterGooglePlus组件。该文件名暂时取名为VueOfficialSvgIcons.vue,接下来我们就可以这样使用这几个SVG图标:

<!-- VueOfficialSvgIcons.vue -->
<template>
    <div class="icons">
        <IconBox icon-name="facebook"><Facebook /></IconBox>
        <IconBox icon-name="twitter"><Twitter /></IconBox>
        <IconBox icon-name="google plus"><GooglePlus /></IconBox>
    </div>
</template>

<script>
    import IconBox from './icons/IconBox'
    import Facebook from './icons/Facebook'
    import Twitter from './icons/Twitter'
    import GooglePlus from './icons/GooglePlus'

    export default {
        name: 'VueOfficialSvgIcons',
        components: {
            IconBox,
            Facebook,
            Twitter,
            GooglePlus
        }
    }
</script>

现在看到的效果,都是默认的尺寸32px x 32px

如果我们需要多种尺寸的图标,可以像下面这样做:

<!-- VueOfficialSvgIcons.vue -->
<IconBox 
    width="16"
    height="16"
    icon-name="facebook"><Facebook /></IconBox>

<IconBox icon-name="twitter"><Twitter /></IconBox>

<IconBox 
    width="48"
    height="48"
    icon-name="google plus"><GooglePlus /></IconBox>

得到的效果如下:

如果你需要不同的图标颜色,可以通过iconColor传一个图标的颜色:

<!-- VueOfficialSvgIcons.vue -->
<IconBox 
    icon-color="#f36"
    icon-name="facebook"><Facebook /></IconBox>

<IconBox 
    icon-color="#f0987a"
    icon-name="twitter"><Twitter /></IconBox>

<IconBox icon-name="google plus"><GooglePlus /></IconBox>

效果如果如下:

这是Vue项目中使用SVG图标最基本的方式。通过上面的示例我们可以看出来,如果你还需要更多的控制SVG图标,可以在IconBox基本组件中的props设置更多的属性。在使用的时候按需传入即可。

SVG Sprite的使用

上面这种方式,每个Icon都需要一个独立的组件。如果你的Web应用有许多图标用在不同的地方时,这种方式较为适合。如果你只在一个页面上重复使用相同图标多次,那么使用SVG Sprite更为理想。

有关于SVG Sprites的使用可以阅读《SVG Sprite》一文。

简单地说,可以通过SVG的<symbol>标签把所以SVG图标合并在一起,然后使用<use>标签来调用<symbol>中指定的图标。即 <use>中的xlink:href属性值与<symbol>标签指定的id相同。

在这里,我们来看看在Vue中怎么使用SVG Sprites来创建图标。基于上面的示例,我们先创建一个名为Icon.vue组件:

<!-- Icon.vue -->
<template>
    <svg :width="width" :height="height" :style="{color: iconColor}">
        <use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="iconId"></use>
    </svg>
</template>

<script>
    export default {
        name: 'Icon',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        computed: {
            iconId() {
                return `#icon-${this.iconName}`
            }
        }
    }
</script>

现在Icon组件是功能性的了,我们需要在src/mail.js中注册到Vue实例中:

<!-- src/main.js -->
import iconComponent from './components/svgSprites/Icon'
Vue.component('icon', iconComponent)

为了区分前面的示例,这里为SVG Sprites单独创建一个独立的组件,比如SvgSpritesIcon.vue组件,然后再这个组件中调用:

<icon icon-name="facebook"></icon>
<icon icon-name="twitter"></icon>
<icon icon-name="google"></icon>

但在页面中并不会看到有任何的Icon。这主要是因为我们还没有创建SVG Sprites。暂时我们人肉创建一个SVG Sprites,比如在App.vue文件中创建一个SVG Sprites:

<!-- App.vue -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
    <symbol viewBox="0 0 32 32" id="icon-facebook" fill="currentColor">
        <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z"></path>      
    </symbol>
    <symbol viewBox="0 0 32 32" id="icon-twitter" fill="currentColor">
        <path d="M28.5714286,0 L3.42857143,0 C1.53571429,0 0,1.53571429 0,3.42857143 L0,28.5714286 C0,30.4642857 1.53571429,32 3.42857143,32 L28.5714286,32 C30.4642857,32 32,30.4642857 32,28.5714286 L32,3.42857143 C32,1.53571429 30.4642857,0 28.5714286,0 Z M25.0785714,11.3428571 C25.0928571,11.5428571 25.0928571,11.75 25.0928571,11.95 C25.0928571,18.1428571 20.3785714,25.2785714 11.7642857,25.2785714 C9.10714286,25.2785714 6.64285714,24.5071429 4.57142857,23.1785714 C4.95,23.2214286 5.31428571,23.2357143 5.7,23.2357143 C7.89285714,23.2357143 9.90714286,22.4928571 11.5142857,21.2357143 C9.45714286,21.1928571 7.72857143,19.8428571 7.13571429,17.9857143 C7.85714286,18.0928571 8.50714286,18.0928571 9.25,17.9 C7.10714286,17.4642857 5.5,15.5785714 5.5,13.3 L5.5,13.2428571 C6.12142857,13.5928571 6.85,13.8071429 7.61428571,13.8357143 C6.30917726,12.9675977 5.52600528,11.5031734 5.52857143,9.93571429 C5.52857143,9.06428571 5.75714286,8.26428571 6.16428571,7.57142857 C8.47142857,10.4142857 11.9357143,12.2714286 15.8214286,12.4714286 C15.1571429,9.29285714 17.5357143,6.71428571 20.3928571,6.71428571 C21.7428571,6.71428571 22.9571429,7.27857143 23.8142857,8.19285714 C24.8714286,7.99285714 25.8857143,7.6 26.7857143,7.06428571 C26.4357143,8.15 25.7,9.06428571 24.7285714,9.64285714 C25.6714286,9.54285714 26.5857143,9.27857143 27.4285714,8.91428571 C26.7928571,9.85 25.9928571,10.6785714 25.0785714,11.3428571 Z"></path>        
    </symbol>
    <symbol viewBox="0 0 32 32" id="icon-google" fill="currentColor">
        <path d="M28.5714286,0 L3.42857143,0 C1.53571429,0 0,1.53571429 0,3.42857143 L0,28.5714286 C0,30.4642857 1.53571429,32 3.42857143,32 L28.5714286,32 C30.4642857,32 32,30.4642857 32,28.5714286 L32,3.42857143 C32,1.53571429 30.4642857,0 28.5714286,0 Z M11.7142857,23.1428571 C7.76428571,23.1428571 4.57142857,19.95 4.57142857,16 C4.57142857,12.05 7.76428571,8.85714286 11.7142857,8.85714286 C13.6428571,8.85714286 15.25,9.55714286 16.5,10.7285714 L14.5642857,12.5928571 C14.0357143,12.0857143 13.1142857,11.4928571 11.7214286,11.4928571 C9.28571429,11.4928571 7.3,13.5071429 7.3,16.0071429 C7.3,18.5 9.28571429,20.5214286 11.7214286,20.5214286 C14.55,20.5214286 15.6071429,18.4857143 15.7785714,17.4428571 L11.7142857,17.4428571 L11.7142857,14.9857143 L18.4571429,14.9857143 C18.5285714,15.3428571 18.5714286,15.7071429 18.5714286,16.1714286 C18.5714286,20.25 15.8357143,23.1428571 11.7142857,23.1428571 Z M27.4285714,17.3 L25.3571429,17.3 L25.3571429,19.3714286 L23.2714286,19.3714286 L23.2714286,17.3 L21.2,17.3 L21.2,15.2142857 L23.2714286,15.2142857 L23.2714286,13.1428571 L25.3571429,13.1428571 L25.3571429,15.2142857 L27.4285714,15.2142857 L27.4285714,17.3 Z"></path> 
    </symbol>
</svg>

此时,你刷新你的页面,可以看到图标,如下图所示:

和前面的示例一样,我们可以通过props中的widthheighticonColor来设置图标的大小和颜色,比如:

<icon icon-name="facebook"></icon>
<icon icon-name="twitter" width="48" height="48"></icon>
<icon icon-name="google" width="64" height="64" icon-color="green"></icon>

此时的效果如下:

上面的示例中,虽然达到我们想要的效果。但SVG Sprite是我们人肉生成的。其实,我们可以借助相关的工具链,自动生成SVG Sprite。比如:

这些工具会在编译时打包 SVG,但是在运行时编辑它们会有一些麻烦,因为 <use>标签在处理一些复杂的事情时存在浏览器兼容问题。同时它们会给你两个嵌套的 viewBox属性,这是两套坐标系。所以实现上稍微复杂了一些。

接下来我们以svg-sprite-loader为例,看看怎么自动生成SVG Sprite。我先从网上下载几个SVG的图标放在项目src/assets/icons/下:

注意,我们的环境是Vue Cli3下。

先进行安装:

npm i svg-sprite-loader -D

现在我们需要做一些配置。Vue Cli内部是利用webpack-chain插件来修改Webpack配置的。所以我们需要在项目根目录下创建一个Vue.config.js文件,在该文件中利用webpack-chain来修改相关的Webpack配置。比如:

// Vue.config.js
const path = require('path')

function resolve(dir) {
    return path.join(__dirname, '.', dir)
}

module.exports = {
    chainWebpack: config => {
        config.module.rules.delete('svg')

        config.module
            .rule('svg-sprite-loader')
            .test(/\.svg$/)
            .include
            .add(resolve('src/assets/icons')) // svg图标的路径
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader')
            .options({
                symbolId: 'icon-[name]' // 设置svg中symbol中id的值
            })
    }
}

前面也提到过了,使用SVG Sprite,需要有一个模板,比如/src/components/svgSprites/Icon.vue组件:

<!-- /src/components/svgSprites/Icon.vue -->
<template>
    <svg :width="width" :height="height" :style="{color: iconColor}">
        <use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="iconId"></use>
    </svg>
</template>

<script>
    export default {
        name: 'Icon',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        computed: {
            iconId() {
                return `#icon-${this.iconName}`
            }
        }
    }
</script>

虽然配置和模板都有了,但要使用还是需要人肉的引用.svg文件。所以我们借助Webpack的一些功能,来自动帮我们引入。接下来在/src目录下创建一个utils/svg-icons目录,并在该目录下创建一个index.js

// src/utils/svg-icons/index.js
import Vue from 'vue'

import iconSpriteLoadComponent from '../../components/svgSprites/Icon.vue'

Vue.component('icon',iconSpriteLoadComponent)

const requireAll = requireContext => requireContext.keys().map(requireContext)
const req = require.context('../../assets/icons', false, /\.svg$/)

requireAll(req)

上面这段代码主要是用于注册Icon组件和批量引入.svg文件。有了这些之后,我们就可以像下面这样使用:

<icon icon-name="close"></icon>
<icon icon-name="love"></icon>
<icon icon-name="star" width="48" height="48"></icon>

将看到的效果如下:

通过Icon.vue组件中的props定义的属性,将相关的值传入进去。同样可以设置大小,但这里有一个小细节,如果你想像前面的示例通过iconColor来给图标传递相应的颜色时会失效,比如:

<icon icon-name="location" width="24" height="24" icon-color="red"></icon>
<icon icon-name="collection" width="48" height="48" icon-color="lime"></icon>

颜色并没有重置过来:

那是因为,我们的.svg文件中path标签中的fill有一个默认值:

如果要让iconColor值生效,我们需要将.svg中的fill(和stroke)属性的值设置为currentColor。修改之后,你就能看到图标有相应的颜色了:

这里是通过人肉去修改每个.svg文件中fillstroke属性,即将其值修改为currentColor。不知道有没有相应的工具,能在合并成SVG Sprite时,自动将fillstroke的值更换为currentColor。找了一圈,没找到。如果您在这方面有相应的经验,欢迎分享出您的方案。

上面是在Vue Cli3环境中的使用,如果你是自己的构建系统,那么可以借助Webpack相关配置,达到相应的效果。只需要修改/build/webpack.base.conf.js文件:

{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    exclude: [resolve('src/assets/icons')], // 新增加
    loader: 'url-loader',
    options: [
        limit: 10000,
        name: utils.assetsPath('img/[name].[hash:7].[ext]')
    ]
}

// 另外增加
{
    test: /\.svg$/,
    loader: 'svg-sprite-loader',
    include: [resolve('src/assets/icons')],
    options: [
        symbolId: 'icon-[name]'
    ]
}

其他地方可以不修改。

注册事项

虽然SVG图标在Web中的运用越来越广泛,但是在使用和设计的时候还是需要注意一些细节,比如.svg文件的优化。大家都知道,用文本编辑器打开一个.svg文件,将看到一大坨的代码,如果你不知道什么可删除,什么不可删除,那么可以借助SVGOMG来优化:

代码上的变化如下:

另外有一点也需要特别注意,那就是SVG 图示的绘制。虽然在很多平台上能直接下载到自己需要的SVG图标,但有时候为了更适合自己的项目,设计师会为自己提供一些更具个性化的SVG图标,这个时候,需要一套较为标准的绘制规则和流程。有关于这方面可以参考iconfont上的图标绘制规则和制作流程

写在最后

有关于在Vue项目中使用SVG的方案和相关的组件库在Github上非常的多。大家可以选择适合自己的组件,如果你喜欢Font Awesome图标的话,那么@E0 大大的Vue-Awesome就是一个不错的选择。

最后再说一点,这篇文章主要是亲测了在Vue项目中怎么灵活或者更好的使用SVG图标。文章开头跟着Vue官网提供的方式,做了一些尝试,感觉较为简单,特别适合那种,项目中运用图标不多的场景,可以为每个图标创建一个独立的组件,不需要全局注册,也可以在任何需要的地方使用。如果图标较多时,管理起来有点蛋疼而以。文章第二部分,介绍如何自动化生成SVG Sprite,并且在相应的脚手架中运用。

可能文章中有不对或者不好的地方,如果你有这方面的经验,欢迎拍正和分享。文章中示例代码可以在Github中获取到

如何在Vue中构建可重用的Transition

$
0
0

在Vue中的transitionanimation都是一些很棒的东东。它们可以让我们的组件带有一定的动效效果。在《Vue的transition》和《Vue的animation》中分别学习了transitionanimation在Vue组件中的运用。这两个特性可以让Web元元素可以像animation.css库中提供的效果一样,实现一些过渡甚至是简单的动画效果。让整个效果看起来很好。

如果我们可以将它们封装到组件中,并且在多个项目中重用,是不是会给我们带来更多的益处呢?在前面两篇文章中,我们了解了在Vue中怎么使用transitionanimation,今天我们来学习几种定义transtion的方法,并且深入研究它们可以真正重用的方法。

创建Vue项目

为了节省大家更多的时间,在这篇文章中咱们直接使用Vue Cli的来构建演示项目:

vue create vue-transitions

因为我们将会涉及多个使用transition方法,在这里通过相关的分支来区分。

git checkout -b demo1

接下来进入实际的案例中,以来阐述Vue中如何构建可重用的transition

示例项目Github对应的地址在这里

原始的transition组件和CSS

在《Vue的transition》一文中,我们了解到,在Vue中定义transition最简单的方法是使用Vue中的<transition><transition-group>。这需要为它们定义一个name和一些CSS。比如:

<!-- transitionDemo.vue -->
<template>
    <div class="demo">
        <div class="toggle toggle--text">
            <input type="checkbox" id="toggle--text" class="toggle--checkbox">
            <label class="toggle--btn" for="toggle--text" data-label-on="show"  data-label-off="hidden" @click="toggle"></label>
        </div>

        <transition name="fade">
            <p v-if="show">Hello W3cplus (^_^) !!!</p>
        </transition>
    </div>
</template>

<script>
    export default {
        name: 'transitionDemo',
        data () {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show
            }
        }
    }
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
    transition: opacity .3s;
}

.fade-enter,
.fade-leave-to {
    opacity: 0;
}

/* === 默认样式省略 == */
</style>

点击按钮,你将看到的效果如下:

看起来很容易,对吧?然而,这种方法有一个问题。这个过渡效果不能在另一个项目中重用

封装一个transition组件

如果我们将前面逻辑封装成一个Vue组件,将会发生什么情况呢?同样的,把Git分支切换到demo2中。接着我们创建一个FadeTransition.vue组件。

<!-- FadeTransition.vue -->
<template>
    <transition name="fade">
        <slot />
    </transition>
</template>

<script>
    export default {
        name: 'FadeTransition'
    }
</script>

<style scoped>
    .fade-enter-active,
    .fade-leave-active {
        transition: opacity .3s;
    }

    .fade-enter,
    .fade-leave-to {
        opacity: 0;
    }
</style>

该组件很简单,和Vue基本的<transition>几乎类似,不同之处是该组件使用了slot。在使用FadeTransition组件时,可以通过slot传递你想要的内容。比如:

<!-- App.vue -->
<template>
    <div id="app">
        <div class="toggle toggle--text">
                <input type="checkbox" id="toggle--text" class="toggle--checkbox">
                <label class="toggle--btn" for="toggle--text" data-label-on="show"  data-label-off="hidden" @click="toggle"></label>
            </div>

        <FadeTransition>
            <div class="box" v-if="show"></div>
        </FadeTransition>
    </div>
</template>

<script>
    import FadeTransition from './components/FadeTransition'

    export default {
        name: 'app',
        components: {
            FadeTransition
        },
        data () {
            return {
                show: true
            }
        },
        methods: {
            toggle () {
                this.show = !this.show
            }
        }
    }
</script>

这个时候看到的效果如下:

这个示例比前面的示例稍微好一点,如果想传递其他特定的值(props)给transition,比如mode或者一些钩子函数,这就有点难搞了。

幸运的是,Vue中有一个特性,允许我们将用户指定的任何额外的props和侦听器传递给组件。如果你还不知道,可以通过$attrs来访问额外的props,并且结合v-bind一起使用,将它们绑定为props

$attrs:包含了父作用域中不作为 prop被识别 (且获取) 的特性绑定 (classstyle除外)。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 (classstyle除外),并且可以通过 v-bind="$attrs"传入内部组件 —— 在创建高级别的组件时非常有用

这种方式同样适用于$listener,通常和v-on绑定使用传入内部组件。

demo2的分支上,切到demo3分支,在FadeTransition组件上新增v-bind="$attrs"v-on="$listeners"

<!-- FadeTransition.vue -->
<template>
    <transition name="fade" v-bind="$attrs" v-on="$listeners">
        <slot />
    </transition>
</template>

<!-- App.vue -->
<template>
    <div id="app">
        <div class="toggle toggle--text">
            <input type="checkbox" id="toggle--text" class="toggle--checkbox">
            <label class="toggle--btn" for="toggle--text" data-label-on="Box"  data-label-off="Circle" @click="toggle"></label>
        </div>

        <FadeTransition mode="out-in">
            <div key="box" v-if="show" class="box"></div>
            <div key="circle" v-else class="circle"></div>
        </FadeTransition>
    </div>
</template>

<script>
    import FadeTransition from './components/FadeTransition'

    export default {
        name: 'app',
        components: {
            FadeTransition
        },
        data () {
            return {
                show: true
            }
        },
        methods: {
            toggle () {
                this.show = !this.show
            }
        }
    }
</script>

这个示例实现了一个盒子过渡成一个圆的效果:

现在FadeTransition组件能像常规的<transition>一样接受事件监听和props,这样也让该组件更加可重用。既然如此,我们给组件添加一个durationprops

Vue的<transition>组件提供了一个duration属性,可以用来定制一个显性的过渡持续时间。在很多情况下,Vue可以自动得出过渡效果的完成时机。默认情况下,Vue会等待其在过渡效果的 根元素的第一个transitionendanimationend事件。然而也可以不这样设定,比如,我们可以拥有一个精心设计过的过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟或更长的过渡效果。在这个时候,就可以用上<transition>组件的duration属性

在我们的示例中,我们需要通过组件的props来控制CSS的animationtransition。实现起来并不太复杂,我们不在CSS中显式设置animation-durationtransition-duration样式,而是在Vue组件中,通过组件生命周期的钩子函数来实现。比如:

<!-- FadeTransition.vue -->
<template>
    <transition 
        name="fade" 
        v-bind="$attrs" 
        v-on="hooks"
        enter-active-class="fadeIn"
        leave-active-class="fadeOut">
        <slot />
    </transition>
</template>

<script>
    export default {
        name: 'FadeTransition',
        props: {
            duration: {
                type: Number,
                default: 300
            }
        },
        computed: {
            hooks() {
                return {
                    beforeEnter:this.setDuration,
                    afterEnter: this.cleanUpDuration,
                    beforeLeave: this.setDuration,
                    afterLeave: this.cleanUpDuration,
                    ...this.$listeners
                }
            }
        },
        methods: {
            setDuration(el) {
                el.style.animationDuration = `${this.duration}ms`
            },
            cleanUpDuration(el){
                el.style.animationDuration= ''
            }
        }
    }
</script>

<style scoped>
    @keyframes fadeIn {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }

    .fadeIn {
        animation-name: fadeIn;
    }

    @keyframes fadeOut {
        from {
            opacity: 1;
        }
        to {
            opacity: 0;
        }
    }
    .fadeOut {
        animation-name: fadeOut;
    }
</style>

<!-- App.vue -->

<template>
    <div id="app">
        <div class="toggle toggle--text">
            <input type="checkbox" id="toggle--text" class="toggle--checkbox">
            <label class="toggle--btn" for="toggle--text" data-label-on="Box"  data-label-off="Circle" @click="toggle"></label>
        </div>
        <div class="duration">
            <label for="duration">持续时间(Duration):</label>
            <input type="range" min="100" max="3000" v-model="duration" id="duration" />
            <span>{{duration}}ms</span>
        </div>
        <FadeTransition mode="out-in" :duration="durationNumber">
            <div key="box" v-if="show" class="box"></div>
            <div key="circle" v-else class="circle"></div>
        </FadeTransition>
    </div>
</template>

<script>
import FadeTransition from './components/FadeTransition'

export default {
    name: 'app',
    components: {
        FadeTransition
    },
    data () {
        return {
            show: true,
            duration: 300
        }
    },
    methods: {
        toggle () {
            this.show = !this.show
        }
    },
    computed: {
        durationNumber() {
            return parseInt(this.duration);
        }
    }
}
</script>

更详细的代码,可以把分支切换到demo4下。

列表过渡

Vue除了提供了<transition>组件之外还另外提供了一个<transition-group>组件。而这个组件也常常被称为列表过渡。该组件有几个特点:

  • 不同于<transition>,它会以一个真实元素呈现:默认是一个<span>,咱们可以通过tag特性更换为其他元素
  • 过渡模式不可用,因为我们不一在相互切换特有的元素
  • 内部元素总是需要提供唯五的key属性值

回到我们的话题中来,如果封装的FadeTransition组件面对列表这样的过渡效果呢?又应该如何呢?

可以大家会认为最简单的方式,就是重新构建一个Vue组件,比如FadeTransitionGroup,并将<transition>换成<transition-group>即可。这样做事实上并不会有问题,如果我们能做得更好,是不是应该选择更好的方式。比如说,同样维护前面创建的FadeTransition组件,而且该组件能让我们在<transition><transition-group>之间进行切换。

幸运的是,在Vue中,我们可以通过渲染(render)函数或借助componentis属性(动态组件)来实现这一点。接下来把分支切换到demo5

<!-- FadeTransition.vue -->
<template>
    <component 
        :is="type"
        :tag="tag"
        v-bind="$attrs" 
        v-on="hooks"
        enter-active-class="fadeIn"
        leave-active-class="fadeOut"
        move-class="fade-move">
        <slot />
    </component>
</template>

<script>
    export default {
        name: 'FadeTransition',
        props: {
            duration: {
                type: Number,
                default: 300
            },
            group : {
                type: Boolean,
                default: false
            },
            tag: {
                type: String,
                default: 'div'
            }
        },
        computed: {
            type() {
                return this.group ? 'transition-group' : 'transition'
            },
            hooks() {
                return {
                    beforeEnter:this.setDuration,
                    afterEnter: this.cleanUpDuration,
                    beforeLeave: this.setDuration,
                    afterLeave: this.cleanUpDuration,
                    leave: this.setAbsolutePosition,
                    ...this.$listeners
                }
            }
        },
        methods: {
            setDuration(el) {
                el.style.animationDuration = `${this.duration}ms`
            },
            cleanUpDuration(el){
                el.style.animationDuration= ''
            },
            setAbsolutePosition(el) {
                if (this.group) {
                    el.style.position = 'absolute'
                }
            }
        }
    }
</script>

<style scoped>
    @keyframes fadeIn {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }

    .fadeIn {
        animation-name: fadeIn;
    }

    @keyframes fadeOut {
        from {
            opacity: 1;
        }
        to {
            opacity: 0;
        }
    }
    .fadeOut {
        animation-name: fadeOut;
    }
    .fade-move {
        transition: transform .3s ease-out;
    }
</style>

<!-- App.vue -->
<template>
    <div id="app">
        <div class="toggle toggle--btn" @click="addItem">添加</div>
        <div class="duration">
            <label for="duration">持续时间(Duration):</label>
            <input type="range" min="100" max="3000" v-model="duration" id="duration" />
            <span>{{duration}}ms</span>
        </div>
        <div class="tips">提示:点击下面的盒子,对应盒子会立即删除</div>
        <div class="wrapper">
            <FadeTransition group :duration="durationNumber">
                <div class="box" v-for="(item, index) in list" @click="remove(index)" :key="item"></div>
            </FadeTransition>
        </div>
    </div>
</template>

<script>
    import FadeTransition from './components/FadeTransition'

    export default {
        name: 'app',
        components: {
            FadeTransition
        },
        data () {
            return {
                show: true,
                duration: 300,
                list: [1,2,3,4,5]
            }
        },
        methods: {
            toggle () {
                this.show = !this.show
            },
            remove(index) {
                this.list.splice(index, 1)
            },
            addItem() {
                let randomIndex = Math.floor(Math.random() * this.list.length)
                this.list.splice(randomIndex, 0, Math.random())
            }
        },
        computed: {
            durationNumber() {
                return parseInt(this.duration);
            }
        }
    }
</script>

效果如下:

这里有一个小细节需要注意,当元素删除时,我们必须为每个元素的position设置为absolute,以实现其他元素的平滑移动。另外手动添加了一个move类,指定transform持续的时间。

再做一些调整,并通过mixin中提取JavaScript逻辑,这样就可以将其应用于新的过渡组件。使用的时候,只需要将其放入到下一个项目中即可。

<!-- src/mixins/baseTransition.js -->
export default {
    inheritAttrs: false,
    props: {
        duration: {
            type: [Number, Object],
            default: 300
        },
        group: {
            type: Boolean,
            default: false
        },
        tag: {
            type: String,
            default: 'div'
        },
        origin: {
            type: String,
            default: ''
        },
        styles: {
            type: Object,
            default: () => {
                return {
                    animationFillMode: 'both',
                    animationTimingFunction: 'ease-out'
                }
            }
        }
    },
    computed: {
        componentType() {
            return this.group ? 'transition-group' : 'transition'
        },
        hooks() {
            return {
                beforeEnter: this.beforeEnter,
                afterEnter: this.cleanUpStyles,
                beforeLeave: this.beforeLeave,
                leave: this.leave,
                afterLeave: this.cleanUpStyles,
                ...this.$listeners
            }
        }
    },
    methods: {
        beforeEnter(el) {
            let enterDuration = this.duration.enter ?  this.duration.enter : this.duration
            el.style.animationDuration = `${enterDuration / 1000}s`
            this.setStyles(el)
        },
        cleanUpStyles(el) {
            Object.keys(this.styles).forEach(key => {
                const styleValue = this.styles[key]
                if (styleValue) {
                    el.style[key] = ''
                }
            })
            el.style.animationDuration = ''
        },
        beforeLeave(el) {
            let leaveDuration = this.duration.leave ? this.duration.leave : this.duration
            el.style.animationDuration = `${leaveDuration / 1000}s`
            this.setStyles(el)
        },
        leave(el) {
            this.setAbsolutePosition(el)
        },
        setStyles(el) {
            this.setTransformOrigin(el)
            Object.keys(this.styles).forEach(key => {
                const styleValue = this.styles[key]
                if (styleValue) {
                    el.style[key] = styleValue
                }
            })
        },
        setAbsolutePosition(el) {
            if (this.group) {
                el.style.position = 'absolute'
            }
            return this
        },
        setTransformOrigin(el) {
            if (this.origin) {
                el.style.transformOrigin = this.origin
            }
            return this
        }
    }
}

<!-- src/mixins/index.js -->
import baseTransition from './baseTransition'

export {
    baseTransition
}

<!-- FadeTransition.vue -->
<template>
    <component :is="componentType"
                :tag="tag"
                v-bind="$attrs"
                v-on="hooks"
                enter-active-class="fadeIn"
                move-class="fade-move"
                leave-active-class="fadeOut">
        <slot></slot>
    </component>
</template>

<script>
    import {baseTransition} from '../mixins/index.js'
    export default {
        name: 'FadeTransition',
        mixins: [baseTransition]
    }
</script>

<style>
    @keyframes fadeIn {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }
    .fadeIn {
        animation-name: fadeIn;
    }
    @keyframes fadeOut {
        from {
            opacity: 1;
        }
        to {
            opacity: 0;
        }
    }
    .fadeOut {
        animation-name: fadeOut;
    }
    .fade-move {
        transition: transform .3s ease-out;
    }
</style>

更详细的您可以fork一份@BinarCodevue2-transitions

另外按照这样的方式,可以把Animate.css库中动画效果都封装成对应的Vue组件。感兴趣的同学,不仿一试。

小结

文章中从一个基本的<transition>示例开始,最后通过可调用duration<transition-group>来创建可重用的<transition>组件。文章中的一些技巧只是一些最基本的技巧,你也可以根据自己的需要封装属于自己的过渡组件,如比@BinarCodevue2-transitions一样,甚至你还可以将Animate.css库按照@BinarCode的方式将所有动效都封装成独立的Vue组件,从而实现可以在多处调用的目标。如果你有更好的方式,欢迎在下面的评论中与我们一起分享。

参考阅读


Vue组件数据通讯新姿势:$attrs 和 $listeners

$
0
0

学习Vue也有一段时间了,在项目中使用Vue也有好几个了,但Vue组件间的状态管理(数据通信)一直是自己的死穴。对于Vue组件间的数据通信,无外呼是父组件向子组件、子组件向父组件、兄弟组件以及嵌套组件之间的数据通信。而且组件之间的通信方式也有很多种。@Gongph的《Vue 父子组件通信的十种方式》一文就详细的介绍了Vue组件,指的是父子组件之间的数据通信就有差不多十种方式。但很多时候我们组件之间的数据通信不仅仅是停留在父子组件之间的数据通信。比如说还有兄弟组件和嵌套组件之间的数据通信。

如果我们抛开嵌套组件之间的数据通信,我们可以用简单的下图来描述Vue组件之间的数据通信:

事实上除了上图方式对数据进行通信之外,还有一些其他的方式,比如父组件获取子组件数据和事件可以通过:

  • 通过给子组件绑定ref属性来获取子组件实例
  • 通过this.$children获取子组件实例

对于子组件获取父组件数据和事件,可以通过:

  • 通过props传递父组件数据和事件,或者通过$emit$on实现事件传递
  • 通过ref属性,调用子组件方法,传递数据;通过props传递父组件数据和事件,或者通过$emit$on实现事件传递
  • 通过this.$parent.$data或者this.$parevent._data获取父组件数据,通过this.$parent执行父组件方法

对于兄弟组件之间数据通信和事件传递,可以通过:

  • 利用eventBus挂载全局事件
  • 利用$parent进行数据传递,$parent.$children调用兄弟组件事件

另外,复杂一点的,可以通过Vuex完成Vue组件数据通信。特别是多级嵌套组件间的数据通信。但如果仅仅是数据之间传递,而不做中间处理,使用Vuex有点浪费。不过,自Vue 2.4版本开始提供了另一种方法:

使用v-bind="$attrs"将父组件中不被认为props特性绑定的属性传递给子组件。

通常该方法会配合interiAttrs一起使用。之所以这样使用是因为两者的出现使得组件之间跨组件的通信在不依赖Vuex和eventBus的情况下变得简洁,业务清晰。

其实这也就是我们今天要了解的另一个知识点。多级嵌套组件之间,我们如何借助$attrs$listeners来实现数据之间的通信。

业务场景

刚才提到过,我们接下来要聊的是多级嵌套组件之间的数据通信。为了让事情不变得太过于复杂(因为太复杂,对于初学者而言不易于理解和学习)。这里我们就拿三级组件之间的嵌套来举例。比如我们有三个组件ComponentAComponentBComponentC,而且它们之间的关系是ComponentA > ComponentB > ComponentC>是包含关系),用下图来描述或许更易于明白他们之间的关系:

就三级嵌套的组件而言,他们的关系相对而言要简单一些:

  • ComponentA组件是ComponentB组件的父组件,他们的关系是父子关系
  • ComponentB组件是ComponentC组件的父组件,他们的关系也是父子关系
  • ComponentA组件是ComponentC组件的祖先组件,他们的关系是祖孙关系

对于这三个组件之间的数据通信,按照我们前面所掌握的知识,估计想到的是:

props向下,$emit向上。

也就是说,ComponentAComponentB可以通过props的方式向子组件传递,ComponentBComponentA通过在ComponentB组件中$emit向上发送事件,然后在ComponentA组件中$on的方式监听发送过来的事件。对于ComponentBComponentC两组件之间的通信也可以使用类似的方式。但对于ComponentA组件到ComponentC组件之间的通信,需要借助ComponentB组件做为中转站,当ComponentA组件需要把信息传递给ComponentC组件时,ComponentB接受ComponentA组件的信息,然后利用属性传递给ComponentC组件。

就此而言,这是一种解决方案,但如果我们嵌套的组件层级过多时将会导致代码繁琐,代码维护也较困难。

除了上述方式可以完成组件之间数据通信外,还有其他的方式,比如借助Vuex的全局状态共享;使用eventBus创建Vue的实例实现事件的监听和发布,从而实现组件之间的数据通信。但都过于太浪费,所以我们应该寻找其他更为简易的解决方案,其中文章开始提到的$attrs以及$listeners

简单地说,利用$attrs实现祖孙组件间的数据传递,$listeners实现祖孙组件间的事件监听。接下来看看怎么使用这两个特性来完成跨级嵌套组件之间的数据通信。

术语解释

在具体掌握$attrs$listeners是如何完成组件数据通信之前,先来简单地了解一下他们具体是什么?

Vue的官网对$attrs$listeners的描述分别是这样的:

$attrs的解释

包含了父作用域中不作为 props被识别 (且获取) 的特性绑定 (classstyle除外)。当一个组件没有声明任何 props时,这里会包含所有父作用域的绑定 (classstyle除外),并且可以通过 v-bind="$attrs"传入内部组件 —— 在创建高级别的组件时非常有用。

$listeners的解释

包含了父作用域中的 (不含 .native修饰器的) v-on事件监听器。它可以通过 v-on="$listeners"传入内部组件 —— 在创建更高层次的组件时非常有用。

官方解释的已经非常的清楚了。事实上,你可以把$attrs$listeners比作两个集合,其中$attrs是一个属性集合,而$listeners是一个事件集合,两者都是以对象的形式来保存数据

更简单地说,利用$attrs实现祖孙组件间的数据传递,$listeners实现祖孙组件间的事件监听。而且$attrs继承所有的父组件属性(除props传递的属性、classstyle,一般用在子组件的子元素上;$listeners是一个对象,里面包含了作用在这个组件上的所有监听器,配合v-on将所有事件监听器指向这个组件的某个特定的子元素(相当于子组件继承父组件的事件)。

为了更易于帮助大家理解这两个属性,我们还是通过一些简单的示例来演示吧。先来看一个简单的示例:

<!-- ChildComponent.vue -->
<template>
    <div class="child-component">
        <h1>我是一个 {{ professional }}</h1>
    </div>
</template>

<script>
    export default {
        name: 'ChildComponent',
        props: {
            professional: {
                type: String,
                default: '码农'
            }
        },
        created () {
            console.log(this.$attrs, this.$listeners)

            // 调用父组件App.vue中的triggerTwo()方法
            this.$listeners.two()
        }
    }
</script>

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <ChildComponent 
        :professional = "professional"
        :name = "name"
        @one.native = "triggerOne"
        @two = "triggerTwo"
        />
    </div>
</template>

<script>
    import ChildComponent from './components/ChildComponent.vue'

    export default {
        name: 'app',
        data() {
            return {
                professional:  '屌丝码农',
                name:'大漠'
            }
        },
        components: {
            ChildComponent
        },
        methods: {
            triggerOne () {
                console.log('one')
            },
            triggerTwo () {
                console.log('two')
            }
        }
    }
</script>

示例代码可以在Github的Vue Demos中获取app-vue-communication项目的step1分支获取。

从上面的代码中我们可以看出来,在父组件App.vue中,调用子组件ChildComponent时有两个属性和两个方法,共别是其中有一个属性是props声明的(professional),事件一个是.native修饰器(监听组件根元素的原生事件)

这个简单的示例告诉我们可以通过$attrs$listeners进行数据传递,在需要的地方进行调用和处理。比如上面子组件ChildComponent中通过this.$listeners.two()访问了父组件App.vue中的triggerTwo()方法。当然,我们还可以通过v-on="$listeners"一级级地往下传递,不管组件嵌套层级有多深。这个后面我们会详细介绍。

另外,上面的示例中,其中有一个属性是props,比如professional属性,另外还有一个非props属性,比如name。组件编译之后会把非props属性当成原始属性对待,从而添加到DOM元素(HTML标签上),比如上例中的name

这样的结果或许并不是大家所想要的,如果想去掉HTML标签中name的属性,以至于该属性不暴露出来,我们可以借助inheritAttrs属性来完成。

inheritAttrs的默认值true,继承所有的父组件属性(除props的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置inheritAttrs: false,但是class属性会继承。简单的说,** inheritAttrs:true继承除props之外的所有属性;inheritAttrs:false只继承class属性**。

如果我们在子组件ChildComponent中添加inheritAttrs: false,重新编译出来的代码中name(非props)属性再不会暴露出来:

多级嵌套组件数据通信

前面花了很长的篇幅解释了$attrs$listeners以及它们是如何在组件中进行数据通信的。回到我们的示例中来,看看文章开头提以的三级嵌套组件之间的数据是如何借助$attrs$listeners实现数据通信。具体代码可以将分支切换到step2中:

<!-- ComponentC.vue -->
<template>
    <div class="component-c">
        <h3>组件C中设置的props: {{ name }}</h3>
        <p>组件C中的$attrs: {{ $attrs }}</p>
        <p>组件C中的$listeners: {{ $listeners }}</p>
    </div>
</template>

<script>
    export default {
        name: 'ComponentC',
        props: {
            name: {
                type: String,
                default: '大漠'
            }
        },
        inheritAttrs: false,
        mounted () {
            this.$emit('test2')
            console.log('ComponentC',this.$attrs, this.$listeners)
        }
    }
</script>

<!-- ComponentB.vue -->
<template>
    <div class="component-b">
        <h3>组件B中的props: {{ age }}</h3>
        <p>组件B中的$attrs: {{ $attrs }}</p>
        <p>组件B中的$listeners: {{ $listeners }}</p>

        <hr />
        <ComponentC v-bind="$attrs" v-on="$listeners" />
    </div>
</template>

<script>
    import ComponentC from './ComponentC'

    export default {
        name: 'ComponentB',
        props: {
            age: {
                type: Number,
                default: 30
            }
        },
        inheritAttrs: false,
        components: {
            ComponentC
        },
        mounted () {
            this.$emit('test1')
            console.log('ComponentB',this.$attrs, this.$listeners)
        }
    }
</script>

<!-- ComponentA.vue -->
<template>
    <div class="component-a">
        <ComponentB :name="name" :age="age"  @on-test1="onTest1" @on-test2="onTest2" />
    </div>
</template>

<script>
    import ComponentB from './ComponentB'

    export default {
        name: 'ComponentA',
        components: {
            ComponentB
        },
        data () {
            return {
                name: '大漠_w3cplus',
                age: 23
            }
        },
        methods: {
            onTest1 () {
                console.log('test1 runing...')
            },
            onTest2 () {
                console.log('test2 running...')
            }
        }
    }
</script>

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <ComponentA />
    </div>
</template>

<script>
    import ComponentA from './components/ComponentA.vue'

    export default {
        name: 'app',
        components: {
            ComponentA
        }
    }
</script>

这个时候你在页面中将看到的结果如下:

其于上面的基础上,我们来看一个简单的示例(切到分支step3),一个模态框的数据通信:

<!-- ModalHeader.vue -->
<template>
    <div class="modal-header">
        <h5 class="modal-title">{{ modalTitle }}</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="close">
        <span aria-hidden="true">&times;</span>
        </button>
    </div>
</template>

<script>
export default {
    name: 'ModalHeader',
    props: {
        modalTitle: {
            type: String,
            default: 'Modal Title'
        }
    },
    inheritAttrs: false,
    methods: {
        close () {
            this.$emit('on-close')
        }
    },
    mounted () {
        console.log('ModalHeader',this.$attrs, this.$listeners)
    }
}
</script>

<!-- ModalBody.vue -->
<template>
    <div class="modal-body">
        <slot>{{ modalContent }}</slot>
    </div>
</template>

<script>
export default {
    name: 'ModalBody',
    props: {
        modalContent: {
            type: String,
            default: 'Modal body text goes here.'
        }
    },
    inheritAttrs: false,
    mounted () {
        console.log('ModalBody',this.$attrs, this.$listeners)
    }
}
</script>

<!-- ModalFooter.vue -->
<template>
    <div class="modal-footer">
        <button class="btn btn-secondary" data-dismiss="modal" @click="close">{{ secondaryButtonContent }}</button>
        <button class="btn btn-primary" @click="save">{{ primaryButtonContent }}</button>
    </div>
</template>

<script>
export default {
    name: 'ModalFooter',
    props: {
        secondaryButtonContent: {
            type: String,
            default: 'Close'
        },
        primaryButtonContent: {
            type: String,
            default: 'Save'
        }
    },
    inheritAttrs: false,
    methods: {
        save () {
            this.$emit('on-save')
        },
        close () {
            this.$emit('on-close')
        }
    },
    mounted () {
        console.log('ModalFooter',this.$attrs, this.$listeners)
    }
}
</script>

<!-- Modal.vue -->
<template>
    <div class="modal" tabindex="-1" role="dialog" v-if="show">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <ModalHeader v-bind="$attrs" v-on="$listeners" />
                <ModalBody v-bind="$attrs" v-on="$listeners" />
                <ModalFooter v-bind="$attrs" v-on="$listeners" />
            </div>
        </div>
    </div>
</template>

<script>
import ModalHeader from './ModalHeader'
import ModalBody from './ModalBody'
import ModalFooter from './ModalFooter'

export default {
    name: 'Modal',
    props: {
        show: {
            type: Boolean,
            default: false
        }
    },
    components: {
        ModalHeader,
        ModalBody,
        ModalFooter
    },
    inheritAttrs: false,
}
</script>

<!--  MaskBackdrop.vue -->
<template>
    <div class="modal-backdrop" v-if="show" @click="close">
    </div>
</template>

<script>

export default {
    name: 'MaskBackdrop',
    props: {
        show: {
            type: Boolean,
            default: false
        }
    },
    inheritAttrs: false,
    mounted () {
        console.log('MaskBackdrop',this.$attrs, this.$listeners)
    },
    methods: {
        close () {
            this.$emit('on-close')
        }
    }
}
</script>

你将看到的效果如下:

在浏览器调试器中,我们可以看以相应$attrs$listeners打印出来的值:

小结

啰嗦了这么多,主要就是阐述了Vue 2.4版本之后的$attrs$listeners是什么以及怎么利用他们来实现组件之间的数据通信。使用这两个特性可以实现跨组件(嵌套)组件之间的数据通信。最后希望这篇文章对大家或多或少有所收获。结合前面的教程,我们可以了解到组件之间数据通信有很多种方式,具体哪种更好应该根据不同的场景来对待,选择最适合的。如果您在这方面有更多的经验或者文章中有不正之处,烦请路过的大神多多拍正。

Vue新指令:v-slot

$
0
0

slot是Vue组件的一个重要机制,因为它使得完全解耦的组件之间可以灵活地被组合。在《Vue组件内容分发》和《Vue的作用域插槽》文章中我们深入的学习了slot怎么在Vue中的使用,但在Vue 3.0版本为slot引入了一套全新的模版语法。为了更好的从2.x过渡到3.0,Vue的v2.6版本引入了新的slot语法,即 v-slot。接下来我们来学习新指令v-slot的使用。

v-slot指令简介

v2.6中,我们为具名插槽和作用域插槽引入了一个新的统一语法,即 v-slot。它取代了slotslot-scope这两个目前已被废弃但未被移除且仍在文档中的特性。这是一个较重大的改变,主要包含了:

  • v-slot指令结合了slotslot-scope的功能
  • scoped slots的简写

有关于v-slot指令形成的讨论过程可以阅读RFC-0001RFC-0002中的描述。

回顾slot的使用

从官网上我们可以获知,Vue中的slot主要分为:单个插槽具名插槽作用域插槽三种。而其使用也较为简单,比如我们有一个SlotDemo组件,可以在该组件中通过<slot>元素作为承载分发内容的出口:

<!-- SlotDemo.vue -->
<template>
    <div class="slot">
        <slot />
    </div>
</template>

<script>
    export default {
        name: 'SlotDemo',
    }
</script>

然后它允许你像下面这样使用SlotDemo组件,在使用该组件时,可以通过<slot>承载你想要的任何内容:

<!-- App.vue -->
<SlotDemo>
    <div class="box">
        <h1>Slot Demo</h1>
        <p>This is slot demo!</p>
    </div>
</SlotDemo>

渲染出来的内容可以看出,div.box中的内容替代了<slot>(也可以理解为该内容插入到了slot)中:

使用slot的时候,我们还可以为其设置一个默认的内容,也就是说没有提供内容的时候被渲染。比如下面这个SubmitButton组件中,默认情况下该按钮显示的文本内容是"提交",这个时候可以在<slot>标签内设置其内容为“提交”:

<!-- SubmitButton.vue -->
<template>
    <button>
        <slot>提交</slot>
    </button>
</template>

<script>
    export default {
        name: 'SubmitButton',
    }
</script>

当我们引用SubmitButton并不给slot插入任何内容时:

<!-- App.vue -->
<SubmitButton />

这个时候渲染出来的内容就是slot标签中的默认内容:

<button data-v-24449ecc="">提交</button>

如果我们想给按钮换成别的内容时,我们可以像下面这样使用:

<SubmitButton>保存</SubmitButton>

这个插入到slot中的内容“保存”会替代slot中默认的内容“提交”:

<button data-v-24449ecc="">保存</button>

最图如下图所示:

除了单个插槽之外,还可以使用 具名插槽,具名插槽的简单用法就是给slot显式的设置一个name值,比如下面这个BaseLayout组件:

<!-- BaseLayout.vue -->
<template>
    <div class="container">
        <header>
            <slot name="header">我们希望把页头放在这里~</slot>
        </header>
        <main>
            <slot>我们希望把主要内容放在这里</slot>
        </main>
        <footer>
            <slot name="footer">我们希望把页脚放在这里</slot>
        </footer>
    </div>
</template>

<script>
    export default {
        name: 'BaseLayout',
    }
</script>

调用具名插槽时,在对应的标签上使用slot="name",比如下面这个简单的用例:

<!-- App.vue -->
<BaseLayout>
    <h1 slot="header">我是一个页头</h1>
    <p slot="footer">&copy;w3cplus</p>
</BaseLayout>

有关于Vue 2.6 版本之前 slot更详细的使用可以阅读早前的学习笔记 《Vue组件内容分发》。

slot还有另一个特性,那就是作用域插槽,用一个简单的示例来描述作用插槽的使用。先创建一个ColorList组件:

<!-- ColorList.vue -->
<template>
    <div class="color-list">
        <h2>{{ title }}</h2>
        <div class="list">
            <div class="list-item" v-for="(item, index) in items" :key="index">
                <slot v-bind="item"></slot>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'ColorList',
        props: {
            title: {
                type: String,
                default: 'Colors'
            },
            items: {
                type: Array
            }
        }
    }
</script>

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">

        <ColorList :items="colors" title="Colors">
            <template scope="color">
                <div :style="{background: color.hex}">{{ color.name }}</div>
            </template>
        </ColorList>
    </div>
</template>

<script>
    import ColorList from './components/ColorList'

    export default {
        name: 'app',
        components: {
            ColorList
        },
        data () {
            return {
                colors: [
                    { name: 'Yellow', hex: '#F4D03F', },
                    { name: 'Green', hex: '#229954' },
                    { name: 'Purple', hex: '#9B59B6' }
                ]
            }
        }
    }
</script>

有关于 Vue 2.6 之前作用域插槽更详细的介绍可以阅读早前整理的《Vue的作用域插槽》一文。

上面示例涉及到的Demo代码,可以从[app-v-slot](//github.com/vuedemos/app-v-slot)中的step1分支中获取。

v-slot的使用

前面再次回顾了Vue 2.6版本之前的slot的使用,知道Vue中的slot主要有单个插槽具名插槽作用域插槽三种。在Vue 2.6版本起对slot有所更新,已经废弃了 slotslot-scope特性。从字面面上的理解就是Vue之后slotslot-scope语法有所更改。自己在V3.0.1版本中亲测下面的使用(step1也是基于该版本测试的),但接下来,主要看v-slot的使用。

同样先来看单个插槽,即没有在<slot>标签中显式的设置name。比如SlotDemo这个组件:

<!-- SlotDemo.vue -->
<template>
    <div class="slot-demo">
        <slot>我是一个slot</slot>
    </div>
</template>

正如前面所述,slot标签中并没有显式设置name值,而且在slot标签中提供了一个默认的内容。我们在使用该组件时,可以通过v-slot:default来调用未显式设置nameslot

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <SlotDemo />

        <SlotDemo v-slot:default>
            <p>v-slot:default 的使用</p>
        </SlotDemo>
    </div>
</template>

但一般建议v-slot:default添加到<template>上:

<!-- App.vue -->
<SlotDemo>
    <template v-slot:default>
        <h3>v-slot:default</h3>
    </template>
</SlotDemo>

同样能正常的渲染出我们预期的结果。

一个不带name<slot>出口会带有隐含的名字default

另外这里有一个特殊之处,当被提供的内容只有默认的插槽时,组件的标签上才可以被当作插槽的模板来使用,也就是说v-slot可以直接使用在组件上。正如上面的示例所示:

<SlotDemo v-slot:default>
    <p>v-slot:default 的使用</p>
</SlotDemo>

接着来看一个具名插槽的示例,同样拿BaseLayout组件来举例:

<!-- BaseLayout.vue -->
<template>
    <div class="container">
        <header>
            <slot name="header">Header Content</slot>
        </header>
        <main>
            <slot>Main Content</slot>
        </main>
        <footer>
            <slot name="footer">Footer Content</slot>
        </footer>
    </div>
</template>

<script>
    export default {
        name: 'BaseLayout'
    }
</script>

v2.6之后使用也是采用v-slot

<!-- V2.6版本之前 具名插槽的使用 -->
<BaseLayout>
    <h1 slot="header">Vue 2.6之前具名插槽</h1>
    <p>我是页面的主内容</p>
    <p slot="footer">&copy;w3cplus</p>
</BaseLayout>

<!-- V2.6之后 具名插槽的使用 -->
<BaseLayout>
    <template v-slot:header>
        <h1>Vue 2.6之后具名插槽</h1>
    </template>
    <template v-slot:default>
        <p>我是页面的主内容</p>
    </template>
    <template v-slot:footer>
        <p>&copy;w3cplus</p>
    </template>
</BaseLayout>

<template>元素中的所有内容都会被传入相应的插槽中。另外,没有任何被包裹在带有v-slot<template>中的内容都会被视为默认插槽的内容。然而,为了使用更具规范或明确一些,仍然可以在一个<template>中包裹默认插槽的内容,同时使用v-slot:default来表示。

v-slotv-onv-bind类似,也可以缩写,即 把参数之前的所有内容(v-slot:)替换为字符 #。例如,上面的示例我们可以改写成:

<BaseLayout>
    <template #header>
        <h1>Vue 2.6之后具名插槽</h1>
    </template>
    <template #default>
        <p>我是页面的主内容</p>
    </template>
    <template #footer>
        <p>&copy;w3cplus</p>
    </template>
</BaseLayout>

接下来,我们再一起来看看作用域插槽中怎么来使用v-slot。同样拿一个List组件来举例,并且它暴露了一个过滤后的列表数据作为它的作用域插槽。

<!-- List.vue -->
<template>
    <div class="list">
        <div class="item" v-for="(item, index) in items" :key="index">
            <slot v-bind="item" />
        </div>
    </div>
</template>

<script>
    export default {
        name: 'List',
        props: {
            items: {
                type: Array,
            }
        }
    }
</script>

我们可以这样来调用List组件:

<!-- App.vue -->

<!-- V2.6版本前 -->
<List :items="colors">
    <template slot-scope="color" slot="default">
        <div :style="{backgroundColor: color.hex}"> {{ color.name }}</div>
    </template>
</List>

<!-- V2.6版本后 -->
<List :items="colors" v-slot="color">
    <div :style="{backgroundColor: color.hex}"> {{ color.name }}</div>
</List>

<script>
    import List from './components/List'

    export default {
        name: 'app',
        components: {
            List
        },
        data () {
            return {
            colors: [
                { name: 'Yellow', hex: '#F4D03F'},
                { name: 'Green', hex: '#229954' },
                { name: 'Purple', hex: '#9B59B6' }
            ]
            }
        }
    }
</script>

从示例上的代码可以看出,使用v-slot,避免了额外的嵌套。另外v-slot指令也提供了一个绑定 slotscope-slot指令的方式,但需要使用一个 :来分割它们。

有关于v-slot示例的相关代码,可以把分支切换到 step2

小结

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot指令)。它取代了 slotslot-scope这两个目前已被废弃但未被移除且仍在文档中的特性。这篇文章主要介绍了Vue中slot新老语法两个版本的使用。希望对大家有所帮助。有关于更详细的介绍可以阅读 Vue官网提供的教程

扩展阅读

Vue 实例

$
0
0

在刚接触Vue的时候,就知道 实例在Vue中是一个重要的概念,在学习之后也整理了一篇有关于Vue实例和生命周期的学习笔记。经过一段时间的学习之后,重新再温习了一遍有关于Vue的实例,整理一下,提供给有需要的同学作为参考资料。

Vue 的基本原理

Vue是一个优势的JavaScript框架,其主要是用来处理 视图层(我的理解就是UI层)。当用户操作 View Model(即JavaScript,在Vue中指的是Vue实例,一个观察者)使其依照一定的逻辑取得需要改变的数据(Model),配合在HTML中Vue提供的模板语法来改变配置,重新渲染后使画面产生相应的变化(View)。用一张图来描述这个过程:

比如我们在浏览器中渲染出来的页面有个区域和“删除”按钮,当用户点击“删除”按钮之后,会移除这个区域。用户交互之后反馈给用户的是,界面(视图)中的区域被移除。看上去非常的简单,其实在Vue中,主要经历了以下几个过程:

  • (1) 用户在界面中点击“Remove”按钮进行交互
  • (2) 通过模板中的绑定触发Vue实例中注册的remove()事件
  • (3) remove()事件通过Ajax向服务器请求数据
  • (4) 服务器取得的数据后重新传给Vue实例
  • (5) Vue实例修改绑定的ViewModel
  • (6) ViewModel改变后触模板重新渲染
  • (7) 改变后的视图反馈给用户(用户看到重新渲染后的视图)

咱们可以用下图来描述这个交互的整个过程:

事实上Vue的原理要比这复杂的多,只不过用上图来阐述对于初学者而言在概念上有一定的辅助作用。但其也遵循一个原则:视图的状态由数据描述,并且通过数据来驱动视图变化

深入的概念还无法理解的情况之下,我们暂时只需要先记住,在Vue中视图和数据之间的关系是一个非常简单的关系,即双向数据绑定

Vue采用发布者 —— 订阅者模式实现双向数据绑定,首先Vue将会获得需要监听的对象的所有属性,通过Object.defineProperty方法完成对象属性的劫持,将其转化为gettersetter,当属性被访问或修改时,立即将变化能知给订阅者,并由订阅者完成相应的逻辑操作。

我们可以用下图来描述双向数据绑定主要流程:

  • Observer:主要处理属性监听逻辑,将监听属性转化为getset属性,当属性被访问时,调用dep.depend()方法,而属性被修改时,则调用了dep.notify()方法
  • Dep:担任发布者的角色,维护访问者列表,负责订阅者的添加和通知工作,上面所提到的depend()notify()方法在这里实现
  • Watcher:担任订阅者角色,即Dep.target,可以订阅多个Dep,在每次收到发布者消息通知时触发update()方法执行更新逻辑

创建Vue实例

接下来,我们来创建一个Vue实例,就是前面提到的ViewModel部分。Vue实例是Vue中很重要的一部分,创建他很简单:

let app = new Vue({

})

创建Vue实例很简单,通过new Vue({})即可。事实上,Vue实例就是一个JavaScript对象,而且常常将这个对象赋值给一个app变量。

Vue的第一个参数是options,它会传给Vue实例这个对象。options这个参数主要包括:

  • el:Vue实例需要挂载的一个DOM元素,一般是index.html文件中的div#app元素
  • data:需要一个输出取得结果的数据
  • methods:方法
  • Vue实例生命周期的钩子函数

比如下面这个示例:

let app = new Vue({
    el: '#app',
    data () {
        return {
            message: 'W3cplus.com'
        }
    },
    methods: {
        getRemoteMessage () {
            Promise.resolve('获取远程数据:大漠')
                .then((res) => {
                    this.message = res
                })
        }
    }
})

除了上面这种方式,Vue实例的挂载还有另外一种方式:

// main.js
new Vue({
    render: h => h(App),
}).$mount('#app')

而给Vue实例传递参数,可以像下面这样的方式:

<!-- App.vue -->
<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'W3cplus.com'
            }
        },
        methods: {
            getRemoteMessage () {
                Promise.resolve('获取远程数据:大漠')
                    .then((res) => {
                        this.message = res
                    })
            }
        }
    }
</script>

上面的示例主要做了三件事情:

  • 将Vue实例挂载在页面中idapp的一个div元素上
  • data中初始化了一个message,并且在<template>中以{{message}}方式将data中的message显示在页面上
  • 定义了一个getRemoteMessage()方法,该方法会以非同步的方式取得message
  • <template>中的一个button绑定了定义好的getRemoteMessage()方法
  • 当用户点击界面中的按钮,将会获取远程的一个数据,并且更改<template>message的值

效果如下:

创建一个Vue实例就是这么的简单。

最开始我们也提到了,Vue实例是Vue中重要的部分之一,甚至可以说,任何一个Vue的应用程序都是从Vue实例开始的,大创建实例后,可以通过操作实例中的参数来改变页面的配置

以上示例代码可以查阅app-vue-instance项目的step1分支。

Vue实例的生命周期

Vue实例中还有另一个重要的内容,那就是Vue实例的生命周期,在Vue实例中会在各个生命周期中提供相应的钩子函数(Hooks事件),这些钩子函数让开发者可以在Vue实例阶段做相应的处理。Vue官方为Vue实例的生命周期提供了一张非常详细的图:

上图展示了Vue实例整个生命周期,其中红框白底就是生命周期中具备的钩子函数:

  • beforeCreate:Vue实例初始化的时候会立即调用,其发生在未创立Vue实例之前,这个时候在Vue实例中的设置都还未配置完成,比如data
  • created:Vue实例创建完成,此时Vue实例中的配置除了$el外全部配置,而$el只有在Vue实例挂载到相应的HTML元素时才会生效
  • beforeMount:在Vue实例挂载到目标元素之前被调用,这时的$el会是还未被Vue实例中的定义渲染的初始设定的模板
  • mounted:Vue实例上的设置已经安装上模板,这里的$el是已经由实例中的定义渲染成真成的页面
  • beforeUpdate:当Vue实例中的data发生变化后或是执行vm.$forceUpdate()调用,这时的页面还未被重新沉浸而改变页面
  • updated:在重新渲染页面后被调用,这时的页面已经被重新渲染成改变后的页面
  • beforeDestroy:Vue实例被销毁前调用,这个时候Vue实例还是拥有完整的功能
  • destroyed:Vue实例被销毁,这个时候Vue实例中的任何定义都已被解除绑定,此时对Vue实例做的任何操作都会失效

接下来看一个小示例,把分支切换到step2,来看看Vue实例中操作各个钩子函数,然后打印出data$el,看看在各个钩子函数中它们是如何变化的。

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <h2>{{ message }}</h2>
        <div>
            <button @click="getRemoteMessage">获取远程数据</button>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'W3cplus.com'
            }
        },
        methods: {
            getRemoteMessage () {
                Promise.resolve('获取远程数据:大漠')
                    .then((res) => {
                        this.message = res
                    })
            }
        }
    }
</script>

根据上图,将Vue实例生命周期中的钩子函数分成四组:

  • beforeCreatecreated:创建Vue实例
  • beforeMountmounted:挂载目标元素
  • beforeUpdateupdated:改变后重新渲染
  • beforeDestroydestroyed:销毁Vue实例

beforeCreatecreated

先来看beforeCreatecreated两个钩子函数,在Vue实例中添加这两个钩子函数,打印出相应的data$el

beforeCreate () {
    console.log(`>>> ==== 钩子函数 beforeCreate ==== >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(`>>> ==== End ==== >>>`)
},
created () {
    console.log(`>>> ==== 钩子函数 created ==== >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(`>>> ==== End ==== >>>`)
}

结果如下:

由于beforeCreate阶段,Vue实例还没有创建,所以message$el都是undefined;而到了created阶段时,Vue实例已经创建了,所以message变成了W3cplus.com,但$el因为还没有挂载到目标元素,所以依旧是unddefined

也就是说,beforeCreate钩子中是不能对Vue实例中的任何东西做操作

beforeMountmounted

和前面的方式一样,在Vue实例中添加钩子函数beforeMountmounted

beforeMount () {
    console.log(`>>> |_==_| 钩子函数 beforeMount |_==_| >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(`>> |_==_| this.$el.outerHTML 开始 |_==_| >>`)
    console.log(this.$el.outerHTML)
    console.log(`>> |_==_| this.$el.outerHTML 结束 |_==_|`)
    console.log(`>>> |_==_| End |_==_| >>>`)
},
mounted () {
    console.log(`>>> |_==_| 钩子函数 mounted |_==_| >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(`>> |_==_| this.$el.outerHTML 开始 |_==_| >>`)
    console.log(this.$el.outerHTML)
    console.log(`>> |_==_| this.$el.outerHTML 结束 |_==_| >>`)
    console.log(`>>> |_==_| End |_==_| >>>`)
}

结果如下:

在Vue实例的流程图中提到,开始执行beforeMount钩子函数时,此时$el还没有生成HTML到页面上,因此在执行this.$el.outerHTML时会报警,如上图所示。而在执行mounted钩子函数时,Vue实例已经绑定到元素上,所以这里看到的是渲染后的结果。

也就是说,beforeMount前同样不能操作DOM元素

beforeUpdateupdated

同样的,把beforeUpdateupdated钩子函数加入到Vue实例中:

beforeUpdate () {
    console.log(`>>> (^_^) 钩子函数 beforeUpdate (^_^) >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(this.$el.outerHTML)
    console.log(`>>> (^_^) End (^_^) >>>`)
},
updated () {
    console.log(`>>> (^_^) 钩子函数 updated (^_^) >>>`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el: ${this.$el}`)
    console.log(this.$el.outerHTML)
    console.log(`>>> (^_^) End (^_^) >>>`)
}

当你点击页面上的按钮时,打印出来的结果如下:

beforeDestroydestroyed

beforeDestroy () {
    console.log(`(^_^) 钩子函数 beforeDestroy (^_^)`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el:${this.$el}`)
    console.log(this.$el.outerHTML)
    console.log(`(^_^) End (^_^)`)
},
destroyed () {
    console.log(`(^_^) 钩子函数 destroyed (^_^)`)
    console.log(`this.message: ${this.message}`)
    console.log(`this.$el:${this.$el}`)
    console.log(this.$el.outerHTML)
    console.log(`(^_^) End (^_^)`)
}

第一次执行app.__vue__.$destroy()会打印出相应的信息,再执行一次将会报错,如下图所示:

正如上面所示,执行beforeDestroy后,即将执行销毁Vue实例,如果想要释放一些资源,可以在这里操作;当执行destroyed时,Vue实例将会销毁。比如上面的示例,执行app.__vue__.$destroy()后,再点击页面上的按钮,不会有任何的反应,这是因为我们的Vue实例已销毁。

小结

本文从Vue的基本原理着手,学习了Vue的一些基本原理以及相关的概念,然后学习Vue实例的创建以及Vue实例生命周期中的钩子函数,并通过简单的实例,演示了Vue实例中不同钩子函数时实例的状态。这样对于学习Vue实例以及生命周期就不仅仅是停在概念上的,而是知道每个不同周期中对应钩子函数中具体的行为。这样一来,在实际使用Vue的时候,更能清楚的什么事情该在什么样的钩子函数中执行,才能起到相应的效果。特别是对于DOM的操作,掌握这些尤其重要。

CSS Grid和自定义属性带来的变化

$
0
0

好久没有整理有关于CSS方面的文章了,说实在心理还是痒痒的,但取舍有度。不过最近看了几篇有关于CSS的文章还是蛮有意思的。两篇是关于页面布局的,另外一篇是关于动画函数的。事实上,布局动画在CSS中都是较为重要的部分。当然,今天要提的知识点并不是什么非常新的知识点,但也是有创意和创新的知识点。比如不通过媒体查询实现响应式布局,比如说容器单位构建强大的布局,比如说动画函数(缓动函数)的反转。听听这些是不是觉得非常有意思,如果你和我也一样,请继续往下阅读。

在继续下面的内容之前,先说一下这几个概念的出处:

不采用媒体查询实现响应式布局

首先要声明一点,在这里不会具体介绍什么是媒体查询,也不会说什么是响应式设计。虽然如此,但大家或许和我类似,在心里有一个概念,响应式设计会在不同的断点有不同的响应(即不同的布局效果),而这个不同的断点就是依赖于媒体查询来控制的。随着Web布局的发展,实现响应式布局我们从此可以抛开媒体查询来实现。正如@Juan Martín García的《Look Ma, No Media Queries! Responsive Layouts Using CSS Grid》文章中提到的相关技术。

中国第五届CSS大会(即将就要到来),知名CSS专家、Nexmo开发大使 @陈慧晶 老师的主题《新时代CSS布局》或许会给我们带来一些更新的布局技巧和姿势。就我自己而言,或许能猜到该主题要介绍的内容,但我还是非常期待。因为我一直有关注@陈慧晶 老师的文章和在国外分享的相关话题。

或许很多人会有疑问,不使用媒体查询怎么来实现响应式布局。针对该问题,我以前也同样没有思考过,自从最近阅读了@Juan Martin Garcia的文章,才恍然大悟,原来还可以这么玩,同时也再次验证了我的想法,未来的布局是CSS Grid的天下。

在小站上有关于CSS的Grid布局的教程也有不少,有关于相关的概念在相关的文章中也有相应的介绍。如果下文中提到的相关东西要是从未理解过,建议花点时间阅读一下有关于CSS Grid布局相关的教程

正因为CSS Grid布局有非常优秀的特性,比如fr单位repeat()函数minmax()函数auto-fitgrid-auto-flow。所以在没有媒体查询前提下,也同样可以很轻易的实现响应式设计。甚至说可以比依赖媒体查询实现响应式设计更为轻巧。

通过示例来向大家演示,怎么借助CSS Grid布局来实现响应式设计。

你可以打开上面的示例,尝试着改变浏览器视窗的大小,你可以看到相应的变化:

在整个页面中主要分为两个部分,一个是全屏banner区域,另一个是卡片列表页,随着浏览器视窗大小变化,布局也会相应的变化。就如上图所示。

往往实现这样的效果都是会借助于媒体查询来实处理,但上面这个示例,如果你查阅了代码,你会发现我们并没有使用媒体查询,而是使用CSS Grid布局中的一些特性来处理的,关键代码如下:

/* 全屏banner区域*/
.hero {
    ...
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    align-items: center;
}

/* 卡片列表 */
.breweries > ul {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    grid-gap: 1rem;
}

从上面的代码中不难发现,这里最为关键的是在grid-template-columns中通过repeat()函数为网格列布局做为重复填充,其中之一是按auto-fill(自动填充)和minmax()来做相应的处理。有关于这几个概念,这里简单的进行一下陈述:

  • repeat():表示网格列表的重复,可以让我们以更紧凑的形式来处理网格列或行的布局模式。它也接受两个值:重复的次娄和重复的值
  • auto-fill:给repeat()函数使用这个关键词,来替代重复次数。这可以根据每列的宽度灵活的改变网格的列数
  • minmax():能够让我们用最简单的CSS控制网格轨道的大小;minmax()函数接受两个参数,一个最小值和一个最大值
  • fr:代表网格容器中可用空间的一等份

是不是非常的简单,只要你了解了CSS Grid布局中相关的概念,可以让我们很轻易的实现响应式的布局。也就是说,我们今后可以在不依赖于媒体查询的条件之下也可以实现响应式布局,而且这种方式要更为简单和轻巧。

有关于这方面更详细的介绍,可以阅读:

采用容器单元构建强大的布局

容器单元这个概念我也是第一次听说,不得不佩服国外同行的创新和想法。简单地说一下,容器单元是什么?

容器单元是一组CSS自定义属性的集合,允许我们使用列和间距构建的网格系统,通过构建出来的网格系统来实现页面的布局或组件的布局。

简单的说,借助以前网格系统(此网格非彼网格)。在我们最早接触的网格系统,比如说960gs网格,我们将一个容器等分成1224列,列与列之间有一个间距,比如下图这样:

仅通过列宽,列间距和列数三个变量提供一组用于度量和计算的体系。

回过头来思考一下,我们不难发现,大多数网格布局都会依赖于它们们的父容器。而这里提到的容器单元可以帮助我们如何使用CSS自定义属性来克服这样的一个限制,以及如何使用容器单元来构建布局,而且是一个健壮的布局。

如果你从未接触过CSS的自定义属性相关的知识,建议你花一点时间阅读有关于这方面的相关知识。

接下来的内容,所设你对CSS自定义属性有所了解了。那么回到我们最为关心的问题:

  • 如何创建一个容器单元
  • 如何通过容器单元构建布局

了解960gs网格系统的同学都应该知道,构建一个12列还是24列的网格系统,他们都有三个关键元素,即容器宽度网格列宽列与列间距,如果我们用自定义属性来表达的话,可以用下面的方式来描述:

:root {
    --grid-width: 960;
    --grid-column-width: 60;
    --grid-columns: 12;
}

这三个值定义了列的宽度网格比例(就上面的代码所示,网格的比例是60/960)。而列间距是从剩余的空间中自动计算出来。

如果我们给容器宽度设置一个值,比如:

:root {
    --container-width: 84vw;
}

如果借助媒体查询,我们可以在不同的断点下设置不同的容器宽度,比如:

@media (min-width: 800px) {
    --container-width: 90vw;
}

@media (min-width: 1200px) {
    --container-width: 85vw;
}

@media (min-width: 1400px) {
    --container-width: 1200px;
}

也就是说,我们可以将上面三个核心的概念分成三个不同的单位,也就是容器单元该具备的单元:

  • --column-unit
  • --gutter-unit
  • --column-and-gutterr-unit

结合起来就下面这样,看起来有点复杂,只要你懂CSS自定义属性,就不会觉得复杂:

:root {
    /* 网格属性 */
    --grid-width: 960;
    --grid-column-width: 60;
    --grid-columns: 12;

    /* 网格逻辑(列间距数量) */
    --grid-gutters: calc(var(--grid-columns) - 1);

    /* 网格比例逻辑 列宽 / 网格宽度 */
    --column-proportion: calc(var(--grid-column-width) / var(--grid-width));
    --gutter-proportion: calc((1 - (var(--grid-columns) * var(--column-proportion))) / var(--grid-gutters));

    /* 容器单元 */
    --column-unit: calc(var(--column-proportion) * var(--container-width));
    --gutter-unit: calc(var(--gutter-proportion) * var(--container-width));
    --column-and-gutter-unit: calc(var(--column-unit) + var(--gutter-unit));

    /* 容器宽度 */
    --container-width: 80vw;
}

/* 媒体查询改变容器宽度 */
@media (min-width: 1000px) {
    :root {
        --container-width: 90vw;
    }
}

@media (min-width: 1400px) {
    :root {
        --container-width: 1300px;
    }
}

在使用网格的时候,我们有的时候会需要跨列合并,使用的时候可以像下面这样:

.panel {
    width: calc(6 * var(--column-and-gutter-unit) - var(--gutter-unit));
}

有关于这方面更详细的教程,可以阅读@Russell BishopBuilding Robust Layouts With Container Units一文。

缓动函数的反转

熟悉CSS中animationtransition的同学,都知道这两个属性中都有缓动函数的概念,即animation-timin-functiontransition-timing-function对应的属性值。这两个属性常见的属性值主要有:linearease-inease-outease-in-out等。除了这几个还有贝塞尔曲线函数cubic-bezier()

从上图中可以看出,cubic-bezier()函数主要由两个点来控制,比如说点(x1, y1)(x2,y2),结合起来就是cubic-bezier(x1,y1,x2,y2)。而在animation中还有另一个属性animation-direction可以让动画反转(animation-direction: reverse)。

为了反转动画的缓动曲线,我们需要在它的轴上旋转180度,找到一个全新的坐标

如果缓动曲线的初始坐标为x1, y1, x2, y2,那么反转之后的坐标即为(1-x2), (1-y2), (1-x1), (1-y1)。既然知道了基本原理之后,我们同样可以借助CSS自定义属性,用代码来表示:

:root {
    --x1: 0.45;
    --y1: 0.25;
    --x2: 0.6;
    --y2: 0.95;

    --originalCurve: cubic-bezier(var(--x1), var(--y1), var(--x2), var(--y2));
}

根据上面的公式,可以计算出反转后的缓动曲线:

:root {
    --reversedCurve: cubic-bezier(calc(1 - var(--x2)), calc(1 - var(--y2)), calc(1 - var(--x1)), calc(1 - var(--y1)));
}

为了更易于理解,把上面的代码稍作调整:

:root {
    /* 原始坐标值 */
    --x1: 0.45;
    --y1: 0.25;
    --x2: 0.6;
    --y2: 0.95;

    --originalCurve: cubic-bezier(var(--x1), var(--y1), var(--x2), var(--y2));

    /* 反转后的坐标值 */
    --x1-r: calc(1 - var(--x2));
    --y1-r: calc(1 - var(--y2));
    --x2-r: calc(1 - var(--x1));
    --y2-r: calc(1 - var(--y1));

    --reversedCurve: cubic-bezier(var(--x1-r), var(--y1-r), var(--x2-r), var(--y2-r));
}

最后来看一个@Michelle Barker的《Reversing an Easing Curve》文中提供的一个示例:

小结

虽然文章中提到的知识点非常的有意思,不管是使用CSS Grid实现响应式设计,还是容器单元构建页面布局,或者说缓动曲线的反转。看上去有点高级和复杂,但简单地说,都是基于一些基础知识来构建的,比如CSS的Grid布局和CSS自定义属性。其实这两个知识点都不是很新的概念,但把这些东西结合起来,可以帮助我们达到更为神奇和强大的功能。如果你还未接触过这方面的知识,那么你需要开始去学习了,在未来的CSS中,这两个东东都是非常重要的知识点,也是非常有用的知识的。在不久的将来,这两个部分都可以运用于你的项目中,我在去年的项目中就已经尝试使用了CSS的自定义属性,基本上能达到预期的效果。让我更易于维护和修改项目的需求。如果你在这方面有更多的经验,欢迎在下面的评论中与我们一起分享。

Vue 模板

$
0
0

在Vue中,Vue模板对应的就是Vue中的View(视图)部分,也是Vue重中之一,而在Vue中要了解Vue模板我们就需要从两个方面来着手,其一是Vue的模板语法,其二就是模板渲染。Vue模板语法是Vue中常用的技术之一,除非在应用程序中不用渲染视图或者你的程序直接采用的是渲染函数render())。相较而言,模板语法较简单一点,但对于模板的渲染(模板编译)就会更为复杂一些,如果需要了解模板渲染就需要对Vue的渲染函数,响应式原理之类的要有所了解。当然,如果你跟我一样是初学者的话,建议你先花一点时间阅读一下下面几篇文章:

那咱们接下来先从Vue模板语法开始入手,应该这部分相对来说较简单一点。

Vue模板语法

先来看一段最简单的代码:

<!-- App.vue -->
<template>
    <div id="app">
        {{ message }}
    </div>
</template>

上面代码演示的仅仅Vue模板中的一种方式,也是最简单和最常见的一种模板方式。在Vue中除了上述这种方式之外还有其他几种方式,较为详细的可以阅读《Vue.js 定义组件模板的七种方式》一文。

这段代码具体的含义是什么暂不说。

在Vue中,模板语法是逻辑视图之间的沟通桥梁,使用模板语法编写的HTML会响应Vue实例中的各种变化,简单地说,Vue实例中的逻辑可以随心所欲的渲染在页面上。正如上面的示例所示,如果我们Vue实例中逻辑让message发生变化时,那么浏览器客户端就立即发生变化。

示例是一个最简单的模板语法,但其中有一个角色是最为重要,那就是插值(Mustache)标签,常用{{}}符号来表示。Vue模板中插值常见的使用方法主要有:文本原始HTML属性JavaScript表达式指令修饰符等。

文本

插值中最常见的就是文本插值,正如上面的示例中的{{ message }},该标签将会被替代为对应数据对象上message属性的值。无论何时,绑定的数据对象上message属性发生变化时,插值处的内容都会更新。

但也有一个额外的场景,那就是在模板语法中看是否使用了其他的指令,比如,在模板中要是使用了v-once指令的话,那么该插值就是一次性地插值。也就是说,当数据改变时,插值处的内容不会更新。其使用如下所示:

<!-- App.vue -->
<template>
    <div id="app">
        <span v-once>{{ message }}</span>
    </div>
</template>

原始HTML

插值语法中(也就是{{}})会将数据解释为普通文本,而非HTML代码,为了输出真正的HTML,需要使用v-html指令,比如下面这个示例:

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <div>{{rawHTML}}</div>
        <div v-html="rawHTML"></div>
    </div>
</template>

<script>
export default {
    name: 'app',
    data () {
        return {
            rawHTML: '<span style="color:red;">原始HTML</span>'
        }
    }
}
</script>

效果如下:

注意:不能使用v-html来复合局部模板,因为Vue不是基于字符串的模板引擎。另外动态渲染任意的HTML会有一定的危险,因为它很容易导致XSS攻击。

属性

插值语法不能作用在HTML元素的属性上,遇到这种情形需要使用v-bind指令:

<div v-bind:id="dynamicId"></div>

在布尔特性的情况下,它们的存在即暗示为truev-bind工作起来略有不同,比如:

<button v-bind:disabled="isButtonDisabled">Button</button>

如果 isButtonDisabled的值是 nullundefinedfalse,则 disabled特性甚至不会被包含在渲染出来的 <button>元素中。

JavaScript表达式

在插值语法中,我们还可以使用JavaScript的表达式,比如:

{{number + 1}}
{{ ok ? 'Yes' : 'No'}}
<div v-bind:id="'list-' + id">

这些表达式会在所属Vue实例的数据作用域下作为JavaScript被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效:

<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}

<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

指令

在Vue中有不少内置的指令,常常以v-前缀的特殊特性,比如前面看到的v-htmlv-oncev-bind等。Vue指令的特性的值预期是单个JavaScript表达式。Vue指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于DOM。比如下面这个v-if示例:

<p v-if="seen">现在你看到我了</p>

这里,v-if指令将根据表达式seen的值的真假来插入或移除<p>元素。

在Vue中一些指令可以接收一个参数,在指令名称之后以冒号(:)表示,比如前面提到的v-bind指令:

<a v-bind:href="url">我是一个链接</a>

这里的href是参数,告诉v-bind指令将该元素的href属性与表达式url的值绑定。另外在V2.6开始,可以用方括号([])绑定一个动态参数,比如:

<a v-bind:[attributeName]="url">我是一个链接</a>

上面代码中的attributeName会被作为一个JavaScript表达式进行动态求值,求得的值将会作为最终的参数来使用。比如,在Vue实例中有一个data属性attributeName,其值为"href",那么这个绑写下将等价于v-bind:href。同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:

<a v-on:[eventName]="doSomething"> ... </a>

同样地,当 eventName的值为 "focus"时,v-on:[eventName]将等价于 v-on:focus

当然,在使用动态参数时有一些约束,比如:

  • 对动态参数的值的约束:动态参数预期会求出一个字符串,异常情况下值为 null。这个特殊的 null值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。
  • 对动态参数表达式的约束:动态参数表达式有一些语法约束,因为某些字符,例如空格和引号,放在 HTML 特性名里是无效的。

使用Vue指令的时候,还可以采用缩写的方式,比如:

<!-- 完整语法 -->
<a v-bind:href="url">...</a>

<!-- 缩写 -->
<a :href="url">...</a>

<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>

<!-- 缩写 -->
<a @click="doSomething">...</a>

它们看起来可能与普通的 HTML 略有不同,但 :@对于特性名来说都是合法字符,在所有支持 Vue 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。

事实上,在Vue中的指令也有不少,最常见的以及其相应的使用方法可以阅读下面相关教程:

上面列的都是Vue内置的一些常见指令,除了内置的指令之外,在Vue中可以根据其相应的机制实现一些自定义的指令,比如这两篇文章中介绍的内容《自定义指令》、《Vue 自定义指令的魅力》。

修饰符

Vue中的指令后面还可以紧跟一个.指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。比如,.prevent修饰符告诉v-on指令对于触发的事件调用event.preventDefault()

<form v-on:submit.prevent="onSubmit">...</form>

上面我们仅仅是Vue中模板语法中面上的一些东东,很多同学对上面了解或掌握已经非常的熟悉了。当然也有部分同学和我类似,希望能知道一些更深的东西,比如模板渲染更深层的东西。接下来咱们尝试一起来尝试了解一下这方面的的知识点。

模板渲染

Vue的模板渲染相对而言要更为复杂,涉及更多底层的知识,如果你能熟读Vue源码,应该更易于理解。如果你和我一样对于源码阅读还有一定的难度,那么我们可以先从别的方面着手,了解模板渲染的一些基本原理。这样一来,就能更清楚Vue的模板是如何工作的,简单地说,就是如何渲染(也就是模板编译)。

在深入了解Vue模板渲染之前,有几个基础概念需要先进行了解:AST数据结构VNode数据结构createElement的问题渲染函数

AST数据结构

AST是Abstract Syntax Tree首字母的简写,即抽象语法树的意思。是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而Vue源码中借鉴的是@John ResigHTML Parrser对模板进行解析,得到的就是AST代码。

<template>转换成抽象语法树(AST)

其中AST是解析器中一个非常重要的概念。在Vue中,ASTNode主要分为:ASTElement(元素)、ASTText(文本)和ASTExpression(表达式)。用type属性区分。

用一个简单的示例来做个简单的阐述:

<div id="app">
    <h1>W3cplus.com</h1>
    <p>{{ 1 + 1 }}</p>
</div>

这段代码生成的AST如下:

看上去是不是和DOM树有点类似。

VNode数据结构

VNode是VDOM(虚拟DOMVirtual DOM)中的概念,是真实DOM元素的简化版,与真实DOM元素是一一对应的关系。

Vue 2.6中VNode数据结构的定义大致像下面这样:

{
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
}

Vue中的渲染函数的生成跟这些属性相关。

document.createElement的问题

我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement这个方法创建的真实 DOM 元素会带来性能上的损失。

let div = document.createElement('div');
for(let k in div) {
    console.log(k);
}

document.createElement创建的元素其属性多达 228个(包含原型链上的属性),而这些属性有 90%多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的DOM,比如属性elm,只包括我们需要的属性,新增了一些在 diff过程中需要使用的属性,例如 isStatic,就可以用来对比新旧 DOM。这也是为什么要使用虚拟DOM的原因

如果你想扩展或深入了解有关于虚拟DOM相关的内容,可以阅读下面这些文章。

渲染函数

render()即是渲染函数,这个函数是通过编译模板文件得到的,其运行结果是VNode(虚拟DOM)。在Vue中使用 Vue.compile(template)方法对模板进行编译。主要会经历三个步骤:

  • 第一步是将 模板字符串 转换成 element ASTs(解析器
  • 第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器
  • 第三步是 使用 element ASTs 生成 render函数代码字符串(代码生成器

其对应的其实就是三个函数:

  • parse()函数:用来解析<template>,即解析器。主要功能是将 template字符串解析成 AST。前面定义了ASTElement的数据结构,parse函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。
  • optimize()函数:用来优化静态内容,即优化器。主要功能就是标记静态节点,为后面 patch过程中对比新旧 VNode 树形结构做优化。被标记为 static的节点在后面的 diff算法中会被直接忽略,不做详细的比较。这里的静态内容指的是 和数据没有关系,不需要每次都刷新的内容
  • generate()函数:用来创建render()字符串,即代码生成器。主要功能就是根据 AST 结构拼接生成 render函数的字符串。

这三个函数也是compile()函数中三个核心部分,根据每个步骤所起的作用,绘制了一张草图:

有了上面这些知识点,继续探究模板渲染的过程就会更易于理解了。

如果结合我们上一节学习的Vue实例的生命周期图,那么模板渲染最重要的两个过程将是 DOM初始化DOM的更新

简单地说,模板中的 DOM初始化部分概括为将eltemplaterender()函数通过一系列的函数,比如compileToFunctions()compile()函数转换为render函数并最终生成真实DOM的过程;而 DOM更新就是数据发生变化后,DOM进行更新的过程。结合生命周期的图和前面所掌握的知识点,我们现在来尝试着将Vue模板渲染过程用图绘制出来。

上图是根据自己阅读相关资料整理的,难免有错,欢迎路过的大婶拍正。

如果想正确的绘制出Vue模板渲染过程的路线图的话,还是需要去尝试阅读Vue的源码。这样会更清楚,更深层的知识点。扩展阅读:

小结

这篇文章整理了学习Vue模板的一些心得和笔记。除了介绍了模板语法相关的知识点之外,更多的是花了不少时间去理解Vue模板编译(渲染)的一个过程。根据相关的文档和自己的理解把模板渲染的过程绘制成了图。毕竟没有熟读源码,难免有错,欢迎路过的大神指正。或者您有这方面的经验,欢迎在下面的评论中一起分享。

CSS的mask-composite

$
0
0

CSS的mask(遮罩),有时也称CSS的蒙层,最早是苹果公司2008年提出的,并且添加到webkit引擎当中。遮罩提供一种基于像素级别的,可以控制元素透明度的能力,类型于png24位或png32位中的alpha透明通道的效果。2012年被纳入到W3C的草案中,但这个版本与苹果公司提出的版本是不同的。时至今日,该规范已经有多个版本,现在是CSS Masking Module Level1版本,属于TR阶段。据Caniuse.com统计来讲,该属性得到的支持度还是有一定的限制,仅部分属性被浏览器支持。虽然如此,但该属性还是非常的有意思,值得大家花点时间去探究,比如今天要聊的mask-composite属性就是非常有意思的一个属性。

mask原理和语法

文章开头也提到过,mask提供一种基于像素级别的,可以控制元素透明度的能力,类似于png24位或png32位中alpha透明通道的效果。

图像是由RGB三个通道以及在每个像素上定义的颜色组成的。但是在他们之上还有第四个通道,即alpha通道。通过高度定义每个像素上的透明度。白色意味着不透明,黑色意味着透明,介于黑白之间的灰色表示半透明。比如下图:

给一个HTML元素使用CSS的mask属性,就会这样处理。不用给图片应用一个alpha通道,只需要给一个图片运用一个mask-imagemask-border-source的样式:

mask-image: url(mouse.png)

他从图片遮罩里读出图片的透明信息,然后应用到HTML元素上,比如下图这个效果:

遮罩可以让头像按照特定形状显示。

CSS遮罩可以使得图片按照任意的形状显示。或者你可能有很长的文本需要滚动显示,那么可以使用遮罩让他从不透明到透明的渐变显示

在使用mask的时候,其中有一个不可或缺的部分就是遮罩(也被称为蒙板),该遮罩可以是半透明的PNG图片CSS的渐变SVG元素,遮罩元素的alpha值为0的时候会覆盖下面的元素,为1的时候会完全显示下面的内容。如下图所示:

而使用CSS的mask也并不太复杂,其语法规则和background非常的类似。在W3C官网上提供了两者相关属性的对照表:

mask属性background属性
mask-clipbackground-clip
mask-imagebackground-image
mask-originbackground-origin
mask-positionbackground-position
mask-repeatbackground-repeat
mask-sizebackground-size
mask-mode 
mask-composite 
 background-attachment
 background-color

具体的语法规则:

mask: <mask-layer>

<mask-layer>对应的值:

<mask-layer> = <mask-reference> <masking-mode>? || <position> [ / <bg-size> ]? ||
<repeat-style> || <geometry-box> || [ <geometry-box> | no-clip ] || <compositing-operator>

即:

mask: [mask-image] [mask-repeat] [mask-position] / [ mask-size]

background属性类似,建议mask-size单独写出来:

mask: [mask-image] [mask-repeat] [mask-position]
mask-size: [mask-size]

上面我们列出的的仅是mask部分属性,在规范中还提供了其他的属性,这几个属性有点类似于CSS中的border属性,比如mask-border-sourcemask-border-modemask-border-slicemask-border-widthmask-border-outsetmask-border-repeatmask-border等。另外还可以用于SVG,比如mask-type

上面简单的介绍了CSS中mask的基本原理和使用规则,但我们今天的重点并不是来了解所有的属性,而是针对性的学习其属性之一,即mask-composite属性。如果你想了解更多有关于mask相关的知识,建议你花点时间阅读下列文章:

mask-composite

mask-composite仅是CSS的mask的子属性之一。该属性的工作原理是什么?它有什么用?使用它的价值是什么?

如果将上述问题一一答出来之后,可以彻底让我们掌握mask-composite的原理和使用方式。@Ana Tudor新出的博文《Mask Compositing: The Crash Course》就详细介绍了这些,也称得上是mask-composite速成记。

特别声明,接下来的内容和图片资源来自于@Ana Tudor的《Mask Compositing: The Crash Course》一文!在此特别感谢@Ana Tudor为我们提供这么优秀的教程。

什么是遮罩合成

遮罩合成指的是我们可以使用不同的操作将多个不同的遮罩层合并成一个独立的遮罩层。那么问题来了?多个遮罩层是如何合并成一个呢?比如我们有两个遮罩层,在这两个遮罩层中取每对对应的像素,在它们的通道上应用特定的合成操作(具体合成的操作细节,后面会介绍),并为最终层获得第三个像素。如下图所示:

上图中左上图和左下图合层起来成了右侧的层。而左上图被称为源(Source),左下图被称为目标层(Destination),这对地我们来说没有多大的意义,因为给我的感觉一个是输入源,一个是输出结果(事实上,这两个都是输入),但是,就上图的结果而言,这两个层(源和目标层)却做了一个合层的操作(也被称为合层计算),从而得到最终的结果(上图右侧的合并层)。

上面演示的是仅有两个层合并,而事实上呢?我们可能会有两个以上的层合并,当有这种情形时,合层是分阶段完成的,从底部开始。

在第一阶段,从底部开始的第二层是源,从底部开始的第一层是目标,这两层被合层,结果成为第二阶段的目标,接着和从底部开始的第三层(源)合并。通过合成前两层的结果合成第三层,我们就得到了第三阶的目标,接着再从底部的第四层源合并。如下图这样的一个合并过程:

以此类推,直到我们达到最后一个阶段,在这里,最顶层由下面所有层的合成结果组成

遮罩合成有什么用?

CSS和SVG的遮罩都有各自的局限性和优缺点。我们可以通过使用CSS遮罩来绕过SVG遮罩的限制,但是,由于CSS的遮罩和SVG遮罩的工作方式不同,采用CSS遮罩我们无法在不进行合成的情况下实现某些结果。

为了更好地理解这一切,让我们来看看下面这张令人敬畏的西伯利亚小老虎的图片:

假设我们使用遮罩期望得到的效果如下图所示:

这个特殊的mask保持了菱形的可见性,而分隔它们的线被遮罩,我们可以通过图像看到后面的元素。

我们希望这种遮罩效果是灵活的。我们不希望与图像的尺寸或长宽比绑定,我们希望能够轻松地随图像缩放和不缩放的mask之间进行切换(只需要将%值更改为px)。

为了做到这一点,我们首先需要了解SVG和CSS的遮罩是如何工作,以及我们可以和不可以使用它们做什么。

SVG遮罩

SVG遮罩默认情况下是亮度(luminance)遮罩。这意味着遮罩元素上白色像素部分是完全不透明,而黑色像素部分是完全透明的,而遮罩元素白色和黑色像素之间的亮度(greypinklime)是半透明的。

给定RGB值对应亮度的计算公式为:.2126·R + .7152·G + .0722·B

对于我们这个示例,这意味着我们需要将菱形区域变成白色,分隔线是黑色,如下图所示:

为了得到上图的效果,使用SVG的rect绘制一个纯白色的矩形,然后使用path在这个矩形中添加两条黑色的对角线(确保stroke的值为black)。

先创建第一条对角线(从左上角到右下角),在左上角使用M命令,然后在右下角使用L命令;接着创建第二条对角线(从右上角到左下角),在右上角使用M命令,然后在左下角使用L命令。对应的SVG代码如下:

<svg viewBox="0 0 837 551" width="837" height="551">
    <rect width="837" height="551" fill="#fff"></rect>
    <path d="M0 0 L837 551 M837 0 L0 551" stroke="#000"></path>
</svg>

得到的效果如下图所示,但离我们想要的菱形图案的效果还相差很远。

我们可以使用stroke-width来改变线条的粗细,然后使用stroke-dasharray属性来改变线条之间的间距:

<svg viewBox="0 0 837 551" width="837" height="551">
    <rect width="837" height="551" fill="#fff"></rect>
    <path d="M0 0 L837 551  M837 0 L0 551" stroke="#000" stroke-width="15%" stroke-dasharray="1% 4%"></path>
</svg>

我们可以继续增大stroke-width的值到150%,最终效果会覆盖整个矩形,而且离我们想要的效果越来越近了:

现在,我们可以将rectpath元素封装在一个mask元素中,并将这个遮罩应用于我们想要的任何元素上,比如我们这个示例中,就是小老虎的img

<svg viewBox="0 0 837 551">
    <mask id="m">
        <rect width="837" height="551" fill="#fff"></rect>
        <path d="M0 0 L837 551 M837 0 L0 551" stroke="#000" stroke-width="15%" stroke-dasharray="1% 7%"></path>
    </mask>
</svg>

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="837"/>

img { 
    mask: url(#m) 
}

上述方法应该是有效的。但遗憾的是,效果并不是我们期许的一样。在Firefox中得么的效果是我们期望的,而在Chrome中并不是我们想要的效果,甚至应用该遮罩之后(mask添加-webkit前缀),元素(img)消失了。

如果我们希望在Chrome浏览器中也能得到我们期望的效果,最简单的做法就是把img元素放在SVG中,使用image元素来替代:

<svg viewBox="0 0 837 551" width="837">
    <mask id="m">
        <rect width="837" height="551" fill="#fff"></rect>
        <path d="M0 0 L837 551  M837 0 L0 551" stroke="#000" stroke-width="150%" stroke-dasharray="1% 7%"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="837" mask="url(#m)"></image>
</svg>

得到的效果和我们期望的很相似了:

虽然得到我们想要的效果,但如果我们想屏蔽另一个HTML元素,而不是img元素,事情就会变得有点复杂,因为我们需要将它放在SVG的foreignObject中。

更糟糕的是,使用这个解决方案,我们要对维度进行硬编码,这令人非常恶心心。我们可以尝试通过maskContentUnits切换到objectBoundingBox

<svg viewBox="0 0 837 551" width="837">
    <mask id="m" maskContentUnits="objectBoundingBox">
        <rect width="1" height="1" fill="#fff"></rect>
        <path d="M0 0 L1 1 M1 0 L0 1" stroke="#000" stroke-width="1.5" stroke-dasharray=".01 .07"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="100%" mask="url(#m)"></image>
</svg>

但我们仍然在viewBox中硬编码尺寸大小,虽然它们的实际值并不重要,但它们的长宽比很重要。此外,我们的遮罩模式现在是在1x1正方形内创建的,然后拉伸到覆盖mask元素。形状拉伸意味着形状的扭曲,这就是为什么我们的菱形不再像以前那样了。

我们可以尝试调整path的起点和终点:

<svg viewBox="0 0 837 551" width="837">
    <mask id="m" maskContentUnits="objectBoundingBox">
        <rect width="1" height="1" fill="#fff"></rect>
        <path d="M-.75 0 L1.75 1 M1.75 0 L-.75 1" stroke="#000" stroke-width="1.5" stroke-dasharray=".01 .07"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="100%" mask="url(#m)"></image>
</svg>

也就是说,要得到一个特定的菱形图案,就需要知道菱形的角度,也就需要知道图像的长宽比。

上面我们看了SVG中遮罩是如何工作的,我们接着来看看CSS中的遮罩是怎么做的。

CSS遮罩

CSS遮罩默认情况下是alpha遮罩。这意味着,完全不透明遮罩像素对应的是遮罩元素像素完全不透明,完全透明遮罩像素对应的是遮罩元素像素完全透明,半透明遮罩像素对应的是遮罩元素的像素是半透明的。简单地说,遮罩元素的每个像素将获得对应遮罩元素像素的alpha通道。

就我们示例而言,菱形区域是不透明的,而分隔它们的线是透明的。在SVG中使用path绘制菱形,而在CSS中,我们可以使用CSS渐变来绘制菱形。为了得到白色菱形区域和黑色分隔线的图案,可以使用两个repating-linear-gradient来绘制:

关键代码如下:

repeating-linear-gradient(-60deg,  #000 0, #000 5px, transparent 0, transparent 35px), 
repeating-linear-gradient(60deg,  #000 0, #000 5px, #fff 0, #fff 35px)

如果我们也想要一个亮度(luminance)的遮罩,那么这个模式就会起作用。

但在alpha遮罩的例子中,并不是黑色的像素给了我们完全的透明度,而是透明的像素;并不是白色像素让我们完全不透明,而是完全不透明的像素。其中redblackwhite都做同样的事情,但我从此人更倾向于使用redtan,因为这意味着只需要输入三个字母,输入的字母更少,出错的机会也就更少。

首先想到的是使用同样的技术得到不透明的菱形区域和透明的分隔红玫瑰。但是这样做的时候,我们遇到了一个问题:第二层渐变层的不透明部分覆盖了第一层部分,但我们希望仍然要保持透明,反之亦然。

我最初的想法是使用带有白色菱形区域和黑色分隔线的模式,结合设置mask-mode的值为luminance,让CSS遮罩像SVG遮罩一样工作来解决。

div {
    background: repeating-linear-gradient(60deg, #000 0, #000 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #000 0, #000 0.5em, #fff 0, #fff 1.5em);
}
div:nth-child(2) {
    background: repeating-linear-gradient(60deg, #fff 0, #fff 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #fff 0, #fff 0.5em, #000 0, #000 1.5em);
}
div:nth-child(n + 3) {
    background: linear-gradient(white, dimgrey);
    --m: repeating-linear-gradient(60deg, #000 0, #000 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #000 0, #000 0.5em, #fff 0, #fff 1.5em);
    -webkit-mask: var(--m);
    mask: var(--m);
    mask-mode: luminance;
    mask-source-type: luminance;
}
div:nth-child(4) {
    --m: repeating-linear-gradient(60deg, #fff 0, #fff 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #fff 0, #fff 0.5em, #000 0, #000 1.5em);
}

此属性仅得到Firefox的支持,下图是Firefox和Chrome两个浏览器的效果:

幸运的是,mask-composite可以实现我们想要的效果。接下来我们来看看这个属性可以取什么值以及它们各自有什么效果?

mask-composite的值及其作用

首先为mask提供了两个渐变的遮罩层(好比文章最开始展示的左侧图)和一张图片(比如小老虎图片)。接下来使用这两个渐变遮罩层来说明mask-composite属性有哪些值,每个值是如何工作的,如下:

--l0: repeating-linear-gradient(90deg, red, red 1em,  transparent 0, transparent 4em);
--l1: linear-gradient(red, transparent);

mask: var(--l1) /* 顶部层(源) */, 
    var(--l0) /* 底部层(目标) */

这两个渐变层的效果如下所示:

--l1是源,--l0是目标。我们将这个遮罩运用在下面这张老虎图上。

接下来看看mask-composite值带来的效果。

add

addmask-composite的初始值(initial),和mask-composite不显式设置值起到的效果相同。在本例中所发生的是将渐变添加到另一个渐变之上,并生成新的遮罩层。

注意,在半透明遮罩层的情况下,不只是简单地添加alpha,不管值的名称是什么。相反,使用下列公式,α₁是顶部层(源),α₀是底部层(目标):

α₁ + α₀ – α₁·α₀

如果至少有一个遮罩层是完全不透明的(它的alpha值是1),那么得到的遮罩就是完全不透明的,并爱理不理遮罩元素的相应像素显示为完全不透明(alpha值为1)。

如果顶层(源)是完全不透明的,即α₁的值是1,如果把该值替换到上面的公式中:

1 + α₀ - 1·α₀ = 1 + α₀ - α₀ = 1

相应的,底层(目标)也是完全不透明的,即α₀的值是1,同样把该值替换到上面的公式中:

α₁ + 1 – α₁·1 = α₁ + 1 – α₁ = 1

当两个遮罩层都是完全透明的(它的alpha的值为0),得到的遮罩就是完全透明的,因此遮罩元素的相应像素也是完全透明的(alpha值为0)。

0 + 0 – 0·0 = 0 + 0 + 0 = 0

下图中,我们可以看到这对于我们正在使用的遮罩图层意味着什么 —— 通过合成得到的图层是什么样子的,以及将它应用到老虎图片上最图得到的效果:

subtract

该值的意思即是从源(顶层)中减去目标(底层)mask-composite取值为subtract时,两个遮罩层合成在一起的计算公式是:

α₁·(1 – α₀)

上面的公式可以得知,任何与0相乘的结果都是0。无论源(顶层)是完全透明的,还是目标层(底层)是完全不透明的,得到的遮罩层也是完全透明的,遮罩元素的相应像素也是完全透明的。

如果源(顶层)是完全透明的,则将公式中的α₁替换为0,得到的值为:

0·(1 – α₀) = 0

如果目标(底层)是完全不透明的,则将公式中的α₀替换为1,得到的值为:

α₁·(1 – 1) = α₁·0 = 0

得到的效果如下:

注意,在本例中,这个价值观式不是对称的,除非α₁α₀是相等的。另外α₁·(1 – α₀)α₀·(1 – α₁)不相同,如果我们交换两个遮罩层,得到的效果将会不一样。

intersect

mask-composite取值为intersect时,对应的公式是两个alpha值相乘:

α₁·α₀

上面公式得到的结果是,无论哪个遮罩层是完全透明的(alpha值为0),计算出来的遮罩层也是完全透明的,遮罩元素的相应像素也是完全透明的。

如果源(顶层)是完全透明的,那么α₁的值为0,将该值替换到上面的公式中,得到的结果是:

0·α₀ = 0

如果目标(底层)是完全透明的,那么α₀的值为0,将该值替换到上面的公式中,得到的结果同样也是0

α₁·0 = 0

另外,如果两个遮罩层都是完全不透明的(它们的alpha值为1),那么计算出来的遮罩层就是完全不透明的,遮罩元素的相应像素也是完全不透明的。这是因为α₁α₀的值都是1,将这两值运用到上面的公式中,得到的结果是1

1·1 = 1

mask-composite:intersect得到的效果如下图所示:

exclude

mask-composite取值为exclude时,两个遮罩层是互斥的,合并遮罩层对应的公式为:

α₁·(1 – α₀) + α₀·(1 – α₁)

实际上,这个公式意味着,无论两个遮罩层都是完全透明的(alpha的值是0)或完全不透明(alpha的值是1),计算出来的遮罩都是完全透明的。遮罩元素的相应像素也是完全透明的。

如果两个遮罩层都是完全透明的,那么α₁α₀的值都是0,将该值相应替换到上面的公式中得到的结果是0

0·(1 – 0) + 0·(1 – 0) = 0·1 + 0·1 = 0 + 0 = 0

如果两个遮罩层都是完全不透明的,那么α₁α₀的值都是1,将该值相应替换到上面的公式中得到的结果也是0

1·(1 – 1) + 1·(1 – 1) = 1·0 + 1·0 = 0 + 0 = 0

它也意味着,只要有一个层是完全透明(alpha值为0),而另一层是完全不透明(alpha值为1),那么计算出来的遮罩层就是完全不透明的,遮罩元素的相应像素也是完全不透明的。

如果源(顶层)完全透明,而目标(底层)完全不透明,那么α₁α₀对应的值分别是01

0·(1 – 1) + 1·(1 – 0) = 0·0 + 1·1 = 0 + 1 = 1

如果源(顶层)完全不透明,而目标(底层)完全透明,那么α₁α₀对应的值分别是10

1·(1 – 0) + 0·(1 – 1) = 1·1 + 0·0 = 1 + 0 = 1

mask-composite:exclude对应的效果如下:

实例

回到上面,采用两个渐变绘制出来想要的菱形图形:

--l1: repeating-linear-gradient(-60deg, transparent 0, transparent 5px, tan 0, tan 35px);
--l0: repeating-linear-gradient(60deg, transparent 0, transparent 5px, tan 0, tan 35px)

如果我们将完全不透明(示例中tan)的部分设置为半透明(rgba(tan, .5)),那么视觉效果就会告诉我们如何进行合成:

$c: rgba(tan, .5);
$sw: 5px;

--l1: repeating-linear-gradient(-60deg, transparent 0, transparent #{$sw}, #{$c} 0, #{$c} #{7*$sw});
--l0: repeating-linear-gradient(60deg, transparent 0, transparent #{$sw}, #{$c} 0, #{$c} #{7*$sw})

html {
    height: 100vh;
    background: var(--l1), var(--l0);
}

得到的效果如下:

我们要研究的菱形区域是在半透明条带的交点处形成的。这意味着使用mask-composite:intersect可以达到我们想要的效果:

$sw: 5px;

--l1: repeating-linear-gradient(-60deg,  transparent 0, transparent #{$sw},  tan 0, tan #{7*$sw});
--l0: repeating-linear-gradient(60deg,  transparent 0, transparent #{$sw},  tan 0, tan #{7*$sw});
mask: var(--l1) intersect, var(--l0)

这不仅给了我们想要的结果,而且,将透明宽度存储到一个变量中,那么将这个值更改为%值(假设$sw:.05%)将使遮罩和图像一起缩放。

如果透明条的宽度是px值,那么菱形和分隔线的大小都保持不变,因为图像会随着视窗进行缩放。

如果透明条宽度是%值,那么菱形和分隔红玫瑰的大小都与图像相关,因此可以随着图像大小进行缩放。

到目前为止,只有Firefox只支mask-composite。但值得庆幸的是,webkit内核提供了一个可替代方案。

扩展支持

webkit内核浏览器使用mask-composite需要添加相应的前缀,而且它需要不同的值才能正常工作:

  • source-over等价于add
  • source-out等价于subtract
  • source-in等价于intersect
  • xor等价于exclude

大家可能会想,只需要额外添加一个webkit版本即可,对吧?事实并没那么简单。首先,我们不能在-webkit-mask简写中使用这个值,比如说,下面的方法是不会起作用的:

-webkit-mask: var(--l1) source-in, var(--l0)

如果要它们起作用,需要把mask-composite单独拿出来,如下:

-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in;
mask: var(--l1) intersect, var(--l0)

整个图像完全消失了!

如果你觉得这很奇怪,请检查一下,使用其他三种操作的任何一种,在Webkit内核的浏览器和Firefox中都能得到预期的结果,只有source-in才会破坏Webkit内核浏览器中的东西:

  • add / source-over
  • subtract / source-out
  • exclude / xor

我们也可以把α₀取值为0,可以得到上面三个值得到的最终值是α₁

  • add / source-over: α₁ + 0 – α₁·0 = α₁ + 0 - 0 = α₁
  • subtract / source-out: α₁·(1 – 0) = α₁·1 = α₁
  • exclude / xor: α₁·(1 – 0) + 0·(1 – α₁) = α₁·1 + 0 = α₁

然而,与空无一物相交则是另一回事。与虚无相交就是虚无!这也说明 intersectsource-in操作中可以将α₀设置为0

α₁·0 = 0

在这种情况之下,结果层的alpha值是0,这也就是图像完全被遮上的原因所在。针对这个现象,想到的第一个修复是使用另一种操作来合成底部的层:

-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in, xor;
mask: var(--l1) intersect, var(--l0)

这种方法确实有效。最终我们示例的结果如下:

有关于mask-composite的Demo效果除了上面文章中提到的案例之外,在Codepen上还有很多类似的案例,如果感兴趣的话可以猛击这里

最后再次感谢@Ana Tudor为我们提代这么优秀的教程:《Mask Compositing: The Crash Course

小结

这篇文章简单的介绍了CSS中mask的基本原理和使用方法,但是大部分内容介绍了mask-composite属性的使用。通过该文的学习,可以深入的了解到该属性的每个值,以及每个值的计算公式。而且每个属性值所起的效果不同之处。感兴趣的同学,可以自己动手撸几个Demo,如果您在这方面有更好的建议或经验,欢迎在下面的评论中与我们一起分享。文章中有关于mask-composite的原理和公式来自于@Ana Tudor的《Mask Compositing: The Crash Course》一文。最后再次感谢@Ana Tudor为我们提供这么优秀的教程。

如何使用JavaScript操作CSS颜色

$
0
0

在学习如何使用JavaScript操作CSS颜色之前,我们需要对CSS如何设置颜色有一个基本的了解。CSS设置颜色模式有多种,最为常见的模型有:RGBHSL。我们先来看一下这两种颜色模式。

颜色模式

RGB

RGBredgreenblue三个单词首字母的缩写,其由三个数字组成,每个数字表示其各自颜色的光在最终颜色中包含多少。在CSS中,这些数字都在0~255之间,可以作为CSS的rgb函数的参数,并且用逗号来分隔。例如rgb(50,100,0)

RGB是一个加色颜色系统,这意味着每个数字越高,最终的颜色就越亮。如果所有值相等,则颜色为灰色;如果所有值都是零,则结果为黑色;如果所有值都是255,结果将是白色。

另外,你也可以使用十六进制表示RGB颜色,在这种表示法中,每种颜色的整数都是从基数10转换为基数16。例如,rgb(50,100,0)转换出来的十六进制就是#326400

虽然我们经常发现自己出于习惯而使用RGB(特别是十六进制),但我经常发现它很难读,而且特别难操作。

HSL

HSL是huesaturationlight三个单词首字母的简写,也是由这三个值组成。色调对应于色盘上的位置,由CSS的角度值表示,最常见的是deg单位。

饱和度用百分比表示,是指颜色的强度。当饱和度为100%时,它是一个彩色;当饱和度越底,颜色就越少,直到0%,就会是一个灰色。

亮度也是用百分比来表示,指的是颜色亮度。常规亮度是50%。无论色调和饱和度如何,100%的亮度将是纯白色,而0%的亮度将是纯黑色。

HSL颜色模型是一个更直观的颜色模型。颜色之间的关系更加明显,而对颜色的操作往往就像调整一个数字一样简单。

有关于CSS颜色更深入的介绍,可以阅读下面相关文章进行扩展:

颜色模型之间的转换

RGB和HSL颜色模型都将颜色分解为不同的属性。要在语法之间进行转换,首先需要计算这些属性。

除了色调,我们讨论过的每个值都可以用百分比表示。甚至RGB值也是字节大小的百分比表示。在下面的公式和函数中,这些百分比将用01之间的小数表示。

我想指出的是,我们在这篇文章中不会深入讨论这些数学,相反,我将简要介绍原始的数学公式,然后将其转换为JavaScript公式。

从RGB中计算亮度

亮度是三个HSL值中最容易计算的。数学上,公式如下,其中M为RGB值的最大值,m为RGB值的最小值:

JavaScript表达即如下:

const rgbToLightness = (r, g, b) => 1 / 2 * (Math.max(r, g, b) + Math.min(r, g, b));

从RGB中计算饱和度

饱和度只比亮度稍微复杂一点。如果亮度为01,则饱和度值为0。对应的数学公式如下(其中L为亮度):

对应的JavaScript代码:

const rgbToSaturation = (r, g, b) => {
    const L = rgbToLightness(r, g, b)
    const max = Math.max(r, g, b)
    const min = Math.min(r, g, b)
    return (L === 0 || L === 1) ? 0 : (max - min) / (1 - Math.abs(2 * L - 1))
}

从RGB中计算色相

从RGB中计算色相的公式有点复杂:

对应的JavaScript代码如下:

const rgbToHue = (r, g, b) => Math.round(Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b,) * 180 / Math.PI)

最后乘以180 / Math.PI是把弧度的值转换为角度的值。

计算HSL

所有这些函数可以封装成一个单一实用的函数:

const rgbToHsl = (r, g, b) => {
    const lightness = rgbToLightness(r, g, b)
    const saturation = rgbToSaturation(r, g, b)
    const hue = rgbToHue(r, g, b)
    return [hue, saturation, lightness]
}

从HSL中计算RGB

在开始计算RGB之前,我们需要一些先决值。首先是色度(chroma)值:

还需要一个临时的色相值,将使用它的范围来决定我们属于色盘中的哪个阶段:

接下来,我们有一个x值,它将用作中间值:

还需要一个m值,用来调整每个亮度值:

根据色调素数,rgb的值将映射到CX0

最后,我们需要映射每个值来调整亮度:

对应的JavaScript代码如下:

const hslToRgb = (h, s, l) => {
    const C = (1 - Math.abs(2 * l - 1)) * s
    const hPrime = h / 60
    const X = C * (1 - Math.abs(hPrime % 2 - 1))
    const m = l - C / 2
    const withLight = (r, g, b) => [r + m, g + m, b + m]
    if (hPrime <= 1) {
        return withLight(C, X, 0)
    } else if (hPrime <= 2) {
        return withLight(X, C, 0)
    } else if (hPrime <= 3) {
        return withLight(0, C, X)
    } else if (hPrime <= 4) {
        return withLight(0, X, C)
    } else if (hPrime <= 5) {
        return withLight(X, 0, C)
    } else if (hPrime <= 6) {
        return withLight(C, 0, X)
    }
}

创建一个颜色对象

为了便于操作属性时的访问,将处理一个JavaScript对象。这可以通过包装之前编写的函数来创建:

const rgbToObject = (red, green, blue) => {
    const [hue, saturation, lightness] = rgbToHsl(red, green, blue)
    return {red, green, blue, hue, saturation, lightness}
}

const hslToObject = (hue, saturation, lightness) => {
    const [red, green, blue] = hslToRgb(hue, saturation, lightness)
    return {red, green, blue, hue, saturation, lightness}
}

示例

比如下面这个示例,当你调整其他属性时,查看每个属性如何交互,这样可以让你更深入地了解两个颜色模型之间是如何相互转换的。

颜色处理

通过上面的内容我们了解和掌握了颜色模型之间转换。接下来看看JavaScript如何操纵这些颜色。

更新属性

我们已经讨论的每个颜色属性都可以单独操作,返回一个新的color对象。例如,我们可以写一个色相角度旋转的函数:

const rotateHue = rotation => ({hue, ...rest}) => {
    const module (x, n) => (x % n + n) % n
    const newHue = module(hue + rotation, 360)
    return {...rest, hue:newHue}
}

rotateHue函数接受一个rotation参数并返回一个新函数,该函数接受并返回一个color对象。这使得创建新的旋转函数变得容易:

const rotate30 = rotateHue(30)
const getComplementary = rotateHue(180)

const getTriadic = color => {
    const first = rotateHue(120)
    const second = rotateHue(-120)
    return [first(color), second(color)]
}

沿着同样的思路,我们可以编写一个颜色的saturatelighten函数,或者desaturatedarken

const saturate = x => ({saturation, ...rest}) => ({
    ...rest,
    saturation: Math.min(1, saturation + x)
})

const desaturate = x => ({saturation, ...rest}) => ({
    ...rest,
    saturation: Math.max(0, saturation - x)
})

const lighten = x => ({lightness, ...rest}) => ({
    ...rest,
    lightness: Math.min(1, lightness + x)
})

const darken = x => ({lightness, ...rest}) => ({
    ...rest,
    lightness: Math.max(0, lightness - x)
})

除了颜色操作,还可以编写颜色判断,即返回布尔值的函数。

const isGrayscale = ({saturation}) => saturation === 0;
const isDark = ({lightness}) => lightness < .5;

颜色数组的处理

颜色过滤

[].filter方法接受一个布尔值,并返回一个符合要求的新数组。

const colors = [/* 颜色对象的数组*/]
const isLight = ({lightness}) => lightness > .5
const lightColors = colors.filter(isLigght)

颜色排序

要对一组颜色进行排序,首先需要编写一个比较器函数。这个函数接受一个数组的两个元素,并返回一个数字来表示“赢家”。一个正数表示第一个元素应该先排序,一个负数表示第二个元素应该排序。零值表示平局。

例如,这里有一个比较两种颜色亮度的函数:

const compareLightness = (a, b) => a.lightness - b.lightness

下面是比较两个元素的饱和度:

const compareSaturation = (a, b) => a.saturaaation - b.saturation

为了防止代码重复,我们可以编写一个高阶函数来返回一个可以比较任何属性的比较函数:

const compareAttribute = attribute => (a,b) => a[attribute] - b[attribute];
const compareLightness = compareAttribute('lightness');
const compareSaturation = compareAttribute('saturation');
const compareHue = compareAttribute('hue');

平均值

可以通过组合各种JavaScript数组方法通过一个颜色数组创建一个平均颜色数组。首先,你可以用reduce和除以数组长度来计算一个属性的平均值:

const colors = [/* 颜色对象数组 */];
const toSum = (a,b) => a + b;
const toAttribute = attribute => element => element[attribute];
const averageOfAttribute = attribute => array => array.map(toAttribute(attribute)).reduce(toSum) / array.length;

你可以用这个来“正常化”一组颜色:

const normalizeAttribute = attribute => array => {
    const averageValue = averageOfAttribute(attribute)(array);
    const normalize = overwriteAttribute(attribute)(averageValue);
    return normalize(array);
}
const normalizeSaturation = normalizeAttribute('saturation');
const normalizeLightness = normalizeAttribute('lightness');
const normalizeHue = normalizeAttribute('hue');

生成随机色

很多时候我们想要的颜色是明确的,但有的时候我们需要一个随机的颜色。如果你的要求不高,使用JavaScript生成一个随机的颜色是非常的简单。比如我们希望得到一个随机的十六进制颜色。大家都知道一个十六进制颜色色是由六个字符组成,每个字符都是0f之间的任意值。我们可以写两个函数:

let getRandomColor = () => {
    let characters = '0123456789ABCDDEF'
    let color = '#'

    for (let i = 0; i < 6; i++) {
        color +=characters[getRandomNumber(0, 15)]
    }
    return color
}

let getRandomNumber = (min, max) => {
    let r = Math.floor(Math.random() * (max - min + 1)) + min
    return r
}

前面我们也提到过了,HSLRGB相关的颜色模式,事实上,我们平时使用HSLA或者RGBA模式较多,因为它们更易于我们控制一个颜色。不管是HSLA还是RGBA,每个颜色值都有一个范围。我们同样可以使用JavaScript来获取你设置范围内的随机颜色。比如下面这个HSLA示例。

let getRandomColor = (h, s, l, a) => {
    let hue = getRandomNumber(h[0], h[1])
    let saturation = getRandomNumber(s[0], s[1])
    let lightness = getRandomNumber(l[0], l[1])
    let alpha = getRandomNumber(a[0] * 100, a[1] * 100) / 100

    return {
        h: hue,
        s: saturation,
        l: lightness,
        a: alpha,
        hslaValue: getHSLAColor(hue, saturation, lightness, alpha)
    }
}

let getRandomNumber = (min, max) => {
    let r = Math.floor(Math.random() * (max - min + 1)) + min
    return r
}

let getHSLAColor = (h, s, l, a) => {
    return `hsla(${h}, ${s}%, ${l}%, ${a})`
}

使用也简单:

let h_range = [0, 360];
let s_range = [90, 100];
let l_range = [0, 90];
let a_range = [1, 1];

let color = getRandomColor(h_range, s_range, l_range, a_range);
document.body.style.backgroundColor = color.hslaValue;

比如下面这样的一个小示例:

有关于RGBA或者HSLRGB相关颜色模型的随机色也可以使用类似的方式生成,感兴趣的朋友不仿一试。

扩展阅读

特别声明,文章中的数学公式来自于@Adam GieseHow to manipulate CSS colors with JavaScript一文。

小结

Web颜色是Web应用程序或页面中不可或缺的一部分,对于CSS处理颜色总是非常的简单和单一。但很多时候我们需要一些特殊的效果,比如随机色等。那么我们就需要借助JavaScript来完成。而JavaScript处理Web颜色的情景也非常的多。正如上面介绍的,颜色模式之间的转换,颜色过滤和排序等。那么这篇文章整理了一些有关于JavaScript操作Web颜色的技巧。希望这些对初学者有所帮助,如果您在这方面有更多的经验,欢迎在下面的评论中与我们一起共享。


Vue指令

$
0
0

Vue使用的模板语言是HTML的超集。在Vue的模板中除了使用插值({{}})之外还可以使用指令。在上一节中,我们主要学习和探讨了Vue模板相关的知识,在这一节中,我们将一起来了解Vue中的指令。在Vue中,指令基本上类似于添加到模板中的HTML属性。它们都是以v-开头,表示这是一个Vue特殊属性。Vue中的指令主要分为内嵌的指令和自定义指令。另外有一些指令还带有相应的修饰符。接下来我们主要围绕着这些点来展开,咱们先从内嵌的Vue指令开始。

Vue内嵌指令

在Vue中常见的内置嵌套指令主要有:

接下来,咱们简单的了解一下内嵌指令的使用。

v-text

Vue模板一节中,我们知道可以使用插值的语法{{}}向模板中插入字符串内容。

<!-- App.vue -->
<template>
    <h1>Vue插值语法:{{}}</h1>
    <div>{{ message }}</div>
</template>

<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'Hello Vue (^_^)'
            }
        }
    }
</script>

如果我们要更新元素的textContent。除了使用Vue插值语法之外,还可以使用v-text指令。他们最终达到的效果是一样的:

<!-- App.vue -->
<h1>v-text</h1>
<div v-text="message"></div>

v-html

有些时候,我们在data中定义的字段会带有HTML的标签,面对这样的场景,如果使用{{}}插值语法或者v-text指令,都会将数据中的HTML当做字符串插入到模板中。这并不是我们期望要的结果。这个时候,使用v-html指令可以解决。v-html指令会更新元素的innerHTML。比如下面这个示例:

<!-- App.vue -->
<h1>v-html</h1>
<div>{{ rawHtml }}</div>
<div v-html="rawHtml"></div>

<script>
export default {
    name: 'app',
    data () {
        return {
            rawHtml: 'Hello, <strong>Vue (^_^) </strong>'
        }
    }
}
</script>

在使用v-html相当于在网站上动态渲染任意HTML是非常危险的,因为容易导致XSS攻击。只在可信内容上使用v-html,永不用在用户提交的内容上。另外在单文件组件里,scoped的样式不会应用在v-html内部,因为那部分HTML没有被Vue的模板编译器处理。如果想对v-html的内容设置带作用域的CSS,可以替换为CSS Modules或用一个额外的全局<style>元素手动设置样式。如果你想在scoped设置v-html内标签元素的样式,需要借助Vue的另外特性,比如 >>>/deep/来设置样式。比如上例中的strong样式,我们可以像下面这样来设置:

<style scoped>
    div >>> strong {
        color: red;
    }

    div /deep/ strong {
        color: green;
    }
</style>

有关于这方面更详细的介绍可以阅读早前整理的一篇博文《Vue中的作用域CSS和CSS模块的差异》。有关于v-textv-html指令更详细的介绍可以阅读《v-textv-html》一文。

v-once

我们知道{{ message }}语法可以将data中的message插入到Vue的模板(或组件)中。当数据中的message发生变更时,模板中对应的值也会发生变更,比如:

<!-- App.vue -->
<h1>v-once</h1>
<div>{{ message }}</div>
<div v-once>{{ message }}</div>
<button @click="changeMessage">修改消息</button>

<script>
    export default {
        name: 'app',
        data () {
            return {
                message: 'Hello Vue (^_^)'
            }
        },
        methods: {
            changeMessage () {
                this.message = 'Hello, 大漠 !'
            }
        }
    }
</script>

当你点击修改消息按钮时,没有使用v-once指令的{{ message }}中的message值会发生变化,反之却不会有任何更新,如下图所示:

上面的示例再次验证了,v-once指令只渲染元素和组件一次。随后的重新渲染,元素或组件及其所有子节点将被视为静态内容并跳过。这可以用于优化更新性能。

v-bind

Vue插值语法只能在HTML标签中内容工作,而HTML属性中是不能使用它。如果要动态地给HTML标签绑定属性,需要使用v-bind。比如:

<!-- App.vue -->
<a :href="url">{{ linkText }}</a>

v-bind还有一个简写的方式,使用:来替代:

<a v-bind:href="url">{{ linkText }}</a>
<a :href="url">{{ linkText }}</a>

这两种写法是等效的。

v-bind不仅仅绑定一个物性,还可以绑定多个物特或一个组件prop到表达式。在绑定classstyle特性时,支持其它类型的值,比如数组对象。在绑定prop时,prop必须在子组件中声明。可以用修饰符指定不同的绑定类型。没有参数时,可以绑定到一个包含键值的对象。

<!-- 绑定一个属性 -->
<img v-bind:src="imageSrc">

<!-- 动态特性名 (2.6.0+) -->
<button v-bind:[key]="value"></button>

<!-- 缩写 -->
<img :src="imageSrc">

<!-- 动态特性名缩写 (2.6.0+) -->
<button :[key]="value"></button>

<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName">

<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">

<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>

<!-- 绑定一个有属性的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- 通过 prop 修饰符绑定 DOM 属性 -->
<div v-bind:text-content.prop="text"></div>

<!-- prop 绑定。“prop”必须在 my-component 中声明。-->
<my-component :prop="someThing"></my-component>

<!-- 通过 $props 将父组件的 props 一起传给子组件 -->
<child-component v-bind="$props"></child-component>

<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>

比如上面的示例中,v-bind中还使用了修饰符,常见的修饰符主要有:

  • .prop:被用于绑定DOM属性
  • .camel:将kebab-case特性名转换为camelCase
  • .sync:这是一个语法糖,会扩展成一个更新父组件绑定值的v-on侦听器

注意,在使用字符串模板或通过vue-loadervueify编译时,不需要使用.camel修饰符。

有关于v-bind指令更详细的介绍可以阅读《v-bind》一文。

v-model

对Vue有一定了解的同学,都知道Vue中可以轻易的实现数据的双向绑定。v-model主要用在表单控件或者组件上创建双向绑定。也就是说,v-model可以绑定一个输入表单(比如一个input),当用户更改input输入的内容时,对应元素的内容也会更新。比如下面这个示例:

<!-- App.vue -->
<input v-model="message" placeholder="输入一个信息" />
<div>你输入的信息:{{ message }}</div>

<select v-model="selected">
    <option disabled value="">选择你想要的选项</option>
    <option>CSS</option>
    <option>JavaScript</option>
    <option>Node</option>
    <option>HTML</option>
</select>
<div>你喜欢选项:{{ selected }}</div>

使用v-model指令的时候,也可以添加一些修饰符,比如:

  • .lazy:取代input监听change事件
  • .number:输入字符串转为有效的数字
  • .trim:输入首尾空格过滤

有关于v-model更详细的介绍可以阅读早前整理的一篇学习笔记《v-model》。

v-show

使用v-show指令可以根据Vue实例的某个特定属性的值(该值是一个条件值,它的值是truefalse)来展示元素。 当值为true时,DOM元素显示;反之为false时,该元素不显示。

<!-- App.vue -->
<div v-show="isTrue">{{ message }}</div>
<button @click="toggleShow">{{ isTrue ? '隐藏' : '显示' }}</button>

在Vue中使用v-show指令时,当Vue实例中某个特定属性值为false时(比如上例中的isTruefalse)时,div元素{{ message }}并不会在DOM中渲染,相当于该元素设置了display: none的效果。 简单地说,v-show只是简单地切换元素的CSS属性display(即blocknone之间的切换)

注意,v-show不支持<template>无素,也不支持v-else

v-ifv-else-ifv-else

v-ifv-else-ifv-else指令属性条件渲染。v-if指令用于条件性的渲染一个元素或一块内容。当Vue实例中data的某个属性值为true时,对应的元素或内容块就会被渲染。另外还可以配合v-else-ifv-else指令添加另外一块显示内容。有点类似于JavaScript中的:

if (conditions A) {
    content A // 当conditions A为true时,显示content A
} else if (conditions B) {
    content B // 当conditions B为true时,显示content B
} else {
    content C // 当 conditions A 和 conditions B 都为 false 时显示 content C
}

在Vue中,我们可以像下面这样使用:

<!-- App.vue -->
<div v-if="isTrue">Hello, {{ message }} !</div>
<div v-else>Hi, {{ message }} !</div>
<button @click="toggleShow">切换</button>

使用v-if的时候,还可以使用在<template>元素上。这样就可以根据条件渲染分组。比如:

<template v-if="isTrue">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
</template>

另会,在使用v-if的时候,还可以使用key管理可复用的元素,而且可以更高效地渲染。例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" />
</template>
<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" />
</template>

在上面的代码中切换loginType将不会清除用户已输入的内容。因为两个模板使用了相同的元素,<input>不会被替换掉,仅仅替换了它的placeholder。事实上这两个元素是完全独立的,不要复用它们。只需要添加一个具有唯一值的key属性即可:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input" />
</template>
<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input" />
</template>

v-showv-if指令看上去都是根据条件来显示(渲染)或隐藏元素。但他们之间还是有一定的差异的:

  • v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
  • v-if也是惰性的,如果在初始渲染时条件为false时,则什么也不做,直到条件第一次为true时,才会开始渲染条件块
  • v-show相对要简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS的display属性进行切换

一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好。

有关于v-ifv-show指令更详细的介绍可以阅读早前整理的相关学习笔记《v-ifv-show》。

v-for

v-for指令是基于数据多次渲染元素或模板块。该指令必须使用特定语法 alias in expression,为当前遍历的元素提供别名:

<!-- App.vue -->
<ul>
    <li v-for="item in items">{{ item }}</li>
</ul>

data () {
    return {
        items: ['CSS', 'JavaScript', 'HTML', 'TypeScript']
    }
}

另外也可以为数组索引指定别名(或用于对象的键):

<div v-for="(item, index) in items"></div>
<div v-for="(val, key) in object"></div>
<div v-for="(val, key, index) in object"></div>

v-for默认行为试着不改变整体,而是替换元素。迫使其重新排序的元素,你需要提供一个key的特殊属性:

<div v-for="item in items" :key="item.id">
    {{ item.text }}
</div>

自V2.6版本起,v-for也可以在实现了 可迭代协议的值上使用,包括原生的MapSet。另外,当和v-if一起使用时,v-for的优先级比v-if更高。

有关于v-for更多的介绍还可以阅读:

v-on

v-on指令允许你侦听DOM事件,并在事件发生时触发一个方法。

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!--  串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

v-on用在普通元素上时,只能监听 原生DOM事件。用在自定义元素组件上时,也可以监听子组件触发的 自定义事件

在使用v-on指令帮定事件时,也可以附上一些修饰符,和v-on配合在一起常见的修饰符主要有:

  • .stop: 调用 event.stopPropagation()
  • .prevent: 调用 event.preventDefault()
  • .capture: 添加事件侦听器时使用 capture模式
  • .self: 只当事件是从侦听器绑定的元素本身触发时才触发回调。
  • .{keyCode | keyAlias}: 只当事件是从特定键触发时才触发回调。
  • .native: 监听组件根元素的原生事件
  • .once: 只触发一次回调
  • .left: 只当点击鼠标左键时触发 (v2.2.0)
  • .right:只当点击鼠标右键时触发 (v2.2.0)
  • .middle:只当点击鼠标中键时触发 (v2.2.0)
  • .passive:以 { passive: true }模式添加侦听器 (v2.3.0)

另外,在使用v-on可以使用简写的方式,即用@来替代,比如下面这个示例,两者起到的作用是相同的:

<a v-on:click="handleClick">Click me!</a>
<a @click="handleClick">Click me!</a>

有关于Vue中v-on更详细的介绍,可以阅读早前整理的一篇学习笔记《v-on》。

v-pre

v-pre指令可以跳过这个元素和它的子元素的编译过程。可以用来显示原始 插值。跳过大量没有指令的节点会加快编译。

<!-- App.vue -->
<div v-pre>{{ message }}</div>

在使用v-pre时,不需要任何的表达式。

v-cloak

v-cloak指令保持在元素上直接到关联实例结束编译。和CSS规则 如 [v-cloak] {display: none}一起用时,这个指令可以隐藏未编译的插值标签,直到实例准备完毕。

<!-- App.vue -->
<div v-cloak>{{ message }}</div>

[v-cloak] {
    display: none
}

v-slot

v-slot指令是v2.6新增加的一个指令。结合了slotslot-scope的功能,同时也是scoped slots的简写。具体使用的示例如下:

<!-- 具名插槽 -->
<base-layout>
    <template v-slot:header>
        Header content
    </template>

    Default slot content

    <template v-slot:footer>
        Footer content
    </template>
</base-layout>

<!-- 接收 prop 的具名插槽 -->
<infinite-scroll>
    <template v-slot:item="slotProps">
        <div class="item">
            {{ slotProps.item.text }}
        </div>
    </template>
</infinite-scroll>

<!-- 接收 prop 的默认插槽,使用了解构 -->
<mouse-position v-slot="{ x, y }">
    Mouse position: {{ x }}, {{ y }}
</mouse-position>

有关于v-slot更详细的介绍,可以阅读前面整理的学习笔记《Vue新指令:v-slot

自定义指令

对Vue稍微有点了解的同学都知道,Vue虽然是用数据来驱动视图,但并非所有情况都能适合用数据来驱动视图(也就是对DOM的操作)。不过,Vue提供了一种指令的操作,可以对DOM进行操作。除了上面介绍的内嵌指令之外,还有自定义指令。而这种自定义指令可以用于定义任何的DOM操作,并且是可以复用的。

在Vue中的自定义指令主要分为全局指令局部指令

它们之间的共同点都是具有一个”指令名称“和”指令钩子函数“。在使用的时候和内嵌指令类似,以v-前缀开头。不同的是:

  • 全局指令是全局注册,在任何组件中都可以使用全局注册的自定义指令
  • 局部指令是局部注册,只能在当前组件使用该自定义指令

注册全局指令时,一般在项目的main.js中使用Vue.directive()来注册,比如:

<!-- main.js -->
Vue.directive('my-custom-directive', {
    // 自定义指令将要做的一些事情,
    // 使用自定义指令的钩子函数
})

就是这么简单地全局注册了一个名称叫my-custom-directive的Vue自定义指令。对于局部注册的自定义指令,是在对应Vue实例中的options选项中的directives中来完成,比如:

<!-- App.vue -->
export default {
    name: 'App',
    directives: {
        'my-custom-dirctive': {
            // 自定义指令要完成的事情
            // 自定义指令中的钩子函数
        }
    }
}

当然,为了更好的维护自定义的指令,在创建全局自定义指令的时候,还可以在src目录下创建一个独立的文件夹,比如directives,用来管理你需要的自定义指令。就上面的示例,我们可以在src/directives/下创建一个my-custom-directive.js文件,在这个文件中输入你需要的自定义指令要做的事情。比如:

<!-- my-custom-directive.js -->
export default {
    // 自定义指令要做的事情
    // 钩子函数
}

这种方式在注册全局自定义指令和局部自定义指令也略有不同。比如全局自定义指令,在main.js中先引入my-custom-directive.js,然后再使用Vue.directive()来注册,比如:

<!-- main.js -->
import MyCustomDirective from './directives/my-custom-directive.js'

Vue.directive('my-custom-directive', MyCustomDirective)

局部注册自定义指令如下:

<!-- App.vue -->
import MyCustomDirective from './directives/my-custom-directive.js'

export default {
    name: 'App',
    directives: {
        'my-custom-directive': MyCustomDirective
    }
}

注意,Vue自定义指令的名称可以随你喜欢的取,但最好还是取一些和功能更接近,带有语意化的名称。如果自定义指令取名时使用的是驼峰命名,比如MyCustomDirective,在使用的时候要将驼峰转为小写,并且每个单词之间使用-符号来连接,比如v-my-custom-directive

钩子函数和参数

在学习Vue实例时,我们知道每个Vue都有生命周期,而生命周期中有相应的钩子函数。Vue自定义指令有点类似,正如前面所提到的,使用Vue.directive(name,{})注册自定义指令时,除了第一个参数用来命名指令之外,第二个参数就是自定义指令的相应的钩子函数了。自定义指令该具备的特性(能做啥事)都是在这些钩子函数中完成的。

一个指令定义对象主要有以五个钩子函数,但这五个钩子函数可以根据自己的需要选用:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
  • update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

比如我们上面所说的my-custom-directive指令,将钩子函数插入到第二个对象参数当中,看起来就像下面这样:

<!-- main.js -->
Vue.directive('my-custom-directive', {
    bind: function(){
        // 指令第一次绑定到元素时调用,做绑定的准备工作
        // 比如添加事件监听器,或是其他只需要执行一次的复杂操作
    },
    inserted: function(){
        // 被绑定标签的父节点加入 DOM 时立即触发
    },
    update: function(){
        // 根据获得的新值执行对应的更新
        // 对于初始值也会调用一次
    },
    componentUpdated: function(){
        // 指令所在组件的 VNode 及其子 VNode 全部更新后调用,一般使用 update 即可
    },
    unbind: function(){
        // 做清理操作
        // 比如移除bind时绑定的事件监听器
    }
})

注意,五个钩子函数是可选的。在很多时候,你可能想在 bindupdate时触发相同行为,而不关心其它的钩子,那么可以简化写法:

Vue.directive('my-custom-directive', function(){
    // update 内的代码块
})

用张图来表示Vue自定义指令中的钩子函数:

特别声明,上图来自于@Sarah Drasner的《The Power of Custom Directives in Vue》一文。

Vue自定义指中所有的钩子函数会被复制到实际的指令对象中,而这个指令对象将会是所有钩子函数的this上下文环境。指令对象上暴露了一些有用的公开属性。也就是说,钩子函数中每一个都可以具有相同的参数,比如elbindingvnode,而且除了updatecomponentUpdated钩子函数之外,还可以暴露oldVnode参数,用于区分传递的旧值和新值。

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding: 一个对象,包含以下属性namevalueoldValueexpressionargmodifiers
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在updatecomponentUpdated钩子函数中可用

另外binding参数中包含属性相应的解释如下:

  • name:指令名,不包括 v-前缀
  • value:指令的绑定值,例如:v-my-custom-directive="1 + 1"中,绑定值为 2
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated钩子中可用。无论值是否改变都可用
  • expression:字符串形式的指令表达式。例如 v-my-custom-directive="1 + 1"中,表达式为 "1 + 1"
  • arg:传给指令的参数,可选。例如 v-my-custom-directive:foo中,参数为 "foo"
  • modifiers:一个包含修饰符的对象。例如:v-my-custom-directive.foo.bar中,修饰符对象为 { foo: true, bar: true }

除了 el之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset来进行。

我们来看一个小示例,通过这个小示例来了解每个钩子函数怎么运行。比如我们来模仿一个v-show指令,从前面介绍的内容我们可以得知,v-show指令等于给DOM元素添加了display:none的效果,用来控制DOM元素显示和隐藏。

<!-- main.js -->
Vue.directive('my-show', {
    // 只调用一次,指令第一次绑定到元素时调用
    bind: function (el, binding, vnode) {
        console.log('bind钩子函数 =>el', el)
        console.log('bind钩子函数 =>binding', binding)
        console.log('bind钩子函数 =>vnode', vnode)

        el.style.display = binding.value ? 'block' : 'none'
    },

    // 被绑定元素插入父节点时调用
    inserted: function (el, binding, vnode) {
        console.log('inserted 钩子函数 =>el', el)
        console.log('inserted 钩子函数 =>binding', binding)
        console.log('inserted 钩子函数 =>vnode', vnode)
    },

    // 所在组件的 VNode 更新时调用
    update: function (el, binding, vnode, oldVnode) {
        console.log('update 钩子函数 el=>', el)
        console.log('update 钩子函数 binding=>', binding)
        console.log('update 钩子函数 vnode=>', vnode)
        console.log('update 钩子函数 oldVnode=>', oldVnode)

        el.style.display = binding.value ? 'block' : 'none'
    },

    // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
    componentUpdated: function (el, binding, vnode, oldVnode) {
        console.log('componentUpdated 钩子函数 el=>', el)
        console.log('componentUpdated 钩子函数 binding=>', binding)
        console.log('componentUpdated 钩子函数 vnode=>', vnode)
        console.log('componentUpdated 钩子函数 oldVnode=>', oldVnode)
    },

    // 指令与元素解绑时调用
    unbind: function (el, binding, vnode) {
        console.log('unbind 钩子函数 el=>', el)
        console.log('unbind 钩子函数 binding=>', binding)
        console.log('unbind 钩子函数 vnode=>', vnode)
    }
})

我们调用my-show指令时,可以像下面这样:

<!-- App.vue -->
<div v-my-show="isTrue">{{ message }}</div>

从效果上我们可以看出来,这个自定义指令v-my-show和Vue内置指令v-show是等效的。通过上面的小示例,我们对自定义指令中五个钩子函数的触发时机有了初步的认识。有疑问的是bindinsertedupdatecomponentUpdated的区别。

先来看bindinserted两个指令之间的区别。为了更好的看出他们之间的差异,在上面的指令的钩子函数中添加一段测试代码:

bind: function (el, binding, vnode) {
    console.log('bind钩子函数 =>el', el)
    console.log('bind钩子函数 =>binding', binding)
    console.log('bind钩子函数 =>vnode', vnode)

    el.style.display = binding.value ? 'block' : 'none'
    el.style.color = 'red'
},

// 被绑定元素插入父节点时调用
inserted: function (el, binding, vnode) {
    console.log('inserted 钩子函数 =>el', el)
    console.log('inserted 钩子函数 =>binding', binding)
    console.log('inserted 钩子函数 =>vnode', vnode)
    el.style.color = 'green'
},

bind钩子函数中设置了:

el.style.color = 'red'

inserted钩子函数中设置了:

el.style.color = 'green'

其中el.style.colorbind钩子函数中并未生效,这是因为bind钩子函数被调用时,bind的第一个参数el拿到对应的DOM元素,但此时DOM元素并未插入到DOM中,因此在这个时候el.style.color并未生效。哪怕是你在加载页面的一瞬间,你也看到的红色的文本。

当DOM元素被插入到DOM树中时,inserted钩子函数就会被调用(生效),此时,inserted钩子中的el.style.color就被执行,也就生效了。在客户端看到的文本颜色是绿色(green)。

虽然我们的指令中写了五个钩子函数,但从页面加载到页面加载完,我们只能看到bindinserted两个钩子函数对应输出的内容,如下图所示:

接着我们再来看看updatecomponentUpdated两个钩子函数的区别。为了更好的向大家演示他们之间的差异,同样在这两个钩子函数中来设置文本的颜色:

// 所在组件的 VNode 更新时调用
update: function (el, binding, vnode, oldVnode) {
    el.style.display = binding.value ? 'block' : 'none'
    el.style.color = 'orange'
},

// 指令所在组件的 VNode 及其子 VNode 全部更新后调用
componentUpdated: function (el, binding, vnode, oldVnode) {
    el.style.color = 'lime'
},

按钮来回点击时,看到的效果如下:

从效果上看,在update指令上设置的文本颜色,也就是橙色并没有看到,而componentUpdated指令上设置的文本颜色可以看到。简单地说,update钩子函数触发时机是自定义指令所在组件的 VNode 更新时, componentUpdated触发时机是指令所在组件的 VNode 及其子 VNode 全部更新后。此处使用 el.style.color设置文本颜色值,从运行结果上看 updatecomponentUpdated是 DOM 更新前和更新后的区别。

将上面介绍的内容浓缩到一个知识图谱中来,如下所示:

纠正:上图中的v-clock应该是v-cloak。谢谢@E0大大指正!

自定义指令示例

了解了Vue中的自定义指令是怎么一回事,来整个示例,加强一下这方面的练习,通过实例更好的来理解Vue的自定义指令。首先来看一个图片延迟加载的案例。

图片延迟加载

在Web中,图片延迟加载是非常常见的一种技术。如果你从未接触过,可以先阅读下面这些文章来了解Web应用中图片延迟加载的相关原理:

使用Vue自定义指令,声明一个lazyload的指令:

<!-- main.js -->
Vue.directive('lazyload', {
    inserted: el => {
        function loadImage () {
            const imageElement = Array.from(el.children).find(el => el.nodeName === 'IMG')

            if (imageElement) {
                imageElement.addEventListener('load', () => {
                    setTimeout(() => el.classList.add('loaded'), 100)
                })
                imageElement.addEventListener('error', () => console.log('error'))
                imageElement.src = imageElement.dataset.url
            }
        }

        function handleIntersect (entries, observer) {
            entries.forEach(entry => {
                if (!entry.isIntersecting) {
                    return
                } else {
                    loadImage()
                    observer.unobserve(el)
                }
            })
        }

        function createObserver () {
            const options = {
                root: null,
                threshold: '0'
            }

            const observer = new IntersectionObserver(handleIntersect, options)

            observer.observe(el)
        }

        if (!window['IntersectionObserver']) {
            loadImage()
        } else {
            createObserver()
        }
    }
})

我们在main.js中声明了Vue指令,在加载图片的时候,我们可以在图片的容器上使用v-lazyload来实现图片的延迟加载效果:

<!-- App.vue -->
<div class="box" v-lazyload v-for="(item, index) in imageSoures" :key="index">
    <div class="ripple">
        <div class="ripple__circle"></div>
        <div class="ripple__circle ripple__inner-circle"></div>
    </div>
    <img :data-url="item.url" :alt="item.title" />
</div>

data () {
    return {
        imageSoures: [
            {
            title: 'Homee Sales Surge',
            url: '//www.nar.realtor/sites/default/files/condo-building-GettyImages-152123643-1200w-628h.jpg'
            },
            // ...
        ]
    }
}

具体效果如下:

该示例详细的代码可以在Github上app-vue-directives仓库的step3分支查看。

有关于Vue针对图片延迟加载相关的自定义指令更详细的介绍可以阅读:

小结

这篇文章主要介绍了Vue的指令。从Vue的内置指令v-textv-htmlv-showv-on等入手。了解了内置指令的使用和相应的作用。但很多时候,这些内置的指令并不能完全满足我们来操作DOM。但Vue还提供了自定义指令的机制,通过这篇文章的学习,我们知道自定义指令的钩子函数和相应的在数,以及如何用这些自定义指令的钩子函数来创建我们所需要的指令。通过自定义指令让我们更好的操作DOM。最后希望这篇文章对初学Vue的同学有所帮助,如果您在这方面有更好的经验欢迎在下面的评论中与我们共享。

CSS技巧(01)

$
0
0

从这周开始,我将会把每周看到有关于CSS有意思的技巧整合成一篇文章。将会在每周的星期天整理发布,每篇文章中将会以CSS的技巧为主线进行介绍,但每个技巧不会深入的阐述。主要目的给对CSS感兴趣的同学增强CSS的眼界,扩大知识面和使用场景。同时也希望能帮助大家将一些CSS技巧运用到实际项目中,另外提高自己在这方面的技术。如果感兴趣的话,可以持续关注,或者有你相关的技巧也可以和我们一起共享。

响应式图片

近来对于响应式设计的热度不怎么多,但事实上在Web的响应式设计中,响应式图片的处理一直都是一个难题,也是较为难处理。虽然有关于响应式图片的处理有难度,但社区一直就没有停过对其探讨。从最的简陋和最见效的方式:

img {
    max-width: 100%;
    height: auto;
}

这个方案虽然有效,也最为直接,但对于用户而言是最不佳的。特别是面对现在场景(终端设备众多),为了高清显示,不管三七二十一,都会加载最高清的图片。如此一来,对于高端设备而言没有太大问题,但对于低端设备而言就过于浪费了。除了加载慢还浪费客户的带宽。

而对于Web而言,图片对用户又非常有吸引力,那么我们在使用图片的时候就必须考虑:

  • 图片格式
  • 图片大小(容器)
  • 渲染尺寸(浏览器中布局的宽度和高度)
  • 图片尺寸(图片原宽高)
  • 纵横比(宽高比)

对于开发者而言,如何正确的选择这些或者更好的混合使用,为用户提供最佳的体验。要知道答案其实也并不难,可以尝试着下面这些问题的答案来进行选择:

  • 图像是动态的(用户创建)还是静态的(设计团队创建)的?
  • 图像的宽高比不成比例变化会影响图片质量吗?
  • 是否所有图像都在相同的宽度和高度中渲染?渲染时,它们必须有一个特定的长宽比还是一个完全不同的长宽比?
  • 在不同的视窗中显示图像时需要考虑什么?

具体操作时,把这些问题的答案记录下来,这样不仅可以助于你理解图像,还能有助于你做出正确的选择。

对于图像来源(动态还是静态)很好确定,而其中静态来源更好的处理,但如果图像资源是动态的,相对而言是较为复杂的。下面有一个例子值得我们研究分析,来确定最重要的设备和视窗大小。

在@Jo Franchetti的博文《Common Responsive Layouts with CSS Grid》中介绍了如何通过grid-template-columns来让布局具有适应性(响应式),也就是说借助该技巧,再配合max-width:100%,我们就可以:

#app {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    grid-gap: 10px;

    img {
        max-width: 100%
    }
}

虽然上面的示例让我们可以不借助媒体查询实现响应式布局,但我们依旧还是使用一张图片来匹配不同终端,始终还是有一定的伪合度:

而我们真正想要的,或者更佳的方案是:

根据设置的不同,提供不同的图片资源;或者根据屏幕高清度(dpr)不同,提供不同的图片资源。一句话就是,给你的用户提供正确的图像!

实现上面的需求,可以通过下面的方案来达到我们的诉求:

  • 使用<img>元素加载图像,使用该元素最新属性srcsetsize给用户加载正确的图像
  • 使用<picture>元素给用户加载正确的图像

比如<img />

再为看<picture>

扩展阅读

纵横比aspect-ratio

CSS有一个常见的问题无法根据长宽比来调整大小。特别是在处理响应式设计时,通常希望能够将宽度设置为百分比,并使用高度对应于某个纵横比。针对该问题,也是负责设计CSS的人员(即CSS工作组)一直在讨论的问题,最近在旧金山举行的CSSWG会议上针对这方面的解决方案的提议得到了一致性的通过。

在该提议之前,Web开发人员一直以各种方式处理纵横比,其中最主要的方案是借助paddingpadding-toppadding-bottom)设置一个百分比值。原理非常的简单,padding的百分比是根据元素的width来计算的。如此一来就可以达成到我们想要的效果(模拟出纵横比效果)。除了这种方案,CSS中还有其他的一些技巧可以实现长宽比。有关于这方面的介绍,在互联网上也较多,比如:

虽然上述提到的方案可以实现纵横比,但大家还在寻找一个通用的解决方案。W3C的CSS Sizing 4 specification规范中就提供了这样的通用解决方案。规范中提供了aspect-ratio属性。该属性将接受一个长宽比的值,比如16/9。如果想要一个宽度和高度相同的盒子,可以像下面这样使用:

.box {
    width: 400px;
    height: auto;
    aspect-ratio: 1/1;
}

如果希望是一个16/9的盒子,只需要把aspect-ratio的值设置为16/9

.box {
    width: 100%;
    height: auto;
    aspect-ratio: 16/9;
}

另外,该属性要是结合在CSS的Grid布局中的话,可以轻易地实现每个元素根据容器宽度来调整自身的高度,比如:

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

.item {
    aspect-ratio: 1/1;
}

aspect-ratio属性能被纳入W3C的规范,我们都需要感谢@Jen Simmons的努力付出,她是第一位提出该方案的,有关于详细的介绍,可以阅读这份PPT

有关于这方面更详细的介绍也可以阅读@Rachel Andrew 的博文《Designing An Aspect Ratio Unit For CSS》。

嵌套选择器

CSS选择器虽然有很多种,但一直以来都没有嵌套选择器。但在CSS处理器中这已经是非常成熟的一种技术了。不过本月初,CSS嵌套选择器发被定义为CSS嵌套模块,而且发布了第一个草案。该草案概述了一种未来的机制,通过这种机制,我们将来能够在本地嵌套CSS选择器。

比如下面这样的示例:

.foo {
    color: blue;

    & > .bar { 
        color: red; 
    }
}
/* 等效于 */
.foo { 
    color: blue; 
}
.foo > .bar { 
    color: red; 
}


.foo {
    color: blue;

    &.bar { 
        color: red; 
    }
}
/* 等效于 */
.foo { 
    color: blue; 
}
.foo.bar { 
    color: red; 
}

.foo, .bar {
    color: blue;

    & + .baz, &.qux { 
        color: red; 
    }
}
/* 等效于 */
.foo, .bar { 
    color: blue; 
}
:matches(.foo, .bar) + .baz,
:matches(.foo, .bar).qux { 
    color: red; 
}

使用嵌套选择器时,&符不可缺,不然是无效的,比如:

.foo {
    color: red;

    .bar { 
        color: blue; 
    }
}

另外还可以使用 @nest规则:

.foo {
    color: red;

    @nest & > .bar {
        color: blue;
    }
}
/* 等效于 */
.foo { 
    color: red; 
}
.foo > .bar { 
    color: blue; 
}


.foo {
    color: red;

    @nest .parent & {
        color: blue;
    }
}
/* 等效于 */
.foo { 
    color: red; 
}
.parent .foo { 
    color: blue; 
}


.foo {
    color: red;

    @nest :not(&) {
        color: blue;
    }
}
/* 等效于 */
.foo { 
    color: red; 
}
:not(.foo) { 
    color: blue; 
}

使用@nest规则时也需要注意,不然也会不生效,比如下面这两个示例代码就是不会生效的选择器:

.foo {
    color: red;

    @nest .bar {
        color: blue;
    }
}

.foo {
    color: red;

    @nest & .bar, .baz {
        color: blue;
    }
}

这是原生的嵌套选择器,不需要借助于任何CSS处理器。注意,该选择器更类似于Sass的嵌套。

扩展阅读

动态生成内容

CSS的世界当中,要生成内容都是依赖于content属性来完成,一般配合伪元素::before::after来完成。除了在content中显式的设置要显示的内容之外,还可以借助attr()属性来获取HTML标签元素上的属性的值。今天要给大家介绍的是另一种方式。借助于CSS计数器的属性counter-reset再配合CSS自定义属性,我们可以做更多的事情。比如下面这个示例,可以在Icon添加一个数字:

<div class="icon">
<svg width="100%" viewBox="0 0 24 24">
    <path d="M4,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4M12,11L20,6H4L12,11M4,18H20V8.37L12,13.36L4,8.37V18Z"></path>
</svg>
</div>

.icon {
    --number-var: 23;
}
.icon::before {
    counter-reset: number var(--number-var);
    content: counter(number);
}

结果如下:

要是借助CSS自定义属性的API,我们可以动态的改变数值:

let styles = getComputedStyle(document.querySelector('.icon'));  
let increaseValue = Number(styles.getPropertyValue('--number-var')) + 1; 
let decreaseValue = Number(styles.getPropertyValue('--number-var')) - 1; 

document.getElementById('increase').addEventListener('click', function(e){
    document.querySelector('.icon').style.setProperty('--number-var', increaseValue++)
})

document.getElementById('decrease').addEventListener('click', function(e){
    document.querySelector('.icon').style.setProperty('--number-var', decreaseValue--)
})

你点击+号或-号按钮都会改变图标上的数字:

扩展阅读

下拉菜单

怎么做下拉菜单对于前端的同学而言已不是什么新事物了,但今天向大家介绍另类的方式,那就是借助HTML5的<details><summary>元素来做一个可点击的下拉菜单。比如下面这个示例:

其实使用<details><summary>元素除了可以做下拉菜单之外还可以做手风琴和对话框

扩展阅读

滚动的另类使用

在《改变用户体验的滚动新特性》和《滚动的特性》两篇文章中介绍了滚动条的一些特性以及带来的用户体验。这些属性对于当今来说都是一些新特性,有些浏览器都还不支持。但这些特性真的很有意思。比如下面这样的一个Demo效果:

当你滚动页面的时候,可以看到卡通人物不断的在更换衣物:

如果不看代码,估计很多同学都会以为这是JavaScript完成的,事实上是纯CSS完成的,而且关键代码就两行:

.main-content {
    scroll-snap-type: y mandatory;
    overflow-y: scroll;
}

section {
    scroll-snap-align: center;
}

简单地说就是CSS捕捉的技术

为了给用户最好的体验,都追求流畅,比如滚动效果,使用scroll-behavior: smooth让滚动流畅。除了使用CSS之外,还可以使用JavaScript来达到相应的效果:

window.scrollTo({
    top: document.body.getBoundingClientRect().height,
    behavior: 'smooth'
});

但上面的方法,我们无法控制滚动的速度。@Jedidiah Hurt发现了一个小技巧,可以改变这一切。借助CSS的transform来帮助我们完成:

const scrollElement = (element, scrollPosition, duration) => {

    // useful while testing to re-run it a bunch.
    // element.removeAttribute("style"); 

    const style = element.style;
    style.transition = duration + 's';
    style.transitionTimingFunction = 'ease-in-out';
    style.transform = 'translate3d(0, ' + -scrollPosition + 'px, 0)';
}

scrollElement(
    document.body, (
        document.body.getBoundingClientRect().height - document.documentElement.clientHeight + 25
    ),
    5
);

JS in CSS

JS in CSS对于很多同学来说是新东西。或许很多同学会说,应该是CSS in JS吧。事实上真是JS in CSS。也就是说,以后在CSS有世界中,不仅仅是写CSS了,可以在CSS中写JS。是不是很神奇。这样一来,我们可以轻易的扩展CSS。当然,有接触过CSS Paint的同学立马就会想到,应该是采用这种技术吧。事实上是这样的,不同的是,在CSS的代码中可以写registerPaint相关的paint函数。所以看起来是JS in CSS。

在CSS代码中直接编写registerPaintpaint函数时,可以接受:

  • ctx,2D渲染上下文
  • geom,元素的几何形状

甚至还可以编写自己的CSS自定义属性,并在JavaScript中使用。

<!-- CSS -->
.el {
    --color: cyan;
    --multiplier: 0.24;
    --pad: 30;
    --slant: 20;
    --background-canvas: (ctx, geom) => {
        let multiplier = var(--multiplier);
        let c = `var(--color)`;
        let pad = var(--pad);
        let slant = var(--slant);

        ctx.moveTo(0, 0);
        ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
        ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
        ctx.lineTo(0, geom.height);
        ctx.fillStyle = c;
        ctx.fill();
    };
    background: paint(background-canvas);
    transition: --multiplier .4s;
}
.el:hover {
    --multiplier: 1;
}

<!-- JS: registerPaint module -->
registerPaint('background-canvas', class {
    static get inputProperties() {
        return ['--background-canvas'];
    }
    paint(ctx, geom, properties) {
        eval(properties.get('--background-canvas').toString())(ctx, geom, properties);
    }
})

效果如下:

上例是CSS Houdini仓库中提供的一个示例

@Una Kravets在Coepen上写了一个更复杂的示例:

有关于这方面更多的示例还可以点击这里查看,对于的代码可以在Github上查看

如此一来,我们可以借助JavaScript的能力,发挥自己的创意,在CSS中做更多有意思的事情。如果你在这方面有较好的创意,欢迎在下面的评论中与我们一起分享。

扩展阅读

Flexbox的计算工具

Flexbox布局已经是非常成熟的布局,但对于flex-growflex-shrinkflex-basis三个属性的演算(算法)的理解还是较为复杂的。推荐一个在线工具Flexulator给大家,帮助大家更好的理解这三个属性以及他们之间的演算。

通过上面的工具,再结合《聊聊Flexbox布局中的flex的演算法》一文,你会更彻底的了解flex-growflex-shrinkflex-basis属性。有关于Flexbox布局更多的教程,可以点击这里阅读

小结

今天介绍了一些小技巧,主要涉及到响应式图片的处理、纵横比属性aspect-ratio、嵌套选择器、自定义属性配合计数器动态生成内容、借助HTML5的<details><summary>元素来做一个可点击的下拉菜单,滚动特性以及JS-in-CSS。其中aspect-ratio、 嵌套选择器和Js-in-CSS是较新的特性,特别是后两者,应该是最有为意思的部分。

最后希望这些小技巧能帮助大家增长见识,也希望大家能喜欢。如果你有更好的建议或相关的经验欢迎大家在下面的评论中与我们一起分享。特别推荐,大家要是有时间可以尝试使用JS-in-CSS,发挥您的创意,制作一些有意思的作品。

Viewing all 1557 articles
Browse latest View live