丢人啊,工作 10 年了,才知道用这种方式实现 CSS 换肤

React
29
0
0
2024-10-24
  • 一、 React19 中 context 的具体改动
  • 二、 use(context) 基础介绍
  • 三、 简单粗暴样式替换实现换肤
  • 四、 利用 css 变量实现换肤

本文共 3219 字,阅读预计使用 5 分钟

1、改动

与之前的版本相比,在 React19 中,context 有一些细微的变化。主要体现在如下三个方面。

!一、删除 旧版 Context 旧版本的 Context 在 2018 年 10 月(v16.6.0)被废弃。但是为了保证平滑的升级,旧版代码一直沿用到了现在。在 React 19 中,这些代码会正式被删除。旧版本的 Context 仅在使用 contextTypesgetChildContext API 的类组件中可用。因此它的删除对现在的项目应该只会造成很小的影响。

如果你在项目中仍然使用了旧版 Context,你可以参考下面新旧版本的对比写法进行调整升级。

// 之前
import PropTypes from 'prop-types';

class Parent extends React.Component {
  static childContextTypes = {
    foo: PropTypes.string.isRequired,
  };

  getChildContext() {
    return { foo: 'bar' };
  }

  render() {
    return <Child />;
  }
}

class Child extends React.Component {
  static contextTypes = {
    foo: PropTypes.string.isRequired,
  };

  render() {
    return <div>{this.context.foo}</div>;
  }
}
// 之后
const FooContext = React.createContext();

class Parent extends React.Component {
  render() {
    return (
      <FooContext value='bar'>
        <Child />
      </FooContext>
    );
  }
}

class Child extends React.Component {
  static contextType = FooContext;

  render() {
    return <div>{this.context}</div>;
  }
}

二、简化 Provider 的使用

const Context = createContext({})

在以前的使用中,我们需要使用 Context.Provider 来包裹子组件。

<Context.Provider value={value}>
  {props.children}
</Context.Provider>

在 React19 中,我们可以直接使用 Context 来代替 Provider,从而让代表变得更加简洁。

<Context value={value}>
  {props.children}
</Context>

三、可以使用 use 获取 context

以前的版本中,在组件内部我们使用 useContext 来获取 context 中的状态。

// before
import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...

在 React19 中,我们可以直接使用 use 来获取

// after
import { use } from 'react';

function MyComponent() {
  const theme = use(ThemeContext);
  // ...

2、重学一次 context

在 React 中,props 能够帮助我们将数据层层往下传递。但是,当数据传递太多层之后,不仅代码上比较繁琐,理解上也容易混乱不清。因此,我们需要一种能够跨越组件层级让直达子组件的数据传递方式,这就是 context.

context 表示组件实例在运行期间能够直接读取的状态和内容。他记录了内存中的活跃数据。我们可以将这些数据使用 useState 来定义。那么,context 中的数据更改,就会驱动所有使用到该数据的 UI 发生变化。

context 的学习主要分为如下三个部分 一、 如何创建 context 二、 顶层组件中如何传递数据 三、 子组件中如何获取数据

一、如何创建 context

我们可以使用 createContext 来创建 context.

const SomeContext = createContext(defaultValue)

defaultValue 表示默认值。他可以作为数据的兜底结果。当你无法从 value 中读取具体的值时,则会使用 defaultValue 中的值。在代码运行过程中,默认值始终保持不变。如果没有默认值,我们至少需要传入一个 null

createContext 执行之后的返回值,就是我们需要的 context

二、如何传递 context

返回的 context 通常是一系列组件的顶层父组件。因此,在使用时,我们通常会首先定义该顶层父组件。

function Provider(props) {
  const value = {...}
  return (
    <SomeContext value={value}>
      {props.children}
    </SomeContext>
  )
}
export default Provider

在该顶层父组件中,我们使用刚才创建的 context 作为父级标签,把子组件包起来。并作为渲染内容返回。

<SomeContext value={value}>
  {props.children}
</SomeContext>

此处的 value 表示我们在上下文中定义好的值。我们可以自己随意定义你想要传递给子组件的所有数据/方法。

i这些数据/方法通常被多个不同的子组件共同使用。否则我们没必要将数据/方法存储在 context 中。
import { createContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  // ...
  return (
    <ThemeContext value={theme}>
      <Page />
    </ThemeContext>
  );
}

此时,案例中 Page 组件的所有后代子组件,都可以直接获取 context 中的值,不管层级有多深。

value 可以是任何类型,你可以根据自己的项目需要设计合适的数据类型。

三、如何获取 context 中的值

在任意被包裹的子组件中,我们可以使用 use 来获取 context 中的值。

function Button() {
  // ✅ Recommended way
  const theme = use(ThemeContext);
  return <button className={theme} />;
}

获取方法非常简单,接下来,我们使用具体的实践案例来分享 context 的使用。

i需要注意,多个 Context 可以嵌套使用,只是在实践中,这种场景不算多见。

3、换肤方案一

先来看一眼我们实现案例的演示效果。我们实现了部分 UI 的皮肤切换,并且记录了切换次数。

在文件中,为了验证 context 的作用,我们将组件结构设计为如下

+ App
  - index.jsx
  - Card.jsx
  - Setting.jsx
  - context.jsx

context.jsx 中,我们会创建好 context,并组织好要传递给子组件的 value,完整代码如下

import {createContext, useState} from 'react'

export const Context = createContext({theme: 'dark'})

export default function Provider(props) {
  const [theme, setTheme] = useState('light')
  const [counter, setCounter] = useState(0)

  const value = {
    theme,
    setTheme,
    counter,
    setCounter
  }

  return (
    <Context value={value}>
      {props.children}
    </Context>
  )
}
i需要稍微注意观察的是,我们这里使用 useState 创建了数据,并将操作数据的方法一并集成在了 value 中,这样做的目的是为了确保数据的变动能触发 UI 的更新

index.jsx 中,我们使用刚才创建好的 context 组件将所有子组件包裹起来。

import Card from './Card'
import Setting from './Setting'
import Provider from './context'
import './index.css'

function Index() {
  return (
    <Provider>
      <div id='tips'>切换主题,并记录切换次数</div>
      <Card />
      <Card />
      <Setting />
    </Provider>
  )
}

export default Index

然后,分别在子组件中,使用 use 获取到组件需要的状态与方法即可。

// Card.jsx
import {use} from 'react'
import {Context} from './context'

export default function Card() {
  const {theme} = use(Context)

  const __cls = `_06_card ${theme}`
  return (
    <>
      <div className={__cls}>
        <div className="title">Canary</div>
        <p>The use API is currently only available in React’s Canary and experimental channels. Learn more about React’s release channels here.</p>
      </div>
    </>
  )
}
// Setting.jsx
import {use} from 'react'
import {Context} from './context'

export default function Card() {
  const {theme, counter, setTheme, setCounter} = use(Context)

  const __switch = () => {
    setTheme(theme == 'light' ? 'dark' : 'light')
    setCounter(counter + 1)
  }
  return (
    <div className='_06_setting'>
      <button onClick={__switch}>点击切换到{theme === 'light' ? '暗黑' : '明亮'}主题</button>
      <div id='tips'>已切换:{counter || 0} 次</div>
    </div>
  )
}

此时,我们用于切换皮肤的方法,是将分别代码不同皮肤的 className 写入到每一个需要使用的元素中。虽然实现了功能,但是在真实项目中,必定会造成大量的工作量。因此这并不是一种好的思路。

._06_card {
  margin: 20px 0;
  padding: 20px;
  border-radius: 10px;
  transition: all 0.2s;
}

._06_card.light {
  background-color: rgba(0, 0, 0, 0.02);
  border: 1px solid rgba(0, 0, 0, 0.1);
  color: rgba(0, 0, 0, 0.88);
}

._06_card.dark {
  background-color: rgba(0, 0, 0, 0.8);
  border: 1px solid rgba(0, 0, 0, 1);
  color: rgba(255, 255, 255, 0.8);
}

._06_card .title {
  font-weight: bold;
  font-size: 20px;
}

._06_setting {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
!除了这种方式,包括暴力重写并覆盖所有样式的方式来切换皮肤,都属于工作量很大的方案。这仅仅适合在项目设计之初没有考虑换肤功能的项目。并不推荐

4、换肤方案二

我们可以换一种高级一点的用法来完成皮肤的切换功能。那就是利用 CSS 变量

CSS 变量又称之为自定义属性。他已经在主流浏览器中被普遍支持,我们可以在许多项目中使用该特性。我们熟知的 antd 中就大量运用了自定义属性。

声明一个自定义属性,需要以 -- 开头,属性值可以是任何有效的 CSS 值。

element {
  --main-bg-color: brown;
}
i注意理解这句话:自定义属性和其他属性一样,是写在规则集之内的。 因此,它的改变,也能触发 transition 动画的执行

并且要注意的是,规则集所指定的选择器定义了自定义属性的可见作用域。通常的最佳实践是定义在根伪类 :root 下,这样就可以在 HTML 文档的任何地方访问到它了

:root {
  --main-bg-color: brown;
}

当然,我也应该根据实践运用灵活选择作用域。

在本案例中,我们把不同的主题定义在如下属性中。

[data-theme=dark] {
  --font-color: rgba(255, 255, 255, 0.8);
  --background-color:  rgba(0, 0, 0, 0.8);
  --border-color: rgba(0, 0, 0, 0.1);
}
[data-theme=light] {
  --font-color: rgba(0, 0, 0, 0.88);
  --background-color:  rgba(0, 0, 0, 0.02);;
  --border-color: rgba(0, 0, 0, 0.1);
}

然后在页面元素样式的运用中,我们使用 var() 获取自定义属性对应的值。而不直接使用值。

._06_card {
  background-color: var(--background-color);
  border: 1px solid var(--border-color);
  color: var(--font-color);
}

这样,我们只需要通过修改父级元素的 data-theme 的值,就可以简单做到主题切换。

const __switch = () => {
  let _theme = theme == 'light' ? 'dark' : 'light'
  document.documentElement.setAttribute('data-theme', _theme)
  setTheme(_theme)
  setCounter(counter + 1)
}

这样,一个简单易用,可维护性强的主题方案就搞定了。