CSS-IN-JS 方案 - Emotion 库

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 项目中大力推广的解决方案。

优点

  1. 让 CSS 代码拥有独立的作用域,阻止 CSS 代码泄露到组件外部,防止样式冲突。
  2. 让组件更具可移植性,实现开箱即用,轻松创建松耦合的应用程序。
  3. 让组件更具可重用性,只需编写一次即可,可以在任何地方运行。不仅可以在同一个应用程序中重用组件,而且可以在使用相同框架构建的其它应用程序中重用组件。
  4. 让样式具有动态功能,可以将复杂的逻辑引用于样式规则,如果要创建需要动态功能的复杂 UI,它是理想的解决方案。

缺点

  1. 为项目增加了额外的复杂性,需要了解学习和应用这种解决方案。
  2. 自动生成的选择器大大降低了代码的可读性。

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' })

官方文档 - The css Prop

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-crareact-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

打印结果:

CSS-IN-JS 方案 - Emotion 库

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

有三种使用方式:

  1. 在样式化组件中通过 props.theme 访问主题对象。
  2. css 属性设置为一个函数时,它接收的参数就是主题对象。
  3. 使用 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

上一篇:二)NextJS集成Styled Components


下一篇:React使用styled-components进行样式初始化(reset.css)