在各种知名样式表或者组件库开箱即用的优势下,原子样式依然流行,说明了成品样式(半定制样式)存在升级、变更或者管理的需求,在维护时与发展或者规范形成了冲突。在大粒度上不能较好的解决问题时,势必下钻寻求更好的解决方式。通过页面的 CSS 定义来生成有效的样式,量体裁衣,避免了样式的臃肿或者浪费。那么原子样式的未来在哪里呢?
1 原子样式的特点
原子样式在实际使用受欢迎,如果用一句话表示,设计先行理念下的设计规范的适应性表达。
从表达方式来看,既有习惯约定配置,也有量化表达。
1.1 习惯约定配置
原子样式事实上将复杂的设计转换为文本类的知识点,同时进行了简化、语义化、内容化。
特点1:越常用的缩写越短,反之尽量使用个性化词汇全拼。
例如:p
表示 padding
,而不是 paragraph
。
rorate
同时表示 transform
和 rotate
两个词汇。
shadow
隐含了 box-shadow: 0 var(--unit-10) var(--unit-20) var(--color-gray-5-a04)
三个数值和一个颜色。
简洁又贴切的表达隐含了设计的细节,降低了学习成本。
特点2:样式名称趋近语义,提高了理解程度。
例如:round
使用 border-radius
属性,赋予 50% 的数值实现了圆形效果。round
在语义上比 radius
更能表明效果。
例如:mx
表示 margin-left
和 margin-right
两个属性。x
表示 left
和 right
两个方向,是一种组合表达。
例如:round-top
使用了 border-top-left-radius
和 border-top-right-radius
两个属性,也是一种组合式表达。
例如:top-10
表示 top: 10rpx
,与 round-top
中的 top 在理解中一致,不混淆。
特点3:强化内容,减少修饰词。
例如:text-28
表示字体高 28 rpx,对应样式为 font-size: 28rpx
,text-primary
表示字体为主题色,对应样式为 color: var(--color-primary)
,text-bold
表示粗体,对应样式为 font-weight: bold
。
text 作为使用的主体,增加不同的内容限定词,即可准确完成 font-size 、 color 和 font-weight 三个样式的表达。
1.2 量化表达
原子样式通过量化表达在规范性上提供了扩展性。
特点1:空间数值的量化
例如:m-20
表示了 margin: (--unit-20)
样式,m-32
表示了 margin: (--unit-32)
样式。w-p20
表示了 width: (--unit-percent-20)
样式。
通过建立空间数值的识别规则,动态支持多个值。
例如:shadow-1
表示 box-shadow: 0 var(--unit-2) var(--unit-4) var(--color-gray-5-a07)
样式,shadow-2
表示 box-shadow: 0 var(--unit-4) var(--unit-8) var(--color-gray-5-a06)
样式。
通过对阴影编号,将复杂的数值组合简单的表达。
特点2:色彩的量化
电子设备可表达上千万种颜色,人眼能识别百万种颜色,但若是用于信息的日常表达,具有较强区别度的色彩则可以降低到百种以内,便于开发工程师使用。
通过确定调色板的色系,分为十阶即可完成基本的量化。灰色作为常用中性色,可以适当扩展数量。
例如:text-red-6
表示 color: var(--color-red-6)
,bg-gray-3
表示 background-color: var(--color-gray-3)
。
通过颜色名称和序号指定色号。
例如:ant-design 推荐了 red、orange、yellow、green、blue、purple、magenta、cyan、lime、gold、volcano 11种色彩。
1.3 适应性表达
原子样式具有明确的含义,对于不同的设备或者不同的设计要求,具备适应能力。
方式1 :支持多种主题,支持多种模式
CSS变量在其中发挥重要的作用,可以通过变更变量达到动态变更,可以通过。
原子样式本身支持多个调色板,可以预先定义。
通过更换调色板,支持更换色系,支持黑夜模式,支持高对比模式,适应红绿弱识别等。
通过更换数值变量,支持字体放大缩小。
方式2:支持多种设备
media 本身的约束支持多种设备的同时支持,对于原子样式并无特别的优势,这点就不赘述了。
2 原子样式的组合
原子样式在使用时附着在元素上,元素需要通过多种样式呈现。因此原子样式在使用时,往往是组合的。
2.1 静态样式定义
2.1.1 组件样式简单定义
元素的样式在定义时即可确定,包括预定义交互行为中使用的确定样式。
例如: tag 标签对应的wxml内容为
<view class="bg-primary text-white text-28 px-8 py-4">默认</view>
因此可以直接定义标签样式
tag = [bg-primary, text-white, text-28, px-8, py-4]
2.1.2 多主题样式定义
这里加入颜色支持,将背景色改为变量,组合样式定义变更为
tag-[color] = [bg-[color], text-white, text-28, px-8, py-4]
相应的可以使用 tag-blue-6
, tag-red-6
, tag-orange-6
等表达其他颜色标签。
加入主题约束,可以联合定义如下:
tag-[color] = [bg-[color], text-white, text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
2.1.3 多风格样式定义
对于可能存在的填充(fill)、线框(line)风格,可以进一步强化定义
tag-fill-[color] = [bg-[color], text-white, text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
tag-line-[color] = [border, border-[color], text-[color], text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
加入风格后,可以联合定义如下:
tag-[style]-[color] = [text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
2.2 动态样式定义
2.2.1 静态样式与动态样式区别
元素的样式在运行时才能确定,通常受到数据元素的影响。动态的样式往往是样式字典,需要动态使用。
静态样式与动态样式之间的区别在于,静态样式是单状态下的单样式,动态样式是多状态下的多样式,但某个时刻只呈现一种样式,具体渲染哪种样式由动态属性决定。
动态样式有两种情况,一是根据属性动态组合,二是根据状态机选择。
在脚本中使用动态样式
动态样式的使用有两种方式,一是在页面上根据需要选择,二是在 Javascript/typeScript 中定义,渲染到页面中。显然前者可以使用工具分析页面跟踪到,但判断的情况较多,对于页面性能可能存在影响,后者则需要拓展工具的能力,防止不能识别原子样式,导致管理失控。
2.2.2 交互样式
在微信小程序中,元素具有 hover-class 属性,因此其点击状态下属性定义更加方便,
<view ... hover-class="bg-primary-a30 text-black">默认</view>
因此可以直接定义标签样式
tag:hover = [bg-primary-a30, text-black]
对应 tag-[style]-[color]
的定义,hover属性样式可以定义为:
tag-[style]-[color]:hover = [text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
, style = { fill, line }
, fill = [bg-[color]-a80, text-white]
, line = [bg-[color]-a10]
2.2.2 可选属性样式
对标签的线宽和是否圆角进一步定义,可以联合定义如下:
tag-[style]-[color] = [text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
, optional = [round-[N], border-[N]]
可选属性也可以不合并,当作追加样式或者覆盖样式。
例如,对英文标签可以追加全部大写、首字母大写,
既可以增加在定义内部,
tag-[style]-[color] = [text-28, px-8, py-4, text-uppercase]
, color = {primary, blue-6, red-6, orange-6}
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
, optional = [round-[N], border-[N]]
也可以与定义组合。
<view class="tag-red-6 text-uppercase">...</view>
2.2.3 行为状态样式
对于标签,行为状态有选中状态和未选中状态。
# 公共样式
tag-[style]-[color] = [text-28, px-8, py-4, text-uppercase]
, color = {primary, blue-6, red-6, orange-6}
, optional = [round-[N], border-[N]]
# hover 样式
tag-[style]-[color]:hover = [text-28, px-8, py-4]
, style = { fill, line }
, fill = [bg-[color]-a80, text-white]
, line = [bg-[color]-a10]
# 状态集合
tag-[style]-[color]:state = { checked, unchecked }
# 选中状态
tag-[style]-[color]:checked = []
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
# 未选中状态
tag-[style]-[color]:unchecked = []
, style = { fill, line }
, fill = [border, border-[color], text-[color]]
, line = [bg-[color], text-white]
2.2.4 内容状态样式
对于标签,属性状态根据内容可以自定义,例如分为主要标签、次要标签,和主题颜色有部分重叠,可以定义如下
# 公共样式
tag-[style]-[color] = [text-28, px-8, py-4, text-uppercase]
, color = {primary, blue-6, red-6, orange-6}
, optional = [round-[N], border-[N]]
# hover 样式
tag-[style]-[color]:hover = [text-28, px-8, py-4]
, style = { fill, line }
, fill = [bg-[color]-a80, text-white]
, line = [bg-[color]-a10]
# 行为状态集合
tag-[style]-[color]:state = { checked, unchecked }
# 选中状态
tag-[style]-[color]:checked = []
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
# 未选中状态
tag-[style]-[color]:unchecked = []
, style = { fill, line }
, fill = [border, border-[color], text-[color]]
, line = [bg-[color], text-white]
# 属性状态集合
tag-[style]-[color]:theme = { primary, secondary }
# 属性状态
tag-[style]-secondary = [tag-[style]-blue-5]
在实际使用中,示例如下:
<view class="tag-fill-primary" hover-class="tag-fill-primary_hover">科技</view>
<view class="tag-fill-secondary" hover-class="tag-fill-secondary_hover">消费电子</view>
2.3 小结
动态样式是在静态样式上继承发展的,一张图示例展示如下。
通过扩展规则,我们可以实现单个元素的多状态的样式定义。往上一步,原子样式在组件中如何工作呢?
3 原子样式到原子设计
元素定制形成组件,组件组合得到模块,模块选用得到页面,这也是原子设计的基础。
在考虑原子样式的组合使用时,需要考虑组件、模块和页面的使用。
3.1 元素的状态样式
原子组件包含一个或者多个元素,每个元素都可以分别定义样式。
以标签为例,标签可以是单个 View 元素,也可以是 View 元素加上 Icon 的组合,Icon 提供关闭功能。
转换为样式函数,伪代码如下:
const componentClassName = (iconName, style = fill, state = unchecked,
theme = primary): setting => {
"container": [gap-10],
"content": [
tag-[style]-[color] = [text-28, px-8, py-4]
, color = {primary, blue-6, red-6, orange-6}
, optional = [round-[N], border-[N]]
tag-[style]-[color]:hover = [text-28, px-8, py-4]
, style = { fill, line }
, fill = [bg-[color]-a80, text-white]
, line = [bg-[color]-a10]
# 行为状态集合
tag-[style]-[color]:state = { checked, unchecked }
# 选中状态
tag-[style]-[color]:checked = []
, style = { fill, line }
, fill = [bg-[color], text-white]
, line = [border, border-[color], text-[color]]
# 未选中状态
tag-[style]-[color]:unchecked = []
, style = { fill, line }
, fill = [border, border-[color], text-[color]]
, line = [bg-[color], text-white]
],
"icon": [
icon-[iconName], wh-36, round
}
}
单状态的样式定义较为复杂,合并后重新定义状态机,在 TypeScript 中实现如下:
状态机的所有参数定义放入 paraDefs 中,元素的样式规则放入 elements,通过固定的样式 compose 和 条件样式 conditons 合并得到元素的最终样式。
整个定义逻辑简单,只有 name, action, compose, conditons, where, paraDefs, elements 7个关键词,状态在 paraDefs, where 中使用,样式均在 compose 中使用。
页面属性到样式的计算示例。
updateClassName() {
const { checked, theme, showClose, style } = this.data
const classNameDict = cne.applyClassName({
state: checked ? "checked" : "unchecked",
color: theme,
icon: showClose ? "close" : "",
style
}, Rule)
console.log(`[tag] classNameDict`, classNameDict)
this.setData({
classNameDict
})
},
页面展示的示例。
<!--components/form/tag/tag.wxml-->
<view class="{{ classNameDict.container }}" catchtap="toggleValue">
<view class="{{ classNameDict.content }}" hover-class="{{classNameDict.content_hover}}">{{ title }}</view>
<image wx:if="{{ showClose }}" src="../img/clear.png" class="{{ classNameDict.icon }}" mode="aspectFill"></image>
</view>
3.2 三种样式生成方式
3.2.1 规则生成
根据 CSS 规则预生成样式,通过参数集合定义最大的样式范围,压缩保存为静态样式文件。
样式生成时机:用户开发以前。
使用成本:用户选择样式使用,学习的时间较多,基本没有配置成本,编码简单。
优点:一次生成,反复使用,长期使用的边际成本非常低。
缺点:需要生成一个全的样式集,分发时具有较大体积,升级维护成本高。
3.2.2 声明生成
开发 CSS 声明提取和 CSS 样式生成工具,通过扫描开发完成的页面文件,提取静态声明的样式名称,生成样式文件。
样式生成时机:在编码时,略微延迟,可以实时预览到效果。
使用成本:用户熟悉样式规则后编写,学习的成本较低,配置成本较低,编码简单。
可以结合静态文件使用。对于特别复杂或者临时使用的部分,可以直接放在静态文件中包含使用,减少工具生成时间和管理成本。
优点:按需生成样式,没有冗余样式,复用效果好。
缺点:对于业务逻辑中动态生成的样式名称难以捕捉,覆盖不全面。
3.2.3 即时编译
开发或者选择 CSS 规则解释工具,配置到开发工具中,在动态页面中编写样式声明,在程序运行时生成样式规则。
样式生成时机:在运行时,按需编译。
使用成本:前置知识较多,准备工作多,有一定配置成本,编码简单。
优点:无需提前编写样式,按需生成,各种情况适应性好。
缺点:在动态页面中使用较好,在静态页面开发中反而带来了成本。
3.3 组件中的样式生成
3.3.1 即时编译是最活跃的方向
在编写页面和自用组件时,方式二即可满足需要。但如果是发布组件,涉及动态样式的部分,由于事先并不能确定参数,要么退到方式一采取预生成策略,要么前进到方式三采取即时编译策略。由于零冗余等其他好处,即时编译的CSS引擎或者工具是最活跃的。
但即时编译的CSS引擎是唯一的方向吗?
3.3.2 样式优化目标
从样式优化的出发点来说,一是减少大小,去除冗余,优化实现,二是提高复用率,将最常用的样式作为关键样式。
但从设计的角度来说,还要讲究设计规范,也就是说,必然会使用一些样式、布局,存在一个空间和色彩的最小集合。从优化的角度上来讲,这部分静态化是效率最高的,这是方式一的长处。
从结果来说,复用率是依靠统计数据计算出来的。CSS复用是页面级,一个页面有多个模块,每个模块有多个组件,复用率的统计跨越多个组件。在动态页面中,复用率统计存在困难,但在页面生成后,复用率统计可能。
那么是否存在一种可能性,在客户端静态生成样式代码?
3.3.2 静态编译的可能性
三个基础:
(1)核心样式规则不多,可以通过 javascript 定义动态生成,只需要设置基本变量即可。
const generateStyleFromClassName = (m) => {
if (/text-(\w+)/.test(m)) {
return `.${m}{ color: ${m.slice(5)} }`
}
if (/text-(\w+)/.test(m)) {
return `.${m}{ font-size: ${m.slice(5)}px }`
}
return "invalid className:" + m
}
(2)终端的计算能力较强,页面端样式提取方便,计算时间短。
const classNames = [...document.querySelectorAll("*[class]")].map(m=>m.className)
.filter((m,index,arr)=>arr.indexOf(m)==index).sort()
(3)页面端样式可以缓存。
可以直接通过 javascript 变量存储,可以通过 storage 存储。
一个基于html的示例如下:
<!doctype html>
<html lang="\">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1 class="text-36 text-red">Hello World</h1>
<p class="text-28 text-black">This is a test</p>
<script
/* 插入样式 */
const importStyle = function importStyle(b) {
const a = document.createElement("style"), c = document;
c.getElementsByTagName("head")[0].appendChild(a);
if (a.styleSheet) {
a.styleSheet.cssText = b
} else {
a.appendChild(c.createTextNode(b))
}
};
/* 获得全部样式名 */
const classNames = [...document.querySelectorAll("*[class]")].map(m => m.className)
.filter((m, index, arr) => arr.indexOf(m) === index).sort()
/* 生成样式 */
const content = classNames.map(m => {
if (/text-(\w+)/.test(m)) {
return `.${m}{ color: ${m.slice(5)} }`
} else if (/text-(\w+)/.test(m)) {
return `.${m}{ font-size: ${m.slice(5)}px }`
}
return ""
}).join("")
/* 插入样式 */
importStyle(content)
</script>
</body>
</html>
这种可能性需要实践来进行验证。
4 小结
组件、模块和页面样式生成的复杂在于影响动态生成的参数是运行时决定的,采用即时编译可以得到原子样式的最大好处,但需要整套工具来辅助。
在即时编译之外,本文探索动态和静态的解决办法。
原子组件抽象了属性和方法,对外不一定显示样式。在页面中,采用基于样式规则支持多状态样式生成,可以实现简单的状态管理。
程序最终运行会得到页面,采用静态编译,在完全获得页面代码后生成样式代码,在带缓存的情况下可能降低复杂度,但有一定的时延。
原子样式到原子设计有多种方式。对于开发人员来说,动态表示是关键,既赋予了开发工程师的静态表达能力,又赋予了设计师的动态调整能力。对于运行来说,静态编译有更好的性能,但也要实时伴随状态计算。