前言
由于小程序的包体积限制,加上业务的不断迭代增加,导致包体积减少一直以来都是困扰业务发展的痛点。因此开始尝试在不同的方向上去进行包体积减小的工作,而这里则主要是对 CSS 样式表文件的体积进行优化。
纵观全局,整个项目中 CSS 样式表的体积占比是比较大的,而且有部分样式是重复的,因此就开始考虑共享 Class 的方式,以达到包体积减少的目的,因此就从这个角度开始了探索和实验。
关于原子化 CSS
原子化 CSS 是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。
原子化 CSS 也可以理解为 CSS 工具类,市面上也已经有了如 Tailwind CSS,Windi CSS 以及 Tachyons 等 CSS 框架,而且有一些 UI 库也会提供自带的 CSS 工具类,如 Bootstrap 等。
传统方案
传统的原子化 CSS 方案其实就是预先提供项目中可能需要用到的所有 CSS 工具类,在项目开发时直接使用。其实和 Bootstrap 库所提供的静态工具类相似。因此该方案主要有以下问题:
- CSS 样式表文件较大的情况(这显然在小程序项目中是不希望的)
- 不支持动态生成工具类,CSS 工具类使用场景有限
按需生成方案
目前市面上的技术方案有 Tailwind CSS 和 Windi CSS。而 Tailwind CSS 是后面提供了 JIT 模式后,才支持按需生成方案。主要生成工具类文件流程如下:
- 扫描项目中工具类使用情况;
- 基于第一步的扫描情况按需生成工具类;
- 生成样式表文件;
Windi CSS 和 Tailwind JIT 都采用了预先扫描源代码的方式,支持 HMR。
而最后是选择了 Windi CSS 框架作为项目中的原子化 CSS 方案,主要出于以下原因考虑:
- 参考了其他项目的落地情况,Tailwind CSS 的开发环境不支持 JIT 使用;
- Tailwind CSS 的动态工具类的使用需要
[]
包住,在 wxss 文件中不支持该符号; - Windi CSS 完美兼容 Tailwind CSS,并且拥有很多额外的功能;
- Windi CSS 支持更加灵活的动态样式生成规则扩展;
说明一下,Windi CSS 和 Tailwind CSS 框架在 Web 项目中的表现是相当优秀的,本文仅针对小程序项目,如果你是 Web 项目,其框架表现的所有能力都有着非常不错的开发体验。
Windi CSS 框架介绍及使用
Windi CSS 官方文档 cn.windicss.org/
通过扫描 HTML 和 CSS 按需生成工具类(utilities),Windi CSS 致力于在开发中提供更快的加载体验以及更快的 HMR
安装及引入
对于小程序来说,由于是基于跨平台框架进行开发,因此可选择所提供的 Webpack 插件 windicss-webpack-plugin
。
同时还提供了 VS Code 插件 Windi CSS Intellisense 以支持自动提示功能,提高开发效率。
安装
npm i windicss-webpack-plugin -D
# yarn add windicss-webpack-plugin -D
复制代码
引入
将插件引入到 Webpack 配置文件的 plugins
配置项中
const WindiCSSWebpackPlugin = require('windicss-webpack-plugin')
export default {
// ...
plugins: [
new WindiCSSWebpackPlugin()
]
}
复制代码
在入口文件或只加载一次的文件中,引入 windi.css
<!--js-->
import 'windi.css'
<!--mpx-->
<style src="windi.css"></style>
复制代码
而小程序中并不需要全部引入,windi.css
中包含了
windi-base.css
:初始基础样式;windi-components.css
:主要生成Shortcuts的样式表;windi-utilities.css
:所使用到的所有工具类样式表;
小程序中其实并不需要使用windi-base.css
样式表,因此只需要引入另外两个即可,拆分后可通过自定义的 CSS 来覆盖已有的层级样式。
<!--js-->
import 'windi-components.css'
import 'windi-utilities.css'
<!--mpx-->
<style src="windi-components.css"></style>
<style src="windi-utilities.css"></style>
复制代码
能力支持
由于最终是希望在小程序项目中落地,而小程序框架语法的局限性,导致 Windi CSS 提供的大部分功能无法在小程序中使用,且多余的能力。
可用能力:
不建议使用或无法使用能力:
- 可变修饰组(无法使用):编译后的代码样式表不支持 wxss 文件格式;(可转译)
- 指令(不支持):经过编译后生产的代码和手写的代码量相同,同时 CSS 预处理器(如 SCSS,LESS)不能与
@apply
一起使用; - 属性化模式(无法使用):可能是由于小程序模版的原因,不支持
<view>
; - 响应式设计(不建议):移动端无需使用该能力;
- 暗色模式(不建议):移动端无需使用该能力;
- RTL(不建议);尽量统一写法;
- Important 前缀(不支持):凡事带有 wxss 文件不支持的符号均会报错;(微信小程序WXSS介绍)
Windi 配置
在项目根目录下添加一个名为 windi.config.ts
的文件。
Windi 配置预设值:github.com/windicss/wi…
在服务器启动时,Windi 将扫描你的代码,并提取工具类使用,Windi CSS 中的配置与 Tailwind CSS 配置几乎一致,但有额外的增强,因此也可以参考 Tailwind CSS 的配置。
下面主要针对小程序项目进行配置。
// defineConfig 是带有类型提示的帮助函数,如果不需要可以忽略
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
prefixer: false, // 是否需要自动兼容平台浏览器(不需要)
prefix: 'c-', // 类名样式前缀(防止样式污染)
extract: {
// 扫描文件范围
include: ['src/**/*.{css,html,mpx}'],
// 忽略扫描文件夹
exclude: ['node_modules', '.git', 'dist'],
},
// shortcuts className 不需要加前缀
shortcuts: {
// 示例
'flex-center': 'flex items-center justify-center'
},
theme: {
screens: null, // 媒体查询(不需要)
animationTimingFunction: null, // 动画渲染函数(不需要)
/* 覆盖或新增预设值... */
extend: {
/* 扩展主题预设值... */
}
},
plugins: [
/* 引入扩展工具类插件... */
],
corePlugins: [
/*
核心插件设置,支持的所有核心插件 https://github.com/windicss/windicss/blob/main/src/interfaces.ts#L98
1. 允许完全禁用掉那些 Windi 默认生成的工具类类,只需要设置为空数组 []
2. 如果只想使用罗列出来的默认工具类,则设置为数组并罗列了想要使用的核心插件列表,如 ['margin', 'padding']
3. 如果只想禁用指定核心插件,则设置为对象,切列举需要禁用的核心插件,如 { float: false }
*/
]
})
复制代码
遇到问题
- 由于 Windi 官方文档对配置项的说明比较少,因此可以通过 Windi 源码和 Tailwind 官网去了解更多内容。
- 在 Window 操作系统中,通过 Windi 构建工具生成样式表文件的时候,样式表文件的命名带有
:
,在 Window 操作系统中是属于非法字符,因此导致文件生成失败的情况,目前的替代方案可以通过 Windi CSS CLI 方式,可自定义输出文件名。
项目实践
回归主题,这里使用原子化 CSS 方案,是为了减小包体积。而 Windi CSS 框架的设计初衷并不是这个,而是为了提供更好的开发体验,因此需要在小程序中进行实验,验证该方案能否达到减小包体积的目的。
经过前期的三轮实验中,整体包体积的实验过程是每轮累加改造一定代码量,论证包体积是否随着工具类使用覆盖率增加而逐渐减小
轮次 | 主包体积 | 工具样式表体积 |
---|---|---|
第一轮 | 显著增加 | 显著增加 |
第二轮 | 相较于上一轮有所减少,但整体增加 | 缓慢增加 |
第三轮 | 整体减小 | 缓慢增加 |
实验结果是达到目标预期,开始对小部分业务进行改造,但当我们去看生成的样式表文件时,可以发现仍然有减少空间,由于 Windi CSS 框架的样式生成规则目的所致,目前主要有以下几个点:
- 背景及字体颜色生成的代码要比我们直接写有所增加;
/* 背景颜色 */
.bg-white {
--tw-bg-opacity: 1;
background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
}
/* 文本颜色 */
.text-gray-500 {
--tw-text-opacity: 1;
color: rgba(107, 114, 128, var(--tw-text-opacity));
}
复制代码
- 动态尺寸设置需要加上
rpx
单位;(默认不加为rem
单位)
/* 宽度尺寸设置 */
.w-24 {
width: 6rem;
}
复制代码
- 间距无法使用简写;
.py-10 {
padding-top: 2.5rem;
padding-bottom: 2.5rem;
}
.my-12 {
margin-top: 3rem;
margin-bottom: 3rem;
}
复制代码
首先要知道为什么使用原子化 CSS 方案能够减小包体积,总结就是我们可以使用简短的 Class 工具类,使用到较长的 Style 样式。其次是框架带来的收益:
- Windi CSS框架提供了可视化分析器,能够分析所有工具类的使用情况,去管理和优化 CSS 使用;
- 提供了动态工具类生成能力,不需要手动去添加静态工具类;
针对上面的问题,则需要使用到 Windi CSS 框架提供的扩展工具类的能力,为小程序定制插件。
Windi CSS 框架插件开发
关于 Windi 框架的插件开发,可参考:cn.windicss.org/plugins/int…
文档内的介绍及说明其实比较简短,只有简单的几个示例供我们参考。这里将主要介绍如何添加动态工具类,定制一个针对小程序项目的插件。首先我们的目的是减少包体积,围绕这个目标我们需要重新定制部分工具类的样式生成规则。因此整理了以下工具类需要重新定制规则:
- 尺寸/间隔/排版/定位/边框的默认单位;
- 扩展 margin 和 padding 的用法规则;(例如支持
.m-2_4
生成margin: 2rpx 4rpx
) - 字体/背景颜色的生成规则;
先来简单看一下基本用法
import plugin from 'windicss/plugin'
plugin(({ addDynamic }) => {
addDynamic('filter', ({ Utility, Style }) => {
return Utility.handler
.handleStatic(Style('filter'))
.createProperty(['-webkit-filter', 'filter'])
})
})
复制代码
Utility 对象
这里需要了解 Utility
对象包含那些属性及方法供我们使用。 具体可以看其源码 github.com/windicss/wi…
假设使用到.-placeholder-real-gray-300
工具类
属性 | 类型 | 描述 | 返回值 |
---|---|---|---|
raw | String | class工具类名 | -placeholder-real-gray-300 |
class | String | class工具类 | .-placeholder-real-gray-300 |
isNegative | Boolean | 判断是否为负数 | true |
absolute | String | 工具类绝对值 | placeholder-real-gray-300 |
identifier | String | 工具类第一个单词 | placeholder |
key | String | 工具类关键字 | placeholder-real-gray |
center | String | 工具类中间值 | real-gray |
amount | String | 数值 | 300 |
body | String | 工具类主体 | real-gray-300 |
match | Function | 正则匹配工具类 | - |
clone | Function | 复制该工具类Utility 示例 |
- |
handler | Function | 处理函数集合 | - |
Utility.handler
通过 Utility.handler
提供的 api
,可以轻松的对扫描到的工具类进行处理,并生成想要的 style 样式。提供了以下 api
:
export type Handler = {
utility: Utility
value?: string
_amount: string
opacity?: string | undefined
color?: colorCallback
handleStatic: (
map?: { [key: string]: string | string[] } | unknown,
callback?: (str: string) => string | undefined
)=> Handler
handleBody: (
map?: { [key: string]: string | string[] } | unknown,
callback?: (str: string) => string | undefined
) => Handler
handleNumber: (
start?: number,
end?: number,
type?: 'int' | 'float',
callback?: (number: number) => string | undefined
) => Handler
handleString: (callback: (string: string) => string | undefined) => Handler
handleSpacing: () => Handler
handleSquareBrackets: (
callback?: (number: string) => string | undefined
) => Handler
handleTime: (
start?: number,
end?: number,
type?: 'int' | 'float',
callback?: (milliseconds: number) => string | undefined
) => Handler
handleColor: (
map?: colorObject | unknown
) => Handler
handleOpacity: (map?: DictStr | unknown) => Handler
handleFraction: (
callback?: (fraction: string) => string | undefined
) => Handler
handleNxl: (
callback?: (number: number) => string | undefined
) => Handler
handleSize: (
callback?: (size: string) => string | undefined
) => Handler
handleVariable: (
callback?: (variable: string) => string | undefined
) => Handler
handleNegative: (
callback?: (value: string) => string | undefined
) => Handler
createProperty: (
name: string | string[], callback?: (value: string) => string
) => Property | undefined
createStyle: (
selector: string,
callback: (value: string) => Property | Property[] | undefined
) => Style | undefined
createColorValue: (
opacityValue?: string | undefined
) => string | undefined
createColorStyle: (
selector: string,
property: string | string[],
opacityVariable?: string | undefined,
wrapRGB?: boolean
) => Style | undefined
callback: (
func: (value: string) => Property | Style | Style[] | undefined
) => Property | Style | Style[] | undefined
}
复制代码
这些函数具体做了什么处理,可参考:github.com/windicss/wi…
下面简单写个示例,该示例主要是处理长度的默认单位设置为 rpx
// set size unit
const handleGeneratorUnit = function(style, prop) {
return function({ Utility, Style }) {
return Utility.handler
.handleStatic(Style(style))
.handleSize()
.handleNumber(1, undefined, 'int', (number) => `${number}rpx`)
.handleNegative()
.createProperty(prop)
}
}
复制代码
主要做了以下处理操作:
- 识别到需要处理到工具类,并设置全局设置到属性名;
- 判断值是否为
size
, 如果是则返回Utility.amount
; - 处理值的范围从 1 到无穷的整型,返回
rpx
单位的尺寸; - 处理是否为负数,如果是则加上负号;
- 创建属性;
Windi 对外提供的创建工具类的方法非常多,且很灵活,如果需要开发复杂的工具类插件,可以参考源码: