透过vanilla-extract 了解 Css in Js
写在前面
前不久,antd5更新了,其中最大的一个更新点就是css in js
, 并称使用css in js
后的antd “太香了”,今天这篇文章,我将用一个热门的css in js库,来带大家了解css in js这个古老而新颖的概念;
之所以说这是个古老而新颖的概念,是因为早在2014年就被提出;但又一直在不断的更新和推出新的解决方案,包括刚才提到的antd5也是其推出了更新的css in js
解决方案应用在了组件库上。
初识Css in Js
我们先来看一个demo;我们搭建一个最简单的webpack项目,如下
|-- src
|-- index.js
|-- index.css
|-- webpack.comfig.js
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'bundle')
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}
]
},
}
src/index.js
import './index.css'
var div = document.createElement('div')
div.classList.add('banner')
var root = document.getElementById('root')
root.append(div)
src/index.css
.banner {
width: 150px;
height: 150px;
background: #f00;
}
代码意思,我们不做解释,看打包结果
虽然我们写了css文件,但打包出来的产物并没有css,并且,访问
index.html
,样式是生效的这说明,css 被打包进了js中,这其实就是
css in js
的概念;或者我们应该认识到一件事就是css是可以打包进js中的
正式进入css in js
为什么会有css in js
这个概念,首先我们先来看一下,传统css 有什么缺点
- 文档级别,内容臃肿,可读性差
- 选择器冲突问题
- 动态变量支持不够友好
- ...
其中最主要的就是动态变量了,虽然我们有各种css 预处理如less、scss这些, 但其呈现方式也有很大弊端
- 构建时的方式,不够灵活
- 运行时的方式,太过臃肿
那么,css in js
能为我们带来什么呢?
- 组件化思考模式,不再需要维护一堆样式表
- CSS-in-JS 利用 JavaScript 环境的全部功能来增强CSS。(最强大,如变量、作用域等等)
- JS CSS 代码共享
- 动态的变量!
实践 - vanilla-extract
接下来,我将用一个比较热的css in js
库,来带大家认识它,在此之前,大家要明白一个道理,css in js是一个概念,不是一个具体实现,所以,我们会发现,市面上有各种各样的实现方式,也呈现出了许多的库;
先来介绍一个 vanilla-extract;
- 零运行时,支持TS(这是其最大的特点,这也将以为着,其产物会得到很大的优化)
- 开源产品
这里我就不从0搭建一个react环境了,我们直接使用create-react-app;在搭建好react环境后,我们需要安装依赖
yarn add @vanilla-extract/css @vanilla-extract/webpack-plugin -D
这里我们将cra通过reject
反编译成webpack配置,方便对其进行插件扩展;
然后增加webpack配置
const {
VanillaExtractPlugin
} = require('@vanilla-extract/webpack-plugin');
module.exports = {
plugins: [new VanillaExtractPlugin()]
};
浅试一下
在vanilla-extract中
- 所有的类名是驼峰模式(camelCase),就像我们在jsx中写style那样;
- 样式文件需要以*.css.ts结尾,是js/ts文件
知道以上两个规则,我们就可以体验一下了
写一个简单样式并应用
在vanilla-extract 中,我们可以想写ts那样写css,我们新建一个'style.css.ts'文件。利用其API,先写一个简单的demo;
style.css.ts
import { style } from '@vanilla-extract/css';
export const demo = style({
width: 150,
height: 150,
backgroundColor: '#f00',
color: '#fff'
});
app.ts
import { demo } from './style.css'
function App() {
return (
<div className={demo}>
css in js
</div>
);
}
export default App;
其中,在style.css.ts中,我们像写ts那样写css,并以JS变量的形式对其进行使用;
可以看到,除了达到了我们的预期外,还在类名上加入hash,就想我们再react中使用css modules那样,我们再也不用担心类名的冲突了;并且还支持ts类型校验,让我们写出更严谨的css
除了上面演示的,vanilla-extract 还支持各种选择器,如,伪类,子选择器等,
export const demo = style({
width: 150,
height: 150,
backgroundColor: '#f00',
color: '#fff',
':hover': {
backgroundColor: '#009'
}
});
并且,在vanilla-extract中,每个样式块只能针对单个元素。意思也就是说你不能直接对其子元素或兄弟元素做调整
比如,我们有这样的需求
.todo-list > li { // 期望的写法
color: green !important;
}
这样写将是错误的
export const todoList = style({
marginTop: '20px',
background: '#ccc',
'& > li': { // 错误的实现
color: green
}
});
也就是说,'只能选择自己',用官网的实例为
import { style } from '@vanilla-extract/css';
// Invalid example:
export const child = style({});
export const parent = style({
selectors: {
// ❌ ERROR: Targetting `child` from `parent`
[`& ${child}`]: {...}
}
});
// Valid example:
export const parent = style({});
export const child = style({
selectors: {
[`${parent} &`]: {...}
}
});
如果想选择其他的元素,请使用globalStyle
,如
import { globalStyle } from '@vanilla-extract/css';
globalStyle('html, body', {
margin: 0
});
export const parentClass = style({});
globalStyle(`${parentClass} > a`, {
color: 'pink'
});
写一个动态换肤功能
在没有vanilla-extract 或者 css in js之前,我们如果要用传统css方案实现一个动态换肤,需要写很多的运行时代码,或者用“换类名”的方式,更换类名,相当麻烦;这里我们利用css in js的JS能力,实现一个动态换肤,要实现换肤,最终要的便是变量的概念。我们可以这样来创建“主题变量”
import { createTheme, createThemeContract } from '@vanilla-extract/css';
const colors = createThemeContract({
color: null,
backgroundColor: null,
});
export const lightTheme = createTheme(colors, {
color: '#000000',
backgroundColor: '#ffffff',
});
export const darkTheme = createTheme(colors, {
color: '#ffffff',
backgroundColor: '#000000',
});
export const vars = { colors };
其中,createThemeContract
为“主题契约”,主要是为了CSS 变量的类型化数据结构,与提供的主题实现的形状相匹配。通过传入一个现有的主题契约,而不是创建新的 CSS 变量,现有的变量被重用,并被分配给一个新的 CSS 类中的新值。之后将 “主题契约” 返回的值做为createTheme()方法的第一个参数传入。
而 vars
是我么存变量的地方,方便我们使用,在晚上了上面的操作后,我们就可以对其进行应用,如
import { useState } from 'react';
import { darkTheme, lightTheme } from './style.css'
const App = () => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
return (
<>
<div className={isDarkTheme ? darkTheme : lightTheme}>
css in js
</div>
<button
type="button"
onClick={() => setIsDarkTheme((currentValue) => !currentValue)}
>
切换 {isDarkTheme ? 'light' : 'dark'} 主题
</button>
</>
);
};
export default App;
createTheme
方法返回一个类名,可以直接作用在标签上,这时,我们看控制台
这并不是css 属性,而是
css变量
,这里引用一下css 变量的使用以及定义
// css 变量声明与使用
:root {
--blue-color: blue;
}
.one {
color: var(--blue-color);
}
.two {
color: var(--blue-color);
}
同样的,我们通过上面的操作后,相当于在一个作用域下定义了一些css变量,接下来,就是在该作用域下使用这些变量
style.css.ts
...
export const vars = { colors };
export const essay = style({
backgroundColor: vars.colors.backgroundColor,
color: vars.colors.color,
})
值得一提的是,在书写过程中的ts提示,可见其强大
在使用完主题变量后,就可以应用这个类了。值得注意的,我们要注意作用域,要在上面定义的主题变量的作用域内使用,像下面这样
// ✅ 类名应用在变量作用域下
<div className={isDarkTheme ? darkTheme : lightTheme}>
<p className={essay}>css in js</p>
</div>
// ❌ 类名应用在作用域外
<p className={essay}>css in js</p>
这时,我们再看,就实现了变量的应用和动态样式的切换(换肤功能)
思考
通过上面的demo,我们不免要发起思考
css in js方案,和行内样式、less这一类预处理方案有什么区别呢?
- 写的是js/ts,你可以利用js的一切能力来加强css
- 产物中不再包含css文件
- 优秀的按需加载(webpack + js能力),从此告别
babel-plugin-import
- hash类名,不用再担心类名冲突
- 零运行时(vanilla-extract ),产物更加简洁,变量模式更加优雅
什么场景下适合 css in js方案呢?
- css in js是一个成熟的概念,且不断有新的实现突破,所以,成熟的css in js实现,是可以代替传统的css 方案的
- 有多场景样式切换、多皮肤需求时,css in js在JS的加持下将更加优雅便利
- 开发一个组件库时,css in js方案可以让产物更加单纯,参考antd5
市面上有哪些流行的css in js库
- emotion
- jss
- styled-components (应用最广,但是运行时的模式,导致产物过大)
- aphrodite
- ...
总结
本文由浅到深的介绍了css in js
方案,并通过介绍vanilla-extract,来让大家了解了css in js
这种样式处理方式的优点。
CSS 设计的初衷是为了全局化的控制样式,通过选择器去扩展丰富实际的页面渲染,而 CSS-in-JS 并不是排斥 CSS 样式,而是说“样式”在现代化的组件颗粒化的发展下,使用 CSS-in-JS 能在瞬息万变的复杂应用场景下更加灵活的解决更多问题。
大家可以尝试使用哦;
本文demo源码: