个人实践几种React在线主题切换方案

8,140 阅读4分钟

方案一:更改body的class命名

通过修改body标签的class名称来进行主题的切换,这是最简单最容易理解的方式。

样式代码如下:

<style>
  .light-theme p {
    color: #FFFFFF;
  }
  .light-theme div {
    background-color: #000000;
  }
  .dark-theme div {
    color: #f4f5f5;
  }
  .dark-theme p {
    color: #333333;
  }
</style>

body代码如下:

...
<body class="normal-theme">
  <div>
    <p>这是测试</p>
  </div>
  <div id="root"></div>
</body>
...

React代码如下:

const [ theme, setTheme ] = useState(false)

const changeTheme = () => {
  console.log('切换主题')
  setTheme(!theme)
  document.getElementsByTagName('body')[0].className = theme ? 'dark-theme' : 'light-theme'
}

优点: 容易理解和实现

缺点: 多主题的时候,需要些多个class,代码容易混乱;需要手动编写

方案二:利用less的modifyVars

  1. 通过less的modifyVars方法改变对应的样式变量,因此我们先需要引入less.js,可以通过npm install less的方式或者CND的方式引入less.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/3.11.1/less.min.js"></script>

这样就可以在项目中使用modifyVars方法了。

  1. 创建public文件夹,在里面继续创建一个header.less文件和style.less文件,其中header.less用来写代码中使用到的样式,style.less作为所有的样式入口。

WeChat88cda40a992a6d8f7223bf85c88729db.png

header.less的内容为:

@primary-color: #15eaf0; // 全局主色
@success-color: #52c41a; // 成功色

.header {
  height: 80px;
  background-color: @primary-color;
  color: @success-color;
}

style.less的内容为:

// 这里引入header.less是为了把style.less当作整体入口来使用
// 后续像footer.less这种样式文件都在这个文件里面引入
// @import './footer.less';
@import './header.less';
  1. 在切换入口html文件中,引入style.less
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui" />
  <title>xxxx</title>
</head>
<body>
   <!-- 这里的link要写在body里面,要是通过CDN引入less.js的话,一定要将其放到这个link后面 -->
  <link rel="stylesheet/less" type="text/css" href="../../public/style.less" />
  <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/3.11.1/less.min.js"></script> -->
  <div id="root"></div>
</body>
</html>
  1. 最后在切换主题的事件中调用modifyVars方法
 import '../../public/header.less'
 import less from 'less'

 ...

 const changeTheme = () => {
    less.modifyVars({  // 调用 `less.modifyVars` 方法来改变变量值
      'primary-color': '#Ff4FFF',
    }).then(() => {
      console.log('修改成功');
    })
 }

 ...

注意点:

  1. 样式文件在html中引入的位置最好放在body标签之内,如果通过CDN的方式引入less.js,那么样式文件一定要在script标签之前引入。
  2. 在webpack中配置publicPath: '/public/',配置后webpack 会在静态文件路径前面添加 publicPath 的值,这里可以防止打包后本地资源文件引入的问题。本项目用的是umijs,因此在umirc.ts中进行配置:
export default defineConfig({
    ...
    publicPath: '/public/'
    ...
})

优点: 样式颜色修改灵活

缺点: 无法压缩混淆

方案三:使用styled-components

styled-component提供<ThemeProvider>包装组件以支持主题,<ThemeProvider>通过contextAPI 为其后代组件提供主题.在其渲染树中的所有组件都能够访问主题。具体实现如下:

  1. 首先通过npm install --save styled-components 安装styled-componets,安装成功之后创建styled.ts,在文件中写一个基本样式组件:
import styled from 'styled-components'

export const HeaderDiv = styled.div`
  background: ${props => props.theme.backgroundColor};
`
  1. 使用<ThemeProvider>,将其放在HeaderDiv外层:
import React, { useState } from 'react'
import { SkinOutlined } from '@ant-design/icons'
import { ThemeProvider } from 'styled-components'
import { HeaderDiv } from './styled'

const Header: React.FC = props => {
  const [ theme, setTheme ] = useState({
    backgroundColor: 'green'
  })

  const changeTheme = () => {
    console.log('切换主题')
    setTheme({
      backgroundColor: 'red'
    })
  }

  return (
    <ThemeProvider theme={theme}>
      <HeaderDiv>
        <SkinOutlined onClick={changeTheme} style={{ marginRight: 10 }} />
      </HeaderDiv>
    <ThemeProvider>
  )
}

export default Header

优点: 可以使用ts,用来封装标准组件方便,不存在兼容性问题

缺点: 对antd的组件,需要二次封装

方案四:使用css变量

通过定义css变量的方式来动态修改对应的颜色,或者修改对应的某个主题;

  • 首先是改变单个颜色变量的做法:
  1. 在你的样式文件中定义css变量并且使用,我这里在header.less中使用
:root {
  --color: #333333;
  --background-color: #FFFFFF;
}

.header {
  color: var(--color);
  background-color: var(--background-color);
}
  1. 修改变量的一些方法
const root = document.documentElement
let _style = getComputedStyle(root)

// 获取颜色变量
_style.getPropertyValue('--background-color').trim()
// 设置颜色变量
root.style.setProperty('--background-color','green')
// 删除颜色变量
root.style.removeProperty('--background-color')
  1. 最终在React中的使用
import React from 'react'
import { SkinOutlined } from '@ant-design/icons'
import style from './header.less'

const Header: React.FC = props => {

  const changeTheme = () => {
    console.log('切换主题')
    const root = document.documentElement
    let _style = getComputedStyle(root)
    root.style.setProperty('--background-color','green')
  }

  return (
    <div className={style.header}>
      <SkinOutlined onClick={changeTheme} />
    </div>
  )
}

export default Header
  • 接下来是改变整个主题的用法:
  1. 将你的主题文件创建到public文件夹中的variable.css中(没错,是.css,这里用less的话无法生效)
// variable.css

:root.light{
  --color: #333333;
  --background-color: #FFFFFF;
}

:root.dark {
  --color: #FFFFFF;
  --background-color: #000000;
}
  1. header.less中使用对应变量:
.header {
  color: var(--color);
  background-color: var(--background-color);
}
  1. 在React中写切换事件
import React, { useEffect } from 'react'
import { SkinOutlined } from '@ant-design/icons'
import style from './header.less'

const Header: React.FC = props => {

  useEffect(() => {
    // 这里设置默认的主题样式
    const root = document.documentElement
    root.className = 'light'
  }, [])

  const changeTheme = () => {
    console.log('切换主题')
    const root = document.documentElement
    // 这里跟上面改变单个变量的方式不同,需要直接改变root的className来改变整体主题
    root.className = 'dark'
  }

  return (
    <div className={style.header}>
      <SkinOutlined onClick={changeTheme} style={{ marginRight: 10 }} />
    </div>
  )
}

export default Header
  1. 注意还要将variable.css文件引入到html文件中,并且配置webpack的publicPath为/public/,就如上述方案二中的配置方式一样。
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1,  minimum-scale=1, user-scalable=no, minimal-ui" />
  <link rel="stylesheet" type="text/css" href="../../public/variable.css" />
</head>
<body>
  <div id="root"></div>
</body>
</html>

缺点: 兼容性存在一定问题

image.png

关于兼容性的话,可以试试使用postcss-custom-propertiescss-vars-ponyfill两款插件,具体我还没尝试过,不做过多赘述。。

关于ant-design

ant-design 参考这里

antt-design-pro 参考这里

关于antd这方面它本身支持的在线主题切换方案好像还存在一些小问题,后续成熟了再看看

更多方案:理论(未实践)