IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 前端夜间模式(换肤)方案与实践 -> 正文阅读

[JavaScript知识库]前端夜间模式(换肤)方案与实践

支持暗色/深色模式,以及“一键换肤” 已经几乎成为了各家系统、应用以及 Web 站点的标配功能。那实现这样的功能的方案都有哪些呢?

首先对概念进行统一:

  • 颜色字面量:指样式文件中确定的颜色值,如?white,?#FFF,?rgb(23,45,0)?等;
  • 颜色变量:指与颜色相关的 CSS 变量,如?--color-primary;

1 多套 CSS 样式实现

1.1 原理

实现多套 CSS 样式,根据用户切换操作,通过动态修改 link 标签的 href 或动态添加删除 link 标签来加载不同的模式的样式,主要解决了多个模式被编译到一个文件中导致单个文件过大。

1.2 具体实现

将 css 部分直接拆分成?ligth.css?和?dark.css?两个文件,通过?setTheme 动态切换模式:

function setTheme(theme = 'ligth') {
  let link = document.querySelector('#theme-link')
  let href = "/theme/" + theme + ".css"
  
  if (!link) {
    let head = document.querySelector('head');
    link = document.createElement('link');
    link.id = '#theme-link';
    link.rel = 'stylesheet';
    link.href = href;
    head.appendChild(link);
  } else {
    link.href = href
  }
}

1.3 优缺点

优点是简单,缺点是维护成本高,具体表现在以下几个方面:

  • 需要重复编写和单独修改多份样式文件;
  • 没有单独提取出可变的样式部分;
  • 需要提前知道打包后的文件路径,否则可能导致模式样式引入错误;

2 CSS 变量实现

2.1 原理

通过?body.style.setProperty(key, value)?动态修改?body?上的 CSS 变量(颜色,字体,宽高等),使得页面上的其他部分可以应用最新的 CSS 变量对应的样式。

2.2 具体实现

在?theme.css?中定义全局的 CSS 变量,其次是在?themeUtil.js?中获取当前对应样式值,以及设置?body?上的 CSS 变量值,有两种具体实现,方式一:

/** theme.css?*/
:root {
  --bg: initial; // 背景色
  --color: initial; // 字体色
  --boder-color: initial; // 边框色
}

const darkTheme = 'rgb(49, 51, 51)'
const lightTheme = '#fff'
const lightBorderTheme = '#d6d6d6'

/** 获取对应的模式的色值 */
export const getThemeMap = (isLight) => {
  const bg = isLight ? lightTheme : darkTheme;
  const color = isLight ? darkTheme : lightTheme;
  const borderColor = isLight ? lightBorderTheme : lightTheme;
  return {
    bg,
    color,
    'boder-color': borderColor,
  }
}

/** 设置模式的色值 */
export const setTheme = (isLight = true) => {
  const themeMap = getThemeMap(isLight)
  const body = document.body;
  Object.keys(themeMap).forEach(key => {
    body.style.setProperty(`--${key}`, themeMap[key])
  })
}

方式二:

/** theme.css?*/
/* 默认值:light */
:root {
  --bg: #fff;
  --color: rgb(51, 50, 50);
  --img-bg: #fff;
  --boder-color: #d6d6d6;
}

/* 暗黑:dark */
[data-theme='dark'] {
  --bg: rgb(49, 51, 51);
  --color: #fff;
  --boder-color: #fff;
}
export const setTheme = (isLight = true) => {
  document.body.style.setProperty('data-theme', isLight ? 'light' : 'dark')
}

而使用时通过?var()?在组件中应用对应 CSS 变量:

<style scoped>
.header {
  ...省略
  color: var(--color);
  border-bottom: 1px solid var(--boder-color);
  background-color: var(--bg);
}
...省略
</style>

2.3 优缺点

优点是简单,缺点是存在兼容性——CSS 变量的兼容性如下:

可以通过?css-vars-ponyfill?对 CSS 变量进行兼容处理,只需要导入该 ponyfill:

import cssVars from "css-vars-ponyfill";

// ... 省略 ...


