一个 CSS Modules 转 Tailwind CSS 的工具
tl;dr 工具在 github.com/shiyangzhao…
前言
前置资料,关于:
对于我来说,二者都是比较优秀的产品,在 VSCode 插件的加持下,都能得到比较好的开发体验。
CSS Modules:
Tailwind CSS:
先说明一下,无论是 CSS in JS,或者 CSS Modules,又或者 Tailwind CSS,都是比较好的方案,选择适合自己的就好了(可维护,大小,namespace还是要考虑的)。
我为什么要把 CSS Modules 转换成 Tailwind?
- 并不是每个人都能把 CSS Modules 用好,目前我们的 CSS 文件,基本上很难维护了,在使用 CSS Modules 的前提下,仍然各种嵌套,冲突/覆盖的样式,然后整个文件的报错就类似下图(找了一个比较夸张的),当然它还是正常工作的
- 我们维护的多数是平台类型的项目,通俗点说,PC 端,我们没有复杂的样式和交互,在使用组件库的前提下,自定义的复杂样式没有那么多。其次是,哪怕是我们自己写的组件,我也是尽量把组件功能尽量细化,这样导致的结果是,经常某个单独功能的简单组件,哪怕 JS 的代码只有几十行甚至十几行,我仍然需要创建相应的 CSS 文件去约束它的样式,CSS 文件也可能只有几行或者十几行,整个过程还是比较痛苦。
- 做过 Tailwind CSS 的调研后,大家都比较看好/喜欢(主要是我的 Leader ==)。
- 很多应用已经尝试使用了 CSS 原子化方案,例如 npm
关于可维护性问题,我的 Leader 是这样说的:“嗯,tailwind 是有学习成本和记忆成本在。utility classname 的命名规律可以通过一些文档帮助大家降低记忆成本。使用上的记忆成本比较低,vscode tailwind 插件会提示每个 classname 的对应 css 明细,CR 会成本提高。但 CR 一般也不看 CSS,一般只会 CR 代码逻辑。”
方案调研
当初决定去做转换的时候,列了两个方案,两个方案的差异也是比较大的
- 只修改 CSS 文件,class 命名还是按照原有的命名形式。优点是更符合大家早先的习惯,apply 的作用类似于 mixin 和 composes,修改的成本相对来说也比较小。缺点是不那么原子化,官方也不是很推介使用 apply 的,本质上与 tailwind 的设计背道相驰
// 转换前: .class { width: 100%; display: flex; align-items: center; justify-content: space-between; } // 转换后 .class { @apply items-center flex justify-between w-full; }
- 对 CSS 文件处理的同时,处理组件的 className。优点是直接升级成原子化的 CSS,接入 tailwind 后,整体的代码风格也能得到统一;缺点是手动改的话,风险和工作量都是巨大的。
import style from 'index.module.css'; // 转换前 const Component = () => <div className={style.class}></div> // 转换后(理想 const Component = () => <div className="items-center flex justify-between w-full"></div>
在不考虑工作量的前提下,方案二无疑是更好的,但我们随便一个项目就有几百个 CSS 文件,方案二需要处理 CSS 文件的同时,也需要处理 JS 文件,那么我们需要处理的文件大概就有 500+ 了,考虑投入产出比,我们是不可能在这个上面投入大量人力和时间的,手动去做修改是不切实际的。
目标
手动修改的方案被否掉后,只能通过自动化工具的方案去解决相关问题。确认工具的功能和目标
- 解决 tailwind 初期接入时,大家不太熟悉使用的情况下,可以使用该工具辅助转换 CSS Modules,即:虽然我接入了 tailwind,但并不是就一定要使用原子化的 CSS 进行开发。
- 解决整个工程项目的问题,我可以使用这个工具直接把我的项目进行转化,而不仅仅只有辅助作用。
方案确认
在确认方案之前,我们先确认工具的入口,即我是如何处理 CSS 文件和 JS 文件的,是否应该单独处理?我觉得不应单独处理,因为 JS 文件和 CSS 文件有明显的关联关系,CSS 文件是通过 import 声明引入到 JS 文件中的,我们实际开发过程也是以组件为粒度开发的,那么我们可以以 JS 文件为入口进行处理。
不以 CSS 文件作为工具入口的的另一个原因是,通过 JS 你能很轻松的查到引入的 CSS 文件,但如果以 CSS 文件为入口去查引用的话,这个查询是异常繁琐的,你需要查询每个 JS 文件,判断其是否引入了,当然你可以在初期查询全部 JS 文件后建立关联关系,但如我们前面所说,工具应该可以去处理单文件,所以还是考虑 JS 作为工具入口。
不难看出,我们是需要“修改代码“的,修改代码的意思是,工具需要改变你的代码内容,例如你可以使用replace
基于旧的字符串来获取新的字符串,我们这里其实也就是依据旧的代码产生新的代码,代码层次的修改,这种我们一般称之为Codemod
。
这里我们需要了解两个工具:
简单来说,两个工具都是可以处理 AST 树的,确定工具的流程
可能遇到的问题
在确认方案以后,判断方案的可行性以及可能遇到的问题
CSS 依赖关系
CSS 文件存在循环依赖,一股脑解析会进入死循环,解决方式很简单,就是后解析+缓存的策略,因为 CSS 可能会被多个 JS 文件引用,缓存也能在一定程度避免重复解析造成的数据错误
填充 className
项目中的 className 一般有以下几种情况
<div className="class1 class2"></div> // 1
<div className={style.selector}></div> // 2
<div className={style['class-test']}></div> // 3
<div className={style[`class-${count}`]}></div> // 4
<div className={`text ${style.selector}`}></div> // 5
<div className={classnames({ [style.class1]: boolean })}></div> // 6
<div className={classnames([style.class1, style.class2])}></div> // 7
const columns = [{
// ...
className: style.class // 8
}];
依次进行分析
- 第1种情况是不需要考虑的,因为我们使用的是 CSS modules
- 第3种通过 camlCase 的方式转成驼峰,实际工作中我是做了处理的,但我觉得这个和第4种都不应该处理,所以都列入不处理范围
- 2、5、6、7、8 相似都是也不同,AST 的操作会非常麻烦,考虑通用型,可以这样设计
缺点就是会带来不必要的嵌套,但优点是绝对是没有问题的,能处理所有场景,AST 树的操作也不会太麻烦// 无论 style.class 在什么容器下,都去进行拆分 {style.class} => {`${style.class} tailwind`} [style.class] => [`${style.class} tailwind`] { [style.class]: boolean } => { [`${style.class} tailwind`]: boolean } className: style.class => className: `${style.class} tailwind` `${style.class} test` => `${`${style.class} tailwind`} test`
是不是所有的 CSS 规则声明都能提取出来
并不是,CSS Modules 里面写 global 样式,也有 atRule 的一些规则,这些场景都不能直接提取 tailwind class,只能使用 apply 来辅助转换了
.class global: .global-class {
display: flex;
}
.class:active {
display: flex;
}
.selector1 {
display: flex;
}
.selector2 {
composes: selector1;
}
@supports (display: flex) {
@media screen and (min-width: 900px) {
.class {
display: flex;
}
}
}
实际使用碰到的问题
解析 CSS 得到数据的缓存
因为大部分解析的逻辑都是写在 jscodeshift
的 transform 逻辑中,jscodeshift
考虑性能问题是会把文件的解析和操作分配到一个个子进程去执行的,如果你在 transform 中去缓存解析 CSS 得到的数据,一定是行不通的,因为每个进程是独立的,也就是数据是不可持久化共享的,但是进程的分配是jscodeshift
去做的,导致的结果是你也没法通过 IPC 通信的形式把数据发到主进程里,让主进程去缓存解析的数据。
这里我是起了个 tcp 的 socket 服务,把数据的读写单独放到一个进程去执行了,transform 中需要存储数据只需要向这个 socket 服务发送消息就可以了。
// 解析后缓存的内容
{
'xx/index.module.css': {
btn: ['text-base', 'cursor-pointer'],
};
}
如何提取 tailwind class
就是在一堆 CSS 规则声明中提取被 tailwind 匹配的 class,考虑的主要问题是多个规则组成的 tailwind class,例如:
你需要在 n 个规则声明中提取优先级最高(最多组成?)的 tailwind class,这里具体不细说了,感兴趣的话我再写自己的处理逻辑。
tailwind 的 class 权重问题
const Component = () => <div class={clsx(style.class1, style.class2)}></div>
.class1 {
margin-left: 0.75rem;
}
.class2 {
margin-left: 0.5rem;
}
margin-left 多少? 转换以后
const Component = () => <div class={clsx("ml-3", "ml-2")}></div>
// tailwind 中 'ml-3 ml-2' === 'ml-3'
// 但在你的 CSS 中,生效的是 0.5rem
结语
说的最多的是方案,技术细节说的不太多,技术方面涉及的点有点多,细说要占很大篇幅,如果感兴趣,我再开一篇介绍jscodeshift
和postCSS
的使用。
更多的使用说明还是看 github.com/shiyangzhao…
附上我当时处理完一个项目的截图(当时也写了个 CSS Modules 转 SCSS Modules的功能):
处理 CSS Modules:
转载自:https://juejin.cn/post/7144591210420633630