React学习 --- React中的样式

3,033 阅读6分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

整个前端已经是组件化的天下, 而CSS的设计之初并不存在组件化,因此原生的css本身并不支持组件化,所以在目前组件化的框架中都在需要一种合适的CSS解决方案。

在组件化中选择合适的CSS解决方案应该符合以下条件:

  • 可以编写局部css:css具备自己的具备作用域,不会随意污染其他组件内的元素
  • 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式
  • 支持所有的css特性:伪类、动画、媒体查询等;
  • 编写起来简洁方便、最好符合一贯的css风格特点
  • 等等...

内联样式

内联样式是官方推荐的一种css样式的写法:

  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串
  • 可以引用state中的状态来设置相关的样式
import { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      color: 'red'
    }
  }

  render() {
    return (
      <>
        <div style={{
          fontSize: '20px',
          color: this.state.color
        }}>内联样式的使用</div>
      </>
    )
  }
}

内联样式的优点:

  • 内联样式, 样式之间不会有冲突
  • 可以动态获取当前state中的状态

内联样式的缺点:

  • 写法上都需要使用驼峰标识
  • 某些样式没有提示
  • 大量的样式, 代码混乱
  • 某些样式无法编写(比如伪类/伪元素)

所以官方依然是希望内联合适和普通的css来结合编写;

普通css

普通的css我们通常会编写到一个单独的文件,之后再进行引入。

这样的编写方式和普通的网页开发中编写方式是一致的

但是这种编写方式有一个很大的问题,就是在默认情况下,所有编写的样式都是全局的,容易产生样式的覆盖

.foo {
  color: red;
}
import { PureComponent } from 'react'
import './index.css'

export default class App extends PureComponent {
  render() {
    return (
      <>
        <div className="foo">内联样式的使用</div>
      </>
    )
  }
}

css modules

css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的

但是,如果在其他项目中使用css modules,那么我们需要自己来进行配置

而在React脚手架中,已经内置了css modules的配置

我们所需要做的仅仅只是将.css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等,之后就可以引用并且进行使用了

import { PureComponent } from 'react'

// 引入css modules
import style from './index.module.css'

export default class App extends PureComponent {
  render() {
    return (
      <>
      	{/* 
      	  1. 使用css modules
          2. 在实际解析的时候,会将样式转换为[组件名]_foo_[唯一标识],从而达到样式不会被覆盖的效果
      	*/}
        <div className={ style.foo }>内联样式的使用</div>
	{/*  
	  style.foo-title在JS中是不合法的,所以需要使用 style['foo-title']或将样式名修改为小驼峰表示,如fooTitle
        */}
	<div className={ style['foo-title'] }>title</div>
      </>
    )
  }
}

css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。

但是这种方案也有自己的缺陷: 那就是不方便动态来修改某些样式,依然需要使用内联样式的方式来引用动态样式

css in js

官方文档有提到过CSS in JS这种方案:

  • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义
  • 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度

传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

但是,在React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法。

同样,React认为样式也是属于UI的一部分

事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态

所以React有被人称之为 All in JS

CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用等功能以及CSS预处理无法做到的动态修改状态等等

所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案

目前比较流行的CSS-in-JS的库有哪些呢?

  • styled-components
  • emotion
  • glamorous

目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components进行举例

# 安装
$ yarn add styled-components

标签模板字符串

const name = 'Klaus'
const age = 23

function foo(...args) {
  console.log(args) // => [ [ 'my name is ', ', my age is ', '' ], 'Klaus', 23 ]
}

// 标签模板字符串
foo`my name is ${name}, my age is ${age}`

style样式设置

在React中设置style样式使用最多的第三方库就是styled-components

styled-components的核心原理就是内部使用了标签模板字符串进行了样式的拼接和处理

style.js

// 引入styled-components
import styled from 'styled-components'

