于2022年的CSS新特性,自己之前也有篇原创,CSS 的未来:Cascade Layers (CSS @layer),专门是在介绍@layer(级联层)的文章,而新的一年会有更多的CSS的新特性会出现在浏览器中。
今天分享一篇大漠老师的文章,主要内容是2022年哪些CSS特性将在浏览器中出现,以下是正文。
原文: https://www.w3cplus.com/css/what-is-new-css-in-2022.html
作者:大漠老师,请联系作者获得授权,非商业转载请注明出处。
虽然近些年 CSS 变化很快,但我认为 2021 年是 CSS 的元年。在即将过去的 2021 年,CSS 变化非常地大,其中新增了很多特性,比如 CSS 容器查询、CSS 父选择器、CSS 层叠控制规则、CSS 子网格等等。而且这些特性已经在个别,甚至是在大部分主流浏览器已经可以看到了。几大主流浏览器(Chrome、Firefox、Safari和Edge)还专注于修复浏览器兼容性痛点,让 Web 开发者的工作变得更轻松。
浏览器兼容性方面的修复,在 Web.dev 上发布了 2021 年的年中和年终报告,报告虽然简单,但我们可以看到浏览器厂商都在致于力磨平 CSS 特性在浏览器上的兼容性。为此,我们应该感谢他们的付出!
随着 2021 年的结束,让我们一起来看看 2022 年,我们可以期待哪些 CSS 特性将会在浏览器中出现。这了是我连续第三年整理有关于 CSS 新特性方面的文章了,另外两篇是(可自行搜索):
你即将看到是 2022 年的,你会发现这三篇文章提到的 CSS 特性有相同的,但他们也是有变化的。
@Bramus 前两天在Twitter 上发了一条信息,他整理了一份 2022 年的将会出现在浏览器的 CSS 清单:
有关于清单中列出的 CSS 相关介绍,可以阅读他的文章:《CSS In 2022》
这份清单中列出的 CSS 特性和 @Adam Argyle 分别在 2020 年 和 2021 年分享的 CSS 新特性列的出的清单差不多,不同的是,在2021年,有些CSS特性已经得到了浏览器的支持,有些已经进入了浏览器的实验属性列表清单,说不定2022年,你就能看到了!
有关于 @Adam Argyle 在 2020 和 2021 分享的 CSS 新特性,我也曾整理过一份,清单有些类似,但内容还是有差异,主要涵盖了一些我自己在 CSS 方面的认识和尝试,分别记录在《2020年CSS有哪些新特性》和《2021年你可能不知道的 CSS 特性》以及《应用于下一代Web的样式》
当然,如果你也是 CSS 的忠实爱好者,不断的在关注和推进 CSS 向前发展,那么你可能已经知道了,自 2019 年开始,每年年低都会有一份关于 CSS 发展状态相关的报告(2019、2020 和 2021)呈现给你。在每年的报告中,我们都可以看到一项关于”开发者期待的CSS特性“的统计:
在第一份报告出来的时候,我特意还花了一点时间去理解这份有关于 CSS的报告:《从9102年的CSS状态报告中看CSS特性的使用》,另外有幸运在 2020年年底和一些大学生一起聊了一个关于CSS状态和学习的话题!如果你对该话题感兴趣的话,可以阅读《CSS现状和如何学习》,文章中附有分享时录制的视频!
除此之外,W3C 的 CSS 工作小组,这两年也都出了 CSS 规范的重点报告,最新的是 2020年 和 2021年的。注意,W3C 关于 CSS 规范的重点报告并不是每年都有的!接下来,我将按着 @Bramus 的 《CSS In 2022》大纲往下梳理,其中会加入一些我自己对这方面的认识和见解。
我需要额外提出的是,接下来内容中提到的 CSS 特性只会涉及到那些全新的或仍然没有得到浏览器支持的(哪怕是实验性方面的)特性。但这并不代表着它们不值得期待,说不定在2022年你就能使用了!如果你对这方面感兴趣的话,请继续往下阅读!
在这节中提到的 CSS 特性是近些年来,Web 开发者一直期待的特性(或者说 CSS 中遗失的特性)。这些特性将在 2022 年的某个时候就得到了主流浏览器的支持。庆幸的是,有些特性已经在一个或多个主流浏览器中得到了支持,其他特性也将随着时间的推移也会随之而来。在 2022 年,学习或了解这些 CSS 特性,我想你会获得收益的!
有意思的是,CSSWG一直还维护着一份关于 CSS 设计中不完整的错语列表。感兴趣的可以点击这里阅读!
CSS 容器查询的特性一直以来都是 Web 设计师和开发者所期待的功能之一,从这几年的 CSS 发展状态的报告就可以看得出来。在没有容器查询之前,Web 开发者大多是依赖于 JavaScript 脚本来做判断,从而改变 UI 网格。虽然 CSS 容器查询存在于 CSS Containment Module 中有些年头,但一直未得到浏览器的支持。庆幸的是,在 2021 年,该特性得到了飞速的发展,时至今日,能在主流浏览器中看到 CSS 容器查询功能。这一切,都离不开 @TerribleMia ,是她为我们带来了这么好的 CSS 特性,并且一直在推进该特性向前发展。
@TerribleMia 除了设计 CSS 容器查询特性之外,还设计了 CSS 另外两个非常优秀的特性,那就是 CSS 的 @layer 和 @scoped 两个 @ (At rule)规则。详细的可以阅读《Miriam's CSS Sandbox》。
CSS 容器查询 @container 有点类似于 CSS 的媒体查询 @media ,只是它将根据元素的父容器(或祖先元素)的尺寸(size)或样式(style)来调整自己或自己后代元素的样式规则。在没有 CSS 容器查询,Web 开发者为了能在不同容器下调整 UI,大多都是依赖于媒体查询来做。也就是说,有了该特性之后,不需要再依赖视窗大小加添加类名的方式来调整UI了:
上图演示了,基于视窗的设计和基于容器的尺寸设计。可以说, CSS 容器查询的出现,除了改变 Web 开发者的开发方式之外,也将给 Web 设计带来变革命。这个论点在 2021 年的 GDS 大会上,@Una Kravets 在分享的 《The New Responsive: Web design in a compoent-driven world》主题(该主题视频可以在 YouTube上访问)中就提出这样的观点:
CSS 容器查询将会成为下一代响应式 Web 设计必不可少的特性之一。
有了 CSS 容器查询之后,我们就可以基于同一个组件,在不同尺寸下调整其UI,比如我们手淘 APP 顶部的搜索框效果:
上面效果的关键性代码如下:
.form__container {
container: inline-size;
}
.form {
display: grid;
align-items: center;
}
@container (min-width: 480px) {
.form {
grid-template-columns: min-content 1fr 200px;
grid-template-areas: "searchIcon searchInput button";
grid-template-rows: 88px;
gap: 10px;
}
}
@container (min-width: 768px) {
.form {
grid-template-columns: min-content 1fr min-content 200px;
grid-template-areas: "searchIcon searchInput cameraIcon button";
grid-template-rows: 88px;
gap: 10px;
}
}
详细代码可以点击这里阅读或获取。
CSS 容器查询在主流浏览器都能体验效果了,但需要开启其相关标记。如果你要运用于生产环境,可以考虑其相应的 Polyfill:
CSS容器查询特性绝对是众望所归的一个特性,在2022年更会全速向前!
有关于 CSS 容器查询更多的介绍可以移步阅读下面这些文章:
CSS 选择器是 CSS 中最基础的知识,也是必不可少的一部分,如果你没掌握好 CSS 选择器的话,那么你将会在 CSS 的世界中有那种”众里寻她千百度,蓦然回首,那人却在灯火阑珊处“的感觉!也正因此,CSS 选择器模块的迭代非常的快,现在已经进入 Level 4 版本了。在 Level 4 版本中,添加了多个伪类选择器,比如 :is() 、:where() 、:not() 和 :has() 等,而其中 :has() 选择器也是我们期待已久的一个选择器。@SaraSoueidan 在 Twitter 上引用 @Jen Simmons 的话说:
:has()选择器本质上就是CSS中期待已久的父选择器!
CSS 的 :has() 伪类选择器和 :not() 有点相似,也被称为结构性伪类选择器,在 CSS 的函数中也称之为 动态伪类函数。它允许你更精细地匹配元素:
:has() 伪类代表一个元素,如果作为参数传递的任何选择器至少与一个元素相匹配!
简单地说,元素只有在传递到 :has() 中的选择器至少匹配一个元素时才会被选中。这样理解起来似乎有点晕,我们来看个简单地示例:
figure img {
aspect-ratio: 21 / 9;
border: 5px solid #3f51b5;
}
figure:has(figcaption) img {
border: 5px solid #9c27b0;
}
上面示例中的 figure img 选择器大家应该都明白,表示选中 <figure> 元素中的所有 <img> 元素;而 figure:has(figcaption) img 选择器表示的是选中 包含了<figcaption> 元素的 <figure> 元素中的所有 <img> 元素。注意,这里:has() 中传了个 figcaption 选择器作为其参数。
<!-- 未匹配,因为 figure 没有包含 figcaption 元素 -->
<figure>
<img alt="" src="" />
</figure>
<!-- 会匹配,因为 figure 包含了 figcaption 元素 -->
<figure>
<figcaption></figcaption>
<img alt="" src="" />
</figure>
在支持的浏览器中你将看到的效果如下:
注意,Safari TP 137 是目前唯一一个默认支持 :has() 选择器的浏览器。
虽然说 :has() 被称为 CSS 的父选择器,但它的作用远不止于此。我们可以使用 :has() 和 :not() 等选择器相互结合,实现一些更复杂的效果。
就上面示例而言,当你在 <input type="email"> 输入的值是否有效时,表单不同状态下有不同的 UI 效果:
是不是很有意思。如果对 CSS 的 :has() 选择器感兴趣的话,还可以阅读《CSS 选择器:is() 和 :where() 与 :has() 有什么功能》一文。
更多关于选择器的内容:
CSS的级联顺序一直以来令众多开发者(特别是不熟悉CSS的开发者)感到困惑和头痛。CSS 新的 @ 规则 @layer 将可以让 CSS 的级联顺序按照你的意图来进行控制。简单地说,@layer 可以通过分层的方式,让你适当控制同源规则的级联排序。
比如下面这个示例:
/* 预设级联层的顺序,并且相邻级联层之间有逗号分隔 */
@layer setting, tool, generic, element, object, component, utilities;
@layer setting {
/* 附加到级联层 setting 中的 CSS */
}
@layer tool {
/* 附加到级联层 tool 中的 CSS */
}
@layer generic {
/* 附加到级联层 generic 中的 CSS */
}
@layer element {
/* 附加到级联层 element 中的 CSS */
}
@layer object {
/* 附加到级联层 object 中的 CSS */
}
@layer component {
/* 附加到级联层 component 中的 CSS */
}
@layer utilities {
/* 附加到级联层 utilities 中的 CSS */
}
上面只是演示了 @layer 规则的一个基本使用,而与 @layer 相关的知识和细节很多,这里就不深入展开了。要是你对该特性感兴趣的话,可以阅读《初探 CSS 的级联层(@layer)》一文。到目前为止,你们可以在 Chromium 99、Firefox 97 和 Safari TP 133 中查阅。注意,Safari 是最早支持该特性的浏览器。
有关于 CSS 中层叠更多的话题还可以阅读:
在 CSS Color Module Level 5 中新增了两个处理颜色的新函数,即 color-mix() 和 color-contrast() ,除此之外,还扩展了以前的颜色函数(比如 rgb() 、hsl() 、hwb() 、lab() 和 lch() 等)功能,可以在一个颜色的基础上改变某一个或某几个参数的值,从而得到一个新的颜色。比如上图部的 hsl() 函数,我们可以基于 --theme-primary 颜色(假设它的值为 hsl(274, 61%, 50%))改变其饱和度(从 50% 调整到 30% )从而得到一个新的颜色 hsl(274, 61%, 30%) 。这个特性,可以让我们在很容易的在 CSS 控制品牌色的色盘:
说个题外话,我最近的主要工作内容之一是将 Design Token 运用于前端生产的工程链路中,并且根据组件可变参数来自动生成不同风格的 UI 组件。那么,在未来的某一天,我就可以使用这种特性来为品牌色构建色盘。
新增的 color-mix() 和 color-contrast() 函数相对来说要更复杂一些。其中 color-mix() 函数有点类似于设计师调色一样:允许你在一个给定的颜色空间中混合两种颜色。
比如:
:root {
--theme-color: #ff0000;
}
.text-primary-dark {
color: color-mix(var(--theme-primary), black 10%);
}
.text-primary-darker {
color: color-mix(var(--theme-primary), black 20%);
}
color-contrast() 函数比较有意思,特别是在用于构建可访问性 Web 的时候特别有用。因为它可以帮助我们提高 Web 可访问性方面的能力(更好的控制文本色和背景色的对比度)。其主要作用是获取一个颜色值,并将其与其他颜色的列表进行比较,从列表中选择对比度最高的一个。
比如color-contrast(white vs red, white, green) ,分别会拿red 、white 和 green 与 white 进行对比,其中 green 和 white 对比度最高,最终会取 green 颜色:
你也还可以像这样使用:color-contrast(wheat vs tan, sienna, #d2691e to AA-large) 。它会将 wheat 与 tan 、sienna 和 #d2691e 进行对比,最终 sienna 颜色获胜,因为它与 wheat 颜色的对比度为 4.273 ,超过了 AA-large 的阈值。
Safari TP 124 已将 hwb()、lch() 、lab() 、color-mix() 和 color-contrast() 置为默认功能!
额外提一下,其中 hwb() 、lch() 和 lab() 所表达的颜色空间和 rgb() 还是有差异的,它们能将颜色表达的更细腻。就拿 hwb() 函数来说:
基于同一色相 0deg 描述的颜色:
有关于 CSS 颜色更多介绍,可以阅读:
”视窗单位“对于大家来说应该不会感到陌生了,特别是针对移动端开发的同学来说。因为移动端现在主流的适配方案之一就是采用视窗单位来处理的。但很多同学所知道的视窗单位应该只是 vw 、vh 、vmin 和 vmax :
视窗单位给移动端开发的适配是带来了极大的优势,但我想你在使用视窗单位的时候,应该也碰到了iOS上 Safari 的兼容性问题。因为,在iOS上的 Safari 有一个长期存在的,极其恼人的Bug,它不能与 vh 单位很好的配合。如果你将一个容器的高度设置为 100vh 时,会导致这个元素有点太高(会出现滚动条)。造成这种现象的主要原因是移动端上的 Safari 在计算 100vh 时忽略了它的部分用户界面。
如果你对 iOS 的 Safari上 100vh 的相关问题以及解决方案感兴趣的话,还可以移步阅读:
如果你不想花过多时间搞清楚这个问题的话,只是想快速解决这个问题,那可以将下面这段代码放到你的代码片段中:
body {
height: 100vh;
}
@supports (-webkit-touch-callout: none) {
body {
height: -webkit-fill-available;
}
}
虽然上面的代码可以解决 100vh 在 iOS Safari 引起的问题,但终究也只能说是一种 Hack 手段(事实上,CSS 中有很多黑魔法,这里不是主要聊这个的,顠过)。庆幸的是,CSS Values and Units Module Level 4 新增了几个与视窗有关系的新单位:
也有相应的单位用于 CSS 的逻辑属性中,比如 svi/svb 。有关于 这几个新增的视窗单位更详细的介绍,可以阅读 @Bramus 的 《The large, Small, and Dynamic Viewports》一文和@Arek Nawo 的 《Investigating the new CSS viewport relative units》一文。既然聊起了 CSS 的单位,那就再多花点篇幅和大家再多聊几个新增的 CSS 单位,也是蛮有意思的。先说 CSS Values and Units Module Level 4 新增的 lh 和 rlh 吧。
简单地回忆一下 CSS 中另一对单位:em 和 rem 。稍微了解 CSS 的同学都知道:- em 是相对于元素自已的 font-size 值计算(除元素自身的 font-size 取值为em时,元素自身的 font-size 值单位为 em时,其相对于其父元素的font-size值计算) - rem 是相对于HTML文档的根元素的 font-size 值计算,文档的根元素一般是指 <html> 元素
这两个新增的 lh 和 rlh 与 em 和 rem 非常的相似,只不过他们相对的是 line-height 的值计算:- lh 相对于元素自己的 line-height 计算 - rlh 相对于文档根元素(<html> )的 line-height 计算
你肯定会问,这样的单位有何用处呢?我想大家在还原 UI 设计稿的时候,肯定碰到了因为字体不同以及 line-height 值不同,让元素在视觉上看上去总是对不齐。那么,有了 lh 和 rlh 之后,事情会变得好一点。比如下图这样的场景:
以前上图这样的标记,常把宽高设置为 1em ,那么现在可以设置 1lh :
::marker {
width: 1lh;
height: 1lh;
}
到目前为止,仅 Safari TP 105 声称支持这两个相对单位 lh 和 rlh 。前面提到 CSS 容器查询特性一直以来是 Web 设计师和开发者一直期待的特性,并且在 2021 年得到快速发展。同时,和 CSS 容器查询特性一起出现的 ”*容器查询单位“ 也是很有意义和作用的。只不过,CSS 容器查询单位与 CSS 容器查询模块放在一起,并未和其他的 CSS 单位纳入一起。CSS 容器查询单位和视窗单位有点类似,不同的是 视窗单位相对于浏览器视窗尺寸计算,而容器查询单位相对于查询容器计算。使用容器查询长度单位的样式表可以更容易将一个组件从一个查询容器中移到另一个查询容器。CSS容器查询的单位主要有:
早其容器查询的单位定义的是 c*(比如,cw 、ch 、cmin 和 cmax )之类的,但其中有些单位会和 ch 单位产生冲突,因此,最终确定的容器查询单位是以 cq* 开始的。也就是上面所列的几个!
正如 @Miriam Suzanne(最初提出建议的人,也是规范的编辑)所分享的,这些 CSS 单位是Chromium中实验性容器查询支持的一部分。
更为有趣的是,@Ahmad Shadeed 在他的文章《CSS Container Query Units》一文中提出 qw 、qh 、qmin 和 qmax 以及他们对应的逻辑属性的单位qi 、qb 。
@Ahmad Shadeed 使用这些新的CSS单位运用在 font-size 上。可以用于容器查询的一坨 CSS 代码,在clamp() 比较函数中使用cqw 单位来取代。
早在2018年和大家聊改变滚动体验的时候就介绍过 overscroll-behavior 属性,我们可以通过该属性覆盖 overscroll 容器(指的是内容宽或高大于容器的宽或高,出现滚动条)时的默认行为。拿一个具体的实例来说,比如我们在构建弹框的时候,弹框内容过高也会出现滚动条,这个时候就会有两个滚动条出现,一个是弹框的,一个是body 的,其默认行为会像下面录屏的效果:
弹框无法滚动时,其底部的内容可以继续滚动。说实话,这样的默认滚动行为给用户的体验是极差的。更多的时候,我们希望弹框无法滚动时,其底部的滚动条也不能滚动。如下所示:
要实现上面视频的效果,我们就需要使用 overscroll-behavior 属性,并且将其值设置为 contain :
.modal {
overscroll-behavior-y: contain;
overflow-y: auto;
}
这个特性早在 Firefox 36 和 Chrome 63 就开始得到支持了,只不过Safari还未得到支持,不过 Safari 也在迎头赶上。
有关于 overscroll-behavior 的相关介绍,可以阅读 @Ahmad Shadeed 的《Prevent Scroll Chaining With Overscroll Behavior》一文。
在 CSS 中改善滚动体验,除了overscroll-behavior 属性之外,还有其他的一些属性,比如滚动捕捉 scroll-snap (Scroll Snap模块中的属性)、pull-to-refresh 等。CSS 除了要以控制滚动行为之外,也可以控制滚动条的样式。在今年以前,CSS 控制滚动条样式是使用浏览器的一些私有属性来搞定:
就在 2021 年 12 月 09日,W3C 发布了的 CSS Scrollbars Styling Module Level 1 规范,该规范提供了 scrollbar-color 和 scrollbar-width 两个属性,用来给滚动条设置样式。以后,我们要以使用它们来轻易完成滚动条样式的定制:
.section { scrollbar-color: #6969dd #e0e0e0; scrollbar-width: thin;}
我想大家都知道,使用 CSS 来美化滚动条样式主要原因之一是因为滚动条在不同的系统平台上显示有差异,外观不统一。除此之外,还有另一个问题。在 Web 上显示滚动条有一个副作用,那就是 内容的布局可能会根据滚动条的类型而改变。如果我们想防止由滚动条引起的一些不必要的布局变化,希望有相应的 CSS 属性来处理。以前没有,但现在有了。CSS Overflow Module Level 4 新增了一个 scrollbar-gutter 属性,有了这个属性,开发者就可以更好的控制滚动条,或者说解决因滚动条类型不同引起布局的差异变化。下图中展示了 scrollbar-gutter 的取值不同时的效果:
如果你对美化滚动条以及 scrollbar-gutter相关的知识感兴趣的话,可以阅读:
就 Web 布局 而言,虽然 Flexbox 很优秀了,但在二维布局中 Flexbox 还是有很大的局限性。在整个 Web 布局的技术体系中,只有 CSS Grid 才是唯一的二维布局。CSS Grd 布局在这几年中一直在不断的向前推进,已经得到了主流浏览器的支持。CSS Grid 能这么快向前推进,除了要感谢浏览器厂商的开发者团队之外,我个人认为还必须要感谢 @Jen Simmons 和 @Rachel Andrew。
@Jen Simmons 和 @Rachel Andrew 除了是 CSS Grid 规范的缔造者,还是 CSS Grid 布道者,她一直在社区努力推进 Grid 向前发展。
CSS Grid 已经有很多年了,该模块中的很多特性在主流浏览器中都得到了支持。在 2021 年,我自已陆续花了近半年的时间,对 CSS Grid 技术进行了系统的学习,并且整理了二十多篇有关于 CSS Grid 方面的系列教程,在这个系列中除了 CSS Grid 的理论知识之外,还有一些实战案例。要是你对这方面感兴趣的话,可以从《2022年不能再错过 CSS 网格布局了》一文中索引。这里着重把 CSS 网格中的子网格 subgrid 单独拿出来是原因的。子网格从定义到今天,经历了很多个版本的演变,而且在 CSS 社区也引起来很大的争议。在 CSS Grid 出现之后,就有人提出:
在 CSS 网格布局系统中应该要有一个子网格,即 subgrid
为此,最早定义的 subgrid 像 grid 和 inline-grid 一样,它只是 display 属性的一个值:
.subgrid {
display: subgrid;
}
不过没过多年,subgrid 就从 display 属性中移除了。使用嵌套网格来模拟子网格:.grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.grid__item { display: grid; grid-template-columns: repeat(4, 1fr);}
简单地说,在需要嵌套网格的网格项目上再次使用 display: grid 以及 grid 相关的属性重新定义一个网格。只不过,这种方式也问题存在:很难将嵌套网格项目与父网格对齐。换句话说,父网格和嵌套网格是两个独立的网格,他们有着自己独立的网格参数。为了让子网格能继续父网格的相关参数,才又将 subgrid 再次引入到 CSS Grid 系统中,只不过,subgrid 不再是 display 的值,而是网格属性 grid-template-columns 或 grid-template-rows 属性的值。
.grid__container { display: grid; grid-template-columns: 1fr 2fr 3fr 2fr 1fr; grid-template-rows: 1fr 2fr 2fr 1fr; gap: 1rem; } .grid__container--subgrid { grid-column: 2 / 5; grid-row: 2 / 4; } .grid__container--subgrid { display: inherit; grid-template-columns: subgrid; grid-template-rows: subgrid; }
正如上图所示,这就是subgrid的作用:通过设置grid-template-columns或grid-template-rows为subgrid,它将与父网格对齐。真正的子网格可以使用父网格的相关参数。简单地说,使用该特性,可以轻易实现像下图这样的卡片布局:
还有一段时间,也有另一种建议,那就是使用 display: contents 来替代 subgrid 。而到今天为止,在网格布局系统中,嵌套网格、子网格和 display: contents 模拟的子网格都同时存在。他们都有着自己的特性。这里就不花过多时间阐述了。subgrid已成为 CSS Grid 模块中不可或缺的一个特性,直到今天为止(2012年的最后一天),也仅 Firefox 浏览器支持。不过,Chrome 已进入 WIP 阶段,我想 2022 年上半年,你就能在 Chrome 浏览器体验 subgrid 的效果。
既然提到 CSS Grid,那就顺嘴说一下 CSS 的 gap 属性吧。该属性不只是 Grid 布局独有的,在 CSS Flexbox 、Grid 和多列布局中都有 gap 属性。在布局中,在某些场景中,给相邻元素之间设置间距要比 margin 容易地多。
从 @Jen Simmons 发的推特消息中可以获知,Safari 14.1 开始也支持 Flexbox 中的 gap 属性。我自测了一下,Grid中的 gap 也得到支持了。也不是说,现在主流浏览器都已支持 gap 属性了:
Web 中有很多控件的 UI 效果是跟随系统走的。比如表单中的一些控件,比如常用的输入框(<input>)、单选按钮,复选框,进度条等。
以往,为了在 UI 的风格上能满足设计师的需求,即 所以平台上使用风格一致的UI效果。为此,Web 开发者需要增加额外的开发工作量,采用自定义表单控件的方式来让 UI 网格统一。为了让开发者能更好的满足设计师的需求,并且快速让各种平台统一 UI 网格, CSS Basic User Interface Module Level 4 新增了 accent-color 属性,可以很轻易的控制 Web 控件 (Widget Accent) UI:
:root { accent-color: deeppink; } @media (prefers-color-scheme: dark) { :root { accent-color: hsl(328 100% 65%); }}
既然提到 Web 表单中的控件,那就给大家提两个有关于 `` 元素的属性。因为这两个属性能给你的用户带来更好的体验,特别是在操作表单的时候。除了给 <input> 元素的 type 指定不同值时,可以提供不同键盘类型之外,还可以使用 inputmode 属性:
inputmode 可以唤起不同类型的软键盘,另一个改善用户体验的是给 <input> 元素的 enterkeyhint 属性设置不同的值,可以改变软键中的 Enter 键的类型的操作行为:
上面提到的,不管是 CSS 还是 HTML 的属性,都是用来改变用户体验的。再提一个和 HTML相关的属性。
自Safari 15 开始,我们在 HTML 的 <meta> 标签为 theme-color 设置颜色,让浏览器自身的 UI 颜色有让开发者来控制:
特别声明,这里的 theme-color 不是 CSS 属性,他是 HTML 的 <meta> 元素的 name 的一个值,结合 content 和 media 能轻易控制系统级的颜色。
似乎跑题了!我们要聊的是 2022 年的 CSS !嗯!那我们继续回到 CSS 的世界中来。
CSS 媒体查询 @media 已不是什么新特性了(除了新增的一些用户偏好的条件设置)。但今天要提出的是他的语法规则的写法。先上一张图吧:
以往在 @media 或者在 @container 规则中写判断条件时使用 min-width 和 max-width 较多,不知道大家是否和我一样有这样的感觉,使用 min-width 和 max-width 很多时候傻傻分不清楚他们的范围。为此,我总是喜欢使用下图来做判断:
自 CSS Media Queries Level 4 开始,我们可以使用较为熟悉的数学表达式了,在媒体条件中使用 > 、>= 、< 或 <= 等数学表达式:
上图中使用 @media 语法表达的话,像下面这样:
/* Old Way */
@media (max-width: 768px) {
…
}
@media (min-width: 375px) {
…
}
@media (min-width: 375px) and (max-width: 768px) {
...
}
/* New Way */
@media (width <= 768px) {
…
}
@media (width >= 375px) {
…
}
@media (375px <= width <= 768px) {
...
}
同样的,数学比较运算符表达式也同样可以运用于 @container 容器查询的条件判断中。
如果你对媒体查询方面的知识感兴趣的话,还可以阅读:
F-mods 是 Font Metrics Override Descriptors(字体度量覆盖描述符)的简称。它对应着 CSS Fonts Module Level 4 规范中的 @font-face 部分的第十一小节,即 默认的字体度量覆盖。简单地说,在 @font-face 规则中新增了 ascent-override、descent-override、line-gap-override 和 advance-override 等属性:
@font-face { font-family: 'Arial' src: local('Arial'); --unitsPerEm: 1000; --lineGap: 10; --descender: -237; --ascender: 1041; --advanceWidthMax: 815; ascent-override: calc(var(--ascender) / var(--unitsPerEm) * 100%); descent-override: calc(var(--descender) / var(--unitsPerEm) * 100%); line-gap-override: calc(var(--lineGap) / var(--unitsPerEm) * 100%); advance-override: calc(var(--advanceWidthMax) / var(--unitsPerEm)); }
其中 advance-override 还未进入 W3C 规范,目前还仅处于提案中;而 ascent-override、descent-override 和 line-gap-override 属性目前已在 Chromium 87+ 和 Firefox 89+ 中实现!
这四个描述符的组合可以让我们告诉浏览器在下载 Web Fonts 之前字符占用多少空间,来覆盖回退字体(系统字体)的布局,使其与 Web Fonts 相匹配。简单地说:这四个描述符可以让你的系统字体更接近 Web Fonts!其中 ascent-override、descent-override 和 line-gap-override 描述符使我们 能够完全消除垂直布局的偏移,因为这三个描述符都会影响行高。在 CSS Fonts Module Level 5 规范中又为 @font-face 规则添加了几个新属性。比如,用来覆盖字体上标(sup)和下标(sub)的 superscript-position-override、subscript-position-override、superscript-size-override 和 subscript-size-override 描述符。虽然这几个属性还没有得到任何浏览器的支持,但对于 Web 开发者而言,这是希望。
除此之外,还新增了 size-adjust 描述符,它允许我们调整字形的比例系数(百分比)。该描述符取代了前面提到的 advance-override描述符。正如 @Addy Osmani在他自己推特上,是这样描述 size-adjust:
size-adjust 已经得到了 Chromium 92+ 和 Firefox 89+ 的支持。
说到 @font-face 那就有必要提到大家也一直期待的另一个 CSS 特性,那就是 leading-trim:
leading-trim 其主要作用就是用来改变文本布局。
在铅印刷术中,为了让铅字块(排成行)之间有一定的间距,排字工人会在行与行之间垫一些铅条,让阅读变得更舒服一些。在那个时代,锅条一般都放置在铅块下方。同样的,在Web排版上也有铅条的身影,只不过他分面上下两个部分。在CSS中引入该特性,它的工作原理就像一个文本框剪切刀。去掉文本框上下之间多余的空间(这个空间其实就是铅条高度的一半):
h1 { text-edge: cap alphabetic; leading-trim: both; }
示例中用到了text-edge 属性,简单地用下图来描述其作用:
开发 Web 时,对于排版方面是较为复杂的,涉及到的CSS知识也较多,如果你感兴趣的话,可以阅读下面这些文章:
这个和前面提到的 CSS 媒体查询有点类似。不是 CSS 的新特性,在 CSS Transform Level 2 规范中把以前用到 transform 属性的 rotate() 、scale() 、translate() 函数变成独立的 CSS 属性。而且它们都已经成为主流浏览器的实验属性,能在浏览器中体验他们。
element {
scale: 2;
rotate: 30deg;
translate: -50% -50%;
}
平移(translate)、旋转(rotate)和缩放(scale)属性允许开发者独立地指定简单的变换,其方式与典型的用户界面使用方式一致,而不必记住变换中的顺序,使transform()、rotate()和scale()的动作保持独立并在屏幕坐标中发挥作用。它们被应用的顺序是,首先是平移,然后是旋转,然后是缩放,而不是你定义它们的顺序。有了独立的变换属性,这也意味着我们可以对它们分别进行动画和过渡。
@keyframes individual {
50% {
translate: 0 50%;
}
75% {
scale: 1;
}
}
element {
transition: rotate 200ms ease-in-out, scale 500ms linear;
}
element:hover {
scale: 2;
rotate: -3deg;
}
有关于这方面更详细的介绍可以阅读:
接下来提到的 CSS 特性只是部分浏览器的实验性功能,不会得到大多数浏览器的支持,或者可能只有在相应的浏览器中开启了实验性功能标记才能看到效果。这些特性在 2022 年,甚至是再往后一两年都有可能得不到所有主流浏览器的支持。除了不会得到浏览器支持,而且规范都有可能会变更。说不定还会从 CSS 中移除!
上图看上去很熟,对吧!但它不是 SCSS 中的嵌套,是原生 CSS 中的嵌套。在 CSS 方面这算是一个突破性的补充,也算是继续 CSS 自定义属性(CSS 变量)的又补充。以往这样的规则,只能在 CSS 的处理器中运行。在 CSS 的嵌套模块中,我们可以使用 & 和 @nest 规则在一个样式规则块中嵌套另一个样式规则块:
table.colortable { & td { text-align: center; &.c { text-transform: uppercase } &:first-child, &:first-child + td { border: 1px solid black } } & th { text-align: center; background: black; color: white; } @nest footer & { font-size: 0.8em; }}
有关于 CSS 的原生嵌套更多的介绍,可以阅读《图解CSS:CSS的嵌套》一文。
CSS Cascading and Inheritance Level 5 中引入了 @layer 规则,将在 Level 6 中会引入另一个规则 @scope 。这是一种将样式范围扩大到DOM树的一种用法。
<!-- HTML -->
<div class="dark-theme">
<a href="#">plum</a>
<div class="light-theme">
<a href="#">also plum???</a>
</div>
</div>
/* 当.light-theme和.dark-theme被嵌套时,你可能不会得到预期的结果 */
.light-theme a {
color: purple;
}
.dark-theme a {
color: plum;
}
/* 通过范围划分,我们可以解决这个问题,因为a元素的样式将由其最近的范围来确定 */
@scope (.light-scheme) {
a {
color: darkmagenta;
}
}
@scope (.dark-scheme) {
a {
color: plum;
}
}
@when ... @else 是 CSS Conditional Rules Module Level 5 新增的特性,他们可以分开来单独使用,也可以组合在一起使用。
@when media(width >= 400px) and media(pointer: fine) and supports(display: flex) {
/* A */
} @else supports(caret-color: pink) and supports(background: double-rainbow()) {
/* B */
} @else {
/* C */
}
@Stefan Judis 将这个CSS 特性(@when ... @else)有前面提到的 CSS特性,即 CSS 容器查询 和 媒体查询的数学运算表达式组合在一起,重新设计了 @Ahmad Shadeed 的 《Conditional Border Radius In CSS》文章中提到的有条件设置圆角:
@Stefan Judis 重新设计的方案:
有关于有条件设置圆角半径除了这里提到的方案,其实还有其他的方案,你要是想一探究竟的话,可以阅读前几天刚整理的《CSS 中的条件圆角技巧》一文。
这里所说的滚动与动画指 Scroll-linked Animations 模块中提供的 @scroll-timeline规则和 animation-timeline 属性。你可以将 CSS 动画与一个滚动容器的滚动偏移量关联起来。当你向上或向下滚动一个容器时,链接的动画将相应的前进或后退。
使用 @scroll-timeline 规则和 animation-timeline 构建类似上面的效果,要以按下面三步来走:
/* (1) Define Keyframes */
@keyframes adjust-progressbar {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
/* (2) Define a ScrollTimeline */
@scroll-timeline scroll-in-document {
source: auto;
orientation: block;
scroll-offsets: 0, 100%;
}
/* (3) Attach the Animation + set the ScrollTimeline as the driver for the Animation */
#progressbar {
animation: 1s linear forwards adjust-progressbar;
animation-timeline: scroll-in-document;
}
有关于这方面更详细的介绍,可以阅读 @Bramus 提供的系列教程:
@property 是用来注册一个变量的,该变量是一个 CSS Houdini 中的变量,但它的使用和 CSS 中的自定义属性(CSS变量)是一样的,不同的是注册方式:
// Chrome 78+
// 需要在 JavaScript脚本中注册
CSS.registerProperty({
'name': '--custom-property-name',
'syntax': '<color>',
'initialValue': 'black',
'inherits': false
})
// Chrome 85+
// 在CSS文件中注册
@property --custom-property-name {
'syntax': '<color>',
'initialValue': 'black',
'inherits': false
}
他的最大特色之一就是可以指定已注册的 CSS 变量的类型、初始值,是否可继承:
虽然它的使用方式和 CSS 的自定义属性相似,但它要更强大,特别是在动效方面的使用,能增强 CSS 的动效能力,甚至实现一些以前 CSS 无法实现的动效。比如
@property --milliseconds {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.counter {
counter-reset: ms var(--milliseconds);
animation: count 1s steps(100) infinite;
}
@keyframes count {
to {
--milliseconds: 100;
}
}
把它和 CSS Houdini 的 Paint API 结合起来,可做的事情更多:
更多这方向的效果可以在 houdini.how 网站上查阅:
CSS Houdini 中的 @property 和 Paint API 是对现在 CSS 能力的扩展,如果你对这方面感兴趣的话,可以阅读:
瀑布流是 Web 中经典布局之一。到目前为止依旧是 JavaScript 实现的瀑布流布局为主要技术方案,虽然使用 CSS 技术在一定条件之下可以完成瀑布流布局,但其局限性也较大。在 CSS Grid 最新模块中提供了原生的 CSS 瀑布流布局方案:
.masonry {
display: grid; gap: 20px;
grid: masonry / repeat(auto-fill, minmax(250px, 1fr));
}
但一直以来他还只是 Firefox 中的一个实验属性,还没有看到其他浏览器有支持,但我期望在 2022 年的某一天能在主流浏览器上都能看到 CSS Grid 实现瀑布流的布局的效果。
上面列出的一些 CSS 新特性(也有新语法),具体有哪些新特性能在 2022 年中出现在浏览器中,还不得而知。上面的提到的一些特性其实已经在一个或多个浏览器中能体验了。感兴趣的可以自己尝试一下。另外要特别提出的是,上面列的 CSS 属性清单来自于社区有同行业专家以及我自己的一些积累和见解,如果有不对之处,欢迎一起探讨。如果所列清单有遗漏,也欢迎大家分享出来。最后,希望上面的内容能帮助大家了解 CSS 的一些新东西,打开一些未知领域!谢谢!(^_^)
avaScript包含各种对典型编程思想有用的一些技巧,在实际开发中,我们通常希望减少代码行数;
将对象的值解构为变量是另一种在传统点表示法之外读取其值的方法。
下面的示例比较了用于读取对象值的经典点表示法和对象解构快捷方式。
const restaurant = {
name: "Classico Italiano",
menu: ["Garlic", "Bread", "Salad", "Pizza", "Pasta"],
openingHours: {
friday: { open: 11, close: 24 },
saturday: { open: 12, close: 23 },
},
};
// Longhand
console.log("value of friday in restaurant:", restaurant.openingHours.friday);
console.log("value of name in restaurant:", restaurant.name);
// Shorthand
const { name, openingHours } = restaurant;
const { openingHours: { friday } } = restaurant;
//we can go further and get the value of open in friday
const { openingHours: { friday: { open } } } = restaurant;
console.log(name, friday, open);
Object.entries() 是 ES8 中引入的一项功能,它允许您将文字对象转换为键/值对数组。
const credits = {
producer: "Open Replay",
editor: "Federico",
assistant: "John",
};
const arr = Object.entries(credits);
console.log(arr);
Output: [
["producer", "Open Replay"],
["editor", "Federico"],
["assistant", "John"],
];
Object.values() 也是 ES8 中引入的新特性,与 Object 具有类似的功能。entries() 但这没有关键部分:
const credits = {
producer: "Open Replay",
editor: "Federico",
assistant: "John",
};
const arr = Object.values(credits);
console.log(arr);
Output: ["Open Replay", "Federico", "John"];
传统的 JavaScript for 循环语法是 for (let i = 0; i < x; i++) { … }。通过引用迭代器的数组长度,此循环语法可用于遍历数组。共有三个 for 循环快捷方式,它们提供了用于遍历数组中的对象的各种方法:
for…of 创建一个循环遍历内置字符串、数组和类似数组的对象,甚至是 Nodelist
for...in 在记录属性名称和值的字符串时访问数组的索引和对象文字上的键。
Array.forEach 使用回调函数对数组元素及其索引执行操作
const arr = ["Yes", "No", "Maybe"];
// Longhand
for (let i = 0; i < arr.length; i++) {
console.log("Here is item: ", arr[i]);
}
// Shorthand
for (let str of arr) {
console.log("Here is item: ", str);
}
arr.forEach((str) => {
console.log("Here is item: ", str);
});
for (let index in arr) {
console.log(`Item at index ${index} is ${arr[index]}`);
}
// For object literals
const obj = { a: 1, b: 3, c: 5 };
for (let key in obj) {
console.log(`Value at key ${key} is ${obj[key]}`);
}
在 JavaScript 中定义对象字面量让生活变得更轻松。ES6 在为对象赋予属性方面提供了更多的简单性。如果变量名和对象键相同,则可以使用速记符号。
通过在对象字面量中声明变量,您可以在 JavaScript 中快速将属性分配给对象。为此,必须使用预期的键命名变量。这通常在您已经有一个与对象属性同名的变量时使用。
const a = 1;
const b = 2;
const c = 3;
// Longhand
const obj = {
a: a,
b: b,
c: c,
};
// Shorthand
const obj = { a, b, c };
如果你想要简单的 JavaScript 并且不想依赖第三方库,这个小技巧很有用。
// Longhand:
const fruits = ['mango', 'peach', 'banana'];
for (let i = 0; i < fruits.length; i++) { /* something */ }
// Shorthand:
for (let fruit of fruits) { /* something */ }
如果您想访问文字对象中的键,这也适用:
const obj = { continent: "Africa", country: "Ghana", city: "Accra" };
for (let key in obj) console.log(key); // output: continent, country, city1.2.
function logArrayElements(element, index, array) {
console.log("a[" + index + "] = " + element);
}
[2, 4, 6].forEach(logArrayElements);
// a[0] = 2
// a[1] = 4
// a[2] = 61.2.3.4.5.6.7.
ES6 的一个有趣的新增功能是解构赋值。解构是一种 JavaScript 表达式,可以将数组值或对象属性分离到单独的变量中。换句话说,我们可以通过从数组和对象中提取数据来将数据分配给变量。
const arr = [2, 3, 4];
// Longhand
const a = arr[0];
const b = arr[1];
const c = arr[2];
// Shorthand
const [a, b, c] = arr;
得益于 ES6 中引入的展开运算符,JavaScript 代码使用起来更加高效和愉快。它可以代替特定的数组函数。展开运算符中有一系列的三个点,我们可以用它来连接和克隆数组。
const odd = [3, 5, 7];
const arr = [1, 2, 3, 4];
// Longhand
const nums = [2, 4, 6].concat(odd);
const arr2 = arr.slice();
// Shorthand
const nums = [2, 4, 6, ...odd];
const arr2 = [...arr];
与 concat() 函数不同,您可以使用扩展运算符将一个数组插入另一个数组中的任意位置。
const odd = [3, 5, 7];
const nums = [2, ...odd, 4, 6]; // 2 3 5 7 4 61.2.
如果您曾经发现自己需要在代码中编写多行字符串,那么您可以这样写:
// Longhand
const lorem =
"Lorem, ipsum dolor sit amet" +
"consectetur adipisicing elit." +
" Quod eaque sint voluptatem aspernatur provident" +
"facere a dolorem consectetur illo reiciendis autem" +
"culpa eos itaque maxime quis iusto quisquam" +
"deserunt similique, dolores dolor repudiandae!" +
"Eaque, facere? Unde architecto ratione minus eaque" +
"accusamus, accusantium facere, sunt" +
"quia ex dolorem fuga, laboriosam atque.";
但是有一种更简单的方法,只需使用反引号即可完成。
// Shorthand:
const lorem = `Lorem, ipsum dolor sit amet
consectetur adipisicing elit.
Quod eaque sint voluptatem aspernatur provident
facere a dolorem consectetur illo reiciendis autem
culpa eos itaque maxime quis iusto quisquam
deserunt similique, dolores dolor repudiandae!
Eaque, facere? Unde architecto ratione minus eaque
accusamus, accusantium facere, sunt
quia ex dolorem fuga, laboriosam atque.`;
您的代码可能偶尔会收到必须以数字格式处理的字符串格式的数据。这不是一个大问题;我们可以快速转换它。
// Longhand
const num1 = parseInt('1000');
const num2 = parseFloat('1000.01')
// Shorthand
const num1 = +'1000'; //converts to int datatype
const num2 = +'1000.01'; //converts to float data type1.2.3.4.5.6.
我们可以在不使用 (+) 的情况下将许多变量附加到字符串中。模板文字在这里派上用场。用反引号包裹你的字符串,用 ${ } 包裹你的变量,以利用模板文字。
const fullName = "Ama Johnson";
const job = "teacher";
const birthYear = 2000;
const year = 2025;
// Longhand
const Fullstr =
"I am " + fullName + ", a " + (year - birthYear) + " years old " + job + ".";
// Shorthand
const Fullstr = `I am ${fullName}, a ${year - birthYear} years old ${job}. `;
模板字面量为我们省去了许多使用 + 运算符连接字符串的麻烦。
将第一个操作数乘以第二个操作数的幂的结果是求幂运算符 ** 返回的结果。它与 Math.pow 相同,只是 BigInts 也被接受为操作数。
// Longhand
Math.pow(2, 3); //8
Math.pow(2, 2); //4
Math.pow(4, 3); //64
// Shorthand
2 ** 3; //8
2 ** 4; //4
4 ** 3; //641.2.3.4.5.6.7.8.
这个可能以前见过。本质上,这是一种写整数的奇特方法,没有最后的零。例如,表达式 1e8 表示 1 后跟八个零。它表示十进制基数 100,000,000,JavaScript 将其解释为浮点类型。
// Longhand
for (let i = 0; i < 10000000; i++) { /* something */ }
// Shorthand
for (let i = 0; i < 1e7; i++) { /* something */ }
// All the below will evaluate to true
1e0 === 1;
1e1 === 10;
1e2 === 100;
1e3 === 1000;
1e4 === 10000;
1e5 === 100000;
1e6 === 1000000;
1e7 === 10000000;
1e8 === 100000000;
短路评估也可以替代 if...else 子句。当变量的预期值为 false 时,此快捷方式使用逻辑或运算符 || 为变量提供默认值。
let str = "";
let finalStr;
// Longhand
if (str !== null && str !== undefined && str != "") {
finalStr = str;
} else {
finalStr = "default string";
}
// Shorthand
let finalStr = str || "default string"; // 'default string'1.2.3.4.5.6.7.8.9.10.
if 语句用于定义函数参数的默认值。在 ES6 中,您可以在函数声明本身中定义默认值。如果没有传递任何值或未定义,则默认函数参数允许使用默认值初始化参数。
以前都是在函数体中测试参数值,如果没有定义就赋值。
默认情况下,JavaScript 中的函数参数是未定义的。然而,设置不同的默认设置通常是有利的。在这里,默认设置可能很有用。
// Longhand
function volume(a, b, c) {
if (b === undefined) b = 3;
if (c === undefined) c = 4;
return a * b * c;
}
// Shorthand
function volume(a, b = 3, c = 4) {
return a * b * c;
}
我们经常使用关键字 return 来指示函数的最终输出。单语句箭头函数将隐式返回其计算结果(该函数必须省略方括号 {} 才能这样做)。
对于多行语句,例如表达式,我们可以将返回表达式包裹在括号 () 中。
function capitalize(name) {
return name.toUpperCase();
}
function add(numG, numH) {
return numG + numH;
}
// Shorthand
const capitalize = (name) => name.toUpperCase();
const add = (numG, numH) => numG + numH;
// Shorthand TypeScript (specifying variable type)
const capitalize = (name: string) => name.toUpperCase();
const add = (numG: number, numH: number) => numG + numH;
在函数的开头声明变量赋值是个好主意。这种精简方法可以帮助您在一次创建大量变量时节省大量时间和空间。
// Longhand
let x;
let y;
let z = 3;
// Shorthand
let x, y, z=3;
但是,请注意,出于易读性考虑,许多人更喜欢用手写方式声明变量。
我们可以使用点表示法访问对象的键或值。我们可以通过可选链接更进一步并读取键或值,即使我们不确定它们是否存在或是否已设置。如果键不存在,则可选链接的值未定义。
下面是一个可选链的例子:
const restaurant = {
details: {
name: "Classico Italiano",
menu: ["Garlic", "Bread", "Salad", "Pizza"],
},
};
// Longhand
console.log(
"Name ",
restaurant.hasOwnProperty("details") &&
restaurant.details.hasOwnProperty("name") &&
restaurant.details.name
);
// Shorthand
console.log("Name:", restaurant.details?.name);
JavaScript 中的内置 Math 对象通常用于访问数学函数和常量。一些函数提供了有用的技术,让我们可以在不引用 Math 对象的情况下调用它们。
例如,我们可以通过使用两次按位非运算符来获得整数的 Math.floor() ~~。
const num = 7.5;
// Longhand
const floorNum = Math.floor(num); // 7
// Shorthand
const floorNum = ~~num; // 71.2.3.4.5.
这些技巧,因为它简洁明了,使编码变得有趣并且代码行易于记忆。
小新 编译自 Insight Data Blog
量子位 出品 | 公众号 QbitAI
写个网页能有多麻烦?在大多数公司里,这项工作分为三步:
1. 产品经理完成用户调研任务后,列出一系列技术要求;
2. 设计师根据这些要求来设计低保真原型,逐渐修改得到高保真原型和UI设计图;
3. 工程师将这些设计图实现为代码,最终变成用户使用的产品。
这么多环节,任何地方出一点问题,都会拉长开发周期。因此,不少公司,比如Airbnb已经开始用机器学习来提高这个过程的效率。
△ Airbnb内部的AI工具,从图纸到代码一步到位
看起来很美好,但Airbnb还没公开该模型中端到端训练的细节,以及手工设计的图像特征对该模型的贡献度。这是该公司特有的闭源解决方案专利,可能不会进行公开。
好在,一个叫Ashwin Kumar的程序员创建了一个开源版本,让开发者/设计师的工作变得更简单。
以下内容翻译自他的博客:
理想上,这个模型可以根据网站设计的简单手绘原型,很快地生成一个可用的HTML网站:
△ SketchCode模型利用手绘线框图来生成HTML网站
事实上,上面例子就是利用训练好的模型在测试集上生成的一个实际网站,代码请访问:https://github.com/ashnkumar/sketch-code。
目前要解决的问题属于一种更广泛的任务,叫做程序综合(program synthesis),即自动生成工作源代码。尽管很多程序综合研究通过自然语言规范或执行追踪法来生成代码,但在当前任务中,我会充分利用源图像,即给出的手绘线框图来展开工作。
在机器学习中有一个十分热门的研究领域,称为图像标注(image caption),目的是构建一种把图像和文本连接在一起的模型,特别是用于生成源图像内容的描述。
△ 图像标注模型生成源图像的文本描述
我从一篇pix2code论文和另一个应用这种方法的相关项目中获得灵感,决定把我的任务按照图像标注方式来实现,把绘制的网站线框图作为输入图像,并将其相应的HTML代码作为其输出内容。
注:上段提到的两个参考项目分别是
pix2code论文:https://arxiv.org/abs/1705.07962
floydhub教程:https://blog.floydhub.com/turning-design-mockups-into-code-with-deep-learning/?source=techstories.org
确定图像标注方法后,理想中使用的训练数据集会包含成千上万对手绘线框图和对应的HTML输出代码。但是,目前还没有我想要的相关数据集,我只好为这个任务来创建数据集。
最开始,我尝试了pix2code论文给出的开源数据集,该数据集由1750张综合生成网站的截图及其相应源代码组成。
△ pix2code数据集中的生成网站图片和源代码
这是一个很好的数据集,有几个有趣的地方:
该数据集中的每个生成网站都包含几个简单的辅助程序元素,如按钮、文本框和DIV对象。尽管这意味着这个模型受限于将这些少数元素作为它的输出内容,但是这些元素可通过选择生成网络来修改和扩展。这种方法应该很容易地推广到更大的元素词汇表。
每个样本的源代码都是由领域专用语言(DSL)的令牌组成,这是该论文作者为该任务所创建的。每个令牌对应于HTML和CSS的一个片段,且加入编译器把DSL转换为运行的HTML代码。
为了修改我的任务数据集,我要让网站图像看起来像手工绘制出的。我尝试使用Python中的OpenCV库和PIL库等工具对每张图像进行修改,包括灰度转换和轮廓检测。
最终,我决定直接修改原始网站的CSS样式表,通过执行以下操作:
1. 更改页面上元素的边框半径来平滑按钮和DIV对象的边缘;
2. 模仿绘制的草图来调整边框的粗细,并添加阴影;
3. 将原有字体更改为类似手写的字体;
最终实现的流程中还增加了一个步骤,通过添加倾斜、移动和旋转来实现图像增强,来模拟实际绘制草图中的变化。
现在,我已经处理好数据集,接下来是构建模型。
我利用了图像标注中使用的模型架构,该架构由三个主要部分组成:
1. 一种使用卷积神经网络(CNN)的计算机视觉模型,从源图像提取图像特征;
2. 一种包含门控单元GRU的语言模型,对源代码令牌序列进行编码;
3. 一个解码器模型,也属于GRU单元,把前两个步骤的输出作为输入,并预测序列中的下一个令牌。
△ 以令牌序列为输入来训练模型
为了训练模型,我将源代码拆分为令牌序列。模型的输入为单个部分序列及它的源图像,其标签是文本中的下一个令牌。该模型使用交叉熵函数作为损失函数,将模型的下个预测令牌与实际的下个令牌进行比较。
在模型从头开始生成代码的过程中,该推理方式稍有不同。图像仍然通过CNN网络进行处理,但文本处理开始时仅采用一个启动序列。在每个步骤中,模型对序列中输出的下个预测令牌将会添加到当前输入序列,并作为新的输入序列送到模型中;重复此操作直到模型的预测令牌为,或该过程达到每个文本中令牌数目的预定义值。
当模型生成一组预测令牌后,编译器就会将DSL令牌转换为HTML代码,这些HTML代码可以在任何浏览器中运行。
我决定使用BLEU分数来评估模型。这是机器翻译任务中常用的一种度量标准,通过在给定相同输入的情况下,衡量机器生成的文本与人类可能产生内容的近似程度。
实际上,BLEU通过比较生成文本和参考文本的N元序列,以创建修改后的准确版本。它非常适用于这个项目,因为它会影响生成HTML代码中的实际元素,以及它们之间的相互关系。
最棒的是,我还可以通过检查生成的网站来比较当前的实际BLEU分数。
△ 观察BLEU分数
当BLEU分数为1.0时,则说明给定源图像后该模型能在正确位置设置合适的元素,而较低的BLEU分数这说明模型预测了错误元素或是把它们放在相对不合适的位置。我们最终模型在评估数据集上的BLEU分数为0.76。
后来,我还想到,由于该模型只生成当前页面的框架,即文本的令牌,因此我可以在编译过程中添加一个定制的CSS层,并立刻得到不同风格的生成网站。
△ 一个手绘图生成多种风格的网页
把风格定制和模型生成两个过程分开,在使用模型时带来了很多好处:
1.如果想要将SketchCode模型应用到自己公司的产品中,前端工程师可以直接使用该模型,只需更改一个CSS文件来匹配该公司的网页设计风格;
2. 该模型内置的可扩展性,即通过单一源图像,模型可以迅速编译出多种不同的预定义风格,因此用户可以设想出多种可能的网站风格,并在浏览器中浏览这些生成网页。
受到图像标注研究的启发,SketchCode模型能够在几秒钟内将手绘网站线框图转换为可用的HTML网站。
但是,该模型还存在一些问题,这也是我接下来可能的工作方向:
1. 由于这个模型只使用了16个元素进行训练,所以它不能预测这些数据以外的令牌。下一步方向可能是使用更多元素来生成更多的网站样本,包括网站图片,下拉菜单和窗体,可参考启动程序组件(https://getbootstrap.com/docs/4.0/components/buttons/)来获得思路;
2. 在实际网站构建中,存在很多变化。创建一个能更好反映这种变化的训练集,是提高生成效果的一种好方法,可以通过获取更多网站的HTML/CSS代码以及内容截图来提高;
3. 手绘图纸也存在很多CSS修改技巧无法捕捉到的变化。解决这个问题的一种好方法是使用生成对抗网络GAN来创建更逼真的绘制网站图像。
代码:https://github.com/ashnkumar/sketch-code
原文:https://blog.insightdatascience.com/automated-front-end-development-using-deep-learning-3169dd086e82
— 完 —
诚挚招聘
量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。
量子位 QbitAI · 头条号签约作者
վ'ᴗ' ի 追踪AI技术和产品新动态
*请认真填写需求信息,我们会在24小时内与您取得联系。