This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
概念
在做 CSS-in-JS 的迁移之前,先了解一下 v5 相对于 v4 新增的一些概念。
Design Token
Design Token 是一个设计系统,简单点说的话可以类比成 v4 就有的 less 变量,是一些原子化的样式变量,比如 border 的线宽、颜色等等,以 JS 变量的形式串联在 antd 组件中。在 v5 中我们用 CSS-in-JS 替代了 less,因此 Design Token 也替代 less 变量支撑起了 v5 的主题系统。
Hashed styles
在 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 隔离开,从而从根本上解决样式污染问题。
如何迁移
启用 CSS-in-JS
这里 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 和对应的选择器:
Token 的组成
可以注意到上文中注入 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 前缀,常用于在组件中处理非当前组件的样式。
添加 ComponentToken
我们可以为组件添加一些组件级别的 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
并不希望暴露给用户,那么我们就可以在组件内添加一些自定义 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 要点
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;
}
}
});
根据具体场景的不同处理方式可能不同,这里仅作举例
CSS Logical Properties and Values
参考链接: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 则可以忽略。
Content
在传统 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"',
});
Keyframes 和 Animation
在 @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 中即可。
辅助工具
Demo Diff
打开页面右下角的 Dynamic Theme, 上方会有一个 Diff 选项,打开后可以看到当前页面和线上版本的区别。虽然只是简单的对比,有些场景覆盖不到(比如弹窗),但还是有一定意义的,后续会继续迭代优化。所以在迁移时确保打开 Diff 后没有任何会让人眼花的图案出现:
查看生成的 CSS
打开页面右下角的 Dynamic Theme, 上方会有一个 眼睛 按钮,点击后左侧会有弹窗显示当前页面 CSS-in-JS 的结果:
VSCode 插件
-
Css to CssInJs: 可用于快速将 CSS 转换为 CSS-in-JS,带有 less 变量的场合下可能不太好用。
-
Token 数值显示: 社区同学 @shezhangzhang 贡献的插件,可以在研发时自动展示 token 对应数值。
FAQ
我应该用 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
,后续再做处理。
为什么类似 &-xxx 或者 &&-xxx 的选择器没有生效?
其实这时可以打开 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