css-in-js的起源
1996 年,Netscape 实现了JSSS ,又名。基于 JavaScript 的样式表作为 CSS 的替代品。 Netscape 甚至将该规范提交给 W3C 进行标准化。然而,它并没有获得足够的关注,因为“CSS 得到了更广泛的行业认可” 。最终,在 2000 年,Netscape 放弃了对 JSSS 的所有支持。
现在所说的CSS-in-JS是在 2014 年创造的。有两位已知的先驱者,即 Facebook 的Christopher Chedeau和 JSS(不要与 1996 年 Netscape 的 JSSS 混淆)的作者Oleg Isonen 。
从本质上讲, CSS-in-JS 只是解决可扩展 CSS 问题的替代解决方案。
- 封装样式:CSS 类哈希是大多数 CSS-in-JS 库借用的一种技术来确定其样式范围
- 显式依赖项:样式的定义与其用法之间存在明确的引用
- 上下文样式:能够在单个样式定义中定义嵌套规则、媒体查询和伪,从而防止代码重复
- 避免僵尸代码:当我们删除该组件或不再导入它时,它的样式在捆绑过程中也将被忽略
超越静态样式
- 纯 CSS 不提供代码导航功能,例如转到定义或查找所有引用。 JavaScript 定义的样式释放了特定于编程语言的工具的强大功能,显着改善了开发体验。
- 动态样式在高度交互的应用程序中很普遍。基于状态的样式(例如切换样式或组件变体以及用户定义的样式)使用 CSS-in-JS 实现起来非常简单且简单。
- 从 (S)CSS 到 JavaScript共享变量(反之亦然)在技术上是可行的,但需要额外的样板文件,而且很容易出错。通过 CSS-in-JS,我们不仅可以共享constants或variables ,还可以共享types 、 functions或任何其他 JS 代码。
- 常规 CSS 无法进行类型检查。如果样式丢失或者类名拼写错误,没有任何工具可以帮助我们注意到问题
- 服务器端渲染页面的关键 CSS 提取在所有支持 SSR 的 CSS-in-JS 库中实现
- 延迟加载样式随动态加载的组件一起出现。
基础知识
让我们想象一个支持典型 CSS-in-JS 功能的虚构库。我们要做的第一件事是导入样式 API,通常是一个名为css或styled函数:
import { css } from "css-in-js-library";
样式定义
``` JavaScript
const title = css({
fontSize: "2rem",
color: DARK_BLUE,
});
```
</div>
<div>
标记模板语法
``` JavaScript
const title = css`
font-size: 2rem;
color: ${DARK_BLUE};
`;
```
</div>
大多数库更喜欢Object 语法,因为它的性能更高,因为标记模板需要从string到object额外解析步骤。然而,一些库(如Emotion 、 Goober 、 JSS或Compiled )非常灵活,支持这两种语法。
现在我们已经定义了样式,让我们探讨如何将它们应用到 HTML 元素。 CSS-in-JS 库通常支持三种不同的方法。
最直观和最流行的 API 返回一个唯一生成的string ,表示元素的 CSS 类名称。
const title = css(/* CSS rules */);
// 返回一个唯一字符串: "1dbj"
这种方法的主要好处是它类似于传统的造型方式。但是,与此同时,此方法与底层 JS 框架无关,可以与任何 JavaScript 框架一起使用,或使用普通的 DOM API:
``` JSX
export const Page = () => (
<h1 className={title}>...</h1>
);
```
</div>
<div>
使用 DOM API
``` JavaScript
document.body.innerHTML = `
h1 class="${title}">...</h1>
`;
```
</div>
第二种方法因styled-components库而流行,它也因此得名
const Title = styled("h1")(/* CSS rules */);
// 返回 new <Title /> component
结果,它将返回一个已应用 CSS 类的新组件。此方法适用于基于组件的方法,将元素的定义及其样式封装起来,并删除标准 CSS 所需的映射。
export const Page = () => (
<Title>...</Title>
);
因此,最终结果将是相同的:
<h1 class="1dbj">...</h1>
支持Styled组件 API 的库包括styled-components 、 Emotion 、 JSS 、 Goober 、 Compiled和Stitches 。值得注意的是,这种方法主要用于基于 JSX 的框架。
第三种方法由Glamour引入并由Emotion推广,使用非标准属性(通常名为css )来指定元素的样式。
export const Page = () => (
<h1 css={/* CSS rules */}>...</h1>
);
prop API 不太流行,支持的库较少,包括Emotion 、 styled-components 、 Compiled或Goober 。这种方法通常使用基于 JSX 的框架来实现,其中属性称为 props,因此得名该方法。
样式输出
浏览器如何处理用 JavaScript 编写的样式呢?
由于浏览器无法解析 JavaScript 文件中定义的 CSS,因此在浏览器中呈现样式的一种方法是在运行时注入它们。这种方法称为运行时样式表,它是现有库中最流行的一种。
额外的 JavaScript 代码(称为库运行时)必须捆绑并发送到浏览器才能正常工作。此代码会将所需的样式注入浏览器,并在用户事件触发样式更改时相应地更新它们。
运行时样式表方法并不是唯一的方法。有些库能够生成静态 CSS 文件,可以像任何常规样式表一样引用它们。此外,其他解决方案甚至可以生成Atomic CSS 。
静态 CSS 提取
像Astroturf 、 Linaria或vanilla-extract这样的库实现静态 CSS 文件提取,在构建时生成实际的.css文件,这些文件作为任何常规 CSS 样式表包含在我们的文档中。
该技术增加了零运行时间成本。因此,我们获得了 CSS-in-JS 在开发体验方面的所有优势,同时,与常规 CSS 类似,没有运行时成本。
原子 CSS
Fela 、 Compiled 、 Stitches和Stylex等一些库将 CSS-in-JS 提升到了另一个水平。他们没有生成包含所有已定义规则的 CSS 类,而是专注于生成独特的原子类。
<Image src={'/images/react/css-in-js/static.png'} width={1600} height={900} alt="atomic" />
现在,Atomic CSS-in-JS 的美妙之处在于我们不必学习特定 Atomic CSS 框架的特定类名集。相反,我们像通常使用 CSS-in-JS 那样编写样式。
以上内容大部分引用自 andreipfeiffer.dev/blog 的博客
antd5
详细的介绍可以看ant-design.antgroup.com/docs/blog/c…
里面提到我们通过独特的CSS-in-JS方案,Ant Design 获得了相较于其他 CSS-in-JS 库更高的性能, 但代价则是牺牲了其在应用中自由使用的灵活性。
频繁的计算 hash会很耗性能,antd通过简化hash的算法和缓存来提高性能
- 计算hash: 对所有的 antd 组件应用相同的 hash, 对当前的版本和主题变量进行 hash 计算, 可以不需要频繁的计算
- 组件缓存: hash 相同的情况下,同一个组件无论使用了多少次、渲染了多少次,样式永远只会在第一次 mount 时生成一次,剩下的时间里都会命中缓存,这便是『组件级』CSS-in-JS 方案的第二重保险
局限
- 由于特殊的 hash 计算方法和组件缓存,在套用 antd 的 CSS-in-JS 方案时,开发者必须自己提供稳定的 hash 和独特的组件名
- 像 css module 这样的自动 hash 的能力反而是更为需要的
以Button组件为例子
组件里引入样式
import useStyle from './style';
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const classes = classNames(
prefixCls,
hashId,
cssVarCls,
)
return wrapCSSVar(
<a
className={classNames(classes, {
[`${prefixCls}-disabled`]: mergedDisabled,
})}
>
{iconNode}
{kids}
</a>,
);
样式文件(4之前是xxx.less)变成了 style.ts
export default genStyleHooks(
'Button',
(token) => {
const buttonToken = prepareToken(token); // 合并后的token, 了解一下antd的token定义
return [
// Shared
genSharedButtonStyle(buttonToken),
// ...
];
},
prepareComponentToken,
{
// ...
},
)
可以看到prefixCls和之前的版本一致,多了hashId来区分。
hashId是通过@ant-design/cssinjs
计算出来,样式也是通过@ant-design/cssinjs
生成插入到style里。所以接下来看看hashId是怎么生成的,还有就是样式是如何插入到<style/>
标签里的
@ant-design/cssinjs
简介:
通过上下文来管理样式,它的主要作用
- 样式缓存共享
- 动态样式转换
- 样式隔离
- 服务端渲染支持
StyleContext.tsx 是 @ant-design/cssinjs 的核心模块之一,主要负责管理样式的上下文。这种上下文机制为动态样式的生成、缓存和注入提供了统一的管理方式。
antd5是通过useStyleRegister来简介调用上下文的共享变量,而Provider没有直接使用StyleProvider,使用自己的ConfigProvider
useStyleRegister 在内部通过 StyleContext 共享样式缓存和配置信息。
cache对象:github.com/ant-design/…
hashId
从使用的函数入手,略过寻找的一些代码,找到hashId的生成
import hash from '@emotion/hash';
// ...
export default function useCacheToken(
theme: Theme<any, any>,
tokens: Partial<DesignToken>[],
option: Option<DerivativeToken, DesignToken> = {},
) {
const {
cache: { instanceId },
container,
} = useContext(StyleContext);
const {
salt = '',
override = EMPTY_OVERRIDE,
formatToken,
getComputedToken: compute,
cssVar,
} = option;
// Basic - We do basic cache here
const mergedToken = memoResult(() => Object.assign({}, ...tokens), tokens);
const tokenStr = flattenToken(mergedToken);
const overrideTokenStr = flattenToken(override);
const cssVarStr = cssVar ? flattenToken(cssVar) : '';
const cachedToken = useGlobalCache(
TOKEN_PREFIX,
[salt, theme.id, tokenStr, overrideTokenStr, cssVarStr],
() => {
let mergedDerivativeToken = compute //...
const tokenKey = token2key(mergedDerivativeToken, salt);
const hashId = `${hashPrefix}-${hash(tokenKey)}`;
mergedDerivativeToken._hashId = hashId; // Not used
return [
mergedDerivativeToken,
hashId,
actualToken,
cssVarsStr,
cssVar?.key || '',
];
},
);
return cachedToken;
}
可以看到const hashId = ${hashPrefix}-${hash(tokenKey)};
是通过@emotion/hash
里传入token来生成的,这个@emotion/hash
咱们问下AI
@emotion/hash
是一个轻量级的 JavaScript 库,用于生成字符串的哈希值,常用于 CSS-in-JS 库(如 Emotion)为动态生成的样式创建唯一标识符。它基于 MurmurHash2 算法,提供了性能优异、冲突率低的哈希计算方式。
MurmurHash 是一个非加密型哈希函数,广泛用于分布式系统和哈希表,因为:
- 它速度快(适合高性能场景)。
- 冲突率低(适合需要唯一标识符的场景)。
- 简单易实现。
MurmurHash2 的特点:
- 输入是一串字符(
string
)。 - 输出是一个 32 位整数。
- 基于位操作和数学运算(如位移、异或)来生成散列值。
hashId和样式
组件里写的样式
const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSSObject => {
const { componentCls, iconCls, fontWeight } = token;
return {
[componentCls]: {
outline: 'none',
position: 'relative',
display: 'inline-flex',
gap: token.marginXS,
},
};
};
export default genStyleHooks(
'Button',
(token) => {
const buttonToken = prepareToken(token); // 合并后的token, 了解一下antd的token定义
return [
// Shared
genSharedButtonStyle(buttonToken),
// ...
];
},
prepareComponentToken,
{
// ...
},
)
在antd/cssinjs会被执行
styleFn(mergedToken, {
hashId,
prefixCls,
rootPrefixCls,
iconPrefixCls,
})
注入style
// 注入 hash 值
function injectSelectorHash(
key: string,
hashId: string,
hashPriority?: HashPriority,
) {
if (!hashId) {
return key;
}
const hashClassName = `.${hashId}`;
const hashSelector =
hashPriority === 'low' ? `:where(${hashClassName})` : hashClassName;
// 注入 hashId
const keys = key.split(',').map((k) => {
const fullPath = k.trim().split(/\s+/);
// 如果 Selector 第一个是 HTML Element,那我们就插到它的后面。反之,就插到最前面。
let firstPath = fullPath[0] || '';
const htmlElement = firstPath.match(/^\w+/)?.[0] || '';
firstPath = `${htmlElement}${hashSelector}${firstPath.slice(
htmlElement.length,
)}`;
return [firstPath, ...fullPath.slice(1)].join(' ');
});
return keys.join(',');
}
// Generate style
const styleObj = styleFn();
const [parsedStyle, effectStyle] = parseStyle(styleObj, {
hashId,
hashPriority,
layer: enableLayer ? layer : undefined,
path: path.join('-'),
transformers,
linters,
});
const styleStr = normalizeStyle(parsedStyle);
const styleId = uniqueHash(fullPath, styleStr);
// ...
// Inject style
const style = updateCSS(styleStr, styleId, mergedCSSConfig);
(style as any)[CSS_IN_JS_INSTANCE] = cache.instanceId;
// Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, tokenKey);
// Debug usage. Dev only
if (process.env.NODE_ENV !== 'production') {
style.setAttribute(ATTR_CACHE_PATH, fullPath.join('|'));
}
大概类似我们写了这样的样式
const styles = {
'.my-class': {
color: 'red',
'&:hover': {
color: 'blue',
},
},
};
会被编译成
.my-class.hashId {
color: red;
}
.my-class.hashId:hover {
color: blue;
}
我们通过useStyle拿到hashId,插入我们的组件,css-in-js会把这个hashId结合的样式注入到style里。
这样管理好hashId,通过缓存机制可以减少计算,具体的细节大家可以看看代码一起讨论。
wrapCSSVar
最后提一下wrapCSSVar
- 将特定的 CSS 属性值或表达式包装为变量(
CSS variable
)。 - 管理样式与动态 token(主题变量)之间的映射。
- 处理一些特殊标记,如
_skip_check_
、_multi_value_
等,用于灵活控制 CSS 属性的解析和组合。
总结
了解了antd5的样式主要是为业务代码的样式更好的和组件样式结合起来,antd4的时候我们通过less变量来做到网站的样式和antd的样式一致
在我们切换antd5的时候,遇到了一些问题,less变量没有了,虽然antd提供了降级的方案,但是我们在写less文件的时候还是觉得结合的不够紧密
通过上面的了解,我们可以通过寻找类似wrapCSSVar的方法来处理我们的样式变量,达到和组件库一致,或者写useStyle来写样式,后者我们实践过,但是需要一些约束,比如组件名不能一样
后期考虑wrapCSSVar这种来处理,来替代我们手动加入的一些变量