// styled[标签名] --- 是一个会返回一个React组件的函数
// 对于返回的React组件就是我们添加上对应样式后的新组件 --- 这类组件一般被称之为styled样式组件
// 我们可以使用加上样式的新组件来替换原本的标签
// 返回的组件会上会生成一个不重复的id作为组件的样式名,从而解决样式冲突问题
export const AppWrapper = styled.div`
  color: red;
  font-size: 20px;

  /* 子元素样式设置 */
  /* 如果需要设置多个样式,多个样式之间需要使用分号进行分割(即分号是不可以省略的,即使是最后一行css样式)  */
  ul,
  li {
    list-style: none;
  }

  li {
    /*  伪类设置 */
    &:hover {
      color: blue;
    }

    /* 伪元素设置 */
    &::before {
      content: 'username: '
    }
  }
`

App.js

import { PureComponent } from 'react'
import { AppWrapper } from './style.js'

export default class App extends PureComponent {
  render() {
    return (
      <AppWrapper>
        <ul>
          <li>Klaus</li>
          <li>Alex</li>
          <li>Steven</li>
        </ul>
      </AppWrapper>
    )
  }
}
属性穿透
import { PureComponent } from 'react'
import styled from 'styled-components'

const CustomInput = styled.input`
  background-color: gray;
`

export default class App extends PureComponent {
  render() {
    return (
      <>
        {/* 
          这里的type属性虽然是设置在CustomInput上
          但是styled-components有属性穿透功能
          所以type属性实际是被设置到了原本的input标签上
        */}
        <CustomInput type="password" />
      </>
    )
  }
}
attrs属性
import { PureComponent } from 'react'
import styled from 'styled-components'

// attrs方法返回依旧是一个函数
// 我们可以在attrs函数中,为对应的组件设置相应的属性
const CustomInput = styled.input.attrs({
  placeholder: 'Klaus'
})`
  background-color: blue;
`

export default class App extends PureComponent {
  render() {
    return (
      <>
        <CustomInput type="password" />
      </>
    )
  }
}
import { PureComponent } from 'react'
import styled from 'styled-components'

// 为组件设置样式的时候,可以使用函数
// 函数有一个参数为props,其值是Object.assign({}, 组件上传入的对象(例如: 本例中的bgColor), attrs函数参数对象)
// 如果在样式模板字符串中,传入的props(除那些实际被作为样式属性值的props属性)其余属性都会作为组件的属性被传入
const CustomInput = styled.input.attrs({
  placeholder: 'Klaus',
  color: 'red'
})`
  background-color: ${ props => props.bgColor || props.color };
`

export default class App extends PureComponent {
  render() {
    return (
      <>
        <CustomInput type="password" bgColor="yellow" />
      </>
    )
  }
}
import { PureComponent } from 'react'
import styled from 'styled-components'

// 如果样式冲突,attrs函数中设置的属性值会覆盖组件上传入的同名属性值
const CustomInput = styled.input.attrs({
  placeholder: 'Klaus',
  color: 'red'
})`
  background-color: ${ props => props.color };
`

export default class App extends PureComponent {
  render() {
    return (
      <>
        <CustomInput type="password" color="yellow" />
      </>
    )
  }
}
使用state中的值设置样式
import { PureComponent } from 'react'
import styled from 'styled-components'

const CustomInput = styled.input.attrs({
  placeholder: 'Klaus',
  color: 'red'
})`
  background-color: ${ props => props['bg-color'] || props.color };
`

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      bgColor: 'yellow'
    }
  }

  render() {
    return (
      <>
        <CustomInput type="password" bg-color={ this.state.bgColor } />
      </>
    )
  }
}
样式继承
import { PureComponent } from 'react'
import styled from 'styled-components'

const CustomButton = styled.button`
  outline: none;
`

// CustomPrimaryButton的样式继承于CustomButton
const CustomPrimaryButton = styled(CustomButton)`
  background-color: green;
  color: #fff;
`

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      bgColor: 'yellow'
    }
  }

  render() {
    return (
      <>
        <CustomButton>CustomButton</CustomButton>
        <CustomPrimaryButton>CustomPrimaryButton</CustomPrimaryButton>
      </>
    )
  }
}
主题样式(全局样式)

App.js

