CSS in JS 迁移指南
在做 CSS-in-JS 的迁移之前,先了解一下 v5 相对于 v4 新增的一些概念。
Design Token 是一个设计系统,简单点说的话可以类比成 v4 就有的 less 变量,是一些原子化的样式变量,比如 border 的线宽、颜色等等,以 JS 变量的形式串联在 antd 组件中。在 v5 中我们用 CSS-in-JS 替代了 less,因此 Design Token 也替代 less 变量支撑起了 v5 的主题系统。
在 v4 及以前的版本中,我们遵循了一套严格且语义化的 class 命名方法,这为每个组件及其不同状态的样式隔离提供了很好的环境。美中不足的是我们需要考虑的还不仅仅是组件间的,还有不同 antd 版本之间的样式隔离,比如在一个系统中存在多个版本的 antd 时,由于 class 的命名都是相通的,不同版本的样式就会产生污染,导致一些预料之外的显示。
在迁移了 CSS-in-JS 之后,这个问题迎刃而解。我们提供了 hashId,只有在 Design Token 或者版本发生变化时,这个 hashId 才会改变。在组件中,hashId 可以作为 className 赋给 HTML 元素,通常会是 css-[hash]
的样子,同时所有的样式都会和这个 hashed class 强相关,比如原本使用的的 .ant-btn
选择器会变成 .css-[hash].ant-btn
。因此不同版本的或者拥有不同 token 的 antd 所产生的样式就被 hashId 隔离开,从而从根本上解决样式污染问题。
这里 Button 组件为例
- 注释
components/button/style/index.less
文件,确保该组件的所有样式都已移除。 - 用以下代码替换
components/button/style/index.tsx
,实际请替换Button
为相应的组件名:
// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken } from '../../_util/theme';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
}
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => [
// Gen-style functions here
],
);
同时在 components/_util/theme/interface.ts
中的 interface OverrideToken
添加相应组件的 ComponentToken
:
// ...
import type { ComponentToken as ButtonComponentToken } from '../../button/style';
export interface OverrideToken {
derivative?: Partial<DerivativeToken & AliasToken>;
// Customize component
Button?: ButtonComponentToken;
}
// ...
- 在组件中引入
components/button/style/index.tsx
导出的 hook,该 hook 要求传入参数是该组件的prefixCls
,返回一个数组,第一个元素是一个 wrapper,要求包裹最终 return 的元素;第二个元素则是上文中提到过的 hashId,我们需要将它作为 className 添加给该组件的最外层元素:
// ...
+ import useStyle from './style';
const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
// ...
const prefixCls = getPrefixCls('btn', customizePrefixCls);
+ // Style
+ const [wrapSSR, hashId] = useStyle(prefixCls);
//...
const classes = classNames(
prefixCls,
+ hashId,
{
// ...
},
className,
);
let buttonNode = (
// ...
);
- return buttonNode;
+ return wrapSSR(buttonNode);
};
到这一步为止, CSS-in-JS 的链路已经打通,我们可以试着为组件添加一些样式:
// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
}
const genButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
border: '1px solid red',
}
};
};
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => [
// Gen-style functions here
genButtonStyle(token),
],
);
随后运行项目,打开 Button 页面,可以看见样式已经生效,同时检查元素,可以看见添加上去的 hashId 和对应的选择器:
可以注意到上文中注入 CSS-in-JS 样式时,我们利用 genButtonStyle
方法返回了一个 CSSObject
,这个 function 的唯一参数就是 token
。
token
是一些变量的集合,其中就包括了上文中提到的 Design Token,在 antd 中我们对这些 Design Token 作了二次封装,产生了一系列 AliasToken
,比如 token.padding
等等,我们可以利用 token
消费 Design Token。除了 Design Token 之外,当前组件中定义的 ComponentToken
也会存在于 token 中,同样作为设计资源消费。
为了辅助注入样式,我们还在 token
中提供了一些常用的变量:
{
prefixCls: string;
componentCls: string;
iconCls: string;
antCls: string;
};
-
prefixCls
:即为调用useStyle
时传入的prefixCls
。 -
componentCls
:实现为.${prefixCls}
,即做了prefixCls
到 CSS 选择器的转化。 -
iconCls
:值为.anticon
,是 ant icon 的固定 className。 -
antCls
:值为.ant
,是 antd 的 className 前缀,常用于在组件中处理非当前组件的样式。
我们可以为组件添加一些组件级别的 token
,这些 token
只能在当前组件被消费,同时可以被用户覆盖。ComponentToken
在上文中的同名 interface 中声明,并且在 genComponentStyleHook
的第三个参数中初始化,随后就会被注入到 token
中,继续消费。
// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
buttonBorderColor: string;
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
}
const genButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
border: `1px solid ${token.buttonBorderColor}`,
}
};
};
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => [
// Gen-style functions here
genButtonStyle(token),
],
{
buttonBorderColor: 'cyan'
}
);
效果:
我们也可以在初始化 ComponentToken
时利用原有的 token
,方法为将 genComponentStyleHook
第三个参数由传入对象改为 function,token
会作为参数提供(仅提供基本的 token
):
// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
buttonBorderColor: string;
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
}
const genButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
border: `1px solid ${token.buttonBorderColor}`,
}
};
};
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => [
// Gen-style functions here
genButtonStyle(token),
],
token => ({
buttonBorderColor: token.colorPrimary,
}),
);
效果:
一些复杂组件需要利用已有 token
进行计算得出一些复杂的计算值,或者需要自定义一些 token
复用,但是这些 token
并不希望暴露给用户,那么我们就可以在组件内添加一些自定义 token,并且需要自行组装并传入 genStyle
方法中。
// deps-lint-skip-all
import { genComponentStyleHook, mergeToken } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
customBorderWidth: number;
}
const genButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
border: `${token.customBorderWidth}px solid green`,
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => {
// Use mergeToken to merge token, DO NOT use { ...token, prop: xxx }
const buttonToken = mergeToken<ButtonToken>(token, {
customBorderWidth: 2,
});
return [
// Gen-style functions here
genButtonStyle(buttonToken),
]
},
);
注意:合并
token
请务必使用mergeToken
方法,不要使用解构语法,会导致token
的统计不准确
效果:
CSS-in-JS 库: @ant-design/cssinjs
CSS-in-JS 也会有相应的检查,平常在 dev 时会在 console 中打印 warning,这些 warning 都是需要被解决的。
在 v5 中,由于 hash 的存在,我们需要对 v4 的一部分样式优先级作调整。所有的 root class 之外的选择器都需要作为其 root class 的子选择器,保证 hash 对所有样式都有控制作用。举个例子:
.btn {
width: 100px;
&-inner {
fontSize: 14px;
}
}
迁移后需要修改为:
const genStyle = () => ({
'.btn': {
width: 100,
'.btn-inner': {
fontSize: 14;
}
}
});
根据具体场景的不同处理方式可能不同,这里仅作举例
参考链接:https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties
Ant Design 目前支持 RTL 模式,在 v4 版本中,对 RTL 的支持通过覆写样式实现,比如:
.ant-component {
paddingLeft: 12px;
&-rtl {
direction: rtl;
paddingLeft: 0;
paddingRight: 12px;
}
}
这样的写法会让我们的代码变得非常臃肿,并且经常会有样式优先级的问题,导致样式不生效等问题。在 v5 中,我们全面使用 CSS Logical Properties and Values,让 RTL 支持只需要改变 direction 就可以实现。所以如果你在编写过程中看到如下 warning,就代表需要用 CSS Logical Properties and Values 替换原有的写法:
Warning: [Ant Design CSS-in-JS] Error in 'ant-btn -> .css-dev-only-do-not-override-1kw74a7.ant-btn': You seem to be using non-logical property 'paddingLeft' which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.
简单解释下 CSS Logical Properties and Values,代表方位的 properties 和 values 可以用另一种形式指定,比如 marginLeft
可以用 marginInlineStart
代替,会被 direction
所影响,在 LTR 下就相当于 marginLeft
,在 RTL 下相当于 marginRight
。基本所有方位相关的 CSS 属性和值都可以被替代,具体请查看 MDN 文档。
在 antd 中,我们仅对 RTL 作支持,所以只会对 left 和 right 两个方位作检查,top 和 bottom 则可以忽略。
在传统 CSS 及其他类似于 less 的预处理器中,我们写 content
时是这样的:
.demo::before {
content: "";
}
但是在 CSS-in-JS 的 CSSObject
中,如果这样写是不会得到预期效果的:
const genStyle = () => ({
content: '',
});
因为 CSS 需要的值是 ""
,而 CSS-in-JS 编译后就只剩一个空字符串了,所以这里需要包裹一层引号:
const genStyle = () => ({
content: '""',
});
同理,如果你想使用 utf-8 编码如 '\a0'
作为 content
的值,在 CSS-in-JS 中需要对 \
做一次转译:
const genStyle = () => ({
content: '"\\a0"',
});
在 @ant-design/cssinjs 中我们提供了 Keyframes
类用于产出 CSS 中的 @keyframes
:
import { Keyframes } from '@ant-design/cssinjs';
const zoomIn = new Keyframes('antZoomIn', {
'0%': {
transform: 'scale(0.2)',
opacity: 0,
},
'100%': {
transform: 'scale(1)',
opacity: 1,
},
});
之后我们就可以直接把 Keyframes
当作 animationName
的值传入,@keyframes
也会被自动注入 :
// deps-lint-skip-all
import { genComponentStyleHook, mergeToken } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';
import { Keyframes } from '@ant-design/cssinjs';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
// Component token here
}
interface ButtonToken extends FullToken<'Button'> {
// Custom token here
}
const zoomIn = new Keyframes('antZoomIn', {
'0%': {
transform: 'scale(0.2)',
opacity: 0,
},
'100%': {
transform: 'scale(1)',
opacity: 1,
},
});
const genButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
animationName: zoomIn,
animationDuration: token.motionDurationMid,
animationTimingFunction: token.motionEaseOutBack,
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook(
'Button',
token => [
// Gen-style functions here
genButtonStyle(token),
],
);
注意:推荐将
Keyframes
直接赋给animationName
,原因是在 hash 模式下 keyframes 也会带上 hashId,如果直接将Keyframes
的name
写入animation
将不会有效果。如果不使用上述方法,需要自行注入 keyframes,将Keyframes
实例声明在 style 中即可。
打开页面右下角的 Dynamic Theme, 上方会有一个 Diff 选项,打开后可以看到当前页面和线上版本的区别。虽然只是简单的对比,有些场景覆盖不到(比如弹窗),但还是有一定意义的,后续会继续迭代优化。所以在迁移时确保打开 Diff 后没有任何会让人眼花的图案出现:
打开页面右下角的 Dynamic Theme, 上方会有一个 眼睛 按钮,点击后左侧会有弹窗显示当前页面 CSS-in-JS 的结果:
-
Css to CssInJs: 可用于快速将 CSS 转换为 CSS-in-JS,带有 less 变量的场合下可能不太好用。
-
Token 数值显示: 社区同学 @shezhangzhang 贡献的插件,可以在研发时自动展示 token 对应数值。
一般来说我们可以通过 v4 使用的 less 变量推断出 v5 中应该使用哪一个 token,比如 v4 中的 @primary-color
在 v5 中就是 colorPriamry
。但还是会有一些 v4 中有但 v5 中没有的变量,比如很多组件级别的变量在 v5 都废弃了,这时可以直接使用原始 token 中提供变量,比如 v4 中有 @input-height: @control-height
,那么在 v5 中就可以直接使用 controlHeight
。如果有一些 hard code 的 style,比如 padding: 12px;
,就可以直接使用已有的 token 替换;如果没有合适的 token 可以使用,那就打上注释 // FIXME: hard code in v4
,后续再做处理。
其实这时可以打开 CSS 面板查看到底生成了什么 CSS
在 hash 模式下,我们要求组件的最外层元素带上 hashId 作为 className,同时在 genComponentStyleHook
中第二个参数返回的 CSSInterpolation
中最外层的选择器都会加上 hashId,举个例子:
export default genComponentStyleHook(
'Button',
token => [
{
[token.componentCls]: {
fontSize: 14,
'&&-lg': {
fontSize: 16,
},
'&-inner': {
padding: token.paddingXS,
}
}
}
],
);
最终产生的 CSS 是:
.css-dev-only-do-not-override-1qwsfmr.ant-btn {
font-size: 14px;
}
.css-dev-only-do-not-override-1qwsfmr.ant-btn.css-dev-only-do-not-override-1qwsfmr.ant-btn-lg {
font-size: 16px;
}
.css-dev-only-do-not-override-1qwsfmr.ant-btn-inner {
padding: 8px;
}
可以发现 &
把 hashId 也一起代入进来了,这里 &-inner
就失去了原本的效果,所以根据不同的场景我们需要对 CSS-in-JS 的结构作一定调整。这里就要注意一下哪些 className 是和 hashId 和 componentCls 在同一个元素上的,如果在同一个元素上, &
就可以继续沿用;如果不在同一个元素上,一般就是子元素上,就需要将&
改为componentCls
(或者其他选择器),增加一层嵌套以关联 hashId:
export default genComponentStyleHook(
'Button',
token => {
const { componentCls } = token;
return [
{
[componentCls]: {
fontSize: 14,
// 同级的 className 可以继续使用 &
'&&-lg': {
fontSize: 16,
},
// 子元素上的 className 需要嵌套处理,替换原有的 &
// &-inner {
// padding: @padding-xs;
// }
[`${componentCls}-inner`]: {
padding: token.paddingXS,
}
}
}
];
},
);
编译结果,符合我们的预期:
.css-dev-only-do-not-override-1qwsfmr.ant-btn {
font-size: 14px;
}
/* 同级 */
.css-dev-only-do-not-override-1qwsfmr.ant-btn.css-dev-only-do-not-override-1qwsfmr.ant-btn-lg {
font-size: 16px;
}
/* 子元素 */
.css-dev-only-do-not-override-1qwsfmr.ant-btn .ant-btn-inner {
padding: 8px;
}
- Home
- Cookbook
- FAQ
- Template for Bug Report in IE8 9
- Contributing
- Maintaining
- Design