模块化 CSS:可配置的网格布局

虽然现在我们有了 CSS Grid,你还是可以用 flexbox 去实现网格布局。尽可能地将你的实现抽象成一个可复用可定制的模块,这将会是一个有趣的练习。来,先看看最直白的做法吧:

    .grid {  
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        padding: calc(var(--margin) / 2);
      }

    .grid > .cell {
        display: block;
        width: calc(100% / var(--columns) - var(--margin));
        margin: calc(var(--margin) / 2);
     }

要如何模块化上面的代码?首先,作为一个模块需要沙盒机制,所以先给它们都加上命名空间,取名有很多种方式,比如 BEM 还有 SMACSS。不过简洁起见。在本文里我们暂时用 my- 作为前缀。再补充上变量定义和一些注释:

    .my-grid {
      /* CSS 变量定义
         网格的列数 网格边距 列与列的间距 */
        --my-grid-columns: 1;
        --my-grid-margin: 16px;
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        /* 兼容代码 */
        padding: 8px;
        padding: calc(var(--my-grid-margin) / 2);
     }

看起来 my-grid 已经被封装成一个像模像样的模块了,那么要如何使用它呢?

     <link rel="stylesheet" href="my-grid.css">

引入样式表之后,根据样式覆盖原则,在默认样式的之后对 CSS 变量赋上新值:

     <link rel="stylesheet" href="my-grid.css">
      <style>
         .my-grid {
             --my-grid-columns: 2;
         }
      </style>

当然,你也可以在其他的选择器中对变量赋值;还能通过媒体查询,在不同的场景下使用不同的值:

    .my-app .my-grid {
        --my-grid-columns: 1;
        --my-grid-margin: 8px;
      }
     @media (min-size: 600px) {
        .my-app .my-grid {
           --my-grid-columns: 3;
           --my-grid-margin: 16px;
        }
     }
     @media (min-size: 1024px) {
        .my-app .my-grid {
           --my-grid-columns: 6;
           --my-grid-margin: 16px;
        }
     }

请注意,以上所有的配置统统是纯 CSS 实现的,不需要切换不同的类名改变网格的布局。这说明,模块开发者不需要提供一系列 CSS 类名给模块使用者了。只要提供了 CSS 变量,模块使用者就能灵活地去设置它们,也不再受限于开发者提供的类名。

应用场景之二:图片长宽比

假如你正在搭建自己的博客,你想给图片元素设置 max-width,防止它超出容器的范围:

    .my-content {
         max-width: 600px;
      }

如果一开始图片没有占位,那么在图片加载的时候页面内容会不断往下移动,所以一开始就要为图片占好位。在 img 标签中直接定义了宽高的话:

    <img src="kitten.jpg" height="1024" width="768" alt="A cute kitten">

图片在自适应窗口宽度的时候,浏览器是不会保证它的横纵比的,它只能保证图片的高度是 768px 同时让图片的宽度不要超过 600px(CSS 中的定义),结果我们会得到一张变形的图片:

那么你要怎么保证图片在任何场景下都不变形?使用 padding 保证图片的长宽比应该是最广为人知的方式(这个技巧的首发于此),如果要实现 16:9 的比例,你可以这么做:

    .aspect-ratio-16-9 {
         position: relative;
     }
     .aspect-ratio-16-9::before {
         display: block;
         padding-top: 56.25%; /* 9 / 16 * 100% */
         content: "";
     }
     .aspect-ratio-16-9-content {
         position: absolute;
         top: 0;
         right: 0;
         bottom: 0;
         left: 0;
      }

HTML 结构如下:

     <div class="aspect-ratio-16-9">
          <div class="aspect-ratio-16-9-content">
               This box will have a 16:9 aspect ratio.
          </div>
       </div>

如果只有几种横纵比还好,但你要做的是网格布局的图片流,就会遇到各种各样尺寸的图片,这个方法明显不能解决问题。

该自定义属性 API 上场了,让我们来实现一个固定长宽比的图片容器吧!

    .my-image-wrapper {
        /* 自定义属性
         * 长宽比 16:9 中的 16
         * 长宽比 16:9 中的 9  */
         --my-image-wrapper-w: 1; 
         --my-image-wrapper-h: 1;
         position: relative;
      }
      .my-image-wrapper::before {
         display: block;
         padding-top: calc(var(--my-image-wrapper-h, 1) / var(--my-image-wrapper-w, 1) * 100%);
         content: "";
       }
      .my-image-wrapper > img {
         position: absolute;
         top: 0;
         right: 0;
         bottom: 0;
         left: 0;
         height: 100%;
         width: 100%;
       }

—my-image-wrapper-w 和 —my-image-wrapper-h 就是模块化的 CSS 接口,使用起来很灵活:

    <div class="my-content my-image-wrapper" style="width:768px; --my-image-wrapper-w:4; --my-image-wrapper-h:3;">
         <img src="kitten.jpg" alt="A cute kitten">
     </div>

现在我们的小猫可以正常显示了!

将这一步结合到自动化流程中的话,就能保证任何图片的长宽比了!

注意:我把这个功能封装为一个 npm 插件 css aspect ratio,欢迎围观。

Web Components

Web Components 使得开发者能够开发和使用完全封装的可复用的 Web 组件。

    <my-component></my-component>

无论你的 Web Components 是否用到了 Shadow DOM,都应该暴露出一些属性接口为异化做准备。这样开发者在使用的时候该组件时,不用深入组件的代码分析,通过文档就能知道可以如何配置组件了。

更换主题是典型的应用场景。 如果组件中可调整的属性没有被暴露,组件使用者不得不一个个选择器地去覆盖原有样式:

    /* 组件使用者编写的组件新样式 */
      my-component > .top-thing {
         background: red;
      }
      my-component > .big-text {
         color: red;
      }
      my-component > .big-text:hover {
         color: red !important; /* bug #42 修复. */
      }
      my-component > .content > .column {
         width: calc(50% - 16px); /* 调整为两列 而不是原来的三列 */
      }

对使用者(以及几个月以后已经忘了自己写过什么的你)来说,如果组件中有任何可以异化的地方,将它们以自定义属性的方式暴露出来是相当友好的:

     my-component {
         /* my-component 自定义属性 
          * 主题色,应用到整个主题上
          * 组件内容将会被分成三列 */
          --my-component-theme-color: blue;
          --my-component-accent-color: red;
          --my-component-columns: 3;
        }

通过这个方式,使用者在未来更新组件表现时也不会有心理负担,他们可以这么写:

     /* 组件使用者编写的组件新样式 */
       my-component {
          --my-component-theme-color: red;
          --my-component-columns: 2;
        }

如果在组件中你使用了 Shadow DOM,引入自定义元素更是必要。要使用者重置 Shadow DOM 样式实在不合理。至于不合理的原因,以及关于 Shadow DOM 的更多考虑请看 Eric Bidelman 的 Shadow DOM v1:独立的网络组件。

你我的工作

将模块(或 Web Components)中的可变部分独立出来很重要。这样使用者才能知道如何根据情况,安全地异化模块。不要说其他使用者,这么做也是为几个月之后的开发者本人铺平道路啊。

通过建立一套完整的自定义属性体系可以让你的模块和组件复用性更高,客户端代码更健壮,甚至可以作为模块或组件的内置文档。