为自己的团队定制 CSS 框架

简介: 如何基于 Tailwind CSS 为自己的团队定制一套舒服的 CSS 框架?
作者 | 陆沉

image.png
去年很火的 Tailwind CSS 是何方神圣,到底是 Atomic CSS 余孽的卷土重来还是真的有点东西。Tailwind CSS 如何帮助我们建立界面样式到设计语言的连接,Utility-first 的 CSS 工作流是怎样的,以及,如何基于 Tailwind CSS 为自己的团队定制一套舒服的 CSS 框架。

我们太有限了,我们只能做我们觉得是对的事情,然后接受它的事与愿违。-- 罗翔

CSS 工程化要解决的问题

至少在中后台研发领域,我觉得团队在 CSS 领域会遇到以下几个问题要解决:

  • 强制一致性:如何强制规约界面的字体、字号和颜色收敛。
  • 设计关联:如何形成界面样式到设计语言的连接与对应。
  • 语义化:关注点分离是银弹还是误解。
  • 内联样式:在何时进行抽象。

强制一致性

在解释什么是强制一致性之前,请大家来猜一下,在 yuque.com 上,有多少种不同的字号、文字颜色和背景颜色。

答案可能跟你预期的有些差异。yuque.com 上一共有 34 种不同的字号、77 种不同的文字颜色和 56 种不同的背景颜色。https://cssstats.com/stats/?url=https://yuque.com/

事实上 yuque.com 已经做得足够好了。因为在 Github.com 上一共有 56 种字号,163 种文字颜色和 147 种背景颜色。在一些企业级的 Web 应用上其实会更可怕,例如 Gitlab 一共有 59 种字号、402 种文字颜色和 239 种背景颜色。

为什么会出现这种事情?当设计师把设计稿交给我们之后,还原设计稿的最便捷方式之一就是使用设计工具的 "Copy as CSS" 功能导出对应的 CSS,看起来不错。
image.png

/* Lorem ipsum dolor si */
position: absolute;
width: 232px;
height: 144px;
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;

/* or 150% */
color: rgba(0, 0, 0, 0.541327);

观察这段 CSS,会发现,在这里,字体、字号和颜色都是一个自由的,没有规约的值。“每一行 CSS 都是一个空白的画布,没有人能阻止你使用任何你想要的值”。这就是为什么同样是视觉设计师想要的语雀品牌绿,在 CSS 中至少有六种写法,并且以下三种我基本看不出有啥区别……
image.png
所以这里所谓的 “强制一致性”指的是开发人员在书写 CSS 的过程中,属性的值应该总是从一个有限的集合中去取。而不是任意取值。

Design Token

再来。这是一份 Google Materia Design 的设计稿。会发现 Google 的设计师除了标注了这些元素的颜色值之外,还贴心的写了个名字 Gray 900。这就是所谓的 Design Token:

Design tokens are all the values needed to construct and maintain a design system — spacing, color, typography, object styles, animation, etc. — represented as data.

image.png一份优秀的设计稿

在一份设计规范中,设计师首先会决定使用一些值。然后给它们设置一个上下文无关的名字,即 Global Token 。用于让其他的 token 引用。

在此之上,在特定的上下文和抽象中,会基于 Global Token 生成一个具名的 Alias Token,用于传达 Global Token 的设计预期。

最后,决定某个组件要使用某个特定 Design Token 时,会创建一个 Component-specific Token,让开发人员能给予 Alias Token 去定义组件的 Token 别名。

image.png
从色值到组件

下面来个例子。

image.png

语义化和关注点分离

image.png
看到这里可能有小伙伴开始懵逼了,是不是有哪里搞错了,不是说好了 Tailwind CSS 就是当初被喷成狗的 Atomic CSS 换了个皮卷土重来么,怎么跟你上面讲的不太一样?甚至官网上的示例都是这样一串 class,是不是你在过度解读 Tailwind CSS,夹带了私货?

前面已经讨论了如何把样式和设计通过 Design Token 连接起来。但接下来可能要讨论一些比较奇怪的东西。

image.png

用雨燕首页的 “最近常访问的应用” 列表为例。按照古典时代“关注点分离”的最佳实践,也就是传说中的 “写 HTML 的时候不用关心样式”,我们会怎么写这样的列表:

<ul class="application-list">
  <li>
    <a href="/yuyan/yuyanAssets">
      <img src="https://gw.alipayobjects.com/zos/basement_prod/9a7a9c64-01ee-45ca-a615-6063a24f70a9.svg" />
      <div>
        <h4>yuyanAssets</h4>
        <span>雨燕前端应用</span>
      </div>
    </a>
  </li>
</ul>
.application-list {
  list-style: none;
  > li {
    background: #fff;
    > a {
      display: block;
      padding: 18px 22px;
      
      > img {
        display: block;
        width: 38px;
        height: 38px;
        float: left;
      }
      
      > div {
        display: inline-block;
        > h4 {
          color: #314659;
          font-weight: 600;
          margin: 0;
        }

        > span {
          color: #697b8c;
          font-size: 12px;
        }
      }
    }
  }
}

现在谁这么写 CSS 绝对可能会被揍。它最大的坏处是 HTML 和 CSS 的层次结构必须完全对应。HTML 怎么嵌套的, CSS 就必须怎么嵌套。

后来我们开始有了 BEM,写出来的 HTML 会不那么欠揍了:

<ul class="application-list">
  <li class="application-list__item">
    <a class="application-list__link" href="/yuyan/yuyanAssets">
      <img class="application-list__img" src="https://gw.alipayobjects.com/zos/basement_prod/9a7a9c64-01ee-45ca-a615-6063a24f70a9.svg" />
      <div class="application-list__content">
        <h4 class="application-list__title">yuyanAssets</h4>
        <span class="application-list__description">雨燕前端应用</span>
      </div>
    </a>
  </li>
</ul>

.application-list {
  list-style: none;
  
  &__item {
    background: #fff;
  }
  
  &__link {
    display: block;
    padding: 18px 22px;
  }
 
  &__img {
    display: block;
    width: 38px;
    height: 38px;
    float: left;
  }
  
  &__content {
    display: inline-block;
  }
  
  &__title {
    color: #314659;
    font-weight: 600;
    margin: 0;
  }

  &__description {
    color: #697b8c;
    font-size: 12px;
  }
  
}

现在看起来舒服多了。但是问题来了。如何复用?

例如现在需要写一个结构非常类似的列表,例如雨燕首页的进行中的迭代的列表,希望最大限度复用上面这个结构。一种不纠结的做法是拷一遍。另一种做法是使用注入 less / sass 的 mixin 或者 extends 功能复用样式。

复用:使用 mixin 或者 extends

<ul class="application-list">
  <li class="application-list__item">
    <a class="application-list__link" href="/yuyan/yuyanAssets">
      <img class="application-list__img" src="https://gw.alipayobjects.com/zos/basement_prod/9a7a9c64-01ee-45ca-a615-6063a24f70a9.svg" />
      <div class="application-list__content">
        <h4 class="application-list__title">yuyanAssets</h4>
        <span class="application-list__description">雨燕前端应用</span>
      </div>
    </a>
  </li>
</ul>


<ul class="sprint-list">
  <li class="sprint-list__item">
    <a class="sprint-list__link" href="/yuyan/yuyanAssets">
      <img class="sprint-list__img" src="https://gw.alipayobjects.com/zos/rmsportal/yeSGzTolyopHKmBeKQHC.svg" />
      <div class="sprint-list__content">
        <h4 class="sprint-list__title">迭代1</h4>
        <span class="sprint-list__description">basement/basementweb</span>
      </div>
    </a>
  </li>
</ul>

.application-list {
  list-style: none;
  
  &__item {
    background: #fff;
  }
  
  &__link {
    display: block;
    padding: 18px 22px;
  }
 
  &__img {
    display: block;
    width: 38px;
    height: 38px;
    float: left;
  }
  
  &__content {
    display: inline-block;
  }
  
  &__title {
    color: #314659;
    font-weight: 600;
    margin: 0;
  }

  &__description {
    color: #697b8c;
    font-size: 12px;
  }
  
}

