天天看點

為自己的團隊定制 CSS 架構

作者 | 陸沉
為自己的團隊定制 CSS 架構

去年很火的 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,看起來不錯。

為自己的團隊定制 CSS 架構
/* 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 中至少有六種寫法,并且以下三種我基本看不出有啥差別……

為自己的團隊定制 CSS 架構

是以這裡所謂的 “強制一緻性”指的是開發人員在書寫 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.
為自己的團隊定制 CSS 架構

一份優秀的設計稿

在一份設計規範中,設計師首先會決定使用一些值。然後給它們設定一個上下文無關的名字,即 Global Token 。用于讓其他的 token 引用。

在此之上,在特定的上下文和抽象中,會基于 Global Token 生成一個具名的 Alias Token,用于傳達 Global Token 的設計預期。

最後,決定某個元件要使用某個特定 Design Token 時,會建立一個 Component-specific Token,讓開發人員能給予 Alias Token 去定義元件的 Token 别名。

為自己的團隊定制 CSS 架構

從色值到元件

下面來個例子。

為自己的團隊定制 CSS 架構

語義化和關注點分離

為自己的團隊定制 CSS 架構

看到這裡可能有小夥伴開始懵逼了,是不是有哪裡搞錯了,不是說好了 Tailwind CSS 就是當初被噴成狗的 Atomic CSS 換了個皮卷土重來麼,怎麼跟你上面講的不太一樣?甚至官網上的示例都是這樣一串 class,是不是你在過度解讀 Tailwind CSS,夾帶了私貨?

前面已經讨論了如何把樣式和設計通過 Design Token 連接配接起來。但接下來可能要讨論一些比較奇怪的東西。

為自己的團隊定制 CSS 架構

用雨燕首頁的 “最近常通路的應用” 清單為例。按照古典時代“關注點分離”的最佳實踐,也就是傳說中的 “寫 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/
為自己的團隊定制 CSS 架構

繼續閱讀