彻底理解React中CSS写法和过渡动画

301 阅读10分钟

写CSS的方式

整个前端已经是组件化的天下,而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在需要一种合适的CSS解决方案

  • 事实上CSS一直是React的痛点,React官方并没有给出在React中统一的样式风格

  • 因此从普通的CSS,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库

  • 大家一致在寻找最好的或者说最适合自己的CSS方案

内联样式

内联样式是官方推荐的一种css样式的写法,style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串,并且可以引用state中的状态来设置相关的样式

  • 优点:
    • 样式之间不会有冲突

    • 可以动态获取当前state中的状态

  • 缺点:
    • 写法上都需要使用驼峰标识

    • 某些样式没有提示

    • 大量的样式,代码混乱

    • 某些样式无法编写(比如伪类/伪元素)

CSS文件

  • 普通的css通常会编写到一个单独的文件再进行引入

  • 组件化开发中总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响

  • 但是普通的css都属于全局的css,样式之间会相互影响,这种编写方式最大的问题是样式之间会相互层叠掉

  • 配置less使用: 这里要配置less环境,ant-design文档有相关配置

    • 需要先安装craco这个工具:npm install @craco/craco

    • package.jsonscriptsreact-script改为craco

    • 创建craco.config.js

    • 引入craco-less 来帮助加载 less 样式和修改变量

    • craco.config.js写配置

      const CracoLessPlugin = require("craco-less");
      
      module.exports = {
        plugins: [
          {
            plugin: CracoLessPlugin,
            options: {
              lessLoaderOptions: {
                lessOptions: {
                  modifyVars: { "@primary-color": "#1DA57A" },
                  javascriptEnabled: true,
                },
              },
            },
          },
        ],
      };
      

css modules

css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的需要配置webpack.config.js中的modules: true

  • React的脚手架已经内置了css modules的配置,.css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等,就可以引用并且进行使用了

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

  • 缺点:

    • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的

    • 所有的className都必须使用{style.className} 的形式来编写

    • 不方便动态来修改某些样式,依然需要使用内联样式的方式

CSS in JS

官方文档也有提到过CSS in JS这种方案,CSS-in-JS是指一种模式,其中 CSSJavaScript 生成而不是在外部文件中定义

  • 此功能并不是 React 的一部分,而是由第三方库提供,React 对样式如何定义并没有明确态度

  • CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态,所以React又被人称之为 All in JS

  • 这种开发的方式也受到了很多的批评: Stop using CSS in JavaScript for web development

  • hackernoon.com/stop-using-…

  • 虽然有批评声音,但不可否认这种方式很强大方便,目前可以说CSS-in-JSReact编写CSS最为受欢迎的一种解决方案

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

styled-components

目前比较流行的CSS-in-JS的库有哪些呢? styled-components、emotion、glamorous,目前可以说styled-components依然是社区最流行的CSS-in-JS

  • 安装styled-componentsnpm install styled-components

  • styled component中,是通过下面方法解析模块字符串,最终生成想要的样式

    • ES6中增加了模板字符串的语法,模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals

    • 调用函数的时候可以使用(),也可以使用``,使用``并插入其他变量时,模板字符串会被拆分了,第一个元素是数组,是被模块字符串拆分的字符串组合,后面的元素是一个个模块字符串传入的内容

      function foo(...args) {
        console.log(args);
      }
      
      foo("hello world"); // ['hello world']
      foo`hello world`; // ['hello world']
      const name = "template";
      foo`hello ${name}`; // 会拆分 [['hello ', ''], 'template']
      
基本使用

styled-components的本质是通过函数的调用,最终创建出一个组件,这个组件会被自动添加上一个不重复的classstyled-components会给该class添加相关的样式

image.png

  • 它支持类似于CSS预处理器一样的样式嵌套

  • 支持直接子代选择器或后代选择器,并且直接编写样式

  • 可以通过&符号获取当前元素

  • 直接伪类选择器、伪元素等

props、attrs属性
  • props可以被传递给styled组件<AppWrapper link_color="green"></AppWrapper>

  • 使用传入的值color: ${props=> props.link_color};

  • attrs可以修改或设置默认值export const AppWrapper = styled.div.attrs((props)=> ({ link_color: props.link_color || 'red' }))``

高级特性
  • 支持样式继承export const ButtonWrapper = styled(AppWrapper)``

  • 可以设置主题image.png

classnames

  • ReactJSX给了开发者足够多的灵活性,可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class image.png

  • 也可以借助第三方库实现动态添加class:先安装npm install classnames,再引入import classNames from 'classnames' image.png

练习代码

// style/comCss.module.css
.btn {
  background: rgb(232, 203, 129);
  padding: 8px 18px;
  font-size: 24px;
  color: cadetblue;
  border: 2px solid salmon;
  border-radius: 8px;
  /* 这里要配置less环境,ant-design文档有相关配置,
    1. 需要先安装craco这个工具
    2. package.json中scripts的react-script改为craco
    3. 创建craco.config.js
    4. 引入craco-less 来帮助加载 less 样式和修改变量
    5. 在craco.config.js写配置
   */
  .link {
    color: cornflowerblue;
    text-decoration: underline;
    &:hover {
      color: blue;
    }
  }
}