import { PureComponent } from 'react'
import { ThemeProvider } from 'styled-components'

import Cpn from '../Cpn'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      bgColor: 'yellow'
    }
  }

  render() {
    return (
      <ThemeProvider theme={{ bgColor: 'blue', fontSize: '20px', color: 'red' }}>
        {/* theme上设置的样式,会传递给所有使用这个theme的组件 */}
       <Cpn />
      </ThemeProvider>
    )
  }
}

Cpn.js

import { PureComponent } from 'react'

import styled from 'styled-components'

// 主题上设置的样式会被挂载到props.theme这个对象上
const Div = styled.div`
  background-color: ${ props => props.theme.bgColor };
  font-size: ${ props => props.theme.fontSize };
  color: ${ props => props.theme.color };
`

export default class App extends PureComponent {
  render() {
    return (
      <Div>内联样式的使用</Div>
    )
  }
}

class样式设置

原生设置
import { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      isActive: true
    }
  }

  render() {
    return (
      < >
        {/* 注意: 多个样式之间必须使用空格进行分割 */}
        <h2 className="foo bar">title</h2>
        <h2 className={ "foo bar" }>title</h2>
        <h2 className={` foo ${ this.state.isActive ? 'active' : '' } `}>title</h2>
        <h2 className={ [ 'foo',  this.state.isActive ? 'active' : ''].join(' ') }>title</h2>
      </>
    )
  }
}
classnames

我们可以看到使用原生的方式去设置React中的类名是十分繁琐的,尤其是相对于Vue而言,为此,React社区提供了一个名为classnames的库来帮助我们解决这个问题

# 安装
$ yarn add classnames
import { PureComponent } from 'react'

import classNames from 'classnames'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      isActive: true
    }
  }

  render() {
    return (
      < >
       <h2 className={classNames('foo bar baz')}>title</h2>
       <h2 className={classNames('foo', 'bar', 'baz')}>title</h2>
       <h2 className={classNames('foo', this.state.isActive ? 'active' : '')}>title</h2>
       <h2 className={classNames('foo', { active: this.state.isActive })}>title</h2>

       <h2 className={classNames(['foo', 'bar'])}>title</h2>
       <h2 className={classNames(['foo', { active: this.state.isActive }])}>title</h2>
      </>
    )
  }
}
import { PureComponent } from 'react'

import classNames from 'classnames'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      isUndefined: undefined,
      isNull: null,
      isZero: 0,
      isEmptyStr: '',
      isNotZero: 10
    }
  }

  render() {
    const {
      isUndefined,
      isNull,
      isZero,
      isNotZero,
      isEmptyStr
    } = this.state

    return (
      < >
        {/*
          如果对应的属性值不是布尔值,那么对应的属性值会经过类型转换,将其转换为boolean
          如果属性值转换为boolean值后,对应的值为false的时候,不会添加对应的样式名
          所以如果属性值为undefined,null或 0的时候,对应的样式名是不会被添加的
          但是如果属性值转换为boolean值后,对应的值是true,那么对应的样式名会被转换为字符串后,在作为样式名添加
          所以这里isNotZero的值为10,最终会为一个名称为'10'的样式存在于h2元素上
        */}
       <h2 className={classNames('foo', isUndefined, isNull, isZero, isEmptyStr, isNotZero)}>title</h2>

       {/* 其余同理 */}
       <h2 className={ classNames({ active: isNull }) }>title</h2>
       <h2 className={ classNames({ active: isNotZero }) }>title</h2>
      </>
    )
  }
}
import { PureComponent } from 'react'

// import classNames from 'classnames'

export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      isActive: true
    }
  }

  render() {
    return (
      < >
      {/*
        和vue不同的是,存在变量的class不可以和确定值的class共存
        如果同时存在,后面一个className会将之前的className所对应的值给覆盖掉
        即后一个同名属性会覆盖前一个同名属性
      */}
       {/* <h2 className="baz" className={classNames(['foo', { active: this.state.isActive }])}>title</h2> */}
      </>
    )
  }
}