作者 | 陸沉
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yMkljNlNDMhR2MwIWMlJDO0UGZ0ADO2UzYhJmMiNTNy8CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.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,看起來不錯。
/* 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 的過程中,屬性的值應該總是從一個有限的集合中去取。而不是任意取值。
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.
一份優秀的設計稿
在一份設計規範中,設計師首先會決定使用一些值。然後給它們設定一個上下文無關的名字,即 Global Token 。用于讓其他的 token 引用。
在此之上,在特定的上下文和抽象中,會基于 Global Token 生成一個具名的 Alias Token,用于傳達 Global Token 的設計預期。
最後,決定某個元件要使用某個特定 Design Token 時,會建立一個 Component-specific Token,讓開發人員能給予 Alias Token 去定義元件的 Token 别名。
從色值到元件
下面來個例子。
語義化和關注點分離
看到這裡可能有小夥伴開始懵逼了,是不是有哪裡搞錯了,不是說好了 Tailwind CSS 就是當初被噴成狗的 Atomic CSS 換了個皮卷土重來麼,怎麼跟你上面講的不太一樣?甚至官網上的示例都是這樣一串 class,是不是你在過度解讀 Tailwind CSS,夾帶了私貨?
前面已經讨論了如何把樣式和設計通過 Design Token 連接配接起來。但接下來可能要讨論一些比較奇怪的東西。
用雨燕首頁的 “最近常通路的應用” 清單為例。按照古典時代“關注點分離”的最佳實踐,也就是傳說中的 “寫 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/