// style/comCss.js
import styled from "styled-components";

// 基础用法
/* export const AppWrapper = styled.div`
  .btn {
    padding: 8px;
    background: bisque;
    border: 2px solid gainsboro;
    border-radius: 8px;
    .link {
      color: red;
      &:hover {
        color: pink;
      }
    }
  }
`; */
const Button = styled.button` // 这里渲染成button,styled.xxxx 这里就会渲染成xxxx
  padding: 8px;
  background: bisque;
  border: 2px solid gainsboro;
  border-radius: 8px;
`
export const ButtonWrapper = styled(Button)` // 继承
  color: olive;
  font-size: large;
`

export const AppWrapper = styled.div.attrs((props)=> ({
  // 使用驼峰命名会报错
  link_color: props.link_color || 'red'
}))`
  background: yellowgreen;
  .btn {
    padding: 8px;
    background: bisque;
    border: 2px solid gainsboro;
    border-radius: 8px;
    .link {
      color: ${props=> props.link_color};
      &:hover {
        color: pink;
      }
    }
  }
  .active {
    color: powderblue;
  }
`



// ComCss.jsx
import React, { PureComponent } from "react";
import comCss from "../style/comCss.module.css";
import { AppWrapper, ButtonWrapper } from "../style/comCss";
import classNames from "classnames";

export class ComCss extends PureComponent {
  constructor() {
    super();
    this.state = {
      isActive: true,
    };
  }
  render() {
    return (
      <div>
        <h3>ComCss-react中写css的方式</h3>
        {/* 第一种直接写内联样式 */}
        <span style={{ color: "red" }}>内联样式</span>
        {/* 第二种使用className,再引入css文件 */}
        {/* 第三种使用模块化和配置less具体操作看style/comCss.module.css */}
        <button className={comCss.btn}>
          按钮
          <span className={comCss.link}>啦啦啦啦</span>
        </button>
        {/* 
          第四种 css in js方式 
          先install styled-components
          link_color不传会使用默认值
        */}
        <AppWrapper link_color="green">
          <button className="btn">
            按钮
            <span className="link">啦啦啦啦</span>
          </button>
        </AppWrapper>
        <ButtonWrapper>继承样式按钮</ButtonWrapper>
        {/* classNames库的使用,先install classnames,在引入import classNames from 'classnames' */}
        <AppWrapper>
          <button
            className={classNames(["btn", { active: this.state.isActive }])}
          >
            classnames库的使用示例按钮
          </button>
        </AppWrapper>
      </div>
    );
  }
}
export default ComCss;

react-transition-group

在开发中想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验;可以通过原生的CSS来实现这些过渡动画,但是React社区提供了react-transition-group用来完成过渡动画

  • React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transitiongroup

  • react-transition-group本身非常小,不会为应用程序增加过多的负担

  • 安装:npm install react-transition-group --save

  • react-transition-group主要学习下面要常用的三个组件

