为自己的团队定制 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,看起来不错。
/* 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 别名。
从色值到组件 下面来个例子。
普通开发 | // css button { background: #2680EB; } | 裸写色值。无法做到强制一致性 |
文艺开发 | // css :root { --blue400: #2680EB; --ctaBackgroundColor: var(--blue400); } button { background: var(--ctaBackgroundColor); } | 通过 css variable 把 Design Token 代码化。 使用的时候引用。不要裸写色值。 这里并没有再创建一遍 Component-specific Token |
XX 开发 | // tailwind.config.js { theme: { extend: { colors: { blue: { 400: '#2680EB' } } } } } // css @layer components { .cta-background-color { @apply bg-blue-400; } .button-cta-background-color { @extend .cta-background-color; } } // html <button class="cta-background-color"> Click Me </button> | 通过 tailwind.config.js 把 Design Token 与值做关注点分离 通过 tailwind 的 layer directive 把 Design Token 与 Alias token 做连接 创建出 Component-specific Token,通过类名暴露给 HTML 组件使用 |
语义化和关注点分离
看到这里可能有小伙伴开始懵逼了,是不是有哪里搞错了,不是说好了 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>
多余的话
一个前端工程师在工作中,花在 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, ), ), ), ], ), ), );
引用
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论