likes
comments
collection
share

经常遇见网站有主题切换得效果

作者站长头像
站长
· 阅读数 10

网站或者应用一键切换主题功能,对每个前端开发者来说已经非常常见了,通常是一深一浅,或自由组合衍生出众多主题、任意主题。这时候,设计一个工程化主题切换功能就显得尤为重要,本文将介绍 Web 前端主题切换的几种常用方案。

一、预设多主题

这种方案首先需要预设多份皮肤样式,并在应用的根元素中设一个 class,这样在用户选择新的“皮肤”时,给 class 赋上对应的值,就可以达到切换皮肤的目标。该方案的关键是如何生成多份皮肤样式文件和如何替换/覆盖样式。

代码示例:

.theme1 button {
  background-color: yellow;
}
.theme2 button {
  background-color: green;
}
export default function App() {
  const [theme, setTheme] = useState('theme1');
  return (
    // 切换应用根元素的 class
    <div className={theme}>
      <h2>{theme}</h2>
      <button onClick={() => {
        setTheme(theme => { if (theme === 'theme1') return 'theme2'; return 'theme1' })
      }}>切换主题</button>
    </div>
  );
}

该方案的缺点:

1.线上只能支持有限数量的皮肤,不能用户自定义色彩;

2.样式文件替换非常复杂;

•如果采用全部样式文件都放在一个文件中,可以简化很多操作。但会极大的增加首屏成本,并且不符合当前前端组件化开发把样式文件放在组件内容旁边的方式;

•如果是分散的样式文件,在切换时,除了要替换当前已经加载的样式文件,对于新页面的样式文件,也需要加载指定色彩样式文件,需要对webpack等打包工具做一些优化调整;

该方案比较适合组件库(当前流行的组件库的主题定制基本也提供这个能力),不是很适合整体网站的换肤方案。

二、css in js

这种方案的基本原理是将主题色从用户上下文中获取后设置到组件上下文中,其他组件通过获取上下文中主题色的值来设置颜色。

该方案的缺点:

1.国内很少有团队使用css in js开发,很有可能你当前的项目不是采用css in js开发;

2.生态不足,很多组件库不是采用css in js, 需要额外的特殊处理;

有一种对没有使用css in js的组件库的特殊处理方案:将组件库的全量样式文件包裹在一个样式组件中,对其中的主题色全部换成js变量。

上述组件改造方案缺点:

1.极大增加了组件库升级成本,每次升级都要重新将组件库全量的样式文件进行css in js改造;

2.丢失组件库样式的按需引入能力。

三、颜色内容替换

浏览器的的document.styleSheets,可以查询到当前网站上所有的样式内容并且可以修改。如

// document.styleSheets.item(*).cssRules[*].style[{styleName}] = {updateValue}
document.styleSheets.item(0).cssRules[0].style['color'] = 'red';

该方式在运行时动态设置,且实现原理非常简单。但存下以下致命缺陷:

1.每一次的调整只针对于网站已经加载后的样式,对于后续加载的新样式,还需要额外的逻辑去处理。比如可以每个页面加载完成后的钩子函数中,或者调整构建逻辑(定制style-loader)在样式资源文件加载成功后修改样式内容;

2.额外消耗客户端计算资源,可能会有一段响应时间,造成了换肤客户在访问页面已开始显示默认色彩,短暂时间后,使用目标色彩,可能会出现闪屏现象;

3.由于采取样式比对方式去修改主题色,会造成一个问题:当第一次修改主题色为一个页面已有的其他颜色(假设为颜色A)后,下一次修改主题色时因为此时已经将颜色A也当作时主题色,会把不是主题色含义的色彩也调整;

4.如果万一出现某一个跟主题色一样的值的固定颜色值,需要特殊处理。

虽然该方案存在严重的缺陷,但它也有极大的优势:

1.对老项目极度友好,几乎无改造成本,几乎兼容老项目的任何样式开发方式,也几乎兼容任何第三方组件库;

2.对业务开发者非常友好,几乎做到了透明化,除直接写在元素style的方式外,不需要增加额外的样式开发规范。

四、css变量

css很早以前已经支持了css变量。在使用色彩的地方,替换为使用css变量,然后控制css变量即可换肤。

代码示例:

// base.less
:root {
  --primary: green;
  --warning: yellow;
  --info: white;
  --danger: red;
}

// var.less
@primary: var(--primary)
@danger: var(--danger)
@info: var(--info)

