w3ctech

[译]Why I'm Excited About Native CSS Variables

原文: Why I'm Excited About Native CSS Variables 原文作者:Philip Walton | Engineer at Google

几周前 CSS 变量,更准确的说是CSS自定义属性,是运行在 Chrome Canary 版的实验性网络平台的功能特性[1]

当 Chrome 工程师 Addy Osmani 第一次在 Twitter 上发表 release 版本,使 negativity, hostilityskepticism 非常惊讶。最近,也令9号彩票我 感到惊讶,让9号彩票我 对这个特性非常兴奋。

快速浏览这些回复之后,很明显99%的抱怨都针对这两点:

  • 语法“丑陋”、“冗长”。
  • Sass 已经支持变量, 那为什么9号彩票我 还需要关心它?

9号彩票我 承认不喜欢他的语法,但理解它不能随意的被选择是很重要的。很多 CSS 工作组9号彩票成员 详细地讨论了语法,为了兼容 CSS 语法并且避免与未来的语法冲突,他们不得不做出选择

9号彩票关于 CSS 变量与 Sass 变量的比较,9号彩票我 认为最大的误解是:

原生的 CSS 变量不仅仅是尝试实现 CSS 预处理器的功能。事实上,如果9号彩票你 读了 初步设计讨论,9号彩票你 会发现原生 CSS 变量最大的动机,是让一些 CSS 预处理器不能做的事成为可能。

CSS 预处理器是梦幻般的9号彩票工具 ,但是他们的变量是静态的,并且有词法作用域。另一方面讲,9号彩票本地 CSS 变量是一种完全不同的变量:他们是动态的,他们作用于 DOM 上。事实上,9号彩票我 认为称他们为变量是令人困惑的。他们实际是 CSS 属性,这给了他们一套完全不同的能力以及允许他们解决一些完全不同的问题。

这篇文章9号彩票我 将讨论使用预处理器变量不能做的事而用 CSS 自定义属性可以做的一些事情。9号彩票我 将演示一些启动自定义属性的新的设计模式。最后,9号彩票我 将讨论为什么在未来,9号彩票9号彩票我 们 极有可能结合预处理器变量和自定义属性两者,充分利用好这两个东西。

注意:这篇文章不是对 CSS 自定义属性的介绍说明。如果9号彩票你 从没听说过他们,或对他们如何工作很陌生,9号彩票我 建议自己先熟悉下

预处理器变量的局限性

在继续之前,9号彩票我 想重点说下,9号彩票我 真的很喜欢CSS预处理器,9号彩票我 把他们用在9号彩票我 所有的项目上。预处理器能做一些十分棒的事,即使9号彩票你 知道他们最终只是吐出一些原始的 CSS,有时,他们还是会让9号彩票你 感到神奇。

那就是说,像其他9号彩票工具 ,他们有自身的局限性。有时候动态力的表现能让这些局限性变得出人意料,尤其是对初学者。

预处理器变量是静态的

也许令初学者惊讶的最常见的预处理器局限性的例子是,在媒体查询里 Sass 不能定义变量或者使用 @extend。既然这篇文章是9号彩票关于 变量的,9号彩票我 将关注前者:

$gutter: 1em;
@media (min-width: 30em) {
  $gutter: 2em;
}
.Container {
  padding: $gutter;
}

如果9号彩票你 编译以上代码,9号彩票你 会得到这样的:

.Container {
  padding: 1em;
}

正如9号彩票你 所见,媒体查询块完全被抛弃并且变量赋值被忽略。

理论上讲 Sass 可能要做一些额外的变量声明工作,这样做将会很有挑战性,并且需要列举所有的排列,最后9号彩票你 的 CSS 大小会呈指数增长。

因为9号彩票你 不能改变一个基于匹配 @media 规则的变量,9号彩票你 唯一的操作是给每个媒体查询分配唯一的变量,用代码写出每个单独的变化。稍后9号彩票更多 介绍。

预处理器变量不能级联

当9号彩票你 使用变量时,作用域的问题会无可避免的发生。这个变量应该是全局的吗?应该作用在某个文件或模块吗?应该作用在块吗?

由于 CSS 最终将装饰 HTML,将会有另一种约束变量作用域的方式:DOM 元素。一旦处理器没有在浏览器运行或者从未找到标记上,他们不会这样做。

考虑一个网站尝试为 增加一个类 user-setting-large-text,为用户呈现他们偏好的大字体。当设置了这个类,大点儿的 $font-size 变量会被赋值应用:

$font-size: 1em;
.user-setting-large-text {
  $font-size: 1.5em;
}
body {
  font-size: $font-size;
}

恰恰相反,就像上面的媒体块例子,Sass 完全忽略了这个变量的赋值,意味着这种写法不可行。输出是这样的:

body {
  font-size: 1em;
}