export const setTheme = (isLight = true) => {
  
  // ... 省略 ...

  cssVars({
    watch: true,?//?添加、删除、修改 <link> 或 <style> 元素的禁用或 href 属性时,ponyfill 将自行调用????
    variables: themeMap,?//?variables?自定义属性名/值对的集合
    onlyLegacy: false,?//?false??默认将 css 变量编译为浏览器识别的 css 样式?;true?当浏览器不支持css变量的时候将css变量编译为识别的css??
  });
}

3 CSS 样式覆盖实现

3.1 原理

通过切换?css?选择器的方式实现皮肤/模式样式的切换:

  • 在组件中保留不变的样式,将需要变化的样式进行抽离;
  • 提供多种样式,给不同的皮肤/模式定义一个对应的 CSS 选择器;
  • 根据不同皮肤/模式切换成对应选择器来设置不同的样式。

3.2 具体实现

通过改变在全局存储和控制的模式?state,切换的伪代码如下:

import { setTheme } from '../style/themeUtil';

const checked = false;

const changeTheme = (event) => {
  setTheme(!checked.value);
}

state: {
  theme: 'light'
}

<div className=['header', state.theme]>
  <span>{{title}}</span>
  <input type="checkbox" class="switch" onChange={changeTheme} />
</div>

在?theme.css?中通过?.light?和?.dark?两个类选择器来区分日间和夜间模式:?

/* light 默认*/
body.light {
  background-color: #fff;
}

.header.light {
  background-color: #fff;
  border-bottom: 1px solid #d6d6d6;
  color: rgb(49, 51, 51);
}

.list.light .title {
  color: rgb(49, 51, 51);
}
.list.light .describe{
  color: rgb(159, 158, 157);
}

.list.light .left{
  border: 1px solid rgb(49, 51, 51);
}

/* dark 夜间模式 */
body.dark {
  background-color: rgb(49, 51, 51);
}

.header.dark {
  background-color: rgb(49, 51, 51);
  border-bottom: 1px solid #fff;
  color: #fff;
}

.list.dark .title {
  color: #fff;
}
.list.dark .describe{
  color: rgb(199, 200, 200);
}
.list.dark .left{
  border: 1px solid #fff;
  background-color: #fff;
}

3.3 优缺点

缺点很明显:

  • 多种皮肤/模式样式都要引入,导致代码量增大;
  • 样式不易管理,查找复杂;
  • 开发效率低,拓展性差;

4 动态 Style 实现

4.1 原理

通过更改?style 标签里面的内容实现——读取 CSS 文件里的样式,通过?css-color-function?把颜色替换成 json 文件中的关键字并返回 colors 对象,再选择一种颜色将关键字再次替换,再将样式通过?style 标签插入DOM里。

4.2 具体实现

文件目录如下:

├── yarn.lock
├── package.json
├── public
│   └── index.html
├── src
│   ├── color.js // 颜色转换为关键字的工具方法
│   ├── formula.json // 定义颜色与关键字之间的对应关系
│   ├── index.js
│   └── app.jsx
└── webpack.config.js

formula.json:

{
  "bgColor-out": "color(primary contrast(50%))",
  "bgColor-in": "color(primary contrast(50%))",
  "textColor": "color(primary tint(50%))",
  "grey": "color(primary tint(50%))"
}

?color.js:

import color from 'css-color-function';
import formula from './formula.json';