.sprint-list {
 .application-list;
}

复用:创建内容无关样式

另一种方案是创建一个内容无关的 CSS,由 application 和 sprint 两个实体列表共同使用。如果需要只修改 sprint 列表中的样式,又不想影响到其他 entiry-list,就需要语义化的增加一个 class,然后通过这个新的语义化 class 来覆盖样式。

<ul class="entity-list">
  <li class="entity-list__item">
    <a class="entity-list__link" href="/yuyan/yuyanAssets">
      <img class="entity-list__img" src="https://gw.alipayobjects.com/zos/basement_prod/9a7a9c64-01ee-45ca-a615-6063a24f70a9.svg" />
      <div class="entity-list__content">
        <h4 class="entity-list__title">yuyanAssets</h4>
        <span class="entity-list__description">雨燕前端应用</span>
      </div>
    </a>
  </li>
</ul>

<ul class="entity-list sprint">
  <li class="entity-list__item">
    <a class="entity-list__link" href="/yuyan/yuyanAssets">
      <img class="entity-list__img" src="https://gw.alipayobjects.com/zos/rmsportal/yeSGzTolyopHKmBeKQHC.svg" />
      <div class="entity-list__content">
        <h4 class="entity-list__title">迭代1</h4>
        <span class="entity-list__description">basement/basementweb</span>
      </div>
    </a>
  </li>
</ul>

.entity-list {
  list-style: none;
  
  &__item {
    background: #fff;
  }
  
  &__link {
    display: block;
    padding: 18px 22px;
  }
 
  &__img {
    display: block;
    width: 38px;
    height: 38px;
    float: left;
  }
  
  &__content {
    display: inline-block;
  }
  
  &__title {
    color: #314659;
    font-weight: 600;
    margin: 0;
  }

  &__description {
    color: #697b8c;
    font-size: 12px;
  }
  
}

.entity-list.sprint {
  .entity-list__img {
    margin-right: 8px;
  }
}

这只是一个选择……

  • 要么保持关注度分离,在写 HTML 的时候(尽量)不关心 CSS,使用 mixin 和 extends 做复用。
  • 要么开始尝试创建内容无关的样式,并以可复用的方式命名所有内容,这就是 Tailwind CSS 作者的理念。

内联样式

if (status === 'FAIL') {
  return <CloseCircleFilled style={{ color: '#F5222D', fontSize: 16, float: 'right' }} />;
}

不知道大家怎么看这样的代码。这是一个 Icon,在这个场景下我们需要去给它设置颜色和字号。这样写内联样式总觉得很奇怪,其实也合理。因为如果我们真的为了这个场景去创建个样式出来,就真的太奇怪了。并且会带来额外的起名负担。还会担心重名(于是我们又引入了 css module),所以很可能你会写出来这样一个 class:


// JSX:
if (status === 'FAIL') {
  return <CloseCircleFilled className="redCloseIconRight" />;
}

// CSS
.redCloseIconAlignRight {
  color: #F5222D;
  fontSize: 16px;
  float: right;
}

内联样式会带来两个问题:

  • 无法做到强制一致性。除非你要在内联样式里写 CSS Variable,否则没办法保证样式值的收敛。
  • 过于复杂的内联样式很恶心,例如 box-shadow、font-family。很容易又转回到创建一个局部 class 的情形。

在这两种情况下,为一些常用的样式设定 Utility Classes 其实非常方便。.clearfix 就是特别典型的例子。Tailwind CSS 的另一个爽点就在这里。通过配置,可以创建出链接到 Design Token 的 Utility Classes。不管在 css 里通过 apply 复用,还是直接在 jsx 里用,都非常方便:

// JSX:
if (status === 'FAIL') {
  return <CloseCircleFilled className="text-red-500 text-base" />;
}

// 加个 shadow 也很方便:
if (status === 'FAIL') {
  return <CloseCircleFilled className="text-red-500 text-base shadow-sm" />;
}

正式介绍一下 Tailwind CSS

写到这里终于可以正式介绍一下 Tailwind CSS 了。

