前端知识库(lizh)
  • README
  • Bugs
    • 前端调试随笔
    • 浏览器常见问题概览
    • 浏览器兼容问题概览
    • HTML常见问题概览
    • CSS常见问题概览
    • JS常见问题概览
    • 移动端兼容性问题概览
    • 微信小程序开发
    • NodeJs常见问题概览
    • Mac常见问题概览
    • 微信开发遇坑指南
    • Npm包常见问题概览
    • 其他问题汇总
  • Css探索系列
    • CSS基础知识
    • CSS常见问答
    • CSS常见问答02
    • CSS应用示例
    • CSS应用示例02
    • 由Z Index引发的层叠上下文思考
    • 由浮动塌陷引发的块级格式上下文思考
    • CSS探索系列 Flex布局
    • CSS探索系列 Margin
    • CSS探索系列 Auto关键字
    • CSS探索系列 Gradient
    • CSS探索系列 Line Height
    • CSS探索系列 元素居中
    • CSS探索系列 动画
    • 为什么使用PostCSS处理CSS?
    • 重新认识伪类与伪元素
    • 自定义表单伪元素样式
    • 如何理解Css中的Display属性
    • 视口和软键盘对视口的影响
    • 关于Css
  • Frontend
    • 00 关于Web前端
    • 01 前端知识概览
    • 02 常用前端库概览
    • 基础 00 前端常见问题01
    • 基础 01 浏览器缓存
    • 基础 02 浏览器工作原理
    • 基础 03 谈谈前端跨源问题及解决方法
    • 进阶 01 Web性能优化
    • 进阶 02 搜索引擎优化(SEO)
    • 进阶 03 前端模块化编程
    • 进阶 04 规范代码:Linter、Prettier、EditorConfig
    • 进阶 11 前端自动化测试
    • 高级 01 前端安全
    • Vue2.X原理篇
    • Vue3初步了解及迁移指南
    • 重读Vue教程
    • React17.X原理篇
    • 你必须知道的React问题
    • 重读React教程
    • 聊一聊Cookie的一些问题
    • 如何理解HTTP响应的状态码
    • HTTP的历史演变及概述
    • Webpack4.X原理篇
    • Webpack基础入门篇
    • Webpack构建优化篇
    • TypeScript使用指南
    • 代码规范
      • 前端规范
      • HTML
      • CSS
      • JS
  • Html探索系列
    • HTML基础知识
    • HTML基础知识02
    • HTML常见问答
    • HTML经典实践用例
    • HTML元素的宽高及位置详解
    • Video元素的使用和常见问题总结
    • Html探索系列 Meta标签
    • DOCTYPE:文档类型与浏览器模式
    • DHTML(动态网页)简介
    • HTML标签详解
    • HTML布局的几种方式
    • HTML全局属性
    • 关于Html
  • Js探索系列
    • 基础知识
    • 常见问答
    • 应用示例
    • 趣味示例
    • 基础篇 05 AJAX
    • 基础篇 06 Window对象
    • 基础篇 07 Error、JSON、Math、Console对象
    • 基础篇 08 History、URL、Screen、Navigator、Location对象
    • 基础篇 09 文档对象模型(DOM)
    • 基础篇 10 Document对象
    • 基础篇 11 Element对象
    • 基础篇 12 Event对象
    • 基础篇 13 键盘、鼠标、触摸事件
    • 基础篇 15 CSS对象模型(CSSOM)
    • 进阶篇 01 Prototype对象和继承
    • 进阶篇 02 Promise对象
    • 进阶篇 07 迭代器(Iterator)
    • 进阶篇 08 Generator和Async函数
    • 进阶篇 09 JavaScript异步编程
    • Date对象和日期时间字符串格式
    • Canvas基础入门篇
    • Canvas进阶篇
    • SVG基础入门篇
    • 四种判断数据类型方法的优缺点
    • 深入理解JavaScript的浅拷贝和深拷贝
    • 谈谈JavaScript的作用域和上下文
    • 复制内容到剪贴板
    • 关于Javascript
  • NodeJs
    • 关于Node.Js
    • Node.Js:三种调试方法
    • Npm包管理器简介及一些机制
    • NPM:Package.Json详解(中文)
    • NPM:从零开始,开发一个软件包
    • NPM:常用命令
    • Node.Js:Fs(文件系统)
    • Node.Js:Global(全局变量)
    • Node.Js:HTTP
    • Node.Js:Module(模块)
    • Node.Js:Path(路径)
    • Node.Js:Readline(逐行读取)
  • Research
    • 极细边框(1px边框)实现方式
    • 如何监控前端异常?
    • H5页面跳转和刷新
    • Web主题切换和个性化定制方法总结
    • Vue SSR(服务端渲染)的简单实现
    • 基于Create React App打造代码规范化的React+Ts项目
    • H5可视化编辑
    • Web常用功能
    • Javascript加密混淆
    • Vue如何导入TypeScript
    • 移动端PDF预览
    • 纯CSS绘制箭头
    • 网站性能测量和优化方法
  • Tech
    • GOOGLE浏览器的搜索技巧
    • Curl的用法指南
    • Sublime3插件篇
    • Charles安装及使用
    • Nginx基础使用
    • 排序算法(Javascript)
    • 代码整洁之道(摘录笔记)
    • Java的24种设计模式与7大原则
    • 观察者和发布订阅模式
  • Tools
    • Git
      • Git基础教程
      • Git常见问题
    • Gitbook
      • Gitbook入门篇
      • Gitbook插件篇
      • Gitbook进阶篇
由 GitBook 提供支持
在本页
  • 切换主题
  • 方法一:class 命名空间
  • 方法二:多个 CSS 文件
  • 个性化定制主题
  • 方法一:动态创建 style 标签
  • 方法二:LESS 在线修改变量
  • 最优雅的切换/自定义主题方案
  • CSS 变量最大的问题是浏览器兼容性
  • 总结
  • 参考链接

这有帮助吗?

  1. Research

Web主题切换和个性化定制方法总结

上一页H5页面跳转和刷新下一页Vue SSR(服务端渲染)的简单实现

最后更新于3年前

这有帮助吗?

切换主题

切换主题:主题是由开发者定义,一般来说只有有限的主题可选。

方法一:class 命名空间

定制不同主题,存于不同的 class 命名空间下,通过 JS 修改 body 或其他需要换主题的标签上的 class 名来实现主题切换。

该方法的关键在于如何为不同主题,生成不同的 class。如果纯手动写多个主题的 class,成本太高,而且后期维护也比较困难。

以下是两种解决方案:postcss-themes 插件、less 混合。

插件(推荐)

将主题相关的变量提取到一个主题变量文件中, postcss-themes 会为使用了主题文件中变量的选择器,新添加一份带有命名空间的选择器。

/* postcss.config.js */
module.exports = {
  plugins: {
    'postcss-themes': {
      themes: { // themes 可以为数组,配置多个主题
        filePath: 'red_theme.css'
      }
    },
    'postcss-css-variables': {},
  }
}

输入 css:

/* index.css */
:root {
  --main-color: green;
}
.foo {
  font-size: 16px;
  color: var(--main-color);
}
/* red_theme */
:root {
  --main-color: red;
}

输出 css:

.foo {
  font-size: 16px;
  color: green;
}
.red_theme .foo {
  color: red;
}

Less 混合

less 混合是将一组属性从一个规则集包含(或混入)到另一个规则集的方法。规则集可以通过传参生成不同的新规则集。该方案需要将所有主题相关的样式集中在一个规则集中,实际执行起来比较困难。

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

JS 切换主题

JS 修改 body 或其他需要换主题的元素的 class 名。

document.body.className = `${document.body.className} ${theme_class_name}`

方法二:多个 CSS 文件

定制不同主题,每个主题生成一份 CSS 文件,然后再通过 JS 切换主题样式文件来实现。

实现步骤上如下:

  • 创建主题变量文件,包含主题相关的变量定义;

  • 根据不同的主题变量文件,编译生成不同主题的 CSS 文件;

  • 通过 JS 切换主题样式文件。

其中重点是,如何根据不同的主题变量文件,编译生成不同主题的 CSS 文件。

以下是两种用 webpack 的实现方式:

多次运行 webapck,生成多个 CSS 文件

一次打包同时生成多份主题 CSS 文件比较麻烦,因此通过打包多次,每次生成一个主题对应的 css 文件来实现,而 JS 文件在这个过程中是不会改变。

// webpack.prod.conf.js
module.exports = theme => {
  return {
    plugins: [
      new MiniCssExtractPlugin({
        filename: `[name]_${theme}.css`,
      }),
    ],
    resolve: {
      alias: {
        themeVars: path.join(__dirname, `../src/style/themes/theme-${theme}.css`),
      }
    }
  }
}
/*variable/color.less*/
@import "~themeVars"
const webpack = require('webpack')
const webpackConfig = require('./webpack.prod.conf')
['default', 'broker'].forEach(theme => {
  webpack(webpackConfig(theme), (err, stats) => {})
})

注意: 使用 less 预处理,@import 可以直接引用 webpack.prod.conf.js 中定义的 alias,而 postcss-loader 是无法使用该 alias 的。

// postcss.config.js
const path = require('path')
const aliasResolver = require('postcss-import-alias-resolver');
module.exports = {
  plugins: {
    'postcss-import': {
      resolve: aliasResolver({
        alias: {
          themeVars: path.join(__dirname, `src/style/themes`),
        }
      })      
    }
  }
}
/*variable/color.less*/
@import '~themeVars/theme-broker.css';

但是,postcss.config.js 中,只能静态指定 alias,想要动态指定,需要在 webpack.prod.conf.js 中配置。实际运行时,发现 postcss-import-alias-resolver 的指定的 alias 在 webpack.prod.conf.js 中无效(??具体原因未找到)。

最后,改用自定义 postcss-import中的 resolve 方法来实现的:

// webpack.prod.conf.js
module.exports = theme => {
  return {
    plugins: [
      new MiniCssExtractPlugin({
        filename: `[name]_${theme}.css`,
      }),
    ],
    module: {
      rules: [
        {
          test: /\.(css|postcss)$/,
          use: [
            'style-loader',
            'css-loader',
            {
              loader: 'postcss-loader',
              options: {
                ident: 'postcss',
                plugins: (loader) => [
                  require('postcss-import')({
                    resolve: (id, basedir, importOptions) => {
                      // 这里会将 @import "./color.css" 的导入文件,指向`../themes/theme-${theme}.css`。所以 color.css 可以是一个并不存在的文件。
                      if (id === './color.css') {
                        return path.join(basedir, `../themes/theme-${theme}.css`)
                      }
                      return path.join(basedir, id)
                    }
                  }),
                  // 注意,webpack的配置会覆盖postcss.config.js。
                  // 所以,如有其他 postcss 插件,也要移到这里
                  ...
                ]
              }
            }
          ]
        }
      ]
    }
  }
}

运行一次 webpack, 生成多个 css 文件 (不推荐)

  • extract-text-webpack-plugin: 多次调用插件实例,生成多个 css 文件。不过,该插件适用于 webapck 3,目前已废弃。

  • mini-css-extract-plugin:有多主题的相关讨论,但还没有可行的解决方案。

JS 切换主题样式文件

  • JS 改变 link 标签的 href 属性

    <link id="skincolor" href="skin-default.css" rel="stylesheet" type="text/css">
    document.getElementById('#skincolor').href = 'skin-red.css';

    注意:该方法需重新加载样式表,会带来加载延迟。样式切换不流畅,体验不太好。

  • JS 修改 link 标签的 disabled 属性

    <link href="reset.css" rel="stylesheet" type="text/css">
    <link href="default.css" rel="stylesheet" type="text/css" title="Default Style">
    <link href="fancy.css" rel="alternate stylesheet" type="text/css" title="Fancy" disabled>
    <link href="basic.css" rel="alternate stylesheet" type="text/css" title="Basic" disabled>

    link 标签的 disabled 属性用来启用或禁用样式表。通过 JS 设置 link 标签 disabled="false",可以让默认不渲染的 CSS 开始渲染。

    以上样式表都可分为3类:

    • 无 title 属性: <link rel="stylesheet">,无论如何都会加载并渲染。如:reset.css。

    • 有 title 属性: <link rel="stylesheet" title="Default Style">,默认样式,CSS 文件加载并渲染。如:default.css。

    • 有 title 属性: <link rel=“alternate stylesheet” title="Fancy">,作为备选样式,CSS 文件加载,默认不渲染。如:fancy.css。

      alternate 意味备用,相当于是 css 预加载进来备用,所以不会有加载换延问题。