CSSTransition

  • 执行过程中有三个状态appear、enter、exit,三种状态需要定义对应的CSS样式:

    • 开始状态:对应的类是-appear、-enter、exit

    • 执行动画:对应的类是-appear-active、-enter-active、-exit-active

    • 执行结束:对应的类是-appear-done、-enter-done、-exit-done

  • CSSTransition常见对应的属性

    • in:触发进入或者退出状态

      如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉

      intrue时,触发进入状态,会添加-enter、-enter-acitveclass开始执行动画

      当动画执行结束后,会移除两个class,并且添加-enter-doneclass

      infalse时,触发退出状态,会添加-exit、-exit-activeclass开始执行动画

      当动画执行结束后,会移除两个class,并且添加-enter-doneclass

    • classNames:决定了在编写css时对应的class名称:比如card-enter、card-enter-active、card-enter-done

    • timeout:过渡动画的时间

    • appear:是否在初次进入添加动画(需要和in同时为true

    • unmountOnExit:退出后卸载组件

    • 其他属性可以参考官网学习:reactcommunity.org/react-trans…

  • CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作

    • onEnter:在进入动画之前被触发

    • onEntering:在应用进入动画时被触发

    • onEntered:在应用进入动画结束后被触发

SwitchTransition

SwitchTransition可以完成两个组件之间切换的炫酷动画,比如有一个按钮需要在onoff之间切换,希望看到on先从左侧退出,off再从右侧进入

  • SwitchTransition中主要有一个属性:mode='in-out':表示新组件先进入,旧组件再移出,mode='out-in':表示旧组件先移出,新组件再进入

  • 如何使用SwitchTransition呢?

    • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件

    • SwitchTransition里面的CSSTransitionTransition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性

    • 代码参考下面的练习代码

TransitionGroup

当有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画 image.png

练习代码

import React, { PureComponent, createRef } from 'react'
import { CSSTransition, SwitchTransition, TransitionGroup } from 'react-transition-group'
import { TransitionWrapper,  } from '../style/comTransite'

export class ComTransite extends PureComponent {
  constructor() {
    super()
    this.state = {
      isCssTrs: false,
      groupList: [
        '哈哈哈哈',
        '嘻嘻嘻嘻',
        '哒哒哒哒',
        '啦啦啦啦',
      ]
    }
    this.nodeRef = createRef()
  }
  changIsCssTrs() {
    this.setState({
      isCssTrs: !this.state.isCssTrs
    })
  }
  delGroupItem(index) {
    const list = [...this.state.groupList]
    list.splice(index, 1)
    this.setState({
      groupList: list
    })
  }
  addGroupItm() {
    let list = [...this.state.groupList]
    list.push('嘿嘿嘿嘿')
    this.setState({
      groupList: list
    })
    this.groupRef = createRef()
  }
  render() {
    return (
      <div>
        <h3>ComTransite-过渡动画</h3>
        {/* 先install react-transition-group */}

        <button onClick={e=> this.changIsCssTrs()}>执行动画</button>

        <TransitionWrapper>
          {/* appear
              默认情况下,子组件在首次安装时不会执行 Enter 转换,无论 的值如何in。如果您想要此行为,请将appear和 都设置in为true。
              unmountOnExit
              默认情况下,子组件在达到该'exited'状态后保持安装状态。unmountOnExit如果您希望在组件退出后卸载该组件,请进行设置。
          */}
          {/* CSSTransition 显示隐藏时可以增加组件的进入和退出动画 */}
          <h3>CSSTransition组件显示隐藏</h3>
          <CSSTransition 
            appear 
            unmountOnExit={true} 
            in={this.state.isCssTrs} 
            nodeRef={this.nodeRef} 
            classNames='cssTrs' 
            timeout={2000}
            onEnter={e=>console.log('进入动画开始了')}
            onEntering={e=>console.log('进入动画执行中')}
            onEntered={e=>console.log('进入动画执行结束')}
            onExit={e=>console.log('结束动画开始了')}
            onExiting={e=>console.log('结束动画执行中')}
            onExited={e=>console.log('结束动画执行结束')}
          >
            <div ref={this.nodeRef}> CSSTransition动画 </div>
          </CSSTransition>
        </TransitionWrapper>

        <TransitionWrapper>
          {/* 可以完成两个组件之间切换的炫酷动画,当您想要控制状态转换之间的渲染时可以使用它 */}
          <h3>SwitchTransition组件之间切换</h3>
          <SwitchTransition mode='out-in'>
            {/* in时判断显示隐藏不能设置,这里设置key根据key判断执行动画 */}
            <CSSTransition
              key={this.state.isCssTrs ? "exit": "login"}
              classNames="login"
              timeout={1000}
            >
              <button onClick={e => this.setState({ isCssTrs: !this.state.isCssTrs })}>
                { this.state.isCssTrs ? "退出": "登录" }
              </button>
            </CSSTransition>
          </SwitchTransition>
        </TransitionWrapper>

        <TransitionWrapper>
          <h3>TransitionGroup管理一组组件随时间的安装和卸载</h3>
          {/* 默认渲染div,可以通过component修改 */}
          <TransitionGroup component="div">
            {
              this.state.groupList.map((m,mi)=> {
                // 这里注意新增的元素和已有的绑定的key值是一致时,页面不会渲染也不会报错只会展示一个,若想重新可以把mi换成m
                return <CSSTransition
                  key={mi}
                  nodeRef={this.groupRef}
                  classNames="group"
                  timeout={1000}
                >
                  <p ref={this.groupRef}>
                    {m}
                    <button onClick={e=>this.delGroupItem(mi)}>删除</button>
                  </p>
                </CSSTransition>
              })
            }
          </TransitionGroup>
        </TransitionWrapper>
        <button onClick={e=>this.addGroupItm()}>addGroupItem</button>
      </div>
    )
  }
}

export default ComTransite


// comTransite.js
import { styled } from "styled-components";

export const TransitionWrapper = styled.div`
  .cssTrs-enter {
    opacity: 0;
  }
  .cssTrs-enter-active {
    opacity: 1;
    transition: opacity 2s ease;
  }
  .cssTrs-exit {
    opacity: 1;
  }
  .cssTrs-exit-active {
    opacity: 0;
    transition: opacity 2s ease;
  }

  .login-enter {
    opacity: 0;
    transform: translateX(-200px);
  }
  .login-enter-active {
    opacity: 1;
    transform: translateX(0px);
    transition: all 1s ease;
  }
  .login-exit {
    opacity: 1;
    transform: translateX(0px);
  }
  .login-exit-active {
    opacity: 0;
    transform: translateX(200px);
    transition: all 1s ease;
  }

  .group-enter {
    transform: translateX(150px);
    opacity: 0;
  }

  .group-enter-active {
    transform: translateX(0);
    opacity: 1;
    transition: all 1s ease;
  }

  .group-exit {
    transform: translateX(0);
    opacity: 1;
  }

  .group-exit-active {
    transform: translateX(150px);
    opacity: 0;
    transition: all 1s ease;
  }
`