Q: Tailwind CSS 是 Atomic CSS 吗?

A: 不是。它是一个 Utility First 的 CSS 框架。提供了对提升 CSS 开发效率的一系列 Utility Class 的抽象,以及自定义 Utility Class 的方法。

Q: 然后呢

A: 以 tailwind.config.js 为桥梁,建立起属于自己团队的从 Design System 到 CSS 框架的连接。

Q: 那如何低成本解决原先有个 class 叫 .black ,然后很多组件都用了,但是突然有需求要把他们改成 蓝色 的问题

A: 按照上面 Design Token 的做法,做 component-layer 封装即可。

如何做?

以 yuyanAssets 为例子:

1. 在 tailwind.config.js 中定义 Design Token

module.exports = {
  darkMode: false, // or 'media' or 'class'
  purge: [
    './src/**/*.{js,jsx,ts,tsx}'
  ],
  theme: {
    extend: {
      fontFamily: {
        mono: [ 'Menlo', 'Consolas', 'monaco', 'monospace' ],
      },
      fontSize: {
        xs: '12px',
        sm: '14px',
        base: '16px',
        lg: '20px',
        xl: '24px',
      },
      fontWeight: {
        light: 300,
        normal: 400,
        medium: 500,
      },
      colors: {
        primary: '#1890ff',
        info: '#2c92f6',
        warn: '#ffbf00',
        success: '#00a854',
        fail: '#f04134',
        doing: '#697b8c',
        pause: '#a3b1bf',
        enable: '#52c41a',
        disable: '#f5222d',
        danger: '#f04135',
        icon: {
          0: '#f04134',
          1: '#00a854',
          2: '#108ee9',
          3: '#f5317f',
          4: '#f56a00',
          5: '#7265e6',
          6: '#ffbf00',
          7: '#00a2ae',
        }
      },
      boxShadow: {
        DEFAULT: '0px 4px 4px rgba(0, 55, 107, 0.04)',
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

2. 把原先 less 中散落的各种 Design Token 使用 apply 描述。

 .panel-body {
    flex: 1;
    background: @background-color-content;
    border-radius: @border-radius-default;
    box-shadow: @shadow-default;
    overflow: hidden;
 }


改成

 @layer components {
    .panel-background {
       @apply gb-white;
    }
}

 .panel-body {
    .panel-background;
    @apply flex-1 rounded shadow;
    overflow: hidden;
 }

3. 去除无用抽象。把内联样式改写成 Utility Class

<div style={{ width: 120, marginLeft: 16, marginRight: 12 }}>
  <Progress percent={progress} format={percent => `${percent}%`} />
</div>

<Avatar className={`icon-product icon-color-${colorIndex}`}>
  {iconLetter}
</Avatar>

// 改成

<div className="w-28 ml-4 mr-3">
  <Progress percent={progress} format={percent => `${percent}%`} />
</div>

<Avatar className={`icon-product bg-color-${colorIndex}`}>
  {iconLetter}
</Avatar>

多余的话

在 2021 年的当下,一个前端工程师在工作中,花在 JavaScript、CSS 和 Html 的上的时间占比大概跟前面的排序一样。JavaScript > CSS >>> Html。早年间前端工程师可能还会通过模板关注到 Html 的结构,而现在,随着 React 接管了 DOM,前端工程师的关注点已经慢慢从 HTML 移动到了 JSX 上。

甚至在整个生产过程也跟古典的“写语义化的 HTML -> 给他们取个 Class -> 写选择器 -> 写 CSS ” 不同了。工程师总是尝试优先使用已经写好的组件(如果没有就写一个),然后组合搭建出整个界面。甚至在布局的时候都很少关注 HTML:比如 antd 已经提供了 Layout 布局组件,又比如 Material Design 整个布局都是基于 Responsive Layout 的,基本上没有考虑有关 HTML 文档流的什么事情。

在 React 刚出来的时候,有很大一部分前端工程师表示 JSX 这种把逻辑和模板混在一起写的方式就是倒退。但随着 Flutter 和 Swift UI 的流行,大家惊奇的发现整个业界都在“倒退”。

所以也许我们可以换个想法,把 HTML 和 CSS 当成 UI 框架输出的结果。在书写代码的过程中,它们是什么样子的,可能并没有那么重要。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Turtle Rock")
                .font(.title)
            Text("Joshua Tree National Park")
                .font(.subheadline)
        }
    }
}
@override
Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () {
            controller
            ..reset()
            ..forward();
        },
        child: RotationTransition(
            turns: animation,
            child: Stack(
                children: [
                    Positioned.fill(
                        child: FlutterLogo(),
                    ),
                    Center(
                        child: Text(
                            'Click me!',
                            style: TextStyle(
                                fontSize: 60.0,
                                fontWeight: FontWeight.bold,
                            ),
                        ),
                    ),
                ],
            ),
        ),
    );

