最近在看
antd5,其中有一个很重大的改变就是将 css 方案由 less 换成了css-in-js的方案。之前用过styled-components和emotion, 但是一直没研究过他们的实现,处于好奇翻了翻他们的源码,下面是一些总结。
大纲
- 什么是
CSS-in-JS CSS-in-JS原理介绍- 支持 css(``) 语法
- 支持 css`` 语法
- 支持 prefix 前缀
- 支持 SSR
什么是 CSS-in-JS ?
CSS-in-JS 是一种将 CSS 样式直接写入 JavaScript 代码中的方法。这种方法的优点在于,它可以让你在 JavaScript 代码中直接使用变量和函数来管理样式,并且可以通过模块系统来组织样式。由于 CSS-in-JS 的方法是在运行时动态地将样式应用到 DOM 元素上,因此可以实现一些传统 CSS 所无法实现的功能,例如在不同媒体查询中应用不同的样式。
CSS-in-JS 原理
简单来说就是通过 js 的 insertRule 方法将 cssRules 动态的插入到页面上。
function insertStyle(style) {
// 创建一个新的样式表
const styleElement = document.createElement('style');
// 将样式表插入到页面的 head 元素中
document.head.appendChild(styleElement);
// 向样式表中插入新的 CSS 规则
styleElement.sheet.insertRule(style);
}
// 在页面中插入一个新的 CSS 规则
insertStyle('body { background-color: red; }');
上面的 body { background-color: red; } 就是 cssRule, css-in-js 库做的事情就是将通过 js 写的 样式代码转化成原生的 css 代码。
支持 css(``) 语法
下面看下 emotion 的简单用法
import { css } from '@emotion/css'
render(
<div
className={css(`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
`)}
>
Hover to change color.
</div>
)
官方示例:https://emotion.sh/docs/introduction
可以看到它提供了一个 css(``) 方法,它接收一个样式规则的字符串,并返回一个 className 并能将该规则插入到样式表
const sheet = document.head.appendChild(document.createElement('style')).sheet;
const css = (styleString) => {
const className = getClassName);
sheet.insertRule(`.${className} { ${styleString} }`);
return className;
};
插入规则其实很简单,通过 getClassName() 方法去生成一个类名,然后将该类名插入到样式表中,其中最关键的点就是如何去实现 getClassName 方法。emotion 的做法是 css + hash_code, 类似css-czz5zq 、css-12mcb0 ,这是一种常规的做法。我们这里简单一点儿,直接用 css + sheet.length
上面我们主要实现了带括号的 css() 语法,但在 emotion 库中大多是使用 css 语法,接下来我们来实现一下。
支持 css`` 语法
const blue = css`
color: blue;
`;
实际上上面一段代码是一个有效的 JavaScript 代码,要了解上面代码实际发生了啥,可以看下面代码
const myConsole = (...args) => console.log(args);
myConsole`COVID-2019 is dangerous`
// => [["COVID-2019 is dangerous"]]
返回一个以我们输入的字符串为元素的数组。让我们看看如果我们在模板文字中引入一些注入变量会发生什么
const myConsole = (...args) => console.log(args);
const virus1 = "Omicron";
const virus2 = "Delta‘";
myConsole`COVID-2019 is dangerous, it will mutate into ${virus1} and ${virus2}`;
// => [['COVID-2019 is dangerous, it will mutate into ', ' and ', ''], "Omicron", "Delta"]
它似乎是将传入的字符串变为了一个二维数组,第一个数值值为原字符,但是将变量的位置给切断了,然后用一个空格占位,其余数组元素用真实的值替代。
它实际上与这样调用 myConsole 是一样的效果
myConsole(["COVID-2019 is dangerous, it will mutate into ', ' and '"], "Omicron", "Delta")
搞清楚了这个就简单了,接下里我们只需要写一个方法,将 myConsole 方法输出为完整的字符串就行了。
const interleave = (strings, interpolations) => {
let output = '';
strings.forEach((s, i) => {
output += s;
output += interpolations[i] === undefined ? '' : interpolations[i];
});
return output;
};
// ...interpolations 使用了解构,表示模板字符串中的变量
const myConsole = (strings, ...interpolations) => interleave(strings, interpolations);
myConsole`COVID-2019 is dangerous, it will mutate into ${virus1} and ${virus2}`;
// => COVID-2019 is dangerous, it will mutate into Omicron and Delta
接下来我们在 css`` 中来应用它
const css = (strings, ...interpolations) => {
const styleString = interleave(strings, interpolations);
const className = `css-${sheet.cssRules.length}`;
sheet.insertRule(`.${className} { ${styleString} }`);
return className;
};
https://codesandbox.io/embed/confident-mestorf-rm0b0c?fontsize=14&hidenavigation=1&theme=dark
支持 prefix 前缀
一些 CSS 属性需要有浏览器供应商前缀才能工作。这可能是因为它们是非标准的, [有关 MDN 的更多信息](https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix)。
通常,供应商前缀是在编译时使用诸如 [autoprefixer](https://github.com/postcss/autoprefixer)之类的工具添加的。但是对于我们的案例,我们将在运行时使用 [tiny-css-prefixer](https://github.com/kitten/tiny-css-prefixer).
**tiny-css-prefixer带有一个简单的prefixProperty()**功能。我们可以在这样的辅助函数中使用它:
import { prefixProperty } from 'tiny-css-prefixer';
const prefix = (prop, value) => {
const flag = prefixProperty(prop);
let css = `${prop}: ${value};\n`;
if (flag & 0b001) css += `-ms-${css}`;
if (flag & 0b010) css += `-moz-${css}`;
if (flag & 0b100) css += `-webkit-${css}`;
return css;
};
prefix('writing-mode', 'auto');
// =>
// writing: auto;
// -ms-writing: auto;
// -webkit-writing: auto;
// -ms-writing: auto;
我们的辅助**prefix()函数可以接收 CSS 属性和值,并向其添加必要的供应商前缀。接下来,我们将在我们的css()**函数中使用这个辅助函数。
https://codesandbox.io/embed/confident-mestorf-rm0b0c?fontsize=14&hidenavigation=1&theme=dark
支持 SSR
最后我们再支持一下服务器端渲染,大体思路就是有一个方法可以来提取样式表并把它们注入到 <styke> 标签中去。因为前面用的方法是基于浏览器端 CSSStyleSheet 提供的能力,在 Nodejs 环境中不存在该对象,所以我们需要手动来模拟它。
const sheet =
typeof window !== 'undefined'
? document.head.appendChild(document.createElement('style')).sheet
: mockSheet();
接下来实现 mockSheet 方法,通过上面的代码可以看得出来,实际上我们需要 cssRules 和 insetRule, 另外需要一个导出所有 cssRules 为 string 的方法
const mockSheet = () => {
// the mock sheet need to have at least 2 properties
// 1. cssRules: an array of rules in the sheet
// 2. insertRule: a method to insert rules to the sheet
// 3. extract: a method to return all the style strings
const cssRules = [];
return {
cssRules,
insertRule: (rule) => {
cssRules.push(rule);
},
extract: () => cssRules.join(''),
};
};
https://codesandbox.io/embed/gallant-christian-ddh5ku?fontsize=14&hidenavigation=1&theme=dark
总结
从上面的讲解来看,实际上 CSS-in-JS 库核心就是利用 CSSStyleSheet 对象的能力,将 js 写的 css 代码动态的插入到