export default function generateColors(primary) {
  let colors = {};
  /** 遍历 formula.json 里的关键字 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;
}

如何使用呢,比如在 app.jsx 中,通过 fetch 发请求来读取CSS文件,然后将颜色转换成关键字,最后生成一个 css 模板,颜色选择器选择一个颜色后,根据 css 模板来修改或生成 style 标签。:

const [styleTemplate, setStyleTemplate] = useState('')
useEffect(() => {
  const colorMap = {
    '#fff': 'bgColorOut',
    '#fff': 'bgColorIn',
    '#333': 'textColor',
    '#1c1f23': 'grey'
  }
  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)
  })
}, [])


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;
    }
})


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

4.3 优缺点

优点是不存在兼容性问题,缺点是上手较难,维护成本高。

5?Sass/Less变量实现

5.1 原理

与 CSS 变量实现类似,也是通过给根元素添加属性/类名来达成切换模式样式的效果。

5.2?具体实现

文件目录如下:

├── 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>
      <button>button</button>
    </div>
  )
}

export default App;

_variable.scss 文件中定义每个模式的变量 :

$themes: (
    light: (
        bg-color-out: #fff,
        bg-color-in: #fff,
        text-color: #333,
        grey: #1c1f23
    ),
    dark: (
        bg-color-out: #16161a,
        bg-color-in: #35363c,
        text-color: #fff,
        grey: #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 使用 mixin 来绑定:

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

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

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

  }
}

5.3?优缺点

优点是不存在兼容性问题,缺点是代码可读性不如CSS,理解成本增加。

6?已有项目支持换肤?

对于已有项目,要支持换肤,若采用颜色变量的方式,需要手动将项目中所有颜色值手动替换为对应颜色变量,工作量巨大,如何自动化实现替换?

6.1 原理

  • 首先,使用 PostCSS?或者?Stylelint 解析识别 css/scss/stylus 等样式文件中的颜色字面量,如:background: white 中的 white
  • 然后,计算识别的颜色字面量是否可以替换为对应颜色变量,如:var(--bg--color--out)。使用 chorma-js?转换任意颜色格式和计算颜色之间的相似度,比如:chroma.distance(color1, color2)。如果颜色字面量与颜色变量对应的颜色值相同或相近则认为可以替换。

PostCSS :通过引入插件,可以将样式代码解析为抽象语法树 (AST),并提供了 API 允许分析和转换 CSS 文件的内容。比如?Sass,?Less,?Stylus以及?JSX?中的样式代码。

Stylelint 是针对样式文件的代码审查工具。Stylelint 底层基于 PostCSS,对样式文件进行解析分析,可以对 CSS/Sass/Stylus/Less 等样式文件进行审查。每个规则在 Stylelint 中都是一个插件,插件的输入是样式文件通过 PostCSS 解析后的抽象语法树,插件可以基于?PostCSS API?遍历语法树,分析/修改样式代码。

相比 PostCSS,使用 Stylelint 的好处是:

  • 不需要关心样式文件的解析,只需要关注拿到解析结果后如何分析;
  • 不需要关心根据不同的样式语言,引入不同的 PostCSS 解析插件;
  • StyleLint 有完善的 CLI 工具以及 VS Code 插件,可以直接复用。

CSS 属性值的颜色字面量识别有以下三类:

  • 穷举的?颜色关键字,如:white, blue 等等;
  • ?'#' 开头的RGB 16 进制形式,如:#FFF;
  • 名称为 rgb,?rgba,?hsl,?hsla,hwb,gray 等的函数形式,如:rgb(255, 255, 0) / rgba(255, 0, 255, 0.5)/?hsl(0deg, 0%, 13%)?/?hsla(0deg, 0%, 13%, 1)?/?hwb(...)/ gray(...)。

因此,可以实现一个 Stylelint 插件,分析并识别样式文件中的颜色字面量,并给出提示,针对有对应的颜色变量的,支持自动替换(autofix)

6.2 具体步骤

可以有以下两种形式进行使用:

  • 通过 StyleLint CLI 命令行工具一键查找和替换所有颜色变量;
/** 安装 stylelint 插件 */
npm i -D stylelint @edmi/stylelint-color-autofix

/** 项目根目录 .stylelintrc.json 配置 @edmi/stylelint-color-autofix 与 color-no-literal 规则(颜色不能是字面量)*/

{
  "plugins": ["@edmi/stylelint-color-autofix"],
  "rules": {
    "color-no-literal": [true, { "severity": "warning", "fixExact": false }]
  }
}


/** 检查 */
$ npx stylelint "**/*.scss"
/** 替换 */
$ npx stylelint "**/*.scss" --fix

?比如,替换前:

..edmi-write-btn {
    color: white;
    background: #eee;
    border: 1px solid rgb(0, 0, 0);
}

替换后:

.edmi-write-btn {
    color: var(--color-white);
    background: var(--color-tertiary-light-hover);
    border: 1px solid var(--color-black);
}
  • 通过 Stylelint VSCode编辑器插件,代码编写过程中提示颜色字面量需要用颜色变量来替代,可设置保存时自动替换;

总结

对于上面提到的实现方案,均不涉及持久化,可将对应的模式保存到本地缓存(localStorage)里,然后每次渲染读取缓存的模式值进行渲染,并且切换的时候对缓存的模式进行修改即可。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-16 21:35:35  更:2022-06-16 21:38:19 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:30:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码