小程序原子化CSS技术方案(Windi CSS)

前言

由于小程序的包体积限制,加上业务的不断迭代增加,导致包体积减少一直以来都是困扰业务发展的痛点。因此开始尝试在不同的方向上去进行包体积减小的工作,而这里则主要是对 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 模式后,才支持按需生成方案。主要生成工具类文件流程如下:

  1. 扫描项目中工具类使用情况;
  2. 基于第一步的扫描情况按需生成工具类;
  3. 生成样式表文件;

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)
  }
}
复制代码

主要做了以下处理操作:

  1. 识别到需要处理到工具类,并设置全局设置到属性名;
  2. 判断值是否为 size, 如果是则返回 Utility.amount;
  3. 处理值的范围从 1 到无穷的整型,返回 rpx 单位的尺寸;
  4. 处理是否为负数,如果是则加上负号;
  5. 创建属性;

Windi 对外提供的创建工具类的方法非常多,且很灵活,如果需要开发复杂的工具类插件,可以参考源码:

参考资料

猜你喜欢

转载自juejin.im/post/7040409435826552846
css