前端暗色模式/换肤实现方案

多套CSS样式实现


通过编写多套CSS样式代码来实现,切换功能就通过动态修改link标签的href或者动态添加删除link标签。这方案就不贴代码了......

优点:简单

缺点:维护成本高

CSS变量实现


在CSS里面定义颜色,字体,宽高等变量,通过使用这些变量来实现换肤功能,核心就是切换属性/类名来切换颜色值。

项目源码

下面贴出代码展示:

html结构如下:main标签里面是内容,上面的div是用来切换皮肤的

<body>
  <div>
    <!-- 这里是切换的按钮 -->
    <img src="../icons/sum.svg" id="theme">
  </div>
  <main class="content">
    <h1>CSS variable</h1>
    <p>abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz</p>
    <p>abcdefghijklmnopqrstuvwxyz</p>
    <p>abcdefghijklmnopqrstuvwxyz</p>
    <p>abcdefghijklmnopqrstuvwxyz</p>
    <button>button</button>
  </main>
</body>
复制代码

css代码如下:当前是使用给Body添加类名的方式来实现切换,定义变量

:root {
  --bg-color-0: #fff;
  --bg-color-1: #fff;
  --text-color: #333;
  --grey-1: #1c1f23;
}

:root .dark {
  --bg-color-0: #16161a;
  --bg-color-1: #35363c;
  --text-color: #fff;
  --grey-1: #f9f9f9;
}
复制代码

使用定义的变量:通过var(param)使用

body {
  margin: 0;
  padding: 0;
  background-color: var(--bg-color-0);
  transition: all 0.3s;
}

.content {
  padding: 20px;
  background-color: var(--bg-color-1);
  color: var(--text-color);
}

.content button {
  width: 100px;
  height: 30px;
  background-color: var(--bg-color-1);
  color: var(--text-color);
  border: 1px solid var(--grey-1);
  outline: none;
}
复制代码

通过js来对切换按钮进行一个给Body增加/删除类名的操作。

 const themebtn = document.getElementById('theme')
  themebtn.addEventListener('click', () => {
    const body = document.body
    // 判断当前是否是黑夜模式,从而切换模式
    if (Array.from(body.classList).indexOf('dark') !== -1) {
      body.classList.remove('dark')
      themebtn.src = '../icons/sum.svg'
    } else {
      body.classList.add('dark')
      themebtn.src = '../icons/moon.svg'
    }
  })
复制代码

效果图:

扫描二维码关注公众号,回复: 13717080 查看本文章

1647849164(1).jpg

1647849516(1).jpg

这种方式实现的一个优缺点

优点:简单易懂

缺点:存在兼容性问题,IE不支持,解决方法也有就是使用css-vars-ponyfill

ponyfill that provides client-side support for CSS custom properties (aka “CSS variables”) in legacy and modern browsers.

在旧版和现代浏览器中为 CSS 自定义属性(又称“CSS 变量”)提供客户端支持的 ponyfill。

css变量兼容性-MDN

Sass/Less变量实现


实现方法跟用CSS变量差不多,也是通过给根元素添加属性/类名来达成切换的效果

项目源码

简单的搭建一个React + SCSS的项目来实现,文件目录如下,主要看_variable.scss_handle.scss两个文件

scss-variable
├── yarn.lock
├── package.json
├── public
│   └── index.html
├── src
│   ├── index.scss
│   ├── _variable.scss
│   ├── _handle.scss
│   ├── index.js
│   └── app.jsx
└── webpack.config.js
复制代码

首先看app.jsx文件,里面编写了页面结构以及切换的逻辑

import { useCallback, useState } from 'react'
import './index.scss'
const App = () => {
  // 定义切换按钮的文案,并且给html附上默认主题类型
  const [themeText, setThemeText] = useState(() => {
    document.documentElement.setAttribute('data-theme', 'light')
    return 'light'
  })
  // 切换逻辑
  const toggleTheme = useCallback(() => {
    if (themeText === 'light') {
      document.documentElement.setAttribute('data-theme', 'dark')
      setThemeText('dark')
    } else {
      document.documentElement.setAttribute('data-theme', 'light')
      setThemeText('light')
    }
  })
  return (
    <div className="content">
      <button onClick={toggleTheme}>{themeText}</button>
      <h1>SCSS variable</h1>
      <p>abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz</p>
      <p>abcdefghijklmnopqrstuvwxyz</p>
      <p>abcdefghijklmnopqrstuvwxyz</p>
      <p>abcdefghijklmnopqrstuvwxyz</p>
      <button>button</button>
    </div>
  )
}

export default App
复制代码

然后就是_variable.scss_handle.scss

_variable.scss:定义每个主题的变量

$themes: (
    light: (
        bg-color-0: #fff,
        bg-color-1: #fff,
        text-color: #333,
        grey-1: #1c1f23
    ),
    dark: (
        bg-color-0: #16161a,
        bg-color-1: #35363c,
        text-color: #fff,
        grey-1: #f9f9f9
    )
);
复制代码

_handle.scss:定义mixin来将每个主题的对应颜色字体宽高等等绑定好,只要根据某个mixin来传入对应的key值就好

@mixin themeify {

  // 遍历_variable.scss定义的主题
  @each $theme-name,
  $theme-map in $themes {
    // 将每个主题提升为全局变量
    $theme-map: $theme-map !global;

    // 绑定某个主题下的样式内容
    [data-theme="#{$theme-name}"] & {
      @content
    }
  }
}

// 定义一个通过key获取主题变量的值函数
@function themed($key) {
  @return map-get($theme-map, $key)
}

// 下面这些mixin绑定在那个主题就那种颜色
@mixin bgColor($color) {
  @include themeify {
    background-color: themed($color);
  }
}

@mixin textColor($color) {
  @include themeify {
    color: themed($color);
  }
}

@mixin borderColor($color) {
  @include themeify {
    border-color: themed($color);
  }
}
复制代码

这里我是已经将_variable.scss_handle.scss两个文件在webpack里面配置了全局引入了,所以能在index.scss里面使用 index.scss:使用mixin来绑定

body {
  margin: 0;
  padding: 0;
  transition: all 0.3s;
  @include bgColor('bg-color-0');
}

.content {
  padding: 20px;
  @include bgColor('bg-color-1');
  @include textColor('text-color');


  button {
    width: 100px;
    height: 30px;
    border: 1px solid;
    outline: none;
    @include bgColor('bg-color-0');
    @include borderColor('grey-1');
    @include textColor('text-color');

  }
}
复制代码

效果跟CSS变量一致

优点:因为使用webpack进行打包,所以兼容性问题可以解决

缺点:代码可读性不如CSS强

动态Style实现


这种核心原理就是通过给style标签里面的内容进行更改来实现,这种方案是参考了ElementUI的换肤来实现。ElementUI换肤DEMO的仓库地址

项目源码

参照着实现:原理就是读取CSS文件里面的样式,通过把颜色替换成关键字,再选择一种颜色将关键字再次替换,将关键字再将样式通过style标签插入DOM里面。

目录文件:formula.json编写颜色,color.js编写颜色转换的逻辑。

style-override
├── yarn.lock
├── package.json
├── public
│   └── index.html
├── src
│   ├── color.js
│   ├── formula.json
│   ├── index.js
│   └── app.jsx
└── webpack.config.js
复制代码

formula.json文件定义颜色与关键字之间的对应关系,这里是用到css-color-function的插件所以写成这样。

{
  "bgColor0": "color(primary contrast(50%))",
  "bgColor1": "color(primary contrast(50%))",
  "textColor": "color(primary tint(50%))",
  "grey1": "color(primary tint(50%))"
}
复制代码

color.js文件:通过css-color-function将颜色替换到json里面的关键字,并且返回colors对象。

import color from 'css-color-function'
import formula from './formula.json'
const generateColors = (primary) => {
  let colors = {}
  // 遍历formula里面的value,将关键字通过css-color-function换成rgb颜色
  Object.keys(formula).forEach((key) => {
    const value = formula[key].replace(/primary/g, primary)
    // css-color-function提供的convert转换颜色
    colors[key] = color.convert(value)
  })

  return colors
}
export default generateColors
复制代码

app.jsx文件拆分讲解,页面结构如下:

<div className="content">
  <input type="color" onChange={toggleTheme} />
  <h1>CSS variable</h1>
  <p>abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz</p>
  <p>abcdefghijklmnopqrstuvwxyz</p>
  <p>abcdefghijklmnopqrstuvwxyz</p>
  <p>abcdefghijklmnopqrstuvwxyz</p>
  <button>button</button>
</div>
复制代码

等组件一渲染就通过fetch发请求来读取CSS文件,然后将颜色转换成关键字,最后生成一个CSS模板。

const [styleTemplate, setStyleTemplate] = useState('')
useEffect(() => {
const colorMap = {
  '#fff': 'bgColor0',
  '#fff': 'bgColor1',
  '#333': 'textColor',
  '#1c1f23': 'grey1'
}
fetch('https://assets.asaki-m.com/template.css')
  .then((r) => r.text())
  .then((data) => {
    let dataText = data
    // 遍历颜色映射表将颜色转换成关键字
    Object.keys(colorMap).forEach((key) => {
      const value = colorMap[key]
      dataText = dataText.replace(new RegExp(key, 'ig'), value)
    })
    // 转换完后设置到变量保存模板
    setStyleTemplate(dataText)
  })
}, [])
复制代码

颜色选择器选择一个颜色后,根据css模板来修改或生成style标签。

const originalStylesheetCount = document.styleSheets.length
const toggleTheme = useCallback((evt) => {
    let cssText = styleTemplate
    // 通过input color选择完颜色后,生成关键字与颜色对应的映射
    let colors = generateColors(evt.target.value)
    // 遍历映射表重新将关键字替换成rgb颜色
    Object.keys(colors).forEach((key) => {
      cssText = cssText.replace(
        new RegExp('(:|\\s+)' + key, 'g'),
        '$1' + colors[key]
      )
    })
    // 判断是否已经存在了style标签,不存在就创建一个,存在就直接覆盖里面的内容
    if (originalStylesheetCount === document.styleSheets.length) {
      const style = document.createElement('style')
      style.innerText = cssText
      document.head.appendChild(style)
    } else {
      document.head.lastChild.innerText = cssText
    }
})
复制代码

优点:兼容性问题解决

缺点:上手难度高,维护成本高

效果图:

1647863592(1).jpg

1647863615(1).jpg

1647863633(1).jpg

总结


总结下来其实就是给根节点添加自定义主题属性/类名,多写几套主题样式,动态生成/修改style。每一种都有自身的缺点,具体使用根据自身条件以及场景使用即可。

上面实现的几种方案都没有做持久化,想要做持久化需要将对应的主题保存到本地缓存里面,然后每次渲染读取缓存的主题,在做一次渲染,并且切换的时候记得修改缓存的主题即可。

当然如果有更好的方案也欢迎指出。

猜你喜欢

转载自juejin.im/post/7077521598143528968