关于保持断点 DRY

Avatar of Eduardo Bouças
Eduardo Bouças on

DigitalOcean 为您的旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

以下是 Eduardo Bouças 的客座文章。 Eduardo 回来跟进他以编程方式处理媒体查询的旅程。 他会告诉你它是怎么开始的,它去了哪里,以及它将如何进行。

一年前,我 踏上了个人探索之旅,寻找使用 CSS 预处理器功能管理断点并在项目中编写媒体查询的最佳方法。 结果是 include-media,一个承诺在 Sass 中实现“简单、优雅和易于维护”媒体查询的库。 但是,这对我来说效果如何呢?

上面的前两个形容词实际上相当主观,因为简单和优雅最终归结为个人喜好——没有必要谈论这些。 相反,我想与您分享我将该库在商业项目中使用一年的个人经验,以及它如何在第三个方面特别帮助了我:可维护性。

回到基础

对类似 include-media 的东西的需求源于开发人员绝对讨厌的一个问题:重复代码。 直到 元素查询 成为现实,我们开始在组件级别思考响应式设计,我们仍然受限于响应全局视窗变化的媒体查询,而不是在元素的上下文中。

因此,开发人员通常会定义“全局断点”,它们实际上只是页面中元素可能改变形式的任意视窗大小。 尽管这是一个简单的概念,可以在原生 CSS 中轻松实现,但预处理器可以立即带来一个优势:能够将这些断点动态定义为变量,允许开发人员仅声明一次并在任何地方引用它们。

$breakpoints: (
  'small': 400px,
  'medium': 900px,
  'large': 1200px
);

在过去的一年中,我一直专注于将该原则推向更远,通过在不同的上下文中使这些断点声明可用。

进入 JavaScript

在 JavaScript 例程中引用窗口宽度并不罕见。 例如,您可能正在执行一段代码,该代码会更改页面中某个元素的行为,但该元素可能只在大视窗中显示。 在这种情况下,如果窗口小于某个值,则完全跳过执行可能是有意义的。

.foo {
  display: none;

  // We're referencing the name of
  // the breakpoint, not the actual value
  @include media('>=large') {
    display: block;
  }
}
function doStuff() {
  // Bollocks, we're still referencing the value here!
  if (window.innerWidth < 1200) {
    return;
  }
    
  doHeavyStuff();
}

但我们又回到这里——那个“某个值”是我们自豪地在样式表中仅声明一次的断点之一,但我们现在重新声明了它的值,因为名称 large 在 JavaScript 中没有任何意义。 关于保持事物 DRY 的说法太少了。

为了解决这个问题,有一个名为 include-media-export 的插件,它是 SCSS 例程(将有关断点的信息写入 DOM)和一个用于读取和处理它的小型 JavaScript 实用程序的组合。 使用该插件,我可以重写上面的函数而不重新声明断点,因此如果有人决定将来更改它的值,一切都会好起来。

function doStuff() {
  // Yay, we're DRY again!
  if (im.lessThan('large')) {
    return;
  }
    
  doHeavyStuff();
}

im 对象通过 im.getActive 公开当前活动断点的名称,而 im.greaterThanim.lessThan 确定窗口是否比某个断点更宽或更窄。

这不是一个新概念,许多其他人过去也写过这方面的内容(主要是 Les JamesMike Herchel)。 这里的想法是将所有内容绑定在同一个生态系统中,为开发人员提供无缝的体验。

还有布局

我本身没有使用任何网格系统,但我发现拥有仅用于布局目的的某些类很方便。 当应用于元素时,这些类将定义其宽度,而不是在单个选择器上手动设置它——这会使样式表保持井井有条,并使标记更具语义性。

我过去会以类似这样的方式结束

.col--1-2 {
  width: 50%;
}

.col--1-3 {
  width: 33.3333%;
}

.col--2-3- { /* etc. */ }

在阅读了 Harry Robert 关于 BEMIT 的文章 之后,我对他的“响应式后缀”方法非常感兴趣。 他建议包含一个包含断点名称的类,以描述元素在该屏幕大小下的状态(例如 .class-name@breakpoint)。 这个想法导致了 include-media-columns,一个根据 include-media 中定义的断点生成列类的插件,遵循 BEMIT 的命名约定。

让我们以之前定义的断点列表为例,并想象一个用户资料组件,它必须

  • 在小视窗中使用其容器的全部宽度
  • 在中等视窗中占据一半的宽度
  • 在大视窗中占据三分之一的宽度
根据视窗改变的 user-profile 元素

我可以通过在 HTML 中为元素赋予正确的类来简单地定义该行为,而无需编写任何媒体查询甚至触碰 CSS。 它带来了语义性强且有意义的标记的巨大优势。

<div class="user-profile col col--1-1 col--1-2@medium col--1-3@large">
  <!-- User profile -->
</div>

后缀在 col--1-1 上有意被省略,作为移动优先方法的一部分,使其成为组件的“默认状态”。 如果您关心支持旧浏览器并且不希望它们获得移动视图,那么 这篇文章 中描述的 $im-media-support 标志可以解决您的问题。

使它成为工作流程的一部分

这一切都很好,但将此部分集成到您的工作流程中有多容易呢? 如果安装和更新很痛苦,那么试图提高可维护性就有点背离了初衷。 include-media 和插件都是 semver 版本的,并且可以作为 Bower 和 NPM 包使用,因此我在开始项目时将它们简单地包含为常规依赖项。

$ npm install include-media include-media-export include-media-columns --save

然后,我将 SCSS 文件导入到我的样式表中。

@import 'path/to/node_modules/include-media/dist/include-media';
@import 'path/to/node_modules/include-media-export/dist/include-media-export';
@import 'path/to/node_modules/include-media-columns/include-media-columns';

Export 插件不需要任何额外的配置,而 Columns 要求您指定要为页面生成多少个细分。

// I want to be able to divide the page in halves, thirds and fifths
@include im-columns(2, 3, 5);

总结

现在所有断点都定义在一个集中位置,任何更改或添加都会传播并提供给脚本和布局——我认为这缓解了我对 DRY 的困扰。

我并不想向您推销这种工作流程,我当然也不声称对任何突破性想法拥有功劳。 所有这些东西以前都存在,但我希望为开发人员提供工具,将所有内容捆绑在一起,使开发更轻松。

我迫不及待地想看到元素查询、CSS 变量、CSS Grid Layout 和其他现代 API 使本文中描述的所有内容完全过时——但在那之前,我对 include-media 和其贡献者一年(和 460 个 GitHub 星星!)给我带来的开发工作流程感到满意。