个性化定制主题

个性化定制主题:主题是由用户定义,一般具有无限种可能,用户可以为主题变量设置任意色值。

方法一:动态创建 style 标签

该方法实现原理就是通过 JS 创建一个 style 元素,填充新样式,append 到 head 元素中,覆盖 css 文件中定义的样式。

一个简单的DEMO

<div class="text_color"> 一个简单的示意 </div>
.text_color { color: red; }
const styleDom = document.createElement('style')
styleDom.innerHTML = '.text_color { color: cyan; }'
document.head.appendChild(styleDom)

该方法可以说是最简单的,也可以说是最复杂的。说简单,是因为它的实现方式直接明了;说复杂,是因为它必需逐个找到需要修改的元素的 class 名,元素越多,实现起来就越复杂。

此外,使用了 css modules 的项目,我们无法锁定需要修改的元素的 class 名。

案例分析

换肤实现过程:

  • 用 Ajax 将当前页面的 CSS 文件请求回来;

  • 处理请求回来的 CSS 数据:将需要替换的色值全部替换成新颜色值。

    Element-UI 换肤的处理过程分三步:

简单源码示例

function changeTheme (url, colorMap = {}) {
  const request = new XMLHttpRequest()
  request.onreadystatechange = res => {
    const resTarget = res.target
    if (resTarget.readyState !== 4) {
      return
    }
    if (resTarget.status === 200) {
      let originalStyle = resTarget.response
      for (let k in colorMap) {
        originalStyle = originalStyle.replace(new RegExp(k, 'ig'), colorMap[k])
      }
      const styleDom = document.createElement('style')
      styleDom.innerText = originalStyle
      document.head.appendChild(styleDom)
    }
  }
  request.open('GET', url)
  request.send()
}
changeTheme('https://xxx.xxx.com/css_file_path/index.css', {
  '#508cee': '#ff5521',
  '#D2DFFE': '#FFDDD2'
})

方法二:LESS 在线修改变量

基于 less 写样式,为主题相关的颜色定义变量。在 HTML 中直接引入 .less 文件,再引入 less.js ,less.js 会在线将 .less 文件编译成浏览器可识别的样式,然后填充在新创建的 style 标签中。我们通过调用 less.modifyVars 方法修改主题变量实现个性化定制主题。

一个简单的DEMO

<body>
  <link rel="stylesheet/less" type="text/css" href="theme.less" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/4.1.1/less.min.js"></script>
  <div class="text_color">LESS 在线修改变量</div>
</body>
/**theme.less**/
@major_color: red;
.text_color {
  color: @major_color;
}

less.js 在线编译 .less 文件,创建一个 style 元素,填入编译后的样式:

<link rel="stylesheet/less" type="text/css" href="theme.less">
<style type="text/css" id="less:pages-common-templatev1-theme">.text_color {color: red;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/4.1.1/less.min.js"></script>

JS 修改变量:

window.less.modifyVars({"@major_color": 'cyan'})
<style type="text/css" id="less:pages-common-templatev1-theme">.text_color {color: cyan;}</style>

方法分析

  • 引入的 .less 文件,需要在浏览器中编译,这对浏览器性能有影响,不建议在生产环境使用。

  • 此方法仅限于用 less 的项目才能使用。sass 没有类似 less.modifyVars 的方法。

  • 引入 .less 文件的 <link> 标签:rel 属性值必需是 stylesheet/less。

  • less.js 一定要在所有的 .less 文件后引用。

  • 最后一个关键问题:如何把项目中的 less 样式(.less文件、.vue 文件中的 <style lang="less"> 等)提取到一个 .less 文件中。

    • 合并 less:通过 antd-pro-merge-less 插件扫描 src 中所有的 less,将其合并为一个 .less 文件。

    • 转化 css-module。

最优雅的切换/自定义主题方案

CSS 变量: CSS 预设主题相关变量, 通过 JS 动态修改 CSS 变量,进而修改主题。

css 变量的设置及使用:

:root {
  --major-color: red;
}
.text_color {
  color: var(--major-color);
}

JS 操作 CSS 变量:

document.documentElement.style.getPropertyValue('--major-color')
document.documentElement.style.setProperty('--major-color', 'purple')
document.documentElement.style.removeProperty('--major-color')

/*
注意:js操作css变量是在标签的style对象上,所以,对于在css文件中定义的变量,如:`:root { --major-color: red; }`,调用 getPropertyValue 方法,返回的值是空字符。
可以改用下面方法获取:
*/
getComputedStyle(document.documentElement).getPropertyValue('--major-color')

CSS 变量最大的问题是浏览器兼容性

目前来看(2021.07),大部分主流浏览器(chrome、safari、firefox、edge等)是支持的,但是,IE 浏览器不支持。

总结

  • class 命名空间:这是个值得推荐的方案。它的唯一问题是,css 文件将多个主题的样式打包进来,文件会增大。不过,使用 postcss-themes 生成多主题样式,只会新增引用了主题相关变量的选择器,文件不会增加太多,并且,它还有个优势:切换主题不会有延迟问题。

  • 多个 CSS 文件:相比 class 命名空间,这个方案优势在于首次加载的 CSS 文件不需要包含其他主题相关的样式。它的缺点:切换主题可能会有加截延迟;webpack 类的打包工具生成的 CSS 文件,我们通常习惯给文件名指定 hash 值,这会导致 JS 动态加载 CSS 时,无法确定正确的文件名;如果有按需加载的模块,切换主题会变得不好处理。

  • 动态创建 style 标签:这个方案是最灵活的,可以任意修改元素的样式。其他方案都有个前提,必需先确定主题相关的变量,主题切换只能修改引用了变量的样式。它的缺点也很明显:主题切换涉及的元素越多,实现成本就越高;不适合使用了 css module 的项目;如果是基于请求已有 css 文件的数据,替换色值来实现,则会有加截延迟问题,另外,按需加载的模块也不好处理。

  • LESS 在线修改变量:这是个不太实用的方案。因为,在浏览器中实时编译 .less 文件,非常影响性能,不适合用于生产环境,另外,将所有主题变量相关的样式提取到一个单独的 .less 文件也比较麻烦。

  • css 变量方法 (推荐):这是最完美的方案,除了不支持 IE 浏览器(目前已确定的是 IE,也可能还有些其他非主流浏览器)。

参考链接

postcss-loader 是通过 postcss-import 来处理 @import 语句。 postcss-import 是不支持 alias 配置的,需要用另一个插件 来实现。

实现:

实现: MDN

Element-UI 换肤:

把默认主题文件中涉及到颜色的 CSS 值替换成关键词。

根据用户选择的色值,生成一系列对应的新颜色值。

把关键词换回刚刚生成的相应的新颜色值。

创建 style 标签,把处理好的数据填进去,再 append 到 head 元素。

目前还没有找到可实现的方案。 有正在试验的方案,但目前还有问题。它的实现步骤:

抽取 less 变量:通过 antd-theme-webpack-plugin 来做到的。它通过遍历 less 的语法树,抽取配置中所有拥有 less 变量的选择器,并且将其组合成一个 color.less 的文件。 可以查看具体实现。

实例预览:

image-20210720160638232

postcss-themes
postcss-import-alias-resolver
webpack如何extract多个css
Support multiple instances of MiniCssExtractPlugin to generate multiple CSS theme output from the single CSS input
腾讯首页个性化换肤demo页面
Alternative style sheets
Element-UI 换肤预览
Element-UI 换肤原理
源码
源码
源码
相关代码
Ant Design Pro 在线更换主题
antd-theme-generator
Ant Design 在线更换主题预览
https://caniuse.com/css-variables
使用 css/less 动态更换主题色(换肤功能)
一文总结前端换肤换主题
实现三方库按需引入与多主题方案
阮一峰-CSS 变量教程