预处理器变量不能继承

尽管9号彩票技术 上继承是级联的一部分,9号彩票我 想单独把继承举出了是因为很多时候9号彩票我 想使用这个特性却不行。

思考这样一种情况,9号彩票你 想给9号彩票你 的 DOM 元素增加一些样式,他们的颜色也适用于他们的父级元素。

.alert { background-color: lightyellow; }
.alert.info { background-color: lightblue; }
.alert.error { background-color: orangered; }
.alert button {
  border-color: darken(background-color, 25%);
}

上面的代码在 Sass(或 CSS)是无效的,但9号彩票你 应该能明白它想要完成的是什么。

最后一个声明是打算用 Sass 的 darken 函数使 <button> 元素的 background-color 属性可以继承自父级元素。如果类 info 或者 error 被添加到 alert(或者使用 JavaScript 或 一个用户样式表任意设置的背景颜色),button 元素也能生效。

现在,很明显在 Sass 里这不起作用,因为预处理器不知道 DOM 的结构,但希望它清楚,为什么这种做法可能是有用的。

讲一个很特别的使用场景:出于无障碍访问的原因,在可继承的 DOM 属性上运行颜色函数,这将是非常方便的。例如:确保文本总是可读的以及背景颜色充分对比,用自定义属性和新的CSS 颜色函数会更可行!

预处理器变量不可互操作

这是预处理器的一个相对明显的缺点,但是9号彩票我 提到它是因为9号彩票我 觉得很重要。如果9号彩票你 用 PostCSS 构建了一个网站,9号彩票你 想使用那些只是利用 Sass 来定义样式的第三方组件,那9号彩票你 就错了。

通过不同的9号彩票工具 集或者托管在 CDN 上的第三方样式表来共用预处理器变量是不行的(目前很难)。

9号彩票本地 CSS 自定义属性可以和任何 CSS 预处理器或者纯 CSS 文件一起很好的工作。反之,则不一定正确。

自定义属性有怎样的不同

9号彩票你 或许会猜测,9号彩票我 上面例举的局限性都不适用于 CSS 自定义属性。但或许比他们不适用更重要的是为什么他们不适用。

CSS 自定义属性就像常规的 CSS 属性,可以用相同的方式操作他们(一个明显的例外是他们不风格化任何东西)。

像常规的 CSS 属性一样, 自定义属性是动态的。在运行时可以修改他们,可以在媒体查询里更新他们或者给 DOM 增加一个新的类。他们可以被赋值到内嵌(一个元素)或者一个正常的 CSS 声明的选择器里。使用所有标准的级联规则或者 JavaScript 可以更新或重写他们。可能最重要的是,他们可继承,那么当他们应用到一个 DOM 元素上,他们会传递给元素的后代们。

为了使他们更简洁,预处理器变量是词法作用域的,编译之后是静态的。自定义属性作用于 DOM,他们是生动的,他们是动态的。

现实9号彩票生活中的例子

如果9号彩票你 还是不确定什么是自定义属性能做的而预处理器变量不能做的,9号彩票我 来给9号彩票你 举一些例子。

对于他的价值,有很多非常不错的例子9号彩票我 可以展示,为了不让这篇文章过长,9号彩票我 举两个例子。

9号彩票我 挑选这些例子因为他们不是纸上谈兵,他们是9号彩票我 在过去所面对的真实挑战。9号彩票我 能清楚的记得尝试用预处理器让他们工作,但就是不行,现在用自定义属性就行了。

响应式属性与媒体查询

很多网站使用“gap” 或 “gutter” 变量来定义页面中不同部分布局项的默认留白以及默认填充。大多数时候,9号彩票你 希望这个 gutter 的值依据浏览器窗口大小而不同。在大屏幕上9号彩票你 希望项目间间距大些,但是小屏幕上9号彩票你 不能提供太多的间距,所以 gutter 需要小点。

正如9号彩票我 上面提到的, Sass 变量不能在媒体查询里工作,所以9号彩票你 必须为不同情况写代码。

下面的例子定义了变量 $gutterSm、$gutterMd 和 $gutterLg,然后为每种情况声明了独立的规则:

/* 声明3个 gutter 值,一个用于每个断点 */
$gutterSm: 1em;
$gutterMd: 2em;
$gutterLg: 3em;
/* 用 $gutterSm 定义小屏幕的基本样式 */
.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: $gutterSm;
}
.Grid {
  display: flex;
  margin: -$gutterSm 0 0 -$gutterSm;
}
.Grid-cell {
  flex: 1;
  padding: $gutterSm 0 0 $gutterSm;
}
/* 用 $gutterMd 重写中屏样式 */
@media (min-width: 30em) {
  .Container {
    padding: $gutterMd;
  }
  .Grid {
    margin: -$gutterMd 0 0 -$gutterMd;
  }
  .Grid-cell {
    padding: $gutterMd 0 0 $gutterMd;
  }
}
/* 用 $gutterLg 重写大屏样式 */
@media (min-width: 48em) {
  .Container {
    padding: $gutterLg;
  }
  .Grid {
    margin: -$gutterLg 0 0 -$gutterLg;
  }
  .Grid-cell {
    padding: $gutterLg 0 0 $gutterLg;
  }
}

