前言
原子化css这一概念相信不少前端同学是不陌生的,其主要推动者就是tailwind css,从业内的满意度调查来看近几年tailwind css一直居于榜首,其原子化 CSS 的特性的确很好用,但是在国内就有点「雷声大雨点小」的意思了,也许是因为国内快糙猛的前端开发风格导致开发者写出来的都是「用完即抛」的 CSS 样式,很难沉淀出一套原子化的样式吧。本篇博客笔者就着重对原子化css进行一些初步尝试的分析,当然很多框架上的使用还是推荐各位同学直接去看对应框架的官网,毕竟官网才是比较全面并且保持更新状态的。
前端样式开发痛点
总所周知的前端开发三剑客:HTML CSS JS,但是在笔者从事前端开发近五年的时间里面以来大部分FEer都是关注js这个方向比较多,注重vue react angular三大框架,对于样式的话 颇感前端开发人对于css的重视程度不高,早期的直接使用css,后面会使用预处理框架,诸如less之类,虽然带来了不少的提升,但是依旧存在不少的痛点。
以笔者的经验大概总结了一些前端开发中的一些痛点或者一些常见的现象
- 直接拷贝设计图的样式没有过多的思考,这点在一些新手前端比较常见,保证设计稿的还原度为第一目标,,加上对样式的不熟悉,所以直接选择复制设计稿的样式解决问题
- 没有做好对样式变量的声明维护,一般的设计会有一套完整的设计规范,或者是直接用UI库的一些常见颜色尺寸变量规范,在项目的初期没有维护好这些变量的话,就会出现在样式中直接使用值而非变量,使得后续维护变得困难。
- 对于样式的code review比较困难,不同于js比较容易看code review,样式更多只能通过页面来判断是否符合预期,这样在代码上就比较难做卡口,保证代码质量。
- 缺少样式相关的分析能力,不同于js的各种分析插件,样式相关的分析工具或者插件还是比较匮乏,使得前端开发在做防劣化的时候无从下手。
why utility css
通过上面的一些分析,我们可以看到不少的痛点,对一个研发来讲,我们选择一个新的事务来解决的无非是两件事,一个是开发体验的提升(就是让我们写代码的时候更爽),一个是性能上的提升(速度上的提升还有包体积的控制) 选择原子化css可以解决下面这些痛点:
- 减少打包的css体积,相比于之前的less定义各种class,原子化的css可以按最小的粒度去分割然后打包到最终的css产物中,例如常见的flex布局,在各种class可能会被声明多次并且都打包进去,在原子化的css中只会被打包进去一次,并且有比较好的tree shaking,没有使用上的样式不会被打包。
- 减少各种语义化类名的声明,相较于之前需要定义各种语义化的类名来保证可读性的情况,原子化的css本身就有一套自己的规范,再也不需要为想一个语义化的类名而烦恼了。
- 完整的设计约束,而不是分散到各个.css或者.less文件中,更加贴合颜色选择、行间距、排版、阴影等设计规范。
- 能够比较处理一些诸如focus之类的状态和伪类,不需要去通过样式去声明这些
- 暗夜模式的支持
当然使用utility css还是有一些不可避免的: - 熟悉utility css事先声明好的类名,基本上约定的类名都是可读性都是比较强的,有些有细微差别的需要自己去看下熟悉熟悉,配合上各种开发插件可以提高开发效率。
- 样式覆盖的问题,前端开发必不可少会出现一些需要覆盖组件库样式的情况,这些目前笔者也只能通过less这些来覆盖。
- 通过devTools调试样式的问题,笔者在调试一些样式问题的时候确实感觉原子化css的调试比较之前还是有一定的不方便,变成了多个类名,所以要更加关注的是使用了哪些”原子类“。
why windi not tailwind
可能会有不少同学问为啥是选择windi这个库作为本次的初探,而不是tailwind,实际上笔者在之前的项目中用的都是tailwind2.0,2.0这个版本也是有不少无法满足实际开发需要的一些功能,这些功能在tailwind3.0和windi中是得到了比较完美的解决,再有就是taiLwind3.0的JIT模式也是参考了windi的实现,另外就是windi提供了分析的工具,这点是笔者比较喜欢的,所以在新的项目中直接使用了windi,tailwind3.0后续有时间笔者也会试用下。
对于tailwind和windi的一些对比可以看下twitter上面的这个对比的结果,后者是windi,有梯子的同学直接看这个:
新建windi demo项目
这里笔者直接在之前的monorepo工程新建一个子工程就好了,之前的工程情况可以看下这个,这回笔者通过modernjs创建我们的应用,这里也安利一下modernjs
我们直接切换到apps
目录下创建我们的工程,windi-demo为这个工程的名称
cd apps
pnpx @modern-js/create windi-demo
然后我们就直接按提示选择和回车即可,笔者是这样选择的
看一下当前的monorepo目录
因为我们是monorepo项目,这里我们先删掉apps/windi-demo/node_modules
,配置一下monorepo之后再重新install
在rush.json
中配置我们的新项目
"projects": [
......
{
"packageName": "windi-demo",
"projectFolder": "apps/windi-demo"
}
]
这时候我们执行一下rush update
更新一下依赖,等到命令运行完我们就可以运行我们的项目了
cd apps/windi-demo
pnpm run dev
看了一下控制台有个tsconfig
配置的警告,应该是缺少了modernjs的tsconfig的包,这里加一下即可
rush add -p @modern-js/tsconfig --dev
看到这个界面的时候就是我们项目初始化完成了
安装windi插件
看自己使用的开发工具的情况添加对应的插件,笔者用的是vscode,这里就找一下对应的插件安装即可,主要是有更多的代码提示,这样会给我们本地开发提效不少,vscode搜索一下
WindiCSS IntelliSense
这个安装即可
安装完成之后可以试用一下,填写className的时候输入就会有代码提示就是符合预期了
接入windi
接入windi需要添加对应的webpack插件,这里我们先安装这个webpack插件
rush add -p windicss-webpack-plugin --dev
安装成功后看下package.json
安装成功之后我们需要引入这个插件,因为modern.js的配置需要单独的一个config文件,所以我们在项目根目录创建一下这个文件
touch modern.config.js
参考这个配置一下插件,主要node的版本要大于14.17.0
,不然运行会有问题
import { defineConfig } from "@modern-js/app-tools";
import WindiCSSWebpackPlugin from "windicss-webpack-plugin";
export default defineConfig({
tools: {
webpack: (_config, { chain }) => {
chain
.plugin("windiCSSWebpackPlugin")
.use(WindiCSSWebpackPlugin, [
{
virtualModulePath: "src",
},
])
.end();
},
},
});
我们在App.tsx
入口引入这个,这个是和我们上面的webpack配置项对应的
import "./virtual:windi.css";
搞定以后我们来看下效果,写一个页面用上原子类
export default function Demo() {
return (
<div className="w-full m-10">
<div className="border border-solid p-3">大家好,我是渣渣辉</div>
</div>
);
}
看下效果
到这一步我们的接入windi也算是完成了,接下来就可以照常开发
配置windi
同taiwind一样,windi也是有提供配置的文件的,而且windi的配置文件是支持windi.config.(j|t)s还有tailwind.(j|t)s,这样是很方便我们从tailwind切换到windi,甚至连配置的名称也不用修改
看下官方文档写的
这里我们就先创建一个windi.config.ts文件来作为我们的配置文件
touch windi.config.ts
在windi.config.ts
文件中就可以添加我们的windi相关的配置,例如这样
import { defineConfig } from 'windicss/helpers';
export default defineConfig({
extract: {
// A common use case is scanning files from the root directory
include: ['**/*.{vue,html,jsx,tsx}'],
// if you are excluding files, make sure you always include node_modules and .git
exclude: ['node_modules', '.git', 'dist'],
},
})
任意值类
在taiLwind2.0的时候我们只能用tailwind预设的一些原子类,但是对于一些和预设原子类不一致的样式我们就无法用原子类的形式来实现,最常见的就是一些宽高字体大小等,例如设计给了一个13px的字体,这时候我们在tailwind中是没有预设的原子类这时候就只能手动写样式实现,还有就是一些宽度的样式,预设的原子类是无法满足各种px的宽度显示的,在tailwind3.0和windi中这个痛点都被解决上了。
我们看一下windi插件给我们带来的提示,可以看到已经可以支持上各种任意值还有计算属性
我们来尝试一下这种自定义任意值的原子类,还是上面那个Demo的组件,我们调整一下之前的w-full
export default function Demo() {
return (
<div className="m-10 w-[166px]">
<div className="border border-solid p-3">大家好,我是渣渣辉</div>
</div>
);
}
看下效果,这时候我们的任意值类是生效了,检查样式的时候可以看到类名及对应的样式
theme接入
theme配置可以方便我们快速拓展原子类,例如颜色,尺寸,屏幕等等,笔者以自己接触过的设计举例子,一般设计师做设计的时候都会有自己的色板,这个色板的定义一般是可读的,比如主题色 品牌色之类的,tailwind和windi内置的色板一般没办法百分百适应,所以前端在开发的时候免不了拓展这些颜色,这时候就是发挥theme配置的能力了。
我们以颜色为例子,配置一些主题色,windi会自动识别到并补充诸如bg-xx,color-xx等值,配置如下:
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
extract: {
// A common use case is scanning files from the root directory
include: ['**/*.{vue,html,jsx,tsx}'],
// if you are excluding files, make sure you always include node_modules and .git
exclude: ['node_modules', '.git', 'dist'],
},
theme: {
extend: {
colors: {
brand: {
/** 品牌色/主色/B1-1-E8F4FF-浅色、白底悬浮 */
1: '#E8F4FF',
/** 品牌色/主色/B1-3-94C2FF-禁用 */
3: '#94C2FF',
/** 品牌色/主色/B1-5-4086FF-悬浮 */
5: '#4086FF',
/** 品牌色/主色/B1-6-1664FF-常规 */
6: '#1664FF',
/** 品牌色/主色/Brand1-7-0E49D2-点击 */
7: '#0E49D2',
},
}
}
}
})
补充好了之后插件也会有对应的提示,我们重新运行项目让其生效(笔者使用的时候是需要重新运行项目的)
pnpm run dev
我们还是在上面的demo组件尝试下这几个新增的颜色值
export default function Demo() {
return (
<div className="m-10 w-[166px]">
<div className="border border-solid p-3">大家好,我是渣渣辉</div>
<p className="bg-brand-1">品牌色1</p>
<p className="bg-brand-3">品牌色3</p>
<p className="bg-brand-5">品牌色5</p>
</div>
);
}
看下页面中的效果
我们通过devtools审查一下样式,可以看到对应的类能够正确看到样式,这样就可以满足我们拓展原子类的需求
shortcut处理
shortcut是windi提供的一个非常有用的能力,这个能力能让我们大大减少一些原子类的书写,而且提高复用程度,举个最简单的例子,比如flex布局,在没有shortcut的情况下,我们每次用到flex布局居中情况都要这样
flex items-center justify-center
这样在每个元素中写多次,复用程度不高,虽然可以通过编写react组件来解决部分这种问题,但是还是会存在挺多需要些多个flex布局的地方。笔者之前也合同事交流过这种情况,类名的复用和原子化之间如何取舍,后面看到了windi的这个shortcut,基本上可以解决当初的痛点了。
我们就以上面的这个flex布局为例,将多个原子类组成一个新的shortcut类名,然后在页面中使用
windi.config.ts
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
extract: {
// A common use case is scanning files from the root directory
include: ['**/*.{vue,html,jsx,tsx}'],
// if you are excluding files, make sure you always include node_modules and .git
exclude: ['node_modules', '.git', 'dist'],
},
shortcuts: {
'flex-center': 'flex items-center justify-center'
},
})
设置完成之后我们在页面中使用这个flex-center
<div className="flex-center w-full">
<span>shortcut content</span>
</div>
看下效果,审查一下样式
从上面的审查元素截图来看,我们的shortcut是已经生效了,可以看到flex-center这个类名。
特别注意,最好不要滥用shortcut,毕竟原子化css的目的就是将样式切分到最小的颗粒度,然后减少体积,shortcut的配置是方便了我们代码编写,但是对于包体积是有影响的,这其中的取舍还是要慎重,笔者还是建议能用原子化css的就直接用原子化css,少用shortcut,并且和团队内的其他成员沟通好这个取舍。
模板字符串动态原子类
可能会有一些同学会和笔者碰到类似的情况,在不同的情况下需要展示不同的样式,如果是任意值类的话会有些不一样,笔者碰到的是一个侧边栏的宽度,展开是一个宽度,收起来是另外一个宽度,诸如此类
我们在原先的demo页面中尝试一下这种情况:
const HEADER_WIDTH = 300;
export default function Demo() {
return (
<div className="m-10 w-[166px]">
<div className="border border-solid p-3">大家好,我是渣渣辉</div>
<p className="bg-brand-1">品牌色1</p>
<p className="bg-brand-3">品牌色3</p>
<p className="bg-brand-5">品牌色5</p>
<div className="flex-center w-full">
<span>shortcut content</span>
</div>
<div
className={`w-[calc(100vw-${HEADER_WIDTH}px)] border border-solid h-8`}
>
模板字符串内容
</div>
</div>
);
}
走读一下,这里实际上是用了模板字符串获取这个高度,然后作为计算属性的一个参数传入,类似于动态的效果,我们看下这时候的页面
通过审查元素我们发现这个模板字符串的类名对应的样式并没有剩下,不符合我们的预期,这种动态样式的需要我们预先告知windi,哪些类名是符合预期的,这样才会生效,这个就需要我们给windi增加一下配置了
windi.config.ts
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
extract: {
// A common use case is scanning files from the root directory
include: ['**/*.{vue,html,jsx,tsx}'],
// if you are excluding files, make sure you always include node_modules and .git
exclude: ['node_modules', '.git', 'dist'],
},
safelist: 'w-[calc(100vw-300px)]'
})
重新看下效果
这时候的样式就符合我们的预期了
分析能力
分析能力是windi提供的一个比较有特色的能力,可以让我们了解原子类的使用情况,这里笔者就用自己之前的项目来看下
在项目目录下运行
npx windicss-analysis
根据提示打开页面看下报告,这些分析就可以提供给我们一些优化的思路,这里暂时不展开细聊,有兴趣的同学可以看下自己的项目分析报告