Web 主题切换与个性化定制
主题能力分两类:固定主题切换(开发者预先定义有限几套,如亮色、暗色、业务皮肤)和用户个性化定制(用户可输入任意色值,组合没有上限)。前者可以在构建期生成样式,后者必须在运行时改变量或覆盖 CSS。
固定主题切换(有限套主题)
主题集合由开发提前定义,用户只能在预设主题中选择。
Class 命名空间
核心思路:为不同主题生成带主题类名的样式规则;切换时,通过 JS 修改 body、根容器的 class,从而激活对应主题样式。
缺点:手写多套主题维护成本高、容易漏写样式,一般借助构建工具自动生成带命名空间的样式。
postcss-themes(推荐)
把主题颜色、尺寸等变量抽离独立主题文件,postcss-themes 会自动复制受变量影响的选择器,并追加主题命名空间前缀。
// postcss.config.js
module.exports = {
plugins: {
'postcss-themes': {
// themes 可以为数组,配置多个主题
themes: {
filePath: 'red_theme.css'
}
},
'postcss-css-variables': {},
}
}
/* 默认主题 */
:root {
--main-color: green;
}
.foo {
font-size: 16px;
color: var(--main-color);
}
/* red_theme.css */
:root {
--main-color: red;
}
构建输出结果:
.foo {
font-size: 16px;
color: green;
}
/* 新增带命名空间的主题样式 */
.red_theme .foo {
color: red;
}
JS 切换主题:
// 给body追加主题class
document.body.classList.add('red_theme');
// 切换其他主题时需要移除旧主题class
document.body.classList.remove('red_theme');
Less Mixin 混合
利用 Less 的 mixin 混入能力,封装一套通用主题规则,传入不同参数生成多套主题。
缺点:需要业务手动把所有主题相关样式收拢到 mixin 内部,业务规模大时维护成本很高。
/* variables.less */
.base_theme(@background: #e2e9f9, @color: #337ab7){
.class_1 {
background: @background;
}
.class_1 a:hover{
color: saturate(@color, 20%);
}
.class_2 {
color: @color;
}
}
@import 'variables.less';
.default_theme {
.base_theme();
}
.coffee_theme {
.base_theme(#f5e2c9, #9f754d);
}
.purple_theme {
.base_theme(#e5d3ed, #b88bcb);
}
多 CSS 独立文件
每个主题单独编译输出一份完整 CSS,运行时通过 JS 控制样式表的加载、启用,切换不同皮肤。
核心问题:webpack + mini-css-extract-plugin 不支持单份源码直接多次实例化输出多份 CSS。
生成多份CSS主题文件
多次执行webpack编译,每次通过alias替换主题变量源文件,分别输出对应主题CSS产物,JS源码保持共用。
// webpack.prod.conf.js
const path = require('path');
module.exports = function(theme) {
return {
plugins: [
new MiniCssExtractPlugin({
filename: `[name]_${theme}.css`,
}),
],
resolve: {
alias: {
themeVars: path.join(__dirname, `../src/style/themes/theme-${theme}.css`),
}
}
}
}
less 中引用主题变量:
@import "~themeVars";
脚本批量触发多主题构建:
const webpack = require('webpack');
const webpackConfig = require('./webpack.prod.conf');
['default', 'broker'].forEach(theme => {
webpack(webpackConfig(theme), (err, stats) => {
if(err) console.error(err);
})
})
修改 link 标签切换主题
JS 改变 link 标签的 href 属性。
缺点:浏览器会重新请求 CSS 资源,存在网络加载延迟,切换有卡顿感;生产环境 CSS 带 hash 文件名,JS 很难维护文件名;路由懒加载模块切换主题适配复杂。
<link id="skin-link" href="skin-default.css" rel="stylesheet">
const link = document.getElementById('skin-link');
link.href = 'skin-red.css';
预加载备选样式
JS 修改 link 标签的 disabled 属性。
浏览器支持备选样式表机制,提前把所有主题 CSS 全部预加载到浏览器,通过修改 disabled 属性控制启用、关闭,无网络等待。
优点:切换无网络请求;缺点:一次性预加载全部主题 CSS,首屏体积开销大。
<!-- 全局通用样式,永远生效 -->
<link href="reset.css" rel="stylesheet">
<!-- 默认启用的主题 -->
<link href="default.css" rel="stylesheet" title="Default Style">
<!-- 备选主题,预加载,默认禁用 -->
<link href="fancy.css" rel="alternate stylesheet" title="Fancy" disabled>
<link href="basic.css" rel="alternate stylesheet" title="Basic" disabled>
用户个性化定制主题(无限主题)
用户可以自由输入任意颜色值,没有预设主题集合,颜色组合理论上无限,不能提前全部构建出 CSS 产物。
动态生成 style 标签覆盖样式
原理:JS 动态创建 <style> 标签,填充自定义样式,挂载到 document.head,覆盖原有样式。
缺点:如果项目使用 CSS Modules,类名被哈希,无法直接写选择器;业务样式量大时,需要维护大量选择器。
<div class="text-color">测试文本</div>
.text-color {
color: red;
}
const styleEl = document.createElement('style');
styleEl.innerHTML = '.text-color { color: cyan; }';
document.head.appendChild(styleEl);
Element UI(Vue 2)在线换肤方案核心逻辑:
- Ajax 拉取组件库完整 CSS 文本;
- 将原始 CSS 中固定颜色值替换为占位关键词;
- 根据用户输入主色,通过颜色公式,计算整套衍生色(hover、active、边框、背景色);
- 将关键词替换为新生成色值;
- 生成 style 标签,插入页面,覆盖原有样式。
Element Plus、Ant Design 等现行组件库已经把主题色做成 CSS 变量,不必再拉整份 CSS 做字符串替换。
function changeThemeByCssUrl(cssUrl, colorMap) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(res) {
const target = res.target;
if(target.readyState !== 4) return;
if(target.status === 200) {
let cssText = target.response;
Object.keys(colorMap).forEach(oldColor => {
const reg = new RegExp(oldColor, 'ig');
cssText = cssText.replace(reg, colorMap[oldColor]);
});
const styleDom = document.createElement('style');
styleDom.innerText = cssText;
document.head.appendChild(styleDom);
}
}
xhr.open('GET', cssUrl);
xhr.send();
}
// 使用:替换两套颜色
changeThemeByCssUrl('https://xxx/theme.css', {
'#508cee': '#ff5521',
'#D2DFFE': '#FFDDD2'
})
缺点:需要请求完整 CSS 文本,存在网络开销;懒加载组件的样式无法被覆盖。
Less 浏览器端在线修改变量
浏览器引入 less.js,直接加载 .less 源码文件,浏览器实时编译;调用 less.modifyVars() 动态修改变量,实现换肤。
缺点:
- 在浏览器实时编译 Less,性能损耗大,不适合生产环境;
- Sass 没有等价的浏览器修改变量 API;
- 需要把项目全部主题相关 Less 抽离为单独文件,工程化成本高;
- 社区如
antd-theme-generator,通过 webpack 插件在构建期抽取 less 变量文件,仅适用于构建阶段。
<link rel="stylesheet/less" href="theme.less" />
<script src="https://cdn.jsdelivr.net/npm/less@4.1.1/dist/less.min.js"></script>
<div class="text-color">LESS在线换肤演示</div>
/* theme.less */
@major-color: red;
.text-color {
color: @major-color;
}
JS 修改变量:
window.less.modifyVars({
"@major-color": "cyan"
})
CSS Variables(现代浏览器优先方案)
核心原理:在 :root 定义一系列 CSS 变量(自定义属性),业务样式全部使用 var() 引用变量;运行时 JS 修改根节点上的变量值,页面样式自动更新。
优点:
- 同时支持固定主题切换和用户无限个性化定制;
- 无需重新加载 CSS 资源,切换即时生效;
- 无需构建阶段生成多套产物,工程成本低;
- 支持局部覆盖,可以给任意 DOM 节点设置局部变量,不限于根节点。
:root {
--major-color: red;
}
.text-color {
color: var(--major-color);
}
JS 读写 CSS 变量:
// 设置变量
document.documentElement.style.setProperty('--major-color', 'purple');
// 获取内联style上设置的变量
document.documentElement.style.getPropertyValue('--major-color');
// 获取css文件中定义的变量(computedStyle)
getComputedStyle(document.documentElement).getPropertyValue('--major-color');
// 删除变量
document.documentElement.style.removeProperty('--major-color');
总结
- CSS 变量(优先推荐):实现简单、体验好。现行主流浏览器均已支持;只有必须兼容 IE 时才需要降级到 class 或多份 CSS。
- class 命名空间(推荐):切换无延迟;搭配
postcss-themes,仅生成用到主题变量的选择器,体积可控。缺点:原生会打包全部主题样式。 - 多 CSS 文件:首屏无多余主题样式。缺点:切换有加载延迟;打包带 hash,JS 难以动态引入;按需模块适配麻烦。
- 动态创建 style 标签:灵活性最强,不依赖预定义变量。缺点:元素多则维护成本高;不兼容 CSS Module;拉取 CSS 替换色值会有延迟;按需模块不好处理。
- LESS 浏览器修改变量(不推荐生产):浏览器实时编译性能差,样式抽离改造繁琐。