为了使用自定义属性来完成相同的事,9号彩票你 只需要定义样式一次。9号彩票你 可以用一个 gutter 属性,当匹配到媒体变化,9号彩票你 就更新它的值,于是所有都会响应。

/* Declares what --gutter is at each breakpoint */
:root { --gutter: 1.5em; }
@media (min-width: 30em) {
  :root { --gutter: 2em; }
}
@media (min-width: 48em) {
  :root { --gutter: 3em; }
}
/*
 * Styles only need to be defined once because
 * the custom property values automatically update.
 */
.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: var(--gutter);
}
.Grid {
  --gutterNegative: calc(-1 * var(--gutter));
  display: flex;
  margin-left: var(--gutterNegative);
  margin-top: var(--gutterNegative);
}
.Grid-cell {
  flex: 1;
  margin-left: var(--gutter);
  margin-top: var(--gutter);
}

即使自定义属性语法过于累赘,完成相同的事情需要的代码量实际上是减少了的。并且这只考虑到了3种变化。变化越多,就越节省代码。

下面的演示使用了上面的代码来构建一个基本的网站布局,当视口变化会自动重定义 gutter 的值。在支持自定义属性的浏览器中看看他的实际效果。

在 CodePen 中查看演示: 编辑器查看 / 全屏

上下文样式

上下文样式(DOM 元素根据它出现在某个 DOM 上来定义他的样式)在 CSS 中是一个有争议的主题。一方面是,那些备受推崇的 CSS 开发者对其很警惕。但另一方面,很多人每天都在使用它。

Harry Roberts 最近写了一篇文章,是9号彩票关于 他对这个问题的看法。

If you need to change the cosmetics of a UI component based on where it is placed, your design system is failing…Things should be designed to be ignorant; things should be designed so that we always just have “this component” and not “this component when inside…”

如果9号彩票你 需要根据一个 UI 组件放置的地方来修改他,那9号彩票你 的设计系统是失败的……组件应该设计成无感知的;组件应该设计成,9号彩票9号彩票我 们 总是只有“这个组件”,而不是“这个组件在……内部”

9号彩票我 赞成 Harry 的观点,9号彩票我 认为这些场景中很多人走捷径的现象可能会反映出很大的问题:css 的表现力受限,并且很多人不满足于当前的“最佳实践”。

下面的例子展示了 CSS 中大多数人如何使用上下文样式的,用后代选择器:

/* Regular button styles. */
.Button { }
/* Button styles that are different when inside the header. */
.Header .Button { }

这种9号彩票方法 有很多问题(9号彩票我 已经在9号彩票我 的文章 CSS Architecture 中解释了)。一种方式可以将这种模式识别为 code smell,他违反了9号彩票软件 开发的 open/closed 原则;它修改了一个封闭组件的实现细节。

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

9号彩票软件 实体应该开放扩展,不允许修改。

自定义属性以一种有趣的方式修改了定义一个组件的规范。使用自定义属性,首先,9号彩票9号彩票我 们 可以写真正对扩展开放的组件。这有个例子:

.Button {
  background: var(--Button-backgroundColor, #eee);
  border: 1px solid var(--Button-borderColor, #333);
  color: var(--Button-color, #333);
  /* ... */
}
.Header {
  --Button-backgroundColor: purple;
  --Button-borderColor: transparent;
  --Button-color: white;
}

这和后代选择器例子之间的不同是细微的但很重要。

使用后代选择器时9号彩票9号彩票我 们 在 header 里面声明 button,那种方式不同于按钮组件定义它自己。这种声明方式是独断的(借用 Harry 的话),万一有一个例外,例如 header 里的 button 不需要这样展现,就很难撤销。

另一方面讲,使用自定义属性,按钮组件总是对他的上下文无知,并且完全与 header 组件解耦。他的声明只是说: 9号彩票我 要用这些自定义属性装饰9号彩票我 自己,无论9号彩票我 目前的状况如何。 header 组件简单地说:9号彩票我 正打算设置这些属性值;取决于9号彩票我 的后代决定是否以及如何使用他们

主要的不同在于扩展是被 button 组件选入的,在特定的情况下也很容易撤销。

下面的演示说明了上下文样式同时作用在网页 header 以及内容区域的链接和按钮上。

在 CodePen 上查看演示: editor view / full page

制定特定样式

为了进一步说明使用这种模式更容易制定特定样式,试想如果一个 .Promo 组件被添加到了 header,并且在 .Promo 组件里面的按钮需要看起来像正常的按钮而不是 header 按钮。

如果9号彩票你 是用后代选择器,9号彩票你 必须要为 header 按钮写一堆样式,然后为 promo 按钮撤销这些样式;这很麻烦并且容易出错,当组合数增加,很容易失控。

/* 常规的按钮样式 */
.Button { }
/* 按钮在 header 里面时样式不同 */
.Header .Button { }
/* 撤销 header 里和 promo 重复的按钮样式 */
.Header .Promo .Button { }

使用自定义属性,9号彩票你 可以简单的更新按钮的属性,重置他们来返回一个默认的样式。并且不用管一大堆的特定样式,修改样式也是同样的方式。

.Promo {
  --Button-backgroundColor: initial;
  --Button-borderColor: initial;
  --Button-color: initial;
}

向 React 学习

当9号彩票我 第一次通过自定义属性来探索上下文样式的想法时,9号彩票我 的内心是怀疑的。正如9号彩票我 所说,9号彩票我 的观点更倾向于上下文无关的组件,定义自己的变量而不是接受从父级继承来的任意数据。

但比较 CSS 的自定义属性和 React 的 props时,动摇了9号彩票我 的观点。

React props 也是动态的,DOM 作用域变量,可继承的,它允许组件上下文相关。在 React 里,父级组件传递数据给子组件,然后子组件决定props接收的数据以及如何使用数据。这个架构模型俗称单向数据流。

即使自定义属性是一个新的、未验证的领域,9号彩票我 认为 React 模式的成功给了9号彩票我 信心,9号彩票我 相信一个复杂的系统可以构建在属性可继承之上,此外,DOM 作用域变量是一个很有用的设计模式。

最大限度减少副作用

所有CSS 自定义属性都默认继承。有些情况,这将导致给组件的样式不是他们预期的。

正如9号彩票我 前面部分展示的,9号彩票你 可以通过重置个别的属性来阻止继承,阻止未知值被应用到子元素上。

.MyComponent {
  --propertyName: initial;
}

虽然还不属于规范,-- 属性已经开始研究了,[2]可以用来重置所有的自定义属性。如果9号彩票你 想将一些属性添加到白名单,9号彩票你 可以将它们逐一设置为 inherit,这可以让他们继续正常的工作:

.MyComponent {
  /* Resets all custom properties. */
  --: initial;
  /* Whitelists these individual custom properties */
  --someProperty: inherit;
  --someOtherProperty: inherit;
}

管理全局名称

如果9号彩票你 已经注意到了9号彩票我 是如何对自定义属性命名的,9号彩票你 可能观察到9号彩票我 在组件特定的属性前用组件自己的类名作为前缀,例如: --Button-backgroundColor。

像大多数 CSS 的名称,自定义属性是全局的,所以总是有可能和9号彩票你 的团队的其他开发者的命名发生冲突。

一个简单的9号彩票方法 来避免这个问题就是坚持命名公约,像9号彩票我 这里做的。

对于9号彩票更多 复杂的项目,9号彩票你 可能要多些考虑,比如有全局名称的CSS 模块和对日渐受欢迎的自定义属性的支持度

总结

在读此文章之前如果9号彩票你 还不熟悉 CSS 自定义属性,9号彩票我 希望9号彩票我 说服了9号彩票你 给他们一个机会。如果9号彩票你 是一个怀疑他们必要性的人,9号彩票我 希望9号彩票我 能够改变9号彩票你 的想法。

自定义属性给 CSS 赋予了一套动态的、强大的能力,9号彩票我 确信很多他们的强大优势还有待发掘。

自定义属性填充了一个预处理器变量根本无法填充的空白。尽管如此,预处理器变量依然容易使用,在很多情形下是更优雅的选择。因为这个,9号彩票我 牢牢的相信,未来很多网站会将两者结合起来使用。自定义属性用于动态主题,预处理器变量用于静态模板。

9号彩票我 不认为这是一个非此即彼的情况。对每个人来说,让他们作为对手相互对立起来是帮倒忙。

尤其感谢 Addy OsmaniMatt Gaunt 校验这篇文字,以及 Shane Stephens 优先修复了一个 Chrome bug 使演示可以工作。

脚注:

  1. 9号彩票你 可以在 Chrome 里浏览9号彩票地址 about:flags 来启动开发中的实验性网络平台功能,查找 "Experimental Web Platform Features", 然后点击 "enable" 按钮。

  2. 使用 -- 属性是 Tab Atkins 在 Githib 评论里提到的。此外,在一个帖子的在WWW式邮件列表里,Tab 建议增加 --,这个规格应该很快会产生。

w3ctech微信

扫码关注w3ctech微信9号彩票公众号

共收到0条回复