外观
第28章—CSSInJS:快速掌握styled-components
3575字约12分钟
2024-09-19
CSS in JS,顾名思义就是用 js 来写 css。
它也是一种很流行的 css 管理方案。
比如 styled-components 的样式是这样写:
可以传参数。
然后用的时候当作组件一样用:
样式用 js 写,可以当成组件用,可以传参,这是 CSS in JS 的方案独有的体验。
接下来我们也体验一下:
npx create-vite styled-components-test
用 vite 创建个项目。
安装 styled-components:
npm install
npm install --save styled-components
去掉 index.css 和 StrictMode:
然后改下 App.tsx:
import { styled } from 'styled-components';
const Title = styled.h1`
font-size: 30px;
text-align: center;
color: blue;
`;
const Header = styled.div`
padding: 20px;
background: pink;
`;
function App() {
return <Header>
<Title>
Hello World!
</Title>
</Header>
}
export default App
跑起来看下:
npm run dev
样式生效了:
打开 devtools 看下:
可以看到 styled.div、styled.h1 会创建对应的标签,然后样式会生成一个唯一的 className。
所以说,用 styled-components 不用担心样式冲突的问题。
继续看,styled-components 的 styled.xx 可以作为组件用,那自然是可以传参的:
import { styled } from 'styled-components';
const Title = styled.h1<{ color?: string; }>`
font-size: 30px;
text-align: center;
color: ${props => props.color || 'blue'}
`;
const Header = styled.div`
padding: 20px;
background: pink;
`;
function App() {
return <Header>
<Title>
Hello World!
</Title>
<Title color='green'>
Hello World!
</Title>
<Title color='black'>
Hello World!
</Title>
</Header>
}
export default App
我们给 Title 样式组件添加一个 color 参数,然后分别传入 green、black。
看下效果:
确实样式组件用起来和其他 React 组件体验一样,加的 ts 类型也会有提示:
这也是为啥这个库叫 styled-components,样式组件。
有的时候,样式需要基于已有的做扩展,比如我有一个 Button 的样式,另一种 Button 和它大部分一样,但有所不同。
这时候就可以这样写:
import { styled } from 'styled-components';
const Button = styled.button<{ color?: string; }>`
font-size: 20px;
margin: 5px 10px;
border: 2px solid #000;
color: ${props => props.color || 'blue'}
`;
const Button2 = styled(Button)`
border-radius: 8px;
`;
function App() {
return <div>
<Button color='red'>Hello World!</Button>
<Button2 color='red'>Hello World!</Button2>
</div>
}
export default App
如果你还想改样式组件的标签,可以用 as:
styled() 除了可以给样式组件扩展样式外,还可以给普通组件加上样式:
import { FC, PropsWithChildren } from 'react';
import { styled } from 'styled-components';
interface LinkProps extends PropsWithChildren {
href: string;
className?: string;
}
const Link: FC<LinkProps> = (props) => {
const {
href,
className,
children
} = props;
return <a href={href} className={className}>{children}</a>
}
const StyledLink = styled(Link)`
color: green;
font-size: 40px;
`;
function App() {
return <div>
<StyledLink href='#aaa'>click me</StyledLink>
</div>
}
export default App
比如我们给 Link 组件加上样式。
这里要注意,Link 组件必须接收 className 参数,因为 styled-components 会把样式放到这个 className 上:
我们知道,样式组件也是可以接受参数的,为了区分两者,我们一般都是样式组件的 props 用 $ 开头:
const StyledLink = styled(Link)<{ $color?: string;}>`
color: ${props => props.$color || 'green'};
font-size: 40px;
`;
function App() {
return <div>
<StyledLink href='#aaa' $color="purple">click me</StyledLink>
</div>
}
默认情况下,样式组件会透传所有不是它的 props 给被包装组件:
样式组件包了一层,自然是可以修改 props 的:
用 attrs 方法,接收传入的 props 返回修改后的 props。
import { FC, PropsWithChildren } from 'react';
import { styled } from 'styled-components';
interface LinkProps extends PropsWithChildren {
href: string;
className?: string;
}
const Link: FC<LinkProps> = (props) => {
console.log(props);
const {
href,
className,
children
} = props;
return <a href={href} className={className}>{children}</a>
}
const StyledLink = styled(Link).attrs<{ $color?: string;}>((props) => {
console.log(props);
props.$color = 'orange';
props.children = props.children + ' 光';
return props;
})`
color: ${props => props.$color || 'green'};
font-size: 40px;
`;
function App() {
return <div>
<StyledLink href='#aaa' $color="purple">click me</StyledLink>
</div>
}
export default App
attrs 支持对象和函数,简单的场景直接传对象也可以:
const Input = styled.input.attrs({ type: 'checkbox'})`
width: 30px;
height: 30px;
`;
那伪类选择器、伪元素选择器这些呢?
当然也是支持的。
import { styled } from 'styled-components';
const ColoredText = styled.div`
color: blue;
&:hover {
color: red;
}
&::before {
content: '* ';
}
`
function App() {
return <>
<ColoredText>Hello styled components</ColoredText>
</>
}
export default App;
写法和之前一样。
但 styled components 这个 & 和 scss 里的 & 含义还不大一样。
它指的是同一个样式组件的实例,这里也就是 ColoredText 的实例。
所以可以这样写:
import { styled } from 'styled-components';
const ColoredText = styled.div`
color: blue;
&:hover {
color: red;
}
&::before {
content: '* ';
}
&.aaa + & {
background: lightblue;
}
&.bbb ~ & {
background: pink;
}
`
function App() {
return <>
<ColoredText>Hello styled components</ColoredText>
<ColoredText className="aaa">Hello styled components</ColoredText>
<ColoredText>Hello styled components</ColoredText>
<ColoredText className="bbb">Hello styled components</ColoredText>
<div>Hello styled components</div>
<ColoredText>Hello styled components</ColoredText>
<ColoredText>Hello styled components</ColoredText>
</>
}
export default App;
这里 &.aaa + & 就是 .aaa 的 ColoredText 样式组件之后的一个 ColoredText 样式组件实例。
&.bbb ~ & 就是 .bbb 的 ColoredText 样式组件之后的所有 ColoredText 样式组件实例。
此外,如果你把 & 全换成 &&,你会发现效果也一样:
那什么时候用 &、什么时候用 && 呢?
当你和全局样式冲突的时候。
styled-components 用 createGlobalStyle 创建全局样式:
我们全局指定 ColoredText 的 color 为 green,然后组件里指定 color 为 blue。
看下效果:
每个 ColorText 组件都会有一个 src-aYaIB 的 className,全局样式就是给这个 className 加了 color 为 green 的样式。
可以看到,组件里写的 color: blue 被覆盖了。
这时候你这样写是没用的:
用 && 才能覆盖:
它通过 .aaa.aaa 这样的方式实现了样式优先级的提升:
那动画怎么写呢?
有单独的 api:
import { styled, keyframes } from 'styled-components';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
font-size: 50px;
padding: 30px;
`;
function App() {
return <Rotate>X</Rotate>
}
export default App;
通过 keyframes 来编写动画,然后在 animation 里引用。
看下效果:
它为 @keyframes 生成了一个唯一 ID:
这大概就是加一个 keyframes 的 api 的意义。
此外,如果你想复用部分 css,要这样写:
const animation = css`
animation: ${rotate} 2s linear infinite;
`
const Rotate = styled.div`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
`;
不加 css 是不会生效的,你可以试一下。
抽出来的 css 也是可以用 props 的:
import { styled, keyframes, css } from 'styled-components';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const animation = css<{ $duration: number }>`
animation: ${rotate} ${props => props.$duration}s linear infinite;
`
const Rotate = styled.div<{ $duration: number }>`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
`;
function App() {
return <Rotate $duration={3}>X</Rotate>
}
export default App;
但是 css 声明了类型,用到了这部分样式的 styled.xxx 也需要声明类型。
如果你希望样式组件用的时候可以传入一些样式,那可以用 RuleSet:
import { styled, keyframes, css, RuleSet } from 'styled-components';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const animation = css<{ $duration: number }>`
animation: ${rotate} ${props => props.$duration}s linear infinite;
`
const Rotate = styled.div<{ $duration: number, otherStyles: RuleSet }>`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
${props => props.otherStyles}
`;
function App() {
return <Rotate $duration={3} otherStyles={ [
{ border: '1px', background: 'pink' },
{ boxShadow: '0 0 3px blue'}
]}>X</Rotate>
}
export default App;
它是一个样式对象的数组类型:
可以用的时候传入一些样式:
最后,styled-components 还有 theme 的 api。
这个也很简单,你会用 react 的 context 就会用这个:
import { styled, ThemeProvider } from 'styled-components';
const Aaa = styled.div`
width: 100px;
height: 100px;
background: ${props => props.theme.dark ? 'black' : '#ccc'}
`
function Content() {
return <Aaa></Aaa>
}
function App() {
return <ThemeProvider theme={{ dark: true }}>
<Content></Content>
</ThemeProvider>
}
export default App;
每个样式组件都有 props.theme 可以读取当前 theme 对象,然后这个对象可以通过 useTheme 读取,通过 ThemeProvider 修改。
import { useState } from 'react';
import { styled, ThemeProvider, useTheme } from 'styled-components';
const Aaa = styled.div`
width: 100px;
height: 100px;
background: ${props => props.theme.dark ? 'black' : '#ccc'}
`
function Content() {
const theme = useTheme();
const [dark, setDark] = useState<boolean>(theme.dark);
return <>
<button onClick={() => setDark(!dark)}>切换</button>
<ThemeProvider theme={{ dark }}>
<Aaa></Aaa>
</ThemeProvider>
</>
}
function App() {
return <ThemeProvider theme={{ dark: true }}>
<Content></Content>
</ThemeProvider>
}
export default App;
我们用 useTheme 读取了当前 theme,然后点击按钮的时候 setState 触发重新渲染,通过 ThemeProvider 修改了 theme 的值。
这就是 styled-components 的 theme 功能。
上面的过一遍,styled-components 就算掌握的差不多了
那最后我们来思考下,用 styled-components 有啥优缺点呢?
先来看下好处:
用了 styled-components 之后,你的 className 都是这样的:
没有样式冲突问题,不需要类似 CSS Modules 这种方案。
而且你可以用 js 来写样式逻辑,而且封装方式也是 React 组件的方式,这个是挺爽的。
不然你要学 scss 的函数的语法,比如这样:
@function multiple-box-shadow($n) {
$value: '#{random(2000)}px #{random(2000)}px #FFF';
@for $i from 2 through $n {
$value: '#{$value} , #{random(2000)}px #{random(2000)}px #FFF';
}
@return unquote($value);
}
#stars {
width: 1px;
height: 1px;
box-shadow: multiple-box-shadow(700);
}
scss 的 for 循环、if else 还有函数等的语法都要单独学习。
相比之下,还是 styled-components 直接用 js 来写样式组件的逻辑更爽。
这就像很多人不喜欢 vue 的 template 写法,更喜欢 React 的 jsx 一样,可以直接用 js 来写逻辑。
当然,styled-components 也有不好的地方,比如:
你的 React 项目里会多出特别多样式组件:
随便找一个组件,一眼望去全是样式组件。
你的 React DevTools 里也是一堆 styled-components 的组件:
当然,这些也不是啥大问题,styled-components 整体还是很好用的。
案例代码上传了小册仓库
总结
CSS in JS 就是用 js 来写 css。
今天我们学习了最流行的 CSS in JS 库 styled-components。
它的特点就是样式组件,用 styled.div、styled() 可以创建样式组件。
样式组件可以传参数,可以通过 attrs() 修改参数。
通过 keyframes 来声明动画样式,通过 css 来复用某段样式,通过 createGlobalStyle 创建全局样式。
写样式的时候,通过 & 代表当前样式组件的实例,当样式和全局样式冲突的时候,还可以 && 提高优先级。
styled-components 还支持 theme,可以通过 ThemeProvider 修改 theme 值,通过 useTheme 来读取,每个样式组件里都可以通过 props.theme 拿到当前 theme,然后展示不同样式。
styled-components 相比 scss 等方案有好有坏:
- 没有 className 冲突问题,不需要 CSS Modules
- 用 js 来写逻辑,不需要学习单独的 scss 语法
- 项目里会多很多的样式组件,和普通组件混在一起
- React DevTools 里会有很多层的样式组件
总体来说,styled-components 还是很有不错,如果你喜欢通过 React 组件的方式来写样式这种方式,可以考虑使用。
我最近在维护的一个项目,用 styled-components 好多年了,大项目用也没问题。