// page.less
.header {
  background-color: @primary;
  color: @info;
}
.content {
  border: 1px solid @danger;
}
// change.js
function changeTheme(themeObj) {
  const vars = Object.keys(themeObj).map(key => `--${key}:${themeObj[key]}`).join(';')
  document.documentElement.setAttribute('style', vars)
}

像一些知名的UI也都采用了这种方法,比如ElementUI的ColorPicker 颜色选择器,是在html标签加 class="dark" ,基于html.dark 和:root配合改变,实现定制主题。Ant Design使用less语言,在html标签加color-scheme 和在body里添加自定义标签data-theme="dark",和:root配合改变。

然而该方案可能存在以下问题:

1.技术比较新,可能当前的项目并不是使用css变量,改造起来成本太高;

2.不兼容ie。

五、css-vars-ponyfill

css-vars-ponyfill的特点正如其在官网上所描述,可以在传统和现代浏览器为css自定义属性,提供客户端支持的 ponyfill。



经常遇见网站有主题切换得效果



css-vars-ponyfill兼容浏览器的方式如图所示:



经常遇见网站有主题切换得效果



示例代码:

  1. 在应用端触发换肤操作的时候,配合 JavaScript 状态管理,同步主题切换的信号,对应触发initThemes 方法;
  let varList = {
    ...colorColor
  }
  let tPrimaryList = themePrimaryList
  initThemes('', tPrimaryList, varList, ''
  1. 切换主题色,通过css-vars-ponyfill,把自定义常量打到对应的DOM节点(通常是html或者body下),从而实现切换主题;

theme.js:

import cssVars from "css-vars-ponyfill";
import { themeTypeList, themePrimaryList } from "./themeList.js";
import { mix, hex2rgb } from "./com/util";
/**
 * initThemes 全局初始化 主题
 * @param theme 主题 [必填]
 * @param tPrimaryList 主题列表[必填] array ['theme1','theme2']
 * @param valList 自定义主题列表 {val1:['theme1-color','theme2-color']} ....
 * @param themeType 主题类型  -深浅 ....
 * @param changeType 区分改的是主题类型,还是主题色 [ 预留字段 ]....
 * @returns {boolean}
 */
 
export const initThemes = (theme, tPrimaryList, varList, themeType) => {
  let variables = getVariables(
    theme || "lightBlue",
    tPrimaryList,
    varList,
    themeType
  );
 
  cssVars({
    watch: true, // 当添加,删除或修改其或元素的禁用或href属性时,ponyfill将自行调用
    variables: variables, // variables 自定义属性名/值对的集合
    onlyLegacy: false, // false  默认将css变量编译为浏览器识别的css样式  true 当浏览器不支持css变量的时候将css变量编译为识别的css
  });
};
  1. themeList.js 里存放预先设置的一些主题和色系(深浅)基础色;
import { light } from './com/light'
import { dark } from './com/dark'
 
// 主题 - 主题色
export const themePrimaryList = {
  dark: [
    {
      color: '#FFAA0E',
      name: '深黄',
      theme: 'darkYellow'
    },
    {
      color: '#FFAA0E',
      name: '深蓝',
      theme: 'darkBlue'
    },
  ],
  light: [
    {
      color: '#FFAA0E',
      name: '深黄',
      theme: 'lightYellow'
    },
    {
      color: '#256DFF',
      name: '浅蓝',
      theme: 'lightBlue'
    }
  ]
}
 
export const themeTypeList = {
  dark: dark,
  light: light,
}

该方案的优势:

1.纯JS实现,不依赖CSS预处理器(sass和less),兼容ie9;

2.抽离深浅色系基础色(统一治理输出),以及主题色,混合颜色(黑白色)都可以动态接口获得;

3.统一规范业务色常量命名,JS定义自定义函数方法 1、 Mix函数实现媲美sass的颜色混合机制,2、十六进制和RGB(rgba)互相转换函数;

4.技术路线不抖,直接用var()函数使用,后期封装成JS库 ,皮肤配置中台,可以提供给各个团队使用;

5.关于业务自定义变量,设计有两个治理方案:(1)全局变量, 全局单独维护(2)局部业务变量,局部单独维护。

总结

大多系统都需要实现千人千面,为了尽量降低成本,常采取在线换肤的方式解决。本文介绍了几种常见的换肤方案,并阐述了不同方式的换肤实现的区别及优缺点。在实际的项目开发时,结合系统本身使用的技术方案和业务要求,选择最合适的方案。