引用
https://spectrum.adobe.com/page/design-tokens/

https://adamwathan.me/css-utility-classes-and-separation-of-concerns/

https://css-tricks.com/bem-101/


image.png

相关文章
|
2月前
|
前端开发
HTML+CSS基础知识(6)背景的设置、表格的设计、表单的设计和框架集
这篇文章详细介绍了如何在HTML和CSS中设置背景、设计表格、创建表单以及使用框架集,并通过代码示例和测试结果展示了具体的实现方法和效果。
HTML+CSS基础知识(6)背景的设置、表格的设计、表单的设计和框架集
|
5月前
|
前端开发
【第18期】一文读懂原子CSS框架
【第18期】一文读懂原子CSS框架
203 0
|
3月前
|
前端开发
vue3 【提效】使用 CSS 框架 UnoCSS 实用教程
vue3 【提效】使用 CSS 框架 UnoCSS 实用教程
183 1
|
5月前
|
Java BI 数据库
基于SSM框架实现面向小微企业的简历管理系统企业简历管理系统(分前后台spring+springmvc+mybatis+maven+jsp+css+echarts)
基于SSM框架实现面向小微企业的简历管理系统企业简历管理系统(分前后台spring+springmvc+mybatis+maven+jsp+css+echarts)
|
5月前
|
前端开发 JavaScript 开发者
【专栏:HTML与CSS实践篇】CSS框架(Bootstrap/Foundation)快速上手
【4月更文挑战第30天】Bootstrap和Foundation是两种流行的CSS框架,用于构建响应式网页。它们包含预定义的样式、栅格系统和组件,加速开发流程。Bootstrap以其12列栅格系统闻名,而Foundation提供更定制化和模块化选项。了解并熟练运用这些框架的基本概念和组件,结合最佳实践和性能优化,能帮助开发者高效创建符合现代设计趋势的网页项目。
107 3
|
5月前
|
存储 前端开发 JavaScript
《CSS 简易速速上手小册》第7章:CSS 预处理器与框架(2024 最新版)
《CSS 简易速速上手小册》第7章:CSS 预处理器与框架(2024 最新版)
67 2
|
5月前
|
前端开发 JavaScript 搜索推荐
CSS框架是前端开发中不可或缺的工具
【4月更文挑战第12天】CSS框架是前端开发中不可或缺的工具
50 2
|
5月前
|
开发框架 前端开发 搜索推荐
标题:【专栏:CSS进阶篇】CSS样式重置与框架:快速构建统一风格的网页
【4月更文挑战第30天】本文探讨了CSS样式重置和框架在确保网页跨浏览器一致性中的作用。样式重置通过消除默认样式差异实现一致外观,而CSS框架如Bootstrap提供预设样式和组件,加速开发并保证页面一致性。框架还有响应式设计和易于维护的优点,但也可能限制自定义和增加性能开销。选择使用哪种工具应根据项目需求、团队技能和设计复杂度来决定。开发者可结合使用两者以平衡灵活性和控制。
50 0
|
5月前
|
前端开发 JavaScript 开发者
编程笔记 html5&css&js 014 网页布局框架
编程笔记 html5&css&js 014 网页布局框架
|
5月前
|
前端开发 JavaScript API
编程笔记 html5&css&js 011 HTML内连框架
编程笔记 html5&css&js 011 HTML内连框架