CSS-IN-JS
"CSS in JS" 是一种在 JavaScript 文件中集成 CSS 代码的解决方案。
这种方案旨在解决 CSS 的局限性,例如缺乏动态功能,作用域和可移植性。
为什么会有 CSS-IN-JS
- 缺乏作用域
- 缺乏可移植性
- 缺乏动态功能
其它参考:阮一峰 - CSS in JS 简介
缺乏作用域
以前开发 web 项目都是以**“页面”为单位,为了将关注点分离**,一般都是将 CSS、JS 以文件的方式引入 HTML 文件。
而现在开发 web 项目都是以**“组件”**为单位,所以很多开发者更倾向于将同一个组件的 HTML、CSS、JS 代码都集成在一起。(例如 React 中已经通过 JS 编写 HTML 代码)
这样组件与组件间的 CSS 就不会产生冲突,这使用的是作用域的概念,而 CSS 没有作用域。
CSS-IN-JS 方案就是通过 JavaScript 的作用域模拟 CSS 的作用域。
缺乏可移植性
将 CSS 文件集中在组件中,这样在使用组件的时候,可以避免遗漏引入必要的 CSS 文件。
缺乏动态功能
CSS 无法通过条件判断,决定给元素设置哪些样式。
如果将 CSS 写在 JavaScript 的文件中,就可以用 JS 的动态功能给元素添加样式了。
CSS-IN-JS 方案的优缺点
CSS-IN-JS 方案的优点大于缺点,是值得在 React 项目中大力推广的解决方案。
优点
- 让 CSS 代码拥有独立的作用域,阻止 CSS 代码泄露到组件外部,防止样式冲突。
- 让组件更具可移植性,实现开箱即用,轻松创建松耦合的应用程序。
- 让组件更具可重用性,只需编写一次即可,可以在任何地方运行。不仅可以在同一个应用程序中重用组件,而且可以在使用相同框架构建的其它应用程序中重用组件。
- 让样式具有动态功能,可以将复杂的逻辑引用于样式规则,如果要创建需要动态功能的复杂 UI,它是理想的解决方案。
缺点
- 为项目增加了额外的复杂性,需要了解学习和应用这种解决方案。
- 自动生成的选择器大大降低了代码的可读性。
Emotion
本文 Emotion 版本:11
介绍
Emotion 是使用 JavaScript 编写 CSS 样式的库,是众多实施 CSS-IN-JS 方案库的其中一个。
安装
React 使用 Emotion 需要安装:
npm install @emotion/react
css 属性
Emotion 提供一个 css
属性,任何可以设置 className
的组件或元素都可以使用 css
属性。
传入 css
属性的样式将被评估,并将计算出的类名应用于 className
。
function App() {
return (
<div css={{fontSize: '20px', background: red}}>App work</div>
)
}
Babel 配置支持 css 属性
现在 css
属性并没有生效,因为 React.createElement
(JSX 表达式被转换为 React.createElement
方法的调用)并不知道该如何解析 css
属性。
Emotion 提供了 jsx
方法去编译转换 JSX 表达式。
有两种方式可以告诉 Babel 使用 Emiotion 的 jsx
方法替换 React.createElement
方法编译 JSX 表达式:
- JSX Pragma
- Babel Preset(推荐)
Input | Output | |
---|---|---|
Before | <img src="avatar.png" /> |
React.createElement('img', { src: 'avatar.png' }) |
After | <img src="avatar.png" /> |
jsx('img', { src: 'avatar.png' }) |
JSX Pragma
Pragma 用于替换编译 JSX 表达式时使用的函数。
- 使用 JSX Pragma
- 引入
jsx
方法
// 此注释告诉 Babel 将 jsx 代码转换为 jsx 函数的调用,而不是 React.createElement
/** @jsx jsx */
import { jsx } from '@emotion/react'
function App() {
return (
<div css={{fontSize: '20px', background: 'red'}}>App works</div>
)
}
export default App
如果使用了 React 17+ 版本,它默认采用了新版自动导入运行时(不需要手动引入 React 模块),则会冲突报错:pragma and pragmaFrag cannot be set when runtime is automatic.
需要指定 Emotion 的自动导入运行时:
/** @jsxImportSource @emotion/react */
function App() {
return (
<div css={{fontSize: '20px', background: 'red'}}>App works</div>
)
}
export default App
不需要再手动导入
jsx
方法。
Babel Preset
#下载 preset
npm install @emotion/babel-preset-css-prop
Babel 配置添加预设:
"presets": [
"@emotion/babel-preset-css-prop"
]
使用 Babel Preset 配置,也不需要在组件文件引入 jsx
方法:
function App() {
return (
<div css={{fontSize: '20px', background: 'red'}}>App works</div>
)
}
export default App
eject 方式(不推荐)
React 需要使用 eject
暴露全部配置,然后在 package.json
中配置:
"babel": {
"presets": [
"react-app",
"@emotion/babel-preset-css-prop"
]
}
customize-cra(推荐)
eject
是不可逆的,推荐使用 customize-cra
和 react-app-rewired
进行自定义配置:
# 安装
npm install customize-cra react-app-rewired
修改 package.json
:
/* package.json */
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
"eject": "react-scripts eject"
}
在根目录创建 config-overrides.js
文件:
/* config-overrides.js */
const { override, addBabelPreset } = require('customize-cra')
module.exports = {
webpack: override(
// emotion css props support
addBabelPreset('@emotion/babel-preset-css-prop')
)
}
设置类型
css
属性有两种设置类型:
- 对象字面量:对象或对象数组
-
css
方法的返回值
ES6 标签模板
参考 阮一峰 - 标签模板
模板字符串的标签模板功能:在函数后面跟模板字符串(反引号包裹的字符串)。
这样调用的函数,会将模板字符串处理成数组再传递给函数:
alert`Hello`
// 等同于
alert(['Hello'])
如果模板字符串中嵌入了变量或表达式,就会将字符串拆分,嵌入的内容作为后续的参数传给函数:
const a = 10, b = 20
foo`${a} + ${b} = ${a + b}`
// 等同于
foo(['', ' + ', ' = ', ''], 10, 20, 30)
模拟 Emotion 的 css
方法的模板字符串方式的调用:
function css(style, ...args) {
let res = ''
let i = 0
while(i < style.length) {
res += style[i]
if (i<args.length) {
res += args[i]
}
i++
}
console.log(res)
}
const fontSize = 14
const bgColor = 'skyblue'
css`
color:red;
font-size: ${fontSize}px;
background:${bgColor};
`
// 打印结果:
/*
color:red;
font-size: 14px;
background:skyblue;
*/
css 方法
css
方法 和 css
属性是配合使用的。
通过 css
方法,可以把原本写在行内的 CSS 样式拿到外面去写,将返回的值传递给 css
属性。
css
方法有两种调用方式:
- String Styles:模板字符串调用方式
- 以字符串形式编写样式
- Object Styles:普通函数调用方式
- 使用对象或对象数组方式编写样式
css
方法的返回值是一个对象,包含定义的样式和代表这个样式的类名。
不论使用哪种方式,都要先引入:
import { css } from '@emotion'
String Styles
使用标签模板的方式调用 css
方法,可以在模板字符串中编写 CSS 语法的样式。
使用模板字符串方式,设置的数值类样式,需加上单位:
width: 200 // 不生效 width: 200px 生效
函数方式的对象不需要,单位会自动加上(如果使用的字符串,例如
"2000"
则不会自动附加)。
import { css } from '@emotion/react'
const style = css`
font-size: 20px;
background: red;
`
console.log(style)
function App() {
return (
<div css={style}>App works</div>
)
}
export default App
打印结果:
Object Styles
const style = css({
fontSize: '20px',
background: 'red'
})
css 属性优先级
props 对象中的 css 属性优先级高于组件内部的 css 属性。
这样,在调用组件的时候可以覆盖组件的默认样式。
// 下面 `styleA` 计算的 `className` 会覆盖 `styleB` 计算的 `className`
// 尽管 css 属性写在 {...props} 后面
// 最终背景颜色是 pink
import { css } from '@emotion/react'
function Foo(props) {
const styleB = css({
background: 'red'
})
return <div {...props} css={styleB}>123123123</div>
}
function App() {
const styleA = css({
background: 'pink'
})
return <Foo css={styleA} />
}
export default App
Styled Components 样式化组件
styled
方法可以创建附加样式的 React 组件,它是 Emotion 提供的为组件或元素添加样式的另一种方式。
安装
styled
由 @emotion/styled
提供
# 安装
npm install @emotion/styled
// 引入
import styled from '@emotion/styled'
创建普通元素的样式化组件
styled
为每个 HTML 元素提供了方法,如果创建 HTML 元素的样式化组件,可以直接调用对应方法:
// 直接调用对应方法
styled.div`
width: 100px;
`
// 将标签名作为参数传入,调用返回的方法
styled('div')`
color: #333;
`
使用方式
styled
也有两种使用方式:
- 模板字符串形式
- 普通函数调用
import styled from '@emotion/styled'
const Button1 = styled.button`
color: blue;
`
const Button2 = styled.button({
color: 'red'
})
const Button3 = styled('button')`
color: pink;
`
const Button4 = styled('button')({
color: 'green'
})
function App() {
return (
<div>
<Button1>Button1</Button1>
<Button2>Button2</Button2>
<Button3>Button3</Button3>
<Button4>Button4</Button4>
</div>
)
}
export default App
基于 props 设置样式
样式化组件定义样式时,可以通过 props
获取组件接收的属性,根据属性设置样式。
- 模板字符串中可以在
${}
中使用一个函数,函数接收一个props
参数,最终返回一段 css - 普通函数调用时,可以传递一个函数,函数接收一个
props
参数,最终返回一个对象或数组对象
import styled from '@emotion/styled'
const Div = styled.div`
width: 200px;
background: ${props => props.bgColor || 'skyblue'};
height: 100px;
`
const Button = styled.button(props => ({
borderRadius: 10,
fontSize: props.fontSize || 20
}))
function App() {
return (
<div>
<Div>
<Button>天蓝色 20px</Button>
</Div>
<Div bgColor="pink">
<Button fontSize={30}>粉色 30px</Button>
</Div>
</div>
)
}
export default App
覆盖默认样式
styled
定义样式时,可以向第二个参数传入一个函数,函数接收 props
返回样式对象,用以覆盖第一个参数设置的默认样式。
import styled from '@emotion/styled'
const Div = styled.div`
width: 200px;
background: ${props => props.bgColor || 'skyblue'};
height: 100px;
`
const Button = styled.button({
// 默认样式
borderRadius: 10,
fontSize: 20
}, props => ({
// 覆盖的样式
fontSize: props.fontSize
}))
function App() {
return (
<div>
<Div>
<Button>天蓝色 20px</Button>
</Div>
<Div bgColor="pink">
<Button fontSize={30}>粉色 30px</Button>
</Div>
</div>
)
}
export default App
上面的 粉色 30px
的 Button 样式最终为:
border-radius: 10px;
font-size: 20px;
font-size: 30px;
为组件添加样式
styled
还可以为 React 组件添加样式,只需要这个组件接收 className
。
类似传递普通元素的名称,给组件添加样式,只需将接收 className
的组件传递给 styled
。
import styled from '@emotion/styled'
function Basic({ className }) {
return <div className={className}>Basic</div>
}
const Fancy = styled(Basic)`
width: 100px;
height: 100px;
background: pink;
`
const Fancy2 = styled(Basic)({
width: 100,
height: 100,
background: 'skyblue'
})
function App() {
return (
<div>
<Fancy />
<Fancy2 />
</div>
)
}
export default App
设置子组件样式
styled
创建的组件,应用在定义样式的模板字符串或样式对象中时,Emotion 会将其解析为对应的 className
。
样式模板中嵌套的样式 和 样式对象中嵌套的对象,会追加上级样式的类名
import styled from '@emotion/styled'
const Child = styled.span({
color: 'red'
})
const Parent = styled.div`
${Child} {
color: blue;
}
`
const Parent2 = styled.div({
// 使用ES6的表达式方式定义属性名
[Child]: {
color: 'green'
}
})
function App() {
return (
<div>
<Child>红色的字</Child>
<Parent>
<Child>蓝色的字</Child>
</Parent>
<Parent2>
<Child>绿色的字</Child>
</Parent2>
</div>
)
}
export default App
CSS 选择器 - &
&
表示组件或元素本身
import styled from '@emotion/styled'
const Div = styled.div`
width: 200px;
height: 200px;
background: skyblue;
&:hover {
background: pink;
}
& > span {
color: red;
}
`
function App() {
return (
<div>
<Div>
<span>red</span>
</Div>
</div>
)
}
export default App
as 属性
要使用样式化组件的样式,但是想更换渲染的元素,可以使用 as
属性。
import styled from '@emotion/styled'
const Span = styled.span`
&:hover {
color: blue;
}
`
function App() {
return (
<div>
<Span as="a" href="http://xxx.com">a标签</Span>
</div>
)
}
export default App
样式组合
css
属性可以接收对象数组。数组中的成员可以是 css
方法创建的样式。
数组后面的样式优先级高于前面的样式。
import { css } from '@emotion/react'
const success = css`
background: pink;
color: green;
`
const danger = css`
font-size: 20px;
color: red;
`
function App() {
return (
<div>
<button css={[success, danger]}>红色</button>
<button css={[danger, success]}>绿色</button>
</div>
)
}
export default App
定义全局样式
Emotion 提供一个 Global 组件用于定义全局样式。
Global 组件的 styles
属性接收和 css
属性一样的值,它定义的样式会应用到全局。
- Global 组件不需要包裹任何内容
- Global 组件设置的样式不会生成
className
, 它类似直接在项目中引入样式
import { Global, css } from '@emotion/react'
const styles = css`
body {
margin: 0;
background: skyblue;
}
a {
text-decoration: none;
color: green;
}
.main {
width: 200px;
height: 200px;
background: pink;
}
`
function App() {
return (
<div class="main">
<Global styles={styles} />
<a href="http://xxx.com">a标签</a>
</div>
)
}
export default App
定义关键帧动画
Emotion 提供 keyframes
方法创建关键帧动画,同样可以使用字符串或对象方式定义。
然后在定义样式的时候使用即可。
import { keyframes, css } from '@emotion/react'
const move = keyframes`
0% {
background: skyblue;
left: 0;
top: 0;
}
100%{
background: tomato;
left: 600px;
top: 300px;
}
`
const box = css`
width: 100px;
height: 100px;
position: absolute;
animation: ${move} 2s ease infinite alternate;
`
function App() {
return (
<div css={box}>App works</div>
)
}
export default App
Theming 主题
Emotion 可以将 ThemeProvider
组件放置到视图最外层,通过 theme
属性定义主题样式对象。
这个主题样式对象不是可以直接用的 css 样式,而是用于定义样式的一些
key-value
。
有三种使用方式:
- 在样式化组件中通过
props.theme
访问主题对象。 - 当
css
属性设置为一个函数时,它接收的参数就是主题对象。 - 使用
useTheme
钩子函数获取主题对象。
import styled from '@emotion/styled'
import { useTheme } from '@emotion/react'
const PrimaryDiv = styled.div`
color: ${props => props.theme.colors.primary}
`
const primaryColor = theme => ({
color: theme.colors.primary
})
function App() {
const theme = useTheme()
return (
<div>
<PrimaryDiv>样式化组件</PrimaryDiv>
<div css={primaryColor}>向css属性传递一个函数</div>
<div css={{color: theme.colors.primary}}>使用钩子函数</div>
</div